diff --git a/Jakefile.js b/Jakefile.js index 4d273152..bb43714b 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -82,6 +82,7 @@ task('build', {async: true}, function () { './src/graph/Popup.js', './src/graph/Groups.js', './src/graph/Images.js', + './src/graph/manipulationMixin.js', './src/graph/SectorsMixin.js', './src/graph/ClusterMixin.js', './src/graph/SelectionMixin.js', diff --git a/examples/graph/20_navigation.html b/examples/graph/20_navigation.html index cc9ed615..5bfaff77 100644 --- a/examples/graph/20_navigation.html +++ b/examples/graph/20_navigation.html @@ -31,7 +31,6 @@ div.table_description { width:100px; } - diff --git a/examples/graph/index.html b/examples/graph/index.html index d48f7c87..53720b5b 100644 --- a/examples/graph/index.html +++ b/examples/graph/index.html @@ -32,6 +32,7 @@

18_fully_random_nodes_clustering.html

19_scale_free_graph_clustering.html

20_navigation.html

+

21_data_manipulation.html

graphviz_gallery.html

diff --git a/package.json b/package.json index 73365fad..943db31a 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "moment": "latest", "hammerjs": "1.0.5", "mousetrap": "latest", + "emitter-component": "latest", "node-watch": "latest" } } diff --git a/src/graph/ClusterMixin.js b/src/graph/ClusterMixin.js index 74436275..4cf7a6e6 100644 --- a/src/graph/ClusterMixin.js +++ b/src/graph/ClusterMixin.js @@ -335,6 +335,10 @@ var ClusterMixin = { // if child node has been added on smaller scale than current, kick out if (childNode.formationScale < this.scale || force == true) { + // remove the selection, first remove the selection from the connected edges + this._unselectConnectedEdges(parentNode); + parentNode.unselect(); + // put the child node back in the global nodes object this.nodes[containedNodeId] = childNode; @@ -383,6 +387,9 @@ var ClusterMixin = { // recalculate the size of the node on the next time the node is rendered parentNode.clearSizeCache(); + + // this unselects the rest of the edges + this._unselectConnectedEdges(parentNode); } // check if a further expansion step is possible if recursivity is enabled diff --git a/src/graph/Edge.js b/src/graph/Edge.js index 574685e8..145dac2b 100644 --- a/src/graph/Edge.js +++ b/src/graph/Edge.js @@ -32,6 +32,7 @@ function Edge (properties, graph, constants) { this.width = constants.edges.width; this.value = undefined; this.length = constants.edges.length; + this.selected = false; this.from = null; // a node this.to = null; // a node @@ -268,7 +269,7 @@ Edge.prototype._drawLine = function(ctx) { * @private */ Edge.prototype._getLineWidth = function() { - if (this.from.selected || this.to.selected) { + if (this.selected == true) { return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv; } else { @@ -617,4 +618,13 @@ Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point */ Edge.prototype.setScale = function(scale) { this.graphScaleInv = 1.0/scale; -}; \ No newline at end of file +}; + + +Edge.prototype.select = function() { + this.selected = true; +} + +Edge.prototype.unselect = function() { + this.selected = false; +} \ No newline at end of file diff --git a/src/graph/Graph.js b/src/graph/Graph.js index 7d81240e..408ab33c 100644 --- a/src/graph/Graph.js +++ b/src/graph/Graph.js @@ -94,6 +94,9 @@ function Graph (container, data, options) { enabled: false, speed: {x: 10, y: 10, zoom: 0.02} }, + dataManipulationToolbar: { + enabled: false + }, minVelocity: 2, // px/s maxIterations: 1000 // maximum number of iteration to stabilize }; @@ -116,15 +119,15 @@ function Graph (container, data, options) { // load the sector system. (mandatory, fully integrated with Graph) this._loadSectorSystem(); - // apply options - this.setOptions(options); - // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it) this._loadClusterSystem(); // load the selection system. (mandatory, required by Graph) this._loadSelectionSystem(); + // apply options + this.setOptions(options); + // other vars var graph = this; this.freezeSimulation = false;// freeze the simulation @@ -135,6 +138,7 @@ function Graph (container, data, options) { this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw. this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw + this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action this.scale = 1; // defining the global scale variable in the constructor @@ -417,6 +421,17 @@ Graph.prototype.setOptions = function (options) { this.constants.keyboard.enabled = false; } + if (options.dataManipulationToolbar) { + this.constants.dataManipulationToolbar.enabled = true; + for (var prop in options.dataManipulationToolbar) { + if (options.dataManipulationToolbar.hasOwnProperty(prop)) { + this.constants.dataManipulationToolbar[prop] = options.dataManipulationToolbar[prop]; + } + } + } + else if (options.dataManipulationToolbar !== undefined) { + this.constants.dataManipulationToolbar.enabled = false; + } // TODO: work out these options and document them if (options.edges) { @@ -478,16 +493,18 @@ Graph.prototype.setOptions = function (options) { } } - this.setSize(this.width, this.height); - this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); - this._setScale(1); - // load the navigation system. this._loadNavigationControls(); + // load the data manipulation system + this._loadManipulationSystem(); + // bind keys. If disabled, this will not do anything; this._createKeyBinds(); + this.setSize(this.width, this.height); + this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); + this._setScale(1); this._redraw(); }; @@ -546,6 +563,7 @@ Graph.prototype._create = function () { this.frame.className = 'graph-frame'; this.frame.style.position = 'relative'; this.frame.style.overflow = 'hidden'; + this.frame.style.zIndex = "1"; // create the graph canvas (HTML canvas element) this.frame.canvas = document.createElement( 'canvas' ); @@ -581,6 +599,7 @@ Graph.prototype._create = function () { // add the frame to the container element this.containerElement.appendChild(this.frame); + }; @@ -616,14 +635,10 @@ Graph.prototype._createKeyBinds = function() { this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown"); this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup"); } - /* - this.mousetrap.bind("=",this.decreaseClusterLevel.bind(me)); - this.mousetrap.bind("-",this.increaseClusterLevel.bind(me)); - this.mousetrap.bind("s",this.singleStep.bind(me)); - this.mousetrap.bind("h",this.updateClustersDefault.bind(me)); - this.mousetrap.bind("c",this._collapseSector.bind(me)); - this.mousetrap.bind("f",this.toggleFreeze.bind(me)); - */ + + if (this.constants.dataManipulationToolbar.enabled == true) { + this.mousetrap.bind("escape",this._createManipulatorBar.bind(me)); + } } /** @@ -670,31 +685,32 @@ Graph.prototype._onDragStart = function () { drag.nodeId = node.id; // select the clicked node if not yet selected if (!node.isSelected()) { - this._selectNode(node,false); + this._selectObject(node,false); } // create an array with the selected nodes and their original location and status - var me = this; - this.selection.forEach(function (id) { - var node = me.nodes[id]; - if (node) { - var s = { - id: id, - node: node, - - // store original x, y, xFixed and yFixed, make the node temporarily Fixed - x: node.x, - y: node.y, - xFixed: node.xFixed, - yFixed: node.yFixed - }; - - node.xFixed = true; - node.yFixed = true; - - drag.selection.push(s); + for (var objectId in this.selectionObj) { + if (this.selectionObj.hasOwnProperty(objectId)) { + var object = this.selectionObj[objectId]; + if (object instanceof Node) { + var s = { + id: object.id, + node: object, + + // store original x, y, xFixed and yFixed, make the node temporarily Fixed + x: object.x, + y: object.y, + xFixed: object.xFixed, + yFixed: object.yFixed + }; + + object.xFixed = true; + object.yFixed = true; + + drag.selection.push(s); + } } - }); + } } }; @@ -771,7 +787,9 @@ Graph.prototype._onDragEnd = function () { */ Graph.prototype._onTap = function (event) { var pointer = this._getPointer(event.gesture.touches[0]); + this.pointerPosition = pointer; this._handleTap(pointer); + }; @@ -792,6 +810,7 @@ Graph.prototype._onDoubleTap = function (event) { */ Graph.prototype._onHold = function (event) { var pointer = this._getPointer(event.gesture.touches[0]); + this.pointerPosition = pointer; this._handleOnHold(pointer); }; @@ -1120,6 +1139,10 @@ Graph.prototype.setSize = function(width, height) { this.frame.canvas.width = this.frame.canvas.clientWidth; this.frame.canvas.height = this.frame.canvas.clientHeight; + if (this.manipulationDiv !== undefined) { + this.manipulationDiv.style.width = this.frame.canvas.clientWidth; + } + if (this.constants.navigation.enabled == true) { this._relocateNavigation(); } @@ -1184,7 +1207,7 @@ Graph.prototype._addNodes = function(ids) { var node = new Node(data, this.images, this.groups, this.constants); this.nodes[id] = node; // note: this may replace an existing node - if (!node.isFixed()) { + if (!node.isFixed() && this.createNodeOnClick != true) { // TODO: position new nodes in a smarter way! var radius = this.constants.edges.length * 2; var count = ids.length; @@ -1200,6 +1223,7 @@ Graph.prototype._addNodes = function(ids) { this._updateNodeIndexList(); this._reconnectEdges(); this._updateValueRange(this.nodes); + this.updateLabels(); }; /** @@ -1468,7 +1492,7 @@ Graph.prototype._redraw = function() { this._doInAllSectors("_drawAllSectorNodes",ctx); this._doInAllSectors("_drawEdges",ctx); - this._doInAllSectors("_drawNodes",ctx); + this._doInAllSectors("_drawNodes",ctx,true); // restore original scaling and translation ctx.restore(); @@ -1722,6 +1746,7 @@ Graph.prototype._calculateForces = function() { // we loop from i over all but the last entree in the array // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j + var a_base = (-2/3); var b = 4/3; for (i = 0; i < this.nodeIndices.length-1; i++) { node1 = nodes[this.nodeIndices[i]]; for (j = i+1; j < this.nodeIndices.length; j++) { @@ -1734,6 +1759,7 @@ Graph.prototype._calculateForces = function() { // clusters have a larger region of influence minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification)); + var a = a_base / minimumDistance; if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 angle = Math.atan2(dy, dx); @@ -1744,13 +1770,13 @@ Graph.prototype._calculateForces = function() { // TODO: correct factor for repulsing force //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force + //repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force + repulsingForce = a * distance + b; // TODO: test the approximation of the function above } // amplify the repulsion for clusters. repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification; repulsingForce *= this.forceFactor; - fx = Math.cos(angle) * repulsingForce; fy = Math.sin(angle) * repulsingForce ; @@ -1963,9 +1989,9 @@ Graph.prototype.start = function() { } }; - - - +/** + * Debug function, does one step of the graph + */ Graph.prototype.singleStep = function() { if (this.moving) { this._initializeForceCalculation(); @@ -2044,7 +2070,6 @@ Graph.prototype._loadSectorSystem = function() { * @private */ Graph.prototype._loadSelectionSystem = function() { - this.selection = []; this.selectionObj = {}; for (var mixinFunction in SelectionMixin) { @@ -2055,6 +2080,37 @@ Graph.prototype._loadSelectionSystem = function() { } + +/** + * Mixin the navigationUI (User Interface) system and initialize the parameters required + * + * @private + */ +Graph.prototype._loadManipulationSystem = function() { + // reset global variables -- these are used by the selection of nodes and edges. + this.blockConnectingEdgeSelection = false; + this.forceAppendSelection = false + + + if (this.constants.dataManipulationToolbar.enabled == true) { + // load the manipulator HTML elements. All styling done in css. + if (this.manipulationDiv === undefined) { + this.manipulationDiv = document.createElement('div'); + this.manipulationDiv.className = 'graph-manipulationDiv'; + this.containerElement.insertBefore(this.manipulationDiv, this.frame); + } + // load the manipulation functions + for (var mixinFunction in manipulationMixin) { + if (manipulationMixin.hasOwnProperty(mixinFunction)) { + Graph.prototype[mixinFunction] = manipulationMixin[mixinFunction]; + } + } + + // create the manipulator toolbar + this._createManipulatorBar(); + } +} + /** * Mixin the navigation (User Interface) system and initialize the parameters required * diff --git a/src/graph/Node.js b/src/graph/Node.js index 513aaeb8..43f34f1e 100644 --- a/src/graph/Node.js +++ b/src/graph/Node.js @@ -188,8 +188,8 @@ Node.prototype.setProperties = function(properties, constants) { } } - this.xFixed = this.xFixed || (properties.x !== undefined); - this.yFixed = this.yFixed || (properties.y !== undefined); + this.xFixed = this.xFixed || (properties.x !== undefined && properties.fixed); + this.yFixed = this.yFixed || (properties.y !== undefined && properties.fixed); this.radiusFixed = this.radiusFixed || (properties.radius !== undefined); if (this.shape == 'image') { diff --git a/src/graph/SelectionMixin.js b/src/graph/SelectionMixin.js index 203ae6ac..7252ac15 100644 --- a/src/graph/SelectionMixin.js +++ b/src/graph/SelectionMixin.js @@ -108,7 +108,7 @@ var SelectionMixin = { _getNodeAt : function (pointer) { // we first check if this is an navigation controls element var positionObject = this._pointerToPositionObject(pointer); - overlappingNodes = this._getAllNodesOverlappingWith(positionObject); + var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); // if there are overlapping nodes, select the last one, this is the // one which is drawn on top of the others @@ -121,6 +121,36 @@ var SelectionMixin = { }, + /** + * retrieve all edges overlapping with given object, selector is around center + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + _getEdgesOverlappingWith : function (object, overlappingEdges) { + var edges = this.edges; + for (var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + if (edges[edgeId].isOverlappingWith(object)) { + overlappingEdges.push(edgeId); + } + } + } + }, + + + /** + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + _getAllEdgesOverlappingWith : function (object) { + var overlappingEdges = []; + this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges); + return overlappingEdges; + }, + /** * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. @@ -130,18 +160,25 @@ var SelectionMixin = { * @private */ _getEdgeAt : function(pointer) { - return null; + var positionObject = this._pointerToPositionObject(pointer); + var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); + + if (overlappingEdges.length > 0) { + return this.edges[overlappingEdges[overlappingEdges.length - 1]]; + } + else { + return null; + } }, /** - * Add object to the selection array. The this.selection id array may not be needed. + * Add object to the selection array. * * @param obj * @private */ _addToSelection : function(obj) { - this.selection.push(obj.id); this.selectionObj[obj.id] = obj; }, @@ -149,16 +186,10 @@ var SelectionMixin = { /** * Remove a single option from selection. * - * @param obj + * @param {Object} obj * @private */ _removeFromSelection : function(obj) { - for (var i = 0; i < this.selection.length; i++) { - if (obj.id == this.selection[i]) { - this.selection.splice(i,1); - break; - } - } delete this.selectionObj[obj.id]; }, @@ -174,10 +205,9 @@ var SelectionMixin = { doNotTrigger = false; } - this.selection = []; - for (var objId in this.selectionObj) { - if (this.selectionObj.hasOwnProperty(objId)) { - this.selectionObj[objId].unselect(); + for (var objectId in this.selectionObj) { + if (this.selectionObj.hasOwnProperty(objectId)) { + this.selectionObj[objectId].unselect(); } } this.selectionObj = {}; @@ -189,6 +219,89 @@ var SelectionMixin = { } }, + /** + * Unselect all clusters. The selectionObj is useful for this. + * + * @param {Boolean} [doNotTrigger] | ignore trigger + * @private + */ + _unselectClusters : function(doNotTrigger) { + if (doNotTrigger === undefined) { + doNotTrigger = false; + } + + for (var objectId in this.selectionObj) { + if (this.selectionObj.hasOwnProperty(objectId)) { + if (this.selectionObj[objectId] instanceof Node) { + if (this.selectionObj[objectId].clusterSize > 1) { + this.selectionObj[objectId].unselect(); + this._removeFromSelection(this.selectionObj[objectId]); + } + } + } + } + + if (doNotTrigger == false) { + this._trigger('select', { + nodes: this.getSelection() + }); + } + }, + + + /** + * return the number of selected nodes + * + * @returns {number} + * @private + */ + _getSelectedNodeCount : function() { + var count = 0; + for (var objectId in this.selectionObj) { + if (this.selectionObj.hasOwnProperty(objectId)) { + if (this.selectionObj[objectId] instanceof Node) { + count += 1; + } + } + } + return count; + }, + + + /** + * return the number of selected edges + * + * @returns {number} + * @private + */ + _getSelectedEdgeCount : function() { + var count = 0; + for (var objectId in this.selectionObj) { + if (this.selectionObj.hasOwnProperty(objectId)) { + if (this.selectionObj[objectId] instanceof Edge) { + count += 1; + } + } + } + return count; + }, + + + /** + * return the number of selected objects. + * + * @returns {number} + * @private + */ + _getSelectedObjectCount : function() { + var count = 0; + for (var objectId in this.selectionObj) { + if (this.selectionObj.hasOwnProperty(objectId)) { + count += 1; + } + } + return count; + }, /** * Check if anything is selected @@ -197,41 +310,93 @@ var SelectionMixin = { * @private */ _selectionIsEmpty : function() { - if (this.selection.length == 0) { - return true; + for(var objectId in this.selectionObj) { + if(this.selectionObj.hasOwnProperty(objectId)) { + return false; + } } - else { - return false; + return true; + }, + + + /** + * check if one of the selected nodes is a cluster. + * + * @returns {boolean} + * @private + */ + _clusterInSelection : function() { + for(var objectId in this.selectionObj) { + if(this.selectionObj.hasOwnProperty(objectId)) { + if (this.selectionObj[objectId] instanceof Node) { + if (this.selectionObj[objectId].clusterSize > 1) { + return true; + } + } + } + } + return false; + }, + + /** + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + _selectConnectedEdges : function(node) { + for (var i = 0; i < node.dynamicEdges.length; i++) { + var edge = node.dynamicEdges[i]; + edge.select(); + this._addToSelection(edge); + } + }, + + + /** + * unselect the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + _unselectConnectedEdges : function(node) { + for (var i = 0; i < node.dynamicEdges.length; i++) { + var edge = node.dynamicEdges[i]; + edge.unselect(); + this._removeFromSelection(edge); } }, + /** * This is called when someone clicks on a node. either select or deselect it. * If there is an existing selection and we don't want to append to it, clear the existing selection * - * @param {Node} node + * @param {Node || Edge} object * @param {Boolean} append * @param {Boolean} [doNotTrigger] | ignore trigger * @private */ - _selectNode : function(node, append, doNotTrigger) { + _selectObject : function(object, append, doNotTrigger) { if (doNotTrigger === undefined) { doNotTrigger = false; } - if (this._selectionIsEmpty() == false && append == false) { + if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) { this._unselectAll(true); } - - if (node.selected == false) { - node.select(); - this._addToSelection(node); + if (object.selected == false) { + object.select(); + this._addToSelection(object); + if (object instanceof Node && this.blockConnectingEdgeSelection == false) { + this._selectConnectedEdges(object); + } } else { - node.unselect(); - this._removeFromSelection(node); + object.unselect(); + this._removeFromSelection(object); } if (doNotTrigger == false) { this._trigger('select', { @@ -251,6 +416,7 @@ var SelectionMixin = { */ _handleTouch : function(pointer) { if (this.constants.navigation.enabled == true) { + this.pointerPosition = pointer; var node = this._getNavigationNodeAt(pointer); if (node != null) { if (this[node.triggerFunction] !== undefined) { @@ -270,10 +436,16 @@ var SelectionMixin = { _handleTap : function(pointer) { var node = this._getNodeAt(pointer); if (node != null) { - this._selectNode(node,false); + this._selectObject(node,false); } else { - this._unselectAll(); + var edge = this._getEdgeAt(pointer); + if (edge != null) { + this._selectObject(edge,false); + } + else { + this._unselectAll(); + } } this._redraw(); }, @@ -305,7 +477,13 @@ var SelectionMixin = { _handleOnHold : function(pointer) { var node = this._getNodeAt(pointer); if (node != null) { - this._selectNode(node,true); + this._selectObject(node,true); + } + else { + var edge = this._getEdgeAt(pointer); + if (edge != null) { + this._selectObject(edge,true); + } } this._redraw(); }, @@ -327,27 +505,54 @@ var SelectionMixin = { /** * - * retrieve the currently selected nodes + * retrieve the currently selected objects * @return {Number[] | String[]} selection An array with the ids of the * selected nodes. */ getSelection : function() { - return this.selection.concat([]); + var nodeIds = this.getSelectedNodes(); + var edgeIds = this.getSelectedEdges(); + return {nodes:nodeIds, edges:edgeIds}; }, /** * - * retrieve the currently selected nodes as objects - * @return {Objects} selection An array with the ids of the + * retrieve the currently selected nodes + * @return {String} selection An array with the ids of the * selected nodes. */ - getSelectionObjects : function() { - return this.selectionObj; + getSelectedNodes : function() { + var idArray = []; + for(var objectId in this.selectionObj) { + if(this.selectionObj.hasOwnProperty(objectId)) { + if (this.selectionObj[objectId] instanceof Node) { + idArray.push(objectId); + } + } + } + return idArray }, /** - * // TODO: rework this function, it is from the old system * + * retrieve the currently selected edges + * @return {Array} selection An array with the ids of the + * selected nodes. + */ + getSelectedEdges : function() { + var idArray = []; + for(var objectId in this.selectionObj) { + if(this.selectionObj.hasOwnProperty(objectId)) { + if (this.selectionObj[objectId] instanceof Edge) { + idArray.push(objectId); + } + } + } + return idArray + }, + + + /** * select zero or more nodes * @param {Number[] | String[]} selection An array with the ids of the * selected nodes. @@ -368,89 +573,34 @@ var SelectionMixin = { if (!node) { throw new RangeError('Node with id "' + id + '" not found'); } - this._selectNode(node,true,true); + this._selectObject(node,true,true); } - this.redraw(); }, /** - * TODO: rework this function, it is from the old system - * * Validate the selection: remove ids of nodes which no longer exist * @private */ _updateSelection : function () { - var i = 0; - while (i < this.selection.length) { - var nodeId = this.selection[i]; - if (!this.nodes.hasOwnProperty(nodeId)) { - this.selection.splice(i, 1); - delete this.selectionObj[nodeId]; - } - else { - i++; - } - } - } - - - /** - * Unselect selected nodes. If no selection array is provided, all nodes - * are unselected - * @param {Object[]} selection Array with selection objects, each selection - * object has a parameter row. Optional - * @param {Boolean} triggerSelect If true (default), the select event - * is triggered when nodes are unselected - * @return {Boolean} changed True if the selection is changed - * @private - */ - /* _unselectNodes : function(selection, triggerSelect) { - var changed = false; - var i, iMax, id; - - if (selection) { - // remove provided selections - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; - if (this.nodes.hasOwnProperty(id)) { - this.nodes[id].unselect(); - } - var j = 0; - while (j < this.selection.length) { - if (this.selection[j] == id) { - this.selection.splice(j, 1); - changed = true; - } - else { - j++; + for(var objectId in this.selectionObj) { + if(this.selectionObj.hasOwnProperty(objectId)) { + if (this.selectionObj[objectId] instanceof Node) { + if (!this.nodes.hasOwnProperty(objectId)) { + delete this.selectionObj[objectId]; } } - } - } - else if (this.selection && this.selection.length) { - // remove all selections - for (i = 0, iMax = this.selection.length; i < iMax; i++) { - id = this.selection[i]; - if (this.nodes.hasOwnProperty(id)) { - this.nodes[id].unselect(); + else { // assuming only edges and nodes are selected + if (!this.edges.hasOwnProperty(objectId)) { + delete this.selectionObj[objectId]; + } } - changed = true; } - this.selection = []; - } - - if (changed && (triggerSelect == true || triggerSelect == undefined)) { - // fire the select event - this._trigger('select', { - nodes: this.getSelection() - }); } + } - return changed; - }, -*/ +} /** * select all nodes on given location x, y * @param {Array} selection an array with node ids @@ -475,40 +625,23 @@ var SelectionMixin = { if (selection[i] != this.selection[i]) { selectionAlreadyThere = false; break; +>>>>>>> develop } } } - if (selectionAlreadyThere) { - return changed; - } - - if (append == undefined || append == false) { - // first deselect any selected node - var triggerSelect = false; - changed = this._unselectNodes(undefined, triggerSelect); - } + } - for (i = 0, iMax = selection.length; i < iMax; i++) { - // add each of the new selections, but only when they are not duplicate - var id = selection[i]; - var isDuplicate = (this.selection.indexOf(id) != -1); - if (!isDuplicate) { - this.nodes[id].select(); - this.selection.push(id); - changed = true; - } - } +<<<<<<< HEAD +======= if (changed) { // fire the select event this._trigger('select', { nodes: this.getSelection() }); } +>>>>>>> develop - return changed; - }, - */ }; diff --git a/src/module/imports.js b/src/module/imports.js index ea8d8c92..bda6f792 100644 --- a/src/module/imports.js +++ b/src/module/imports.js @@ -6,6 +6,7 @@ // If not available there, load via require. var moment = (typeof window !== 'undefined') && window['moment'] || require('moment'); +var Emitter = require('emitter-component'); var Hammer; if (typeof window !== 'undefined') { @@ -28,5 +29,3 @@ else { throw Error('mouseTrap is only available in a browser, not in node.js.'); } } - - diff --git a/src/timeline/Controller.js b/src/timeline/Controller.js index 185d341b..ebb3e494 100644 --- a/src/timeline/Controller.js +++ b/src/timeline/Controller.js @@ -11,6 +11,9 @@ function Controller () { this.reflowTimer = undefined; } +// Extend controller with Emitter mixin +Emitter(Controller.prototype); + /** * Add a component to the controller * @param {Component} component @@ -26,7 +29,7 @@ Controller.prototype.add = function add(component) { } // add the component - component.controller = this; + component.setController(this); this.components[component.id] = component; }; @@ -38,13 +41,17 @@ Controller.prototype.remove = function remove(component) { var id; for (id in this.components) { if (this.components.hasOwnProperty(id)) { - if (id == component || this.components[id] == component) { + if (id == component || this.components[id] === component) { break; } } } if (id) { + // unregister the controller (gives the component the ability to unregister + // event listeners and clean up other stuff) + this.components[id].setController(null); + delete this.components[id]; } }; @@ -54,6 +61,7 @@ Controller.prototype.remove = function remove(component) { * @param {Boolean} [force] If true, an immediate reflow is forced. Default * is false. */ +// TODO: change requestReflow into an event Controller.prototype.requestReflow = function requestReflow(force) { if (force) { this.reflow(); @@ -74,6 +82,7 @@ Controller.prototype.requestReflow = function requestReflow(force) { * @param {Boolean} [force] If true, an immediate repaint is forced. Default * is false. */ +// TODO: change requestReflow into an event Controller.prototype.requestRepaint = function requestRepaint(force) { if (force) { this.repaint(); diff --git a/src/timeline/Range.js b/src/timeline/Range.js index 70c9083d..0bea4d63 100644 --- a/src/timeline/Range.js +++ b/src/timeline/Range.js @@ -48,42 +48,48 @@ function validateDirection (direction) { /** * Add listeners for mouse and touch events to the component - * @param {Component} component + * @param {Controller} controller + * @param {Component} component Should be a rootpanel * @param {String} event Available events: 'move', 'zoom' * @param {String} direction Available directions: 'horizontal', 'vertical' */ -Range.prototype.subscribe = function (component, event, direction) { +Range.prototype.subscribe = function (controller, component, event, direction) { var me = this; if (event == 'move') { // drag start listener - component.on('dragstart', function (event) { + controller.on('dragstart', function (event) { me._onDragStart(event, component); }); // drag listener - component.on('drag', function (event) { + controller.on('drag', function (event) { me._onDrag(event, component, direction); }); // drag end listener - component.on('dragend', function (event) { + controller.on('dragend', function (event) { me._onDragEnd(event, component); }); + + // ignore dragging when holding + controller.on('hold', function (event) { + me._onHold(); + }); } else if (event == 'zoom') { // mouse wheel function mousewheel (event) { me._onMouseWheel(event, component, direction); } - component.on('mousewheel', mousewheel); - component.on('DOMMouseScroll', mousewheel); // For FF + controller.on('mousewheel', mousewheel); + controller.on('DOMMouseScroll', mousewheel); // For FF // pinch - component.on('touch', function (event) { - me._onTouch(); + controller.on('touch', function (event) { + me._onTouch(event); }); - component.on('pinch', function (event) { + controller.on('pinch', function (event) { me._onPinch(event, component, direction); }); } @@ -311,7 +317,7 @@ var touchParams = {}; Range.prototype._onDragStart = function(event, component) { // refuse to drag when we where pinching to prevent the timeline make a jump // when releasing the fingers in opposite order from the touch screen - if (touchParams.pinching) return; + if (touchParams.ignore) return; touchParams.start = this.start; touchParams.end = this.end; @@ -334,7 +340,7 @@ Range.prototype._onDrag = function (event, component, direction) { // refuse to drag when we where pinching to prevent the timeline make a jump // when releasing the fingers in opposite order from the touch screen - if (touchParams.pinching) return; + if (touchParams.ignore) return; var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, interval = (touchParams.end - touchParams.start), @@ -356,7 +362,7 @@ Range.prototype._onDrag = function (event, component, direction) { Range.prototype._onDragEnd = function (event, component) { // refuse to drag when we where pinching to prevent the timeline make a jump // when releasing the fingers in opposite order from the touch screen - if (touchParams.pinching) return; + if (touchParams.ignore) return; if (component.frame) { component.frame.style.cursor = 'auto'; @@ -417,14 +423,29 @@ Range.prototype._onMouseWheel = function(event, component, direction) { }; /** - * On start of a touch gesture, initialize scale to 1 + * Start of a touch gesture * @private */ -Range.prototype._onTouch = function () { +Range.prototype._onTouch = function (event) { touchParams.start = this.start; touchParams.end = this.end; - touchParams.pinching = false; + touchParams.ignore = false; touchParams.center = null; + + // don't move the range when dragging a selected event + // TODO: it's not so neat to have to know about the state of the ItemSet + var item = ItemSet.itemFromTarget(event); + if (item && item.selected) { + touchParams.ignore = true; + } +}; + +/** + * On start of a hold gesture + * @private + */ +Range.prototype._onHold = function () { + touchParams.ignore = true; }; /** @@ -435,7 +456,7 @@ Range.prototype._onTouch = function () { * @private */ Range.prototype._onPinch = function (event, component, direction) { - touchParams.pinching = true; + touchParams.ignore = true; if (event.gesture.touches.length > 1) { if (!touchParams.center) { diff --git a/src/timeline/Stack.js b/src/timeline/Stack.js index 017c98ce..16ac34b7 100644 --- a/src/timeline/Stack.js +++ b/src/timeline/Stack.js @@ -1,11 +1,11 @@ /** * @constructor Stack * Stacks items on top of each other. - * @param {ItemSet} parent + * @param {ItemSet} itemset * @param {Object} [options] */ -function Stack (parent, options) { - this.parent = parent; +function Stack (itemset, options) { + this.itemset = itemset; this.options = options || {}; this.defaultOptions = { @@ -43,14 +43,14 @@ function Stack (parent, options) { /** * Set options for the stack * @param {Object} options Available options: - * {ItemSet} parent + * {ItemSet} itemset * {Number} margin * {function} order Stacking order */ Stack.prototype.setOptions = function setOptions (options) { util.extend(this.options, options); - // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately + // TODO: register on data changes at the connected itemset, and update the changed part only and immediately }; /** @@ -70,9 +70,9 @@ Stack.prototype.update = function update() { * @private */ Stack.prototype._order = function _order () { - var items = this.parent.items; + var items = this.itemset.items; if (!items) { - throw new Error('Cannot stack items: parent does not contain items'); + throw new Error('Cannot stack items: ItemSet does not contain items'); } // TODO: store the sorted items, to have less work later on diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index 38c19fae..664bc20c 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -1,7 +1,7 @@ /** * Create a timeline visualization * @param {HTMLElement} container - * @param {vis.DataSet | Array | DataTable} [items] + * @param {vis.DataSet | Array | google.visualization.DataTable} [items] * @param {Object} [options] See Timeline.setOptions for the available options. * @constructor */ @@ -45,6 +45,13 @@ function Timeline (container, items, options) { this.rootPanel = new RootPanel(container, rootOptions); this.controller.add(this.rootPanel); + // single select (or unselect) when tapping an item + // TODO: implement ctrl+click + this.controller.on('tap', this._onSelectItem.bind(this)); + + // multi select when holding mouse/touch, or on ctrl+click + this.controller.on('hold', this._onMultiSelectItem.bind(this)); + // item panel var itemOptions = Object.create(this.options); itemOptions.left = function () { @@ -84,26 +91,20 @@ function Timeline (container, items, options) { // TODO: reckon with options moveable and zoomable // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable - this.range.subscribe(this.rootPanel, 'move', 'horizontal'); - this.range.subscribe(this.rootPanel, 'zoom', 'horizontal'); + // TODO: enable moving again + this.range.subscribe(this.controller, this.rootPanel, 'move', 'horizontal'); + this.range.subscribe(this.controller, this.rootPanel, 'zoom', 'horizontal'); this.range.on('rangechange', function (properties) { var force = true; me.controller.requestReflow(force); - me._trigger('rangechange', properties); + me.emit('rangechange', properties); }); this.range.on('rangechanged', function (properties) { var force = true; me.controller.requestReflow(force); - me._trigger('rangechanged', properties); + me.emit('rangechanged', properties); }); - // single select (or unselect) when tapping an item - // TODO: implement ctrl+click - this.rootPanel.on('tap', this._onSelectItem.bind(this)); - - // multi select when holding mouse/touch, or on ctrl+click - this.rootPanel.on('hold', this._onMultiSelectItem.bind(this)); - // time axis var timeaxisOptions = Object.create(rootOptions); timeaxisOptions.range = this.range; @@ -140,6 +141,9 @@ function Timeline (container, items, options) { } } +// extend Timeline with the Emitter mixin +Emitter(Timeline.prototype); + /** * Set options * @param {Object} options TODO: describe the available options @@ -173,7 +177,7 @@ Timeline.prototype.getCustomTime = function() { /** * Set items - * @param {vis.DataSet | Array | DataTable | null} items + * @param {vis.DataSet | Array | google.visualization.DataTable | null} items */ Timeline.prototype.setItems = function(items) { var initialLoad = (this.itemsData == null); @@ -234,7 +238,7 @@ Timeline.prototype.setItems = function(items) { /** * Set groups - * @param {vis.DataSet | Array | DataTable} groups + * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ Timeline.prototype.setGroups = function(groups) { var me = this; @@ -367,56 +371,19 @@ Timeline.prototype.getSelection = function getSelection() { return this.content ? this.content.getSelection() : []; }; -/** - * Add event listener - * @param {String} event Event name. Available events: - * 'rangechange', 'rangechanged', 'select' - * @param {function} callback Callback function, invoked as callback(properties) - * where properties is an optional object containing - * event specific properties. - */ -Timeline.prototype.on = function on (event, callback) { - var available = ['rangechange', 'rangechanged', 'select']; - - if (available.indexOf(event) == -1) { - throw new Error('Unknown event "' + event + '". Choose from ' + available.join()); - } - - events.addListener(this, event, callback); -}; - -/** - * Remove an event listener - * @param {String} event Event name - * @param {function} callback Callback function - */ -Timeline.prototype.off = function off (event, callback) { - events.removeListener(this, event, callback); -}; - -/** - * Trigger an event - * @param {String} event Event name, available events: 'rangechange', - * 'rangechanged', 'select' - * @param {Object} [properties] Event specific properties - * @private - */ -Timeline.prototype._trigger = function _trigger(event, properties) { - events.trigger(this, event, properties || {}); -}; - /** * Handle selecting/deselecting an item when tapping it * @param {Event} event * @private */ +// TODO: move this function to ItemSet Timeline.prototype._onSelectItem = function (event) { - var item = this._itemFromTarget(event); + var item = ItemSet.itemFromTarget(event); var selection = item ? [item.id] : []; this.setSelection(selection); - this._trigger('select', { + this.emit('select', { items: this.getSelection() }); @@ -428,9 +395,10 @@ Timeline.prototype._onSelectItem = function (event) { * @param {Event} event * @private */ +// TODO: move this function to ItemSet Timeline.prototype._onMultiSelectItem = function (event) { var selection, - item = this._itemFromTarget(event); + item = ItemSet.itemFromTarget(event); if (!item) { // do nothing... @@ -449,28 +417,9 @@ Timeline.prototype._onMultiSelectItem = function (event) { } this.setSelection(selection); - this._trigger('select', { + this.emit('select', { items: this.getSelection() }); event.stopPropagation(); }; - -/** - * Find an item from an event target: - * searches for the attribute 'timeline-item' in the event target's element tree - * @param {Event} event - * @return {Item | null| item - * @private - */ -Timeline.prototype._itemFromTarget = function _itemFromTarget (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('timeline-item')) { - return target['timeline-item']; - } - target = target.parentNode; - } - - return null; -}; \ No newline at end of file diff --git a/src/timeline/component/Component.js b/src/timeline/component/Component.js index 8d15e0c6..c7c8cd3c 100644 --- a/src/timeline/component/Component.js +++ b/src/timeline/component/Component.js @@ -55,6 +55,23 @@ Component.prototype.getOption = function getOption(name) { return value; }; +/** + * Set controller for this component, or remove current controller by passing + * null as parameter value. + * @param {Controller | null} controller + */ +Component.prototype.setController = function setController (controller) { + this.controller = controller || null; +}; + +/** + * Get controller of this component + * @return {Controller} controller + */ +Component.prototype.getController = function getController () { + return this.controller; +}; + /** * Get the container element of the component, which can be used by a child to * add its own widgets. Not all components do have a container for childs, in diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index 20d7259b..e44d4c87 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -16,6 +16,13 @@ function ItemSet(parent, depends, options) { this.parent = parent; this.depends = depends; + // event listeners + this.eventListeners = { + dragstart: this._onDragStart.bind(this), + drag: this._onDrag.bind(this), + dragend: this._onDragEnd.bind(this) + }; + // one options object is shared by this itemset and all its items this.options = options || {}; this.defaultOptions = { @@ -35,6 +42,7 @@ function ItemSet(parent, depends, options) { this.itemsData = null; // DataSet this.range = null; // Range or Object {start: number, end: number} + // data change listeners this.listeners = { 'add': function (event, params, senderId) { if (senderId != me.id) { @@ -59,6 +67,8 @@ function ItemSet(parent, depends, options) { this.stack = new Stack(this, Object.create(this.options)); this.conversion = null; + this.touchParams = {}; // stores properties while dragging + // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis } @@ -99,6 +109,55 @@ ItemSet.types = { */ ItemSet.prototype.setOptions = Component.prototype.setOptions; + + +/** + * Set controller for this component + * @param {Controller | null} controller + */ +ItemSet.prototype.setController = function setController (controller) { + var event; + + // unregister old event listeners + if (this.controller) { + for (event in this.eventListeners) { + if (this.eventListeners.hasOwnProperty(event)) { + this.controller.off(event, this.eventListeners[event]); + } + } + } + + this.controller = controller || null; + + // register new event listeners + if (this.controller) { + for (event in this.eventListeners) { + if (this.eventListeners.hasOwnProperty(event)) { + this.controller.on(event, this.eventListeners[event]); + } + } + } +}; + +// attach event listeners for dragging items to the controller +(function (me) { + var _controller = null; + var _onDragStart = null; + var _onDrag = null; + var _onDragEnd = null; + + Object.defineProperty(me, 'controller', { + get: function () { + return _controller; + }, + + set: function (controller) { + + } + }); +}) (this); + + /** * Set range (start and end). * @param {Range | Object} range A Range or an object containing start and end. @@ -195,6 +254,7 @@ ItemSet.prototype.repaint = function repaint() { if (!frame) { frame = document.createElement('div'); frame.className = 'itemset'; + frame['timeline-itemset'] = this; var className = options.className; if (className) { @@ -610,3 +670,124 @@ ItemSet.prototype.toScreen = function toScreen(time) { var conversion = this.conversion; return (time.valueOf() - conversion.offset) * conversion.scale; }; + +/** + * Start dragging the selected events + * @param {Event} event + * @private + */ +ItemSet.prototype._onDragStart = function (event) { + var itemSet = ItemSet.itemSetFromTarget(event), + item = ItemSet.itemFromTarget(event), + me = this; + + if (item && item.selected) { + this.touchParams.items = this.getSelection().map(function (id) { + return me.items[id]; + }); + + event.stopPropagation(); + } +}; + +/** + * Drag selected items + * @param {Event} event + * @private + */ +ItemSet.prototype._onDrag = function (event) { + if (this.touchParams.items) { + var deltaX = event.gesture.deltaX; + + // adjust the offset of the items being dragged + this.touchParams.items.forEach(function (item) { + item.setOffset(deltaX); + }); + + // TODO: implement snapping to nice dates + + // TODO: implement dragging from one group to another + + this.requestReflow(); + + event.stopPropagation(); + } +}; + +/** + * End of dragging selected items + * @param {Event} event + * @private + */ +ItemSet.prototype._onDragEnd = function (event) { + if (this.touchParams.items) { + var deltaX = event.gesture.deltaX, + scale = this.conversion.scale; + + // prepare a changeset for the changed items + var changes = this.touchParams.items.map(function (item) { + item.setOffset(0); + + var change = { + id: item.id + }; + + if ('start' in item.data) { + change.start = new Date(item.data.start.valueOf() + deltaX / scale); + } + if ('end' in item.data) { + change.end = new Date(item.data.end.valueOf() + deltaX / scale); + } + + return change; + }); + this.touchParams.items = null; + + // find the root DataSet from our DataSet/DataView + var data = this.itemsData; + while (data instanceof DataView) { + data = data.data; + } + + // apply the changes to the data + data.update(changes); + + event.stopPropagation(); + } +}; + +/** + * Find an item from an event target: + * searches for the attribute 'timeline-item' in the event target's element tree + * @param {Event} event + * @return {Item | null} item + */ +ItemSet.itemFromTarget = function itemFromTarget (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-item')) { + return target['timeline-item']; + } + target = target.parentNode; + } + + return null; +}; + +/** + * Find the ItemSet from an event target: + * searches for the attribute 'timeline-itemset' in the event target's element tree + * @param {Event} event + * @return {ItemSet | null} item + */ +ItemSet.itemSetFromTarget = function itemSetFromTarget (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-itemset')) { + return target['timeline-itemset']; + } + target = target.parentNode; + } + + return null; +}; diff --git a/src/timeline/component/RootPanel.js b/src/timeline/component/RootPanel.js index 6bfd2db2..e4c6166d 100644 --- a/src/timeline/component/RootPanel.js +++ b/src/timeline/component/RootPanel.js @@ -10,12 +10,29 @@ function RootPanel(container, options) { this.id = util.randomUUID(); this.container = container; + // create functions to be used as DOM event listeners + var me = this; + this.hammer = null; + + // create listeners for all interesting events, these events will be emitted + // via the controller + var events = [ + 'touch', 'pinch', 'tap', 'hold', + 'dragstart', 'drag', 'dragend', + 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox + ]; + this.listeners = {}; + events.forEach(function (event) { + me.listeners[event] = function () { + var args = [event].concat(Array.prototype.slice.call(arguments, 0)); + me.controller.emit.apply(me.controller, args); + }; + }); + this.options = options || {}; this.defaultOptions = { autoResize: true }; - - this.listeners = {}; // event listeners } RootPanel.prototype = new Panel(); @@ -48,6 +65,8 @@ RootPanel.prototype.repaint = function () { this.frame = frame; + this._registerListeners(); + changed += 1; } if (!frame.parentNode) { @@ -69,7 +88,6 @@ RootPanel.prototype.repaint = function () { changed += update(frame.style, 'width', asSize(options.width, '100%')); changed += update(frame.style, 'height', asSize(options.height, '100%')); - this._updateEventEmitters(); this._updateWatch(); return (changed > 0); @@ -158,58 +176,51 @@ RootPanel.prototype._unwatch = function () { }; /** - * Event handler - * @param {String} event name of the event, for example 'click', 'mousemove' - * @param {function} callback callback handler, invoked with the raw HTML Event - * as parameter. + * Set controller for this component, or remove current controller by passing + * null as parameter value. + * @param {Controller | null} controller */ -RootPanel.prototype.on = function (event, callback) { - // register the listener at this component - var arr = this.listeners[event]; - if (!arr) { - arr = []; - this.listeners[event] = arr; - } - arr.push(callback); +RootPanel.prototype.setController = function setController (controller) { + this.controller = controller || null; - this._updateEventEmitters(); + if (this.controller) { + this._registerListeners(); + } + else { + this._unregisterListeners(); + } }; /** - * Update the event listeners for all event emitters + * Register event emitters emitted by the rootpanel * @private */ -RootPanel.prototype._updateEventEmitters = function () { - if (this.listeners) { - var me = this; - util.forEach(this.listeners, function (listeners, event) { - if (!me.emitters) { - me.emitters = {}; +RootPanel.prototype._registerListeners = function () { + if (this.frame && this.controller && !this.hammer) { + this.hammer = Hammer(this.frame, { + prevent_default: true + }); + + for (var event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + this.hammer.on(event, this.listeners[event]); } - if (!(event in me.emitters)) { - // create event - var frame = me.frame; - if (frame) { - //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging - var callback = function(event) { - listeners.forEach(function (listener) { - // TODO: filter on event target! - listener(event); - }); - }; - me.emitters[event] = callback; - - if (!me.hammer) { - me.hammer = Hammer(frame, { - prevent_default: true - }); - } - me.hammer.on(event, callback); - } + } + } +}; + +/** + * Unregister event emitters from the rootpanel + * @private + */ +RootPanel.prototype._unregisterListeners = function () { + if (this.hammer) { + for (var event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + this.hammer.off(event, this.listeners[event]); } - }); + } - // TODO: be able to delete event listeners - // TODO: be able to move event listeners to a parent when available + this.hammer = null; } }; diff --git a/src/timeline/component/item/Item.js b/src/timeline/component/item/Item.js index 590ee551..cdb69c76 100644 --- a/src/timeline/component/item/Item.js +++ b/src/timeline/component/item/Item.js @@ -20,6 +20,7 @@ function Item (parent, data, options, defaultOptions) { this.left = 0; this.width = 0; this.height = 0; + this.offset = 0; } /** @@ -72,10 +73,18 @@ Item.prototype.reflow = function reflow() { return false; }; +/** + * Give the item a display offset in pixels + * @param {Number} offset Offset on screen in pixels + */ +Item.prototype.setOffset = function setOffset(offset) { + this.offset = offset; +}; + /** * Return the items width - * @return {Integer} width + * @return {Number} width */ Item.prototype.getWidth = function getWidth() { return this.width; -} +}; diff --git a/src/timeline/component/item/ItemBox.js b/src/timeline/component/item/ItemBox.js index cde86f5a..8ea236c9 100644 --- a/src/timeline/component/item/ItemBox.js +++ b/src/timeline/component/item/ItemBox.js @@ -187,7 +187,7 @@ ItemBox.prototype.reflow = function reflow() { update = util.updateProperty; props = this.props; options = this.options; - start = this.parent.toScreen(this.data.start); + start = this.parent.toScreen(this.data.start) + this.offset; align = options.align || this.defaultOptions.align; margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; orientation = options.orientation || this.defaultOptions.orientation; diff --git a/src/timeline/component/item/ItemPoint.js b/src/timeline/component/item/ItemPoint.js index 1a78a92b..2d5124e1 100644 --- a/src/timeline/component/item/ItemPoint.js +++ b/src/timeline/component/item/ItemPoint.js @@ -157,7 +157,7 @@ ItemPoint.prototype.reflow = function reflow() { options = this.options; orientation = options.orientation || this.defaultOptions.orientation; margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; - start = this.parent.toScreen(this.data.start); + start = this.parent.toScreen(this.data.start) + this.offset; changed += update(this, 'width', dom.point.offsetWidth); changed += update(this, 'height', dom.point.offsetHeight); diff --git a/src/timeline/component/item/ItemRange.js b/src/timeline/component/item/ItemRange.js index a2feec99..bb7b8386 100644 --- a/src/timeline/component/item/ItemRange.js +++ b/src/timeline/component/item/ItemRange.js @@ -157,8 +157,8 @@ ItemRange.prototype.reflow = function reflow() { props = this.props; options = this.options; parent = this.parent; - start = parent.toScreen(this.data.start); - end = parent.toScreen(this.data.end); + start = parent.toScreen(this.data.start) + this.offset; + end = parent.toScreen(this.data.end) + this.offset; update = util.updateProperty; box = dom.box; parentWidth = parent.width; diff --git a/src/util.js b/src/util.js index 9036691e..8495d68c 100644 --- a/src/util.js +++ b/src/util.js @@ -671,3 +671,157 @@ util.option.asElement = function (value, defaultValue) { return value || defaultValue || null; }; + + + +util.GiveDec = function GiveDec(Hex) +{ + if(Hex == "A") + Value = 10; + else + if(Hex == "B") + Value = 11; + else + if(Hex == "C") + Value = 12; + else + if(Hex == "D") + Value = 13; + else + if(Hex == "E") + Value = 14; + else + if(Hex == "F") + Value = 15; + else + Value = eval(Hex) + return Value; +} + +util.GiveHex = function GiveHex(Dec) +{ + if(Dec == 10) + Value = "A"; + else + if(Dec == 11) + Value = "B"; + else + if(Dec == 12) + Value = "C"; + else + if(Dec == 13) + Value = "D"; + else + if(Dec == 14) + Value = "E"; + else + if(Dec == 15) + Value = "F"; + else + Value = "" + Dec; + return Value; +} + +/** + * http://www.yellowpipe.com/yis/tools/hex-to-rgb/color-converter.php + * + * @param {String} hex + * @returns {{r: *, g: *, b: *}} + */ +util.hexToRGB = function hexToRGB(hex) { + hex = hex.replace("#","").toUpperCase(); + + var a = util.GiveDec(hex.substring(0, 1)); + var b = util.GiveDec(hex.substring(1, 2)); + var c = util.GiveDec(hex.substring(2, 3)); + var d = util.GiveDec(hex.substring(3, 4)); + var e = util.GiveDec(hex.substring(4, 5)); + var f = util.GiveDec(hex.substring(5, 6)); + + var r = (a * 16) + b; + var g = (c * 16) + d; + var b = (e * 16) + f; + + return {r:r,g:g,b:b}; +}; + +util.RGBToHex = function RGBToHex(red,green,blue) { + var a = util.GiveHex(Math.floor(red / 16)); + var b = util.GiveHex(red % 16); + var c = util.GiveHex(Math.floor(green / 16)); + var d = util.GiveHex(green % 16); + var e = util.GiveHex(Math.floor(blue / 16)); + var f = util.GiveHex(blue % 16); + + var hex = a + b + c + d + e + f; + return "#" + hex; +}; + + +/** + * http://www.javascripter.net/faq/rgb2hsv.htm + * + * @param red + * @param green + * @param blue + * @returns {*} + * @constructor + */ +util.RGBToHSV = function RGBToHSV (red,green,blue) { + red=red/255; green=green/255; blue=blue/255; + var minRGB = Math.min(red,Math.min(green,blue)); + var maxRGB = Math.max(red,Math.max(green,blue)); + + // Black-gray-white + if (minRGB == maxRGB) { + return {h:0,s:0,v:minRGB}; + } + + // Colors other than black-gray-white: + var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red); + var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5); + var hue = 60*(h - d/(maxRGB - minRGB))/360; + var saturation = (maxRGB - minRGB)/maxRGB; + var value = maxRGB; + return {h:hue,s:saturation,v:value}; +}; + + +/** + * https://gist.github.com/mjijackson/5311256 + * @param hue + * @param saturation + * @param value + * @returns {{r: number, g: number, b: number}} + * @constructor + */ +util.HSVToRGB = function HSVToRGB(h, s, v) { + var r, g, b; + + var i = Math.floor(h * 6); + var f = h * 6 - i; + var p = v * (1 - s); + var q = v * (1 - f * s); + var t = v * (1 - (1 - f) * s); + + switch (i % 6) { + case 0: r = v, g = t, b = p; break; + case 1: r = q, g = v, b = p; break; + case 2: r = p, g = v, b = t; break; + case 3: r = p, g = q, b = v; break; + case 4: r = t, g = p, b = v; break; + case 5: r = v, g = p, b = q; break; + } + + return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) }; +}; + +util.HSVToHex = function HSVToHex(h,s,v) { + var rgb = util.HSVToRGB(h,s,v); + return util.RGBToHex(rgb.r,rgb.g,rgb.b); +} + +util.hexToHSV = function hexToHSV(hex) { + var rgb = util.hexToRGB(hex); + return util.RGBToHSV(rgb.r,rgb.g,rgb.b); +} \ No newline at end of file