diff --git a/build.js b/build.js index 6e6022ae..21eca4c9 100644 --- a/build.js +++ b/build.js @@ -198,6 +198,7 @@ var filesToInclude = [ 'src/shapes/polygon.class.js', 'src/shapes/path.class.js', 'src/shapes/group.class.js', + 'src/shapes/active_selection.class.js', 'src/shapes/image.class.js', ifSpecifiedInclude('object_straightening', 'src/mixins/object_straightening.mixin.js'), diff --git a/src/canvas.class.js b/src/canvas.class.js index 6478807d..ab0ec5a4 100644 --- a/src/canvas.class.js +++ b/src/canvas.class.js @@ -328,25 +328,25 @@ * @return {Array} objects to render immediately and pushes the other in the activeGroup. */ _chooseObjectsToRender: function() { - var activeGroup = this.getActiveGroup(), - activeObject = this.getActiveObject(), - object, objsToRender = [], activeGroupObjects = []; + var activeObjects = this.getActiveObjects(), + object, objsToRender, activeGroupObjects; - if ((activeGroup || activeObject) && !this.preserveObjectStacking) { + if (activeObjects.length > 0 && !this.preserveObjectStacking) { + objsToRender = []; + activeGroupObjects = []; for (var i = 0, length = this._objects.length; i < length; i++) { object = this._objects[i]; - if ((!activeGroup || !activeGroup.contains(object)) && object !== activeObject) { + if (activeObjects.indexOf(object) === -1 ) { objsToRender.push(object); } else { activeGroupObjects.push(object); } } - if (activeGroup) { - activeGroup._set('_objects', activeGroupObjects); - objsToRender.push(activeGroup); + if (activeObjects.length > 1) { + this._activeObject._objects = activeGroupObjects; } - activeObject && objsToRender.push(activeObject); + objsToRender.push.apply(objsToRender, activeGroupObjects); } else { objsToRender = this._objects; @@ -449,7 +449,7 @@ pointer = point || this.getPointer(e, ignoreZoom), xy; - if (target.group && target.group === this.getActiveGroup()) { + if (target.group && target.group === this._activeObject && target.group.type === 'activeSelection') { xy = this._normalizePointer(target.group, pointer); } else { @@ -511,16 +511,16 @@ * @param {fabric.Object} target */ _shouldClearSelection: function (e, target) { - var activeGroup = this.getActiveGroup(), - activeObject = this.getActiveObject(); - + var activeObjects = this.getActiveObjects(), + activeObject = this._activeObject; return ( !target || (target && - activeGroup && - !activeGroup.contains(target) && - activeGroup !== target && + activeObject && + activeObjects.length > 1 && + activeObjects.indexOf(target) === -1 && + activeObject !== target && !e[this.selectionKey]) || (target && !target.evented) @@ -1104,23 +1104,25 @@ var ignoreZoom = true, pointer = this.getPointer(e, ignoreZoom), - activeGroup = this.getActiveGroup(), - activeObject = this.getActiveObject(), + activeObject = this._activeObject, + aObjects = this.getActiveObjects(), activeTarget; // first check current group (if one exists) // active group does not check sub targets like normal groups. // if active group just exits. this.targets = []; - if (activeGroup && !skipGroup && activeGroup === this._searchPossibleTargets([activeGroup], pointer)) { - this._fireOverOutEvents(activeGroup, e); - return activeGroup; - } - // if we hit the corner of an activeObject, let's return that. - if (activeObject && activeObject._findTargetCorner(pointer)) { + + if (aObjects.length > 1 && !skipGroup && activeObject === this._searchPossibleTargets([activeObject], pointer)) { this._fireOverOutEvents(activeObject, e); return activeObject; } - if (activeObject && activeObject === this._searchPossibleTargets([activeObject], pointer)) { + // if we hit the corner of an activeObject, let's return that. + if (aObjects.length === 1 && activeObject._findTargetCorner(pointer)) { + this._fireOverOutEvents(activeObject, e); + return activeObject; + } + if (aObjects.length === 1 && + activeObject === this._searchPossibleTargets([activeObject], pointer)) { if (!this.preserveObjectStacking) { this._fireOverOutEvents(activeObject, e); return activeObject; @@ -1129,7 +1131,6 @@ activeTarget = activeObject; } } - var target = this._searchPossibleTargets(this._objects, pointer); if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) { target = activeTarget; @@ -1374,41 +1375,6 @@ return this.upperCanvasEl; }, - /** - * @private - * @param {Object} object - */ - _setActiveObject: function(object) { - var obj = this._activeObject; - if (obj) { - obj.set('active', false); - if (object !== obj && obj.onDeselect && typeof obj.onDeselect === 'function') { - obj.onDeselect(); - } - } - this._activeObject = object; - object.set('active', true); - }, - - /** - * Sets given object as the only active object on canvas - * @param {fabric.Object} object Object to set as an active one - * @param {Event} [e] Event (passed along when firing "object:selected") - * @return {fabric.Canvas} thisArg - * @chainable - */ - setActiveObject: function (object, e) { - var currentActiveObject = this.getActiveObject(); - if (currentActiveObject && currentActiveObject !== object) { - currentActiveObject.fire('deselected', { e: e }); - } - this._setActiveObject(object); - this.fire('object:selected', { target: object, e: e }); - object.fire('selected', { e: e }); - this.requestRenderAll(); - return this; - }, - /** * Returns currently active object * @return {fabric.Object} active object @@ -1417,13 +1383,30 @@ return this._activeObject; }, + /** + * Returns an array with the current selected objects + * @return {fabric.Object} active object + */ + getActiveObjects: function () { + var active = this._activeObject; + if (active) { + if (active.type === 'activeSelection' && active._objects) { + return active._objects; + } + else { + return [active]; + } + } + return []; + }, + /** * @private * @param {fabric.Object} obj Object that was removed */ _onObjectRemoved: function(obj) { // removing active object should fire "selection:cleared" events - if (this.getActiveObject() === obj) { + if (obj === this._activeObject) { this.fire('before:selection:cleared', { target: obj }); this._discardActiveObject(); this.fire('selection:cleared', { target: obj }); @@ -1435,18 +1418,57 @@ this.callSuper('_onObjectRemoved', obj); }, + /** + * Sets given object as the only active object on canvas + * @param {fabric.Object} object Object to set as an active one + * @param {Event} [e] Event (passed along when firing "object:selected") + * @return {fabric.Canvas} thisArg + * @chainable + */ + setActiveObject: function (object, e) { + var currentActiveObject = this._activeObject; + if (object === currentActiveObject) { + return this; + } + if (this._setActiveObject(object)) { + currentActiveObject && currentActiveObject.fire('deselected', { e: e }); + this.fire('object:selected', { target: object, e: e }); + object.fire('selected', { e: e }); + }; + return this; + }, + + /** + * @private + * @param {Object} object + */ + _setActiveObject: function(object) { + var active = this._activeObject; + if (active === object) { + return false; + } + if (this._discardActiveObject()) { + this._activeObject = object; + object.set('active', true); + return true; + } + return false; + }, + /** * @private */ _discardActiveObject: function() { var obj = this._activeObject; - if (obj) { - obj.set('active', false); - if (obj.onDeselect && typeof obj.onDeselect === 'function') { - obj.onDeselect(); + if (obj && obj.onDeselect && typeof obj.onDeselect === 'function') { + // onDeselect return TRUE to cancel selection; + if (obj.onDeselect()) { + return false; } + obj.set('active', false); + this._activeObject = null; } - this._activeObject = null; + return true; }, /** @@ -1462,121 +1484,14 @@ var activeObject = this._activeObject; if (activeObject) { this.fire('before:selection:cleared', { target: activeObject, e: e }); - this._discardActiveObject(); - this.fire('selection:cleared', { e: e }); - activeObject.fire('deselected', { e: e }); + if (this._discardActiveObject()) { + this.fire('selection:cleared', { e: e }); + activeObject.fire('deselected', { e: e }); + } } return this; }, - /** - * @private - * @param {fabric.Group} group - */ - _setActiveGroup: function(group) { - this._activeGroup = group; - if (group) { - group.set('active', true); - } - }, - - /** - * Sets active group to a specified one. If the function is called by fabric - * as a consequence of a mouse event, the event is passed as a parmater and - * sent to the fire function for the custom events. When used as a method the - * e param does not have any application. - * @param {fabric.Group} group Group to set as a current one - * @param {Event} e Event object - * @return {fabric.Canvas} thisArg - * @chainable - */ - setActiveGroup: function (group, e) { - this._setActiveGroup(group); - if (group) { - this.fire('object:selected', { target: group, e: e }); - group.fire('selected', { e: e }); - } - return this; - }, - - /** - * Returns currently active group - * @return {fabric.Group} Current group - */ - getActiveGroup: function () { - return this._activeGroup; - }, - - /** - * @private - */ - _discardActiveGroup: function() { - var g = this.getActiveGroup(); - if (g) { - g.destroy(); - } - this.setActiveGroup(null); - }, - - /** - * Discards currently active group and fire events If the function is called by fabric - * as a consequence of a mouse event, the event is passed as a parmater and - * sent to the fire function for the custom events. When used as a method the - * e param does not have any application. - * @return {fabric.Canvas} thisArg - * @chainable - */ - discardActiveGroup: function (e) { - var g = this.getActiveGroup(); - if (g) { - this.fire('before:selection:cleared', { e: e, target: g }); - this._discardActiveGroup(); - this.fire('selection:cleared', { e: e }); - } - return this; - }, - - /** - * Deactivates all objects on canvas, removing any active group or object - * @return {fabric.Canvas} thisArg - * @chainable - */ - deactivateAll: function () { - var allObjects = this.getObjects(), - i = 0, - len = allObjects.length, - obj; - for ( ; i < len; i++) { - obj = allObjects[i]; - obj && obj.set('active', false); - } - this._discardActiveGroup(); - this._discardActiveObject(); - return this; - }, - - /** - * Deactivates all objects and dispatches appropriate events If the function is called by fabric - * as a consequence of a mouse event, the event is passed as a parmater and - * sent to the fire function for the custom events. When used as a method the - * e param does not have any application. - * @return {fabric.Canvas} thisArg - * @chainable - */ - deactivateAllWithDispatch: function (e) { - var allObjects = this.getObjects(), - i = 0, - len = allObjects.length, - obj; - for ( ; i < len; i++) { - obj = allObjects[i]; - obj && obj.set('active', false); - } - this.discardActiveGroup(e); - this.discardActiveObject(e); - return this; - }, - /** * Clears a canvas element and removes all event listeners * @return {fabric.Canvas} thisArg @@ -1602,7 +1517,7 @@ * @chainable */ clear: function () { - this.discardActiveGroup(); + // this.discardActiveGroup(); this.discardActiveObject(); this.clearContext(this.contextTop); return this.callSuper('clear'); @@ -1613,27 +1528,10 @@ * @param {CanvasRenderingContext2D} ctx Context to render controls on */ drawControls: function(ctx) { - var activeGroup = this.getActiveGroup(); + var activeObject = this._activeObject; - if (activeGroup) { - activeGroup._renderControls(ctx); - } - else { - this._drawObjectsControls(ctx); - } - }, - - /** - * @private - */ - _drawObjectsControls: function(ctx) { - var object; - for (var i = 0, len = this._objects.length; i < len; ++i) { - object = this._objects[i]; - if (!object || !object.active) { - continue; - } - object._renderControls(ctx); + if (activeObject) { + activeObject._renderControls(ctx); } }, @@ -1659,14 +1557,14 @@ * @returns the original values of instance which were changed */ _realizeGroupTransformOnObject: function(instance) { - var layoutProps = ['angle', 'flipX', 'flipY', 'height', 'left', 'scaleX', 'scaleY', 'top', 'width']; - if (instance.group && instance.group === this.getActiveGroup()) { + if (instance.group && instance.group.type === 'activeSelection' && this._activeObject === instance.group) { + var layoutProps = ['angle', 'flipX', 'flipY', 'left', 'scaleX', 'scaleY', 'skewX', 'skewY', 'top']; //Copy all the positionally relevant properties across now var originalValues = {}; layoutProps.forEach(function(prop) { originalValues[prop] = instance[prop]; }); - this.getActiveGroup().realizeTransform(instance); + this._activeObject.realizeTransform(instance); return originalValues; } else { @@ -1690,10 +1588,9 @@ * @private */ _setSVGObject: function(markup, instance, reviver) { - var originalProperties; //If the object is in a selection group, simulate what would happen to that //object when the group is deselected - originalProperties = this._realizeGroupTransformOnObject(instance); + var originalProperties = this._realizeGroupTransformOnObject(instance); this.callSuper('_setSVGObject', markup, instance, reviver); this._unwindGroupTransformOnObject(instance, originalProperties); }, diff --git a/src/mixins/canvas_events.mixin.js b/src/mixins/canvas_events.mixin.js index 7fc8f465..84bd9746 100644 --- a/src/mixins/canvas_events.mixin.js +++ b/src/mixins/canvas_events.mixin.js @@ -294,7 +294,7 @@ * @param {Object} pointer */ _shouldRender: function(target, pointer) { - var activeObject = this.getActiveGroup() || this.getActiveObject(); + var activeObject = this._activeObject; if (activeObject && activeObject.isEditing && target === activeObject) { // if we mouse up/down over a editing textbox a cursor change, @@ -533,16 +533,14 @@ // save pointer for check in __onMouseUp event var pointer = this.getPointer(e, true); this._previousPointer = pointer; - var shouldRender = this._shouldRender(target, pointer), shouldGroup = this._shouldGroup(e, target); - if (this._shouldClearSelection(e, target)) { - this.deactivateAllWithDispatch(e); + this.discardActiveObject(e); } else if (shouldGroup) { this._handleGrouping(e, target); - target = this.getActiveGroup(); + target = this._activeObject; } if (this.selection && (!target || (!target.selectable && !target.isEditing))) { @@ -559,13 +557,11 @@ this._beforeTransform(e, target); this._setupCurrentTransform(e, target); } - var activeObject = this.getActiveObject(); - if (target !== this.getActiveGroup() && target !== activeObject) { - this.deactivateAll(); - if (target.selectable) { - activeObject && activeObject.fire('deselected', { e: e }); - this.setActiveObject(target, e); - } + if (target.selectable) { + this.setActiveObject(target, e); + } + else { + this.discardActiveObject(); } } this._handleEvent(e, 'down', target ? target : null); @@ -806,10 +802,10 @@ } var hoverCursor = target.hoverCursor || this.hoverCursor, - activeGroup = this.getActiveGroup(), + activeSelection = this._activeObject && this._activeObject.type === 'activeSelection' ? + this._activeObject : null, // only show proper corner when group selection is not active - corner = target._findTargetCorner - && (!activeGroup || !activeGroup.contains(target)) + corner = (!activeSelection || !activeSelection.contains(target)) && target._findTargetCorner(this.getPointer(e, true)); if (!corner) { diff --git a/src/mixins/canvas_grouping.mixin.js b/src/mixins/canvas_grouping.mixin.js index 4eb16286..cc9b0e06 100644 --- a/src/mixins/canvas_grouping.mixin.js +++ b/src/mixins/canvas_grouping.mixin.js @@ -12,10 +12,9 @@ * @return {Boolean} */ _shouldGroup: function(e, target) { - var activeObject = this.getActiveObject(); - return e[this.selectionKey] && target && target.selectable && - (this.getActiveGroup() || (activeObject && activeObject !== target)) - && this.selection; + var activeObject = this._activeObject; + return e[this.selectionKey] && target && target.selectable && this.selection && + (activeObject !== target || activeObject.type === 'activeSelection'); }, /** @@ -24,67 +23,52 @@ * @param {fabric.Object} target */ _handleGrouping: function (e, target) { - var activeGroup = this.getActiveGroup(); - - if (target === activeGroup) { + var activeObject = this._activeObject; + if (activeObject.__corner !== 0) { + return; + } + if (target === activeObject) { // if it's a group, find target again, using activeGroup objects target = this.findTarget(e, true); - // if even object is not found, bail out + // if even object is not found or we are on activeObjectCorner, bail out if (!target) { return; } } - if (activeGroup) { - this._updateActiveGroup(target, e); + if (activeObject && activeObject.type === 'activeSelection') { + this._updateActiveSelection(target, e); } else { - this._createActiveGroup(target, e); + this._createActiveSelection(target, e); } }, /** * @private */ - _updateActiveGroup: function(target, e) { - var activeGroup = this.getActiveGroup(); - - if (activeGroup.contains(target)) { - - activeGroup.removeWithUpdate(target); - target.set('active', false); - - if (activeGroup.size() === 1) { - // remove group alltogether if after removal it only contains 1 object - this.discardActiveGroup(e); + _updateActiveSelection: function(target, e) { + var activeSelection = this._activeObject; + if (activeSelection.contains(target)) { + activeSelection.removeWithUpdate(target); + if (activeSelection.size() === 1) { // activate last remaining object - this.setActiveObject(activeGroup.item(0), e); + this.setActiveObject(activeSelection.item(0), e); return; } } else { - activeGroup.addWithUpdate(target); + activeSelection.addWithUpdate(target); } - this.fire('selection:created', { target: activeGroup, e: e }); - activeGroup.set('active', true); + this.fire('selection:created', { target: activeSelection, e: e }); }, /** * @private */ - _createActiveGroup: function(target, e) { - - if (this._activeObject && target !== this._activeObject) { - - var group = this._createGroup(target); - group.addWithUpdate(); - - this.setActiveGroup(group, e); - this._activeObject = null; - - this.fire('selection:created', { target: group, e: e }); - } - - target.set('active', true); + _createActiveSelection: function(target, e) { + var group = this._createGroup(target); + this.setActiveObject(group, e); + this.fire('selection:created', { target: group, e: e }); }, /** @@ -92,14 +76,13 @@ * @param {Object} target */ _createGroup: function(target) { - var objects = this.getObjects(), isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target), groupObjects = isActiveLower ? [this._activeObject, target] : [target, this._activeObject]; this._activeObject.isEditing && this._activeObject.exitEditing(); - return new fabric.Group(groupObjects, { + return new fabric.ActiveSelection(groupObjects, { canvas: this }); }, @@ -117,11 +100,10 @@ this.setActiveObject(group[0], e); } else if (group.length > 1) { - group = new fabric.Group(group.reverse(), { + group = new fabric.ActiveSelection(group.reverse(), { canvas: this }); - group.addWithUpdate(); - this.setActiveGroup(group, e); + this.setActiveObject(group, e); this.fire('selection:created', { target: group, e: e }); this.requestRenderAll(); } @@ -153,7 +135,6 @@ currentObject.containsPoint(selectionX1Y1) || currentObject.containsPoint(selectionX2Y2) ) { - currentObject.set('active', true); group.push(currentObject); // only add one object if it's a click @@ -173,14 +154,7 @@ if (this.selection && this._groupSelector) { this._groupSelectedObjects(e); } - - var activeGroup = this.getActiveGroup(); - if (activeGroup) { - activeGroup.setObjectsCoords().setCoords(); - activeGroup.isMoving = false; - this.setCursor(this.defaultCursor); - } - + this.setCursor(this.defaultCursor); // clear selection and current transformation this._groupSelector = null; this._currentTransform = null; diff --git a/src/mixins/object_geometry.mixin.js b/src/mixins/object_geometry.mixin.js index 740659b1..0ff70fa8 100644 --- a/src/mixins/object_geometry.mixin.js +++ b/src/mixins/object_geometry.mixin.js @@ -467,25 +467,40 @@ calcTransformMatrix: function(skipGroup) { var center = this.getCenterPoint(), translateMatrix = [1, 0, 0, 1, center.x, center.y], - rotateMatrix = this._calcRotateMatrix(), + rotateMatrix, dimensionMatrix = this._calcDimensionsTransformMatrix(this.skewX, this.skewY, true), - matrix = this.group && !skipGroup ? this.group.calcTransformMatrix() : fabric.iMatrix.concat(); - matrix = multiplyMatrices(matrix, translateMatrix); - matrix = multiplyMatrices(matrix, rotateMatrix); + matrix; + if (this.group && !skipGroup) { + matrix = multiplyMatrices(this.group.calcTransformMatrix(), translateMatrix); + } + else { + matrix = translateMatrix; + } + if (this.angle) { + rotateMatrix = this._calcRotateMatrix(); + matrix = multiplyMatrices(matrix, rotateMatrix); + } matrix = multiplyMatrices(matrix, dimensionMatrix); return matrix; }, _calcDimensionsTransformMatrix: function(skewX, skewY, flipping) { - var skewMatrixX = [1, 0, Math.tan(degreesToRadians(skewX)), 1], - skewMatrixY = [1, Math.tan(degreesToRadians(skewY)), 0, 1], + var skewMatrix, scaleX = this.scaleX * (flipping && this.flipX ? -1 : 1), scaleY = this.scaleY * (flipping && this.flipY ? -1 : 1), - scaleMatrix = [scaleX, 0, 0, scaleY], - m = multiplyMatrices(scaleMatrix, skewMatrixX, true); - return multiplyMatrices(m, skewMatrixY, true); + scaleMatrix = [scaleX, 0, 0, scaleY, 0, 0]; + if (skewX) { + skewMatrix = [1, 0, Math.tan(degreesToRadians(skewX)), 1]; + scaleMatrix = multiplyMatrices(scaleMatrix, skewMatrix, true); + } + if (skewY) { + skewMatrix = [1, Math.tan(degreesToRadians(skewY)), 0, 1]; + scaleMatrix = multiplyMatrices(scaleMatrix, skewMatrix, true); + } + return scaleMatrix; }, + /* * Calculate object dimensions from its properties * @private diff --git a/src/mixins/object_interactivity.mixin.js b/src/mixins/object_interactivity.mixin.js index 14ecfc13..146b252a 100644 --- a/src/mixins/object_interactivity.mixin.js +++ b/src/mixins/object_interactivity.mixin.js @@ -17,7 +17,9 @@ * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found */ _findTargetCorner: function(pointer) { - if (!this.hasControls || !this.active) { + // objects in group, anykind, are not self modificable, + // must not return an hovered corner. + if (!this.hasControls || !this.active || this.group) { return false; } @@ -115,7 +117,7 @@ * @chainable */ drawSelectionBackground: function(ctx) { - if (!this.selectionBackgroundColor || this.group || !this.active || + if (!this.selectionBackgroundColor || !this.active || (this.canvas && !this.canvas.interactive)) { return this; } diff --git a/src/shapes/active_selection.class.js b/src/shapes/active_selection.class.js new file mode 100644 index 00000000..81d3256e --- /dev/null +++ b/src/shapes/active_selection.class.js @@ -0,0 +1,193 @@ +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.ActiveSelection) { + return; + } + + /** + * Group class + * @class fabric.ActiveSelection + * @extends fabric.Group + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups} + * @see {@link fabric.ActiveSelection#initialize} for constructor definition + */ + fabric.ActiveSelection = fabric.util.createClass(fabric.Group, /** @lends fabric.ActiveSelection.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'activeSelection', + + /** + * Constructor + * @param {Object} objects ActiveSelection objects + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(objects, options) { + options = options || { }; + + this._objects = objects || []; + for (var i = this._objects.length; i--; ) { + this._objects[i].group = this; + } + + if (options.originX) { + this.originX = options.originX; + } + if (options.originY) { + this.originY = options.originY; + } + this._calcBounds(); + this._updateObjectsCoords(); + fabric.Object.prototype.initialize.call(this, options); + this.setCoords(); + }, + + /** + * Change te activeSelection to a normal group, + * High level function that automatically adds it to canvas as + * active object. no events fired. + * @since 2.0.0 + * @return {fabric.Group} + */ + toGroup: function() { + var objects = this._objects; + this._objects = []; + var options = this.toObject(); + var newGroup = new fabric.Group([]); + newGroup.set(options); + newGroup.type = 'group'; + objects.forEach(function(object) { + object.group = newGroup; + object.canvas.remove(object); + }); + newGroup._objects = objects; + if (!this.canvas) { + return newGroup; + } + var canvas = this.canvas; + canvas._activeObject = newGroup; + canvas.add(newGroup); + newGroup.setCoords(); + return newGroup; + }, + + /** + * If returns true, deselection is cancelled. + * @since 2.0.0 + * @return {Boolean} [cancel] + */ + onDeselect: function() { + this.destroy(); + return false; + }, + + /** + * Returns string represenation of a group + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * @private + */ + _set: function(key, value) { + var i = this._objects.length; + if (key === 'canvas') { + while (i--) { + this._objects[i].set(key, value); + } + } + if (this.useSetOnGroup) { + while (i--) { + this._objects[i].setOnGroup(key, value); + } + } + fabric.Object.prototype._set.call(this, key, value); + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * objectCaching is a global flag, wins over everything + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * @return {Boolean} + */ + shouldCache: function() { + return false; + }, + + /** + * Check if this object or a child object will cast a shadow + * @return {Boolean} + */ + willDrawShadow: function() { + if (this.shadow) { + return this.callSuper('willDrawShadow'); + } + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].willDrawShadow()) { + return true; + } + } + return false; + }, + + /** + * Check if this group or its parent group are caching, recursively up + * @return {Boolean} + */ + isOnACache: function() { + return false; + }, + + /** + * Renders controls and borders for the object + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} [styleOverride] properties to override the object style + * @param {Object} [childrenOverride] properties to override the children overrides + */ + _renderControls: function(ctx, styleOverride, childrenOverride) { + ctx.save(); + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + this.callSuper('_renderControls', ctx, styleOverride); + childrenOverride = childrenOverride || { }; + if (typeof childrenOverride.hasControls === 'undefined') { + childrenOverride.hasControls = false; + } + if (typeof childrenOverride.hasRotatingPoint === 'undefined') { + childrenOverride.hasRotatingPoint = false; + } + childrenOverride.forActiveSelection = true; + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i]._renderControls(ctx, childrenOverride); + } + ctx.restore(); + }, + }); + + /** + * Returns {@link fabric.ActiveSelection} instance from an object representation + * @static + * @memberOf fabric.ActiveSelection + * @param {Object} object Object to create a group from + * @param {Function} [callback] Callback to invoke when an ActiveSelection instance is created + */ + fabric.ActiveSelection.fromObject = function(object, callback) { + fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { + delete object.objects; + callback && callback(new fabric.ActiveSelection(enlivenedObjects, object, true)); + }); + }; + +})(typeof exports !== 'undefined' ? exports : this); diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index e2a59bfd..86c6e9ac 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -86,17 +86,7 @@ this.originY = options.originY; } - if (isAlreadyGrouped) { - // do not change coordinate of objects enclosed in a group, - // because objects coordinate system have been group coodinate system already. - var object; - for (var i = this._objects.length; i--; ) { - object = this._objects[i]; - object.__origHasControls = object.hasControls; - object.hasControls = false; - } - } - else { + if (!isAlreadyGrouped) { var center = options && options.centerPoint; // if coming from svg i do not want to calc bounds. // i assume width and height are passed along options @@ -126,10 +116,6 @@ * @param {fabric.Point} center, current center of group. */ _updateObjectCoords: function(object, center) { - // do not display corners of objects enclosed in a group - object.__origHasControls = object.hasControls; - object.hasControls = false; - var objectLeft = object.left, objectTop = object.top, ignoreZoom = true, skipAbsolute = true; @@ -138,6 +124,7 @@ left: objectLeft - center.x, top: objectTop - center.y }); + object.group = this; object.setCoords(ignoreZoom, skipAbsolute); }, @@ -163,8 +150,6 @@ object.group = this; object._set('canvas', this.canvas); } - // since _restoreObjectsState set objects inactive - this.forEachObject(this._setObjectActive, this); this._calcBounds(); this._updateObjectsCoords(); this.setCoords(); @@ -172,14 +157,6 @@ return this; }, - /** - * @private - */ - _setObjectActive: function(object) { - object.set('active', true); - object.group = this; - }, - /** * Removes an object from a group; Then recalculates group's dimension, position. * @param {Object} object @@ -189,8 +166,6 @@ removeWithUpdate: function(object) { this._restoreObjectsState(); fabric.util.resetObjectTransform(this); - // since _restoreObjectsState set objects inactive - this.forEachObject(this._setObjectActive, this); this.remove(object); this._calcBounds(); @@ -206,7 +181,6 @@ _onObjectAdded: function(object) { this.dirty = true; object.group = this; - object._set('canvas', this.canvas); }, /** @@ -215,7 +189,6 @@ _onObjectRemoved: function(object) { this.dirty = true; delete object.group; - object.set('active', false); }, /** @@ -223,18 +196,11 @@ */ _set: function(key, value) { var i = this._objects.length; - - if (key === 'canvas') { - while (i--) { - this._objects[i].set(key, value); - } - } if (this.useSetOnGroup) { while (i--) { this._objects[i].setOnGroup(key, value); } } - this.callSuper('_set', key, value); }, @@ -342,7 +308,7 @@ */ drawObject: function(ctx) { for (var i = 0, len = this._objects.length; i < len; i++) { - this._renderObject(this._objects[i], ctx); + this._objects[i].render(ctx); } }, @@ -369,34 +335,6 @@ return false; }, - /** - * Renders controls and borders for the object - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Object} [styleOverride] properties to override the object style - * @param {Object} [childrenOverride] properties to override the children overrides - */ - _renderControls: function(ctx, styleOverride, childrenOverride) { - ctx.save(); - ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; - this.callSuper('_renderControls', ctx, styleOverride); - if (this.canvas && this === this.canvas.getActiveGroup()) { - for (var i = 0, len = this._objects.length; i < len; i++) { - this._objects[i]._renderControls(ctx, childrenOverride); - } - } - ctx.restore(); - }, - - /** - * @private - */ - _renderObject: function(object, ctx) { - var originalHasRotatingPoint = object.hasRotatingPoint; - object.hasRotatingPoint = false; - object.render(ctx); - object.hasRotatingPoint = originalHasRotatingPoint; - }, - /** * Retores original state of each of group objects (original state is that which was before group was created). * @private @@ -440,11 +378,7 @@ _restoreObjectState: function(object) { this.realizeTransform(object); object.setCoords(); - object.hasControls = object.__origHasControls; - delete object.__origHasControls; - object.set('active', false); delete object.group; - return this; }, @@ -457,6 +391,43 @@ return this._restoreObjectsState(); }, + /** + * make a group an active selection, remove the group from canvas + * the group has to be on canvas for this to work. + * @return {fabric.ActiveSelection} thisArg + * @chainable + */ + toActiveSelection: function() { + if (!this.canvas) { + return; + } + var objects = this._objects, canvas = this.canvas; + this._objects = []; + var options = this.toObject(); + var activeSelection = new fabric.ActiveSelection([]); + activeSelection.set(options); + activeSelection.type = 'activeSelection'; + canvas.remove(this); + objects.forEach(function(object) { + object.group = activeSelection; + object.dirty = true; + canvas.add(object); + }); + activeSelection._objects = objects; + canvas._activeObject = activeSelection; + activeSelection.setCoords(); + return activeSelection; + }, + + /** + * Destroys a group (restoring state of its objects) + * @return {fabric.Group} thisArg + * @chainable + */ + ungroupOnCanvas: function() { + return this._restoreObjectsState(); + }, + /** * Sets coordinates of all objects inside group * @return {fabric.Group} thisArg diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index e1586c0d..86efe8b5 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -784,7 +784,6 @@ }; fabric.util.populateWithProperties(this, object, propertiesToInclude); - if (!this.includeDefaultValues) { object = this._removeDefaultValues(object); } @@ -1163,7 +1162,7 @@ if (!this.group) { ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; } - if (this.group && this.group === this.canvas.getActiveGroup()) { + if (styleOverride.forActiveSelection) { ctx.rotate(degreesToRadians(options.angle)); drawBorders && this.drawBordersInGroup(ctx, options, styleOverride); } diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index 0fc4b866..b1f417d4 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -671,14 +671,14 @@ * @chainable true */ setViewportTransform: function (vpt) { - var activeGroup = this._activeGroup, object, ignoreVpt = false, skipAbsolute = true; + var activeObject = this._activeObject, object, ignoreVpt = false, skipAbsolute = true; this.viewportTransform = vpt; for (var i = 0, len = this._objects.length; i < len; i++) { object = this._objects[i]; object.group || object.setCoords(ignoreVpt, skipAbsolute); } - if (activeGroup) { - activeGroup.setCoords(ignoreVpt, skipAbsolute); + if (activeObject && activeObject.type === 'activeSelection') { + activeObject.setCoords(ignoreVpt, skipAbsolute); } this.calcViewportBoundaries(); this.renderOnAddRemove && this.requestRenderAll(); @@ -1376,7 +1376,6 @@ }, /** - * push single object svg representation in the markup * @private */ _setSVGObject: function(markup, instance, reviver) { @@ -1440,10 +1439,10 @@ if (!object) { return this; } - var activeGroup = this._activeGroup, + var activeSelection = this._activeObject, i, obj, objs; - if (object === activeGroup) { - objs = activeGroup._objects; + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; for (i = objs.length; i--;) { obj = objs[i]; removeFromArray(this._objects, obj); @@ -1469,10 +1468,10 @@ if (!object) { return this; } - var activeGroup = this._activeGroup, + var activeSelection = this._activeObject, i, obj, objs; - if (object === activeGroup) { - objs = activeGroup._objects; + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; for (i = 0; i < objs.length; i++) { obj = objs[i]; removeFromArray(this._objects, obj); @@ -1498,11 +1497,11 @@ if (!object) { return this; } - var activeGroup = this._activeGroup, + var activeSelection = this._activeObject, i, obj, idx, newIdx, objs; - if (object === activeGroup) { - objs = activeGroup._objects; + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; for (i = 0; i < objs.length; i++) { obj = objs[i]; idx = this._objects.indexOf(obj); @@ -1566,11 +1565,11 @@ if (!object) { return this; } - var activeGroup = this._activeGroup, + var activeSelection = this._activeObject, i, obj, idx, newIdx, objs; - if (object === activeGroup) { - objs = activeGroup._objects; + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; for (i = objs.length; i--;) { obj = objs[i]; idx = this._objects.indexOf(obj); diff --git a/test.js b/test.js index 9917d1e9..9404a3b9 100644 --- a/test.js +++ b/test.js @@ -12,6 +12,7 @@ testrunner.run({ deps: "./test/fixtures/test_script.js", code: "./dist/fabric.js", tests: [ + './test/unit/activeselection.js', './test/unit/animation.js', './test/unit/rect.js', './test/unit/ellipse.js', diff --git a/test/unit/activeselection.js b/test/unit/activeselection.js new file mode 100644 index 00000000..19e398d5 --- /dev/null +++ b/test/unit/activeselection.js @@ -0,0 +1,235 @@ +(function() { + + var el = fabric.document.createElement('canvas'); + el.width = 600; el.height = 600; + + var canvas = this.canvas = fabric.isLikelyNode ? fabric.createCanvasForNode(600, 600, {enableRetinaScaling: false}) : new fabric.Canvas(el, {enableRetinaScaling: false}); + + function makeAsWith2Objects() { + var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10, strokeWidth: 0 }), + rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40, strokeWidth: 0 }); + + return new fabric.ActiveSelection([rect1, rect2], {strokeWidth: 0}); + } + + function makeAsWith2ObjectsWithOpacity() { + var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10, strokeWidth: 0, opacity: 0.5 }), + rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40, strokeWidth: 0, opacity: 0.8 }); + + return new fabric.ActiveSelection([rect1, rect2], {strokeWidth: 0}); + } + + function makeAsWith4Objects() { + var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10 }), + rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40 }), + rect3 = new fabric.Rect({ top: 40, left: 0, width: 20, height: 40 }), + rect4 = new fabric.Rect({ top: 75, left: 75, width: 40, height: 40 }); + + return new fabric.ActiveSelection([rect1, rect2, rect3, rect4]); + } + + QUnit.module('fabric.ActiveSelection', { + teardown: function() { + canvas.clear(); + canvas.backgroundColor = fabric.Canvas.prototype.backgroundColor; + canvas.calcOffset(); + } + }); + + test('constructor', function() { + var group = makeAsWith2Objects(); + + ok(group); + ok(group instanceof fabric.ActiveSelection, 'should be instance of fabric.ActiveSelection'); + }); + + test('toString', function() { + var group = makeAsWith2Objects(); + equal(group.toString(), '#', 'should return proper representation'); + }); + + test('toObject', function() { + var group = makeAsWith2Objects(); + + ok(typeof group.toObject === 'function'); + + var clone = group.toObject(); + + var expectedObject = { + 'type': 'activeSelection', + 'originX': 'left', + 'originY': 'top', + 'left': 50, + 'top': 100, + 'width': 80, + 'height': 60, + 'fill': 'rgb(0,0,0)', + 'stroke': null, + 'strokeWidth': 0, + 'strokeDashArray': null, + 'strokeLineCap': 'butt', + 'strokeLineJoin': 'miter', + 'strokeMiterLimit': 10, + 'scaleX': 1, + 'scaleY': 1, + 'shadow': null, + 'visible': true, + 'backgroundColor': '', + 'clipTo': null, + 'angle': 0, + 'flipX': false, + 'flipY': false, + 'opacity': 1, + 'fillRule': 'nonzero', + 'globalCompositeOperation': 'source-over', + 'transformMatrix': null, + 'skewX': 0, + 'skewY': 0, + 'objects': clone.objects + }; + + deepEqual(clone, expectedObject); + + ok(group !== clone, 'should produce different object'); + ok(group.getObjects() !== clone.objects, 'should produce different object array'); + ok(group.getObjects()[0] !== clone.objects[0], 'should produce different objects in array'); + }); + + test('toObject without default values', function() { + var group = makeAsWith2Objects(); + group.includeDefaultValues = false; + var clone = group.toObject(); + var objects = [{ + type: 'rect', + left: 10, + top: -30, + width: 30, + height: 10, + strokeWidth: 0 + }, { + type: 'rect', + left: -40, + top: -10, + width: 10, + height: 40, + strokeWidth: 0 + }]; + var expectedObject = { + 'type': 'activeSelection', + 'left': 50, + 'top': 100, + 'width': 80, + 'height': 60, + 'objects': objects + }; + deepEqual(clone, expectedObject); + }); + + test('_renderControls', function() { + ok(typeof fabric.ActiveSelection.prototype._renderControls === 'function'); + }); + + asyncTest('fromObject', function() { + var group = makeAsWith2ObjectsWithOpacity(); + + ok(typeof fabric.ActiveSelection.fromObject === 'function'); + var groupObject = group.toObject(); + + fabric.ActiveSelection.fromObject(groupObject, function(newGroupFromObject) { + + var objectFromOldGroup = group.toObject(); + var objectFromNewGroup = newGroupFromObject.toObject(); + + ok(newGroupFromObject instanceof fabric.ActiveSelection); + + deepEqual(objectFromOldGroup.objects[0], objectFromNewGroup.objects[0]); + deepEqual(objectFromOldGroup.objects[1], objectFromNewGroup.objects[1]); + + // delete `objects` arrays, since `assertHashEqual` fails to compare them for equality + delete objectFromOldGroup.objects; + delete objectFromNewGroup.objects; + + deepEqual(objectFromOldGroup, objectFromNewGroup); + + start(); + }); + }); + + test('get with locked objects', function() { + var group = makeAsWith2Objects(); + + equal(group.get('lockMovementX'), false); + + // TODO acitveGroup + // group.getObjects()[0].lockMovementX = true; + // equal(group.get('lockMovementX'), true); + // + // group.getObjects()[0].lockMovementX = false; + // equal(group.get('lockMovementX'), false); + + group.set('lockMovementX', true); + equal(group.get('lockMovementX'), true); + + // group.set('lockMovementX', false); + // group.getObjects()[0].lockMovementY = true; + // group.getObjects()[1].lockRotation = true; + // + // equal(group.get('lockMovementY'), true); + // equal(group.get('lockRotation'), true); + }); + + test('insertAt', function() { + var rect1 = new fabric.Rect(), + rect2 = new fabric.Rect(), + group = new fabric.Group(); + + group.add(rect1, rect2); + + ok(typeof group.insertAt == 'function', 'should respond to `insertAt` method'); + + group.insertAt(rect1, 1); + equal(group.item(1), rect1); + group.insertAt(rect2, 2); + equal(group.item(2), rect2); + equal(group.insertAt(rect1, 2), group, 'should be chainable'); + }); + + test('group addWithUpdate', function() { + var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + group = new fabric.Group([rect1]); + + var coords = group.oCoords; + group.addWithUpdate(rect2); + var newCoords = group.oCoords; + notEqual(coords, newCoords, 'object coords have been recalculated - add'); + }); + + test('group removeWithUpdate', function() { + var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + group = new fabric.Group([rect1, rect2]); + + var coords = group.oCoords; + group.removeWithUpdate(rect2); + var newCoords = group.oCoords; + notEqual(coords, newCoords, 'object coords have been recalculated - remove'); + }); + + test('ActiveSelection shouldCache', function() { + var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), + rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), + group = new fabric.ActiveSelection([rect1, rect2], { objectCaching: true}); + + equal(group.shouldCache(), false, 'Active selection do not cache'); + }); + + test('canvas property propagation', function() { + var g2 = makeAsWith4Objects(); + + canvas.add(g2); + equal(g2.canvas, canvas); + equal(g2._objects[3].canvas, canvas); + }); + +})(); diff --git a/test/unit/canvas.js b/test/unit/canvas.js index f8d7bac9..da3c7229 100644 --- a/test/unit/canvas.js +++ b/test/unit/canvas.js @@ -105,7 +105,6 @@ }, teardown: function() { canvas.clear(); - canvas.setActiveGroup(null); canvas.backgroundColor = fabric.Canvas.prototype.backgroundColor; canvas.overlayColor = fabric.Canvas.prototype.overlayColor; canvas.calcOffset(); @@ -495,8 +494,8 @@ canvas.add(rect1); canvas.add(rect2); canvas.add(rect3); - var group = new fabric.Group([rect1, rect2]); - canvas.setActiveGroup(group); + var group = new fabric.ActiveSelection([rect1, rect2]); + canvas.setActiveObject(group); target = canvas.findTarget({ clientX: 5, clientY: 5 }); @@ -526,8 +525,8 @@ canvas.preserveObjectStacking = true; canvas.add(rect1); canvas.add(rect2); - var group = new fabric.Group([rect1, rect2]); - canvas.setActiveGroup(group); + var group = new fabric.ActiveSelection([rect1, rect2]); + canvas.setActiveObject(group); target = canvas.findTarget({ clientX: 8, clientY: 8 }); @@ -541,7 +540,7 @@ canvas.preserveObjectStacking = false; }); - test('activeGroup sendToBack', function() { + test('ActiveSelection sendToBack', function() { var rect1 = makeRect(), rect2 = makeRect(), @@ -550,11 +549,11 @@ canvas.add(rect1, rect2, rect3, rect4); - var group = new fabric.Group([rect3, rect4]); - canvas.setActiveGroup(group); + var activeSel = new fabric.ActiveSelection([rect3, rect4]); + canvas.setActiveObject(activeSel); equal(canvas._objects[0], rect1, 'rect1 should be last'); equal(canvas._objects[1], rect2, 'rect2 should be second'); - canvas.sendToBack(group); + canvas.sendToBack(activeSel); equal(canvas._objects[0], rect3, 'rect3 should be the new last'); equal(canvas._objects[1], rect4, 'rect3 should be the new second'); equal(canvas._objects[2], rect1, 'rect1 should be the third object'); @@ -570,11 +569,11 @@ canvas.add(rect1, rect2, rect3, rect4); - var group = new fabric.Group([rect1, rect2]); - canvas.setActiveGroup(group); + var activeSel = new fabric.ActiveSelection([rect1, rect2]); + canvas.setActiveObject(activeSel); equal(canvas._objects[0], rect1, 'rect1 should be last'); equal(canvas._objects[1], rect2, 'rect2 should be second'); - canvas.bringToFront(group); + canvas.bringToFront(activeSel); equal(canvas._objects[0], rect3, 'rect3 should be the new last'); equal(canvas._objects[1], rect4, 'rect3 should be the new second'); equal(canvas._objects[2], rect1, 'rect1 should be the third object'); @@ -590,11 +589,11 @@ canvas.add(rect1, rect2, rect3, rect4); - var group = new fabric.Group([rect1, rect2]); - canvas.setActiveGroup(group); + var activeSel = new fabric.ActiveSelection([rect1, rect2]); + canvas.setActiveObject(activeSel); equal(canvas._objects[0], rect1, 'rect1 should be last'); equal(canvas._objects[1], rect2, 'rect2 should be second'); - canvas.bringForward(group); + canvas.bringForward(activeSel); equal(canvas._objects[0], rect3, 'rect3 should be the new last'); equal(canvas._objects[1], rect1, 'rect1 should be the new second'); equal(canvas._objects[2], rect2, 'rect2 should be the third object'); @@ -609,11 +608,11 @@ canvas.add(rect1, rect2, rect3, rect4); - var group = new fabric.Group([rect3, rect4]); - canvas.setActiveGroup(group); + var activeSel = new fabric.ActiveSelection([rect3, rect4]); + canvas.setActiveObject(activeSel); equal(canvas._objects[0], rect1, 'rect1 should be last'); equal(canvas._objects[1], rect2, 'rect2 should be second'); - canvas.sendBackwards(group); + canvas.sendBackwards(activeSel); equal(canvas._objects[0], rect1, 'rect1 is still last'); equal(canvas._objects[1], rect3, 'rect3 should be shifted down by 1'); equal(canvas._objects[2], rect4, 'rect4 should be shifted down by 1'); @@ -718,7 +717,7 @@ canvas.add(rect, circle); var json = JSON.stringify(canvas); - canvas.setActiveGroup(new fabric.Group([rect, circle])).renderAll(); + canvas.setActiveObject(new fabric.ActiveSelection([rect, circle], { canvas: canvas })); var jsonWithActiveGroup = JSON.stringify(canvas); equal(json, jsonWithActiveGroup); @@ -979,7 +978,7 @@ canvas.add(group); canvas.renderAll(); - canvas.deactivateAll(); + canvas._discardActiveObject(); var json = JSON.stringify( canvas.toDatalessJSON() ); canvas.clear(); canvas.loadFromDatalessJSON(json, function() { @@ -1226,7 +1225,7 @@ test('getActiveObject', function() { ok(typeof canvas.getActiveObject == 'function'); - + equal(canvas.getActiveObject(), null, 'should initially be null'); var rect1 = makeRect(), rect2 = makeRect(); @@ -1239,19 +1238,16 @@ equal(canvas.getActiveObject(), rect2); }); - test('getSetActiveGroup', function() { - ok(typeof canvas.getActiveGroup == 'function'); - ok(typeof canvas.setActiveGroup == 'function'); - - equal(canvas.getActiveGroup(), null, 'should initially be null'); + test('getsetActiveObject', function() { + equal(canvas.getActiveObject(), null, 'should initially be null'); var group = new fabric.Group([ makeRect({ left: 10, top: 10 }), makeRect({ left: 20, top: 20 }) ]); - equal(canvas.setActiveGroup(group), canvas, 'should be chainable'); - equal(canvas.getActiveGroup(), group); + equal(canvas.setActiveObject(group), canvas, 'should be chainable'); + equal(canvas.getActiveObject(), group); }); test('item', function() { @@ -1270,28 +1266,25 @@ equal(canvas.item(0), rect2); }); - test('discardActiveGroup', function() { - ok(typeof canvas.discardActiveGroup == 'function'); - var group = new fabric.Group([makeRect(), makeRect()]); - canvas.setActiveGroup(group); - equal(canvas.discardActiveGroup(), canvas, 'should be chainable'); - equal(canvas.getActiveGroup(), null, 'removing active group sets it to null'); + test('discardActiveObject on ActiveSelection', function() { + var group = new fabric.ActiveSelection([makeRect(), makeRect()]); + canvas.setActiveObject(group); + equal(canvas.discardActiveObject(), canvas, 'should be chainable'); + equal(canvas.getActiveObject(), null, 'removing active group sets it to null'); }); - test('deactivateAll', function() { - ok(typeof canvas.deactivateAll == 'function'); + test('_discardActiveObject', function() { canvas.add(makeRect()); canvas.setActiveObject(canvas.item(0)); - canvas.deactivateAll(); + canvas._discardActiveObject(); ok(!canvas.item(0).active); equal(canvas.getActiveObject(), null); - equal(canvas.getActiveGroup(), null); }); - test('deactivateAllWithDispatch', function() { - ok(typeof canvas.deactivateAllWithDispatch == 'function'); + test('discardActiveObject', function() { + ok(typeof canvas.discardActiveObject == 'function'); canvas.add(makeRect()); canvas.setActiveObject(canvas.item(0)); @@ -1301,7 +1294,7 @@ makeRect({ left: 20, top: 20 }) ]); - canvas.setActiveGroup(group); + canvas.setActiveObject(group); var eventsFired = { selectionCleared: false @@ -1311,10 +1304,10 @@ eventsFired.selectionCleared = true; }); - canvas.deactivateAllWithDispatch(); + canvas.discardActiveObject(); ok(!canvas.item(0).active); equal(canvas.getActiveObject(), null); - equal(canvas.getActiveGroup(), null); + equal(canvas.getActiveObject(), null); for (var prop in eventsFired) { ok(eventsFired[prop]); @@ -1347,7 +1340,7 @@ canvas.add(rect, circle); var svg = canvas.toSVG(); - canvas.setActiveGroup(new fabric.Group([rect, circle])).renderAll(); + canvas.setActiveObject(new fabric.ActiveSelection([rect, circle])); var svgWithActiveGroup = canvas.toSVG(); equal(svg, svgWithActiveGroup); @@ -1363,13 +1356,13 @@ equal(canvas._objects[1], rect2); equal(canvas._objects[2], circle1); equal(canvas._objects[3], circle2); - var aGroup = new fabric.Group([rect2, circle2, rect1, circle1]); + var aGroup = new fabric.ActiveSelection([rect2, circle2, rect1, circle1]); // before rendering objects are ordered in insert order equal(aGroup._objects[0], rect2); equal(aGroup._objects[1], circle2); equal(aGroup._objects[2], rect1); equal(aGroup._objects[3], circle1); - canvas.setActiveGroup(aGroup).renderAll(); + canvas.setActiveObject(aGroup).renderAll(); // after rendering objects are ordered in canvas stack order equal(aGroup._objects[0], rect1); equal(aGroup._objects[1], rect2); diff --git a/test/unit/group.js b/test/unit/group.js index 4676dd42..57271bc8 100644 --- a/test/unit/group.js +++ b/test/unit/group.js @@ -35,7 +35,6 @@ QUnit.module('fabric.Group', { teardown: function() { canvas.clear(); - canvas.setActiveGroup(null); canvas.backgroundColor = fabric.Canvas.prototype.backgroundColor; canvas.calcOffset(); } @@ -484,26 +483,6 @@ equal(group.insertAt(rect1, 2), group, 'should be chainable'); }); - test('canvas property propagation', function() { - var g1 = makeGroupWith4Objects(), - g2 = makeGroupWith4Objects(), - rect1 = new fabric.Rect(), - rect2 = new fabric.Rect(), - group1 = new fabric.Group([g1]); - - group1.add(g2); - group1.insertAt(rect1, 0); - g2.insertAt(rect2, 0); - - canvas.add(group1); - equal(g2.canvas, canvas); - equal(g2._objects[3].canvas, canvas); - equal(g1.canvas, canvas); - equal(g1._objects[3].canvas, canvas); - equal(rect2.canvas, canvas); - equal(rect1.canvas, canvas); - }); - test('dirty flag propagation from children up', function() { var g1 = makeGroupWith4Objects(); var obj = g1.item(0);