From f22cc37df702569f943d5163a92311465481d02f Mon Sep 17 00:00:00 2001 From: zadam Date: Wed, 15 May 2019 21:50:27 +0200 Subject: [PATCH] middle click can now open links in new tab (and close tab) --- libraries/jsplumb.js | 2179 ++++++++++------- libraries/panzoom.js | 78 +- src/public/javascripts/services/link.js | 37 +- .../javascripts/services/note_autocomplete.js | 11 +- .../javascripts/services/note_detail.js | 5 +- .../services/note_detail_relation_map.js | 22 +- src/public/javascripts/services/tab_row.js | 14 + src/public/javascripts/services/tree.js | 16 + src/public/stylesheets/desktop.css | 1 + src/public/stylesheets/relation_map.css | 4 +- 10 files changed, 1411 insertions(+), 956 deletions(-) diff --git a/libraries/jsplumb.js b/libraries/jsplumb.js index 96e9ac90a..0e62651ac 100644 --- a/libraries/jsplumb.js +++ b/libraries/jsplumb.js @@ -147,7 +147,7 @@ var lb = Math.max(0, k - m), ub = Math.min(k, n); for (i = lb; i <= ub; i++) { - j = k - i; + var j = k - i; w[i+j].y += cdTable[j][i] * z[j][i]; } } @@ -202,7 +202,8 @@ b = curve[degree].x - curve[0].x; c = curve[0].x * curve[degree].y - curve[degree].x * curve[0].y; - var max_distance_above = max_distance_below = 0.0; + var max_distance_above, max_distance_below; + max_distance_above = max_distance_below = 0.0; for (var i = 1; i < degree; i++) { var value = a * curve[i].x + b * curve[i].y + c; @@ -308,7 +309,7 @@ }; var _isPoint = function(curve) { - return curve[0].x == curve[1].x && curve[0].y == curve[1].y; + return curve[0].x === curve[1].x && curve[0].y === curve[1].y; }; /** @@ -381,7 +382,7 @@ var p1 = _pointOnPath(curve, location), p2 = _pointOnPath(curve.slice(0, curve.length - 1), location), dy = p2.y - p1.y, dx = p2.x - p1.x; - return dy == 0 ? Infinity : Math.atan(dy / dx); + return dy === 0 ? Infinity : Math.atan(dy / dx); }; /** @@ -410,6 +411,157 @@ return [{x:p.point.x + x, y:p.point.y + y}, {x:p.point.x - x, y:p.point.y - y}]; }; + /** + * Calculates all intersections of the given line with the given curve. + * @param x1 + * @param y1 + * @param x2 + * @param y2 + * @param curve + * @returns {Array} + */ + var _lineIntersection = function(x1, y1, x2, y2, curve) { + var a = y2 - y1, + b = x1 - x2, + c = (x1 * (y1 - y2)) + (y1 * (x2-x1)), + coeffs = _computeCoefficients(curve), + p = [ + (a*coeffs[0][0]) + (b * coeffs[1][0]), + (a*coeffs[0][1])+(b*coeffs[1][1]), + (a*coeffs[0][2])+(b*coeffs[1][2]), + (a*coeffs[0][3])+(b*coeffs[1][3]) + c + ], + r = _cubicRoots.apply(null, p), + intersections = []; + + if (r != null) { + + for (var i = 0; i < 3; i++) { + var t = r[i], + t2 = Math.pow(t, 2), + t3 = Math.pow(t, 3), + x = [ + (coeffs[0][0] * t3) + (coeffs[0][1] * t2) + (coeffs[0][2] * t) + coeffs[0][3], + (coeffs[1][0] * t3) + (coeffs[1][1] * t2) + (coeffs[1][2] * t) + coeffs[1][3] + ]; + + // check bounds of the line + var s; + if ((x2 - x1) !== 0) { + s = (x[0] - x1) / (x2 - x1); + } + else { + s = (x[1] - y1) / (y2 - y1); + } + + if (t >= 0 && t <= 1.0 && s >= 0 && s <= 1.0) { + intersections.push(x); + } + } + } + + return intersections; + }; + + /** + * Calculates all intersections of the given box with the given curve. + * @param x X position of top left corner of box + * @param y Y position of top left corner of box + * @param w width of box + * @param h height of box + * @param curve + * @returns {Array} + */ + var _boxIntersection = function(x, y, w, h, curve) { + var i = []; + i.push.apply(i, _lineIntersection(x, y, x + w, y, curve)); + i.push.apply(i, _lineIntersection(x + w, y, x + w, y + h, curve)); + i.push.apply(i, _lineIntersection(x + w, y + h, x, y + h, curve)); + i.push.apply(i, _lineIntersection(x, y + h, x, y, curve)); + return i; + }; + + /** + * Calculates all intersections of the given bounding box with the given curve. + * @param boundingBox Bounding box, in { x:.., y:..., w:..., h:... } format. + * @param curve + * @returns {Array} + */ + var _boundingBoxIntersection = function(boundingBox, curve) { + var i = []; + i.push.apply(i, _lineIntersection(boundingBox.x, boundingBox.y, boundingBox.x + boundingBox.w, boundingBox.y, curve)); + i.push.apply(i, _lineIntersection(boundingBox.x + boundingBox.w, boundingBox.y, boundingBox.x + boundingBox.w, boundingBox.y + boundingBox.h, curve)); + i.push.apply(i, _lineIntersection(boundingBox.x + boundingBox.w, boundingBox.y + boundingBox.h, boundingBox.x, boundingBox.y + boundingBox.h, curve)); + i.push.apply(i, _lineIntersection(boundingBox.x, boundingBox.y + boundingBox.h, boundingBox.x, boundingBox.y, curve)); + return i; + }; + + + function _computeCoefficientsForAxis(curve, axis) { + return [ + -(curve[0][axis]) + (3*curve[1][axis]) + (-3 * curve[2][axis]) + curve[3][axis], + (3*(curve[0][axis])) - (6*(curve[1][axis])) + (3*(curve[2][axis])), + -3*curve[0][axis] + 3*curve[1][axis], + curve[0][axis] + ]; + } + + function _computeCoefficients(curve) + { + return [ + _computeCoefficientsForAxis(curve, "x"), + _computeCoefficientsForAxis(curve, "y") + ]; + } + + function sgn(x) { + return x < 0 ? -1 : x > 0 ? 1 : 0; + } + + function _cubicRoots(a, b, c, d) { + var A = b / a, + B = c / a, + C = d / a, + Q = (3*B - Math.pow(A, 2))/9, + R = (9*A*B - 27*C - 2*Math.pow(A, 3))/54, + D = Math.pow(Q, 3) + Math.pow(R, 2), + S, + T, + t = []; + + if (D >= 0) // complex or duplicate roots + { + S = sgn(R + Math.sqrt(D))*Math.pow(Math.abs(R + Math.sqrt(D)),(1/3)); + T = sgn(R - Math.sqrt(D))*Math.pow(Math.abs(R - Math.sqrt(D)),(1/3)); + + t[0] = -A/3 + (S + T); + t[1] = -A/3 - (S + T)/2; + t[2] = -A/3 - (S + T)/2; + + /*discard complex roots*/ + if (Math.abs(Math.sqrt(3)*(S - T)/2) !== 0) { + t[1] = -1; + t[2] = -1; + } + } + else // distinct real roots + { + var th = Math.acos(R/Math.sqrt(-Math.pow(Q, 3))); + t[0] = 2*Math.sqrt(-Q)*Math.cos(th/3) - A/3; + t[1] = 2*Math.sqrt(-Q)*Math.cos((th + 2*Math.PI)/3) - A/3; + t[2] = 2*Math.sqrt(-Q)*Math.cos((th + 4*Math.PI)/3) - A/3; + } + + // discard out of spec roots + for (var i = 0; i < 3; i++) { + if (t[i] < 0 || t[i] > 1.0) { + t[i] = -1; + } + } + + return t; + } + var jsBezier = this.jsBezier = { distanceFromCurve : _distanceFromCurve, gradientAtPoint : _gradientAtPoint, @@ -420,6 +572,9 @@ perpendicularToCurveAt : _perpendicularToPathAt, locationAlongCurveFrom:_locationAlongPathFrom, getLength:_length, + lineIntersection:_lineIntersection, + boxIntersection:_boxIntersection, + boundingBoxIntersection:_boundingBoxIntersection, version:"0.9.0" }; @@ -1406,6 +1561,33 @@ } }; + /** + * Finds all elements matching the given selector, for the given parent. In order to support "scoped root" selectors, + * ie. things like "> .someClass", that is .someClass elements that are direct children of `parentElement`, we have to + * jump through a small hoop here: when a delegate draggable is registered, we write a `katavorio-draggable` attribute + * on the element on which the draggable is registered. Then when this method runs, we grab the value of that attribute and + * prepend it as part of the selector we're looking for. So "> .someClass" ends up being written as + * "[katavorio-draggable='...' > .someClass]", which works with querySelectorAll. + * + * @param availableSelectors + * @param parentElement + * @param childElement + * @returns {*} + */ + var findMatchingSelector = function(availableSelectors, parentElement, childElement) { + var el = null; + var draggableId = parentElement.getAttribute("katavorio-draggable"), + prefix = draggableId != null ? "[katavorio-draggable='" + draggableId + "'] " : ""; + + for (var i = 0; i < availableSelectors.length; i++) { + el = findDelegateElement(parentElement, childElement, prefix + availableSelectors[i].selector); + if (el != null) { + return [ availableSelectors[i], el ]; + } + } + return null; + }; + var iev = (function() { var rv = -1; if (navigator.appName === 'Microsoft Internet Explorer') { @@ -1439,6 +1621,7 @@ [ e ]; }, _classes = { + delegatedDraggable:"katavorio-delegated-draggable", // elements that are the delegated drag handler for a bunch of other elements draggable:"katavorio-draggable", // draggable elements droppable:"katavorio-droppable", // droppable elements drag : "katavorio-drag", // elements currently being dragged @@ -1546,8 +1729,25 @@ isConstrained = false, useGhostProxy = params.ghostProxy === true ? TRUE : params.ghostProxy && typeof params.ghostProxy === "function" ? params.ghostProxy : FALSE, ghostProxy = function(el) { return el.cloneNode(true); }, - selector = params.selector, - elementToDrag = null; + elementToDrag = null, + availableSelectors = [], + activeSelectorParams = null, // which, if any, selector config is currently active. + ghostProxyParent = params.ghostProxyParent, + currentParentPosition, + ghostParentPosition, + ghostDx, + ghostDy; + + // if an initial selector was provided, push the entire set of params as a selector config. + if (params.selector) { + var draggableId = el.getAttribute("katavorio-draggable"); + if (draggableId == null) { + draggableId = "" + new Date().getTime(); + el.setAttribute("katavorio-draggable", draggableId); + } + + availableSelectors.push(params); + } var snapThreshold = params.snapThreshold, _snap = function(pos, gridX, gridY, thresholdX, thresholdY) { @@ -1630,6 +1830,10 @@ revertFunction = fn; }; + if (this.params.revert) { + revertFunction = this.params.revert; + } + var _assignId = function(obj) { if (typeof obj === "function") { obj._katavorioId = _uuid(); @@ -1684,15 +1888,36 @@ matchingDroppables = [], intersectingDroppables = []; + this.addSelector = function(params) { + if (params.selector) { + availableSelectors.push(params); + } + }; + this.downListener = function(e) { + if (e.defaultPrevented) { return; } var isNotRightClick = this.rightButtonCanDrag || (e.which !== 3 && e.button !== 2); if (isNotRightClick && this.isEnabled() && this.canDrag()) { var _f = _testFilter(e) && _inputFilter(e, this.el, this.k); if (_f) { - if (selector) { - elementToDrag = findDelegateElement(this.el, e.target || e.srcElement, selector); + activeSelectorParams = null; + elementToDrag = null; + + // if (selector) { + // elementToDrag = findDelegateElement(this.el, e.target || e.srcElement, selector); + // if(elementToDrag == null) { + // return; + // } + // } + if (availableSelectors.length > 0) { + var match = findMatchingSelector(availableSelectors, this.el, e.target || e.srcElement); + if (match != null) { + activeSelectorParams = match[0]; + elementToDrag = match[1]; + } + // elementToDrag = findDelegateElement(this.el, e.target || e.srcElement, selector); if(elementToDrag == null) { return; } @@ -1793,23 +2018,20 @@ k.unmarkPosses(this, e); this.stop(e); - //k.notifySelectionDragStop(this, e); removed in 1.1.0 under the "leave it for one release in case it breaks" rule. - // it isnt necessary to fire this as the normal stop event now includes a `selection` member that has every dragged element. - // firing this event causes consumers who use the `selection` array to process a lot more drag stop events than is necessary - k.notifyPosseDragStop(this, e); moving = false; + intersectingDroppables.length = 0; + if (clone) { dragEl && dragEl.parentNode && dragEl.parentNode.removeChild(dragEl); dragEl = null; + } else { + if (revertFunction && revertFunction(dragEl, this.params.getPosition(dragEl)) === true) { + this.params.setPosition(dragEl, posAtDown); + _dispatch("revert", dragEl); + } } - intersectingDroppables.length = 0; - - if (revertFunction && revertFunction(this.el, this.params.getPosition(this.el)) === true) { - this.params.setPosition(this.el, posAtDown); - _dispatch("revert", this.el); - } } }.bind(this); @@ -1856,7 +2078,9 @@ var _dispatch = function(evt, value) { var result = null; - if (listeners[evt]) { + if (activeSelectorParams && activeSelectorParams[evt]) { + activeSelectorParams[evt](value); + } else if (listeners[evt]) { for (var i = 0; i < listeners[evt].length; i++) { try { var v = listeners[evt][i](value); @@ -1880,7 +2104,7 @@ sel = k.getSelection(), dPos = this.params.getPosition(dragEl); - if (sel.length > 1) { + if (sel.length > 0) { for (var i = 0; i < sel.length; i++) { var p = this.params.getPosition(sel[i].el); positions.push([ sel[i].el, { left: p[0], top: p[1] }, sel[i] ]); @@ -1918,6 +2142,9 @@ } constrainRect = {w: cs[0], h: cs[1]}; + ghostDx = 0; + ghostDy = 0; + if (andNotify) { k.notifySelectionDragStart(this); } @@ -1927,8 +2154,8 @@ _setDroppablesActive(matchingDroppables, false, true, this); if (isConstrained && useGhostProxy(elementToDrag)) { - ghostProxyOffsets = [dragEl.offsetLeft, dragEl.offsetTop]; - elementToDrag.parentNode.removeChild(dragEl); + ghostProxyOffsets = [dragEl.offsetLeft - ghostDx, dragEl.offsetTop - ghostDy]; + dragEl.parentNode.removeChild(dragEl); dragEl = elementToDrag; } else { @@ -1951,25 +2178,54 @@ }; this.moveBy = function(dx, dy, e) { intersectingDroppables.length = 0; + var desiredLoc = this.toGrid([posAtDown[0] + dx, posAtDown[1] + dy]), cPos = constrain(desiredLoc, dragEl, constrainRect, this.size); + // if we should use a ghost proxy... if (useGhostProxy(this.el)) { + // and the element has been dragged outside of its parent bounds if (desiredLoc[0] !== cPos[0] || desiredLoc[1] !== cPos[1]) { + + // ...if ghost proxy not yet created if (!isConstrained) { + // create it var gp = ghostProxy(elementToDrag); params.addClass(gp, _classes.ghostProxy); - elementToDrag.parentNode.appendChild(gp); + + if (ghostProxyParent) { + ghostProxyParent.appendChild(gp); + // find offset between drag el's parent the ghost parent + currentParentPosition = params.getPosition(elementToDrag.parentNode, true); + ghostParentPosition = params.getPosition(params.ghostProxyParent, true); + ghostDx = currentParentPosition[0] - ghostParentPosition[0]; + ghostDy = currentParentPosition[1] - ghostParentPosition[1]; + + } else { + elementToDrag.parentNode.appendChild(gp); + } + + // the ghost proxy is the drag element dragEl = gp; + // set this flag so we dont recreate the ghost proxy isConstrained = true; } + // now the drag position can be the desired position, as the ghost proxy can support it. cPos = desiredLoc; } else { + // if the element is not outside of its parent bounds, and ghost proxy is in place, if (isConstrained) { - elementToDrag.parentNode.removeChild(dragEl); + // remove the ghost proxy from the dom + dragEl.parentNode.removeChild(dragEl); + // reset the drag element to the original element dragEl = elementToDrag; + // clear this flag. isConstrained = false; + currentParentPosition = null; + ghostParentPosition = null; + ghostDx = 0; + ghostDy = 0; } } } @@ -1978,7 +2234,8 @@ pageRect = { x:rect.x + pageDelta[0], y:rect.y + pageDelta[1], w:rect.w, h:rect.h}, focusDropElement = null; - this.params.setPosition(dragEl, cPos); + this.params.setPosition(dragEl, [cPos[0] + ghostDx, cPos[1] + ghostDy]); + for (var i = 0; i < matchingDroppables.length; i++) { var r2 = { x:matchingDroppables[i].pagePosition[0], y:matchingDroppables[i].pagePosition[1], w:matchingDroppables[i].size[0], h:matchingDroppables[i].size[1]}; if (this.params.intersects(pageRect, r2) && (_multipleDrop || focusDropElement == null || focusDropElement === matchingDroppables[i].el) && matchingDroppables[i].canDrop(this)) { @@ -2011,7 +2268,7 @@ // init:register mousedown, and perhaps set a filter this.params.bind(this.el, "mousedown", this.downListener); - // if handle provded, use that. otherwise, try to set a filter. + // if handle provided, use that. otherwise, try to set a filter. // note that a `handle` selector always results in filterExclude being set to false, ie. // the selector defines the handle element(s). if (this.params.handle) @@ -2210,7 +2467,7 @@ _el._katavorioDrag = new Drag(_el, p, _css, _scope); _reg(_el._katavorioDrag, this._dragsByScope); o.push(_el._katavorioDrag); - katavorioParams.addClass(_el, _css.draggable); + katavorioParams.addClass(_el, p.selector ? _css.delegatedDraggable : _css.draggable); } else { _mistletoe(_el._katavorioDrag, params); @@ -3308,9 +3565,9 @@ return "" + (new Date()).getTime(); }, - // helper method to update the hover style whenever it, or paintStyle, changes. - // we use paintStyle as the foundation and merge hoverPaintStyle over the - // top. + // helper method to update the hover style whenever it, or paintStyle, changes. + // we use paintStyle as the foundation and merge hoverPaintStyle over the + // top. _updateHoverStyle = function (component) { if (component._jsPlumb.paintStyle && component._jsPlumb.hoverPaintStyle) { var mergedHoverStyle = {}; @@ -3749,7 +4006,7 @@ var jsPlumbInstance = root.jsPlumbInstance = function (_defaults) { - this.version = "2.8.4"; + this.version = "2.9.3"; this.Defaults = { Anchor: "Bottom", @@ -3940,11 +4197,11 @@ var log = null, initialized = false, - // TODO remove from window scope + // TODO remove from window scope connections = [], - // map of element id -> endpoint lists. an element can have an arbitrary - // number of endpoints on it, and not all of them have to be connected - // to anything. + // map of element id -> endpoint lists. an element can have an arbitrary + // number of endpoints on it, and not all of them have to be connected + // to anything. endpointsByElement = {}, endpointsByUUID = {}, managedElements = {}, @@ -3961,14 +4218,14 @@ return "" + _curIdStamp++; }, - // - // appends an element to some other element, which is calculated as follows: - // - // 1. if Container exists, use that element. - // 2. if the 'parent' parameter exists, use that. - // 3. otherwise just use the root element. - // - // + // + // appends an element to some other element, which is calculated as follows: + // + // 1. if Container exists, use that element. + // 2. if the 'parent' parameter exists, use that. + // 3. otherwise just use the root element. + // + // _appendElement = function (el, parent) { if (_container) { _container.appendChild(el); @@ -3981,15 +4238,15 @@ } }.bind(this), - // - // Draws an endpoint and its connections. this is the main entry point into drawing connections as well - // as endpoints, since jsPlumb is endpoint-centric under the hood. - // - // @param element element to draw (of type library specific element object) - // @param ui UI object from current library's event system. optional. - // @param timestamp timestamp for this paint cycle. used to speed things up a little by cutting down the amount of offset calculations we do. - // @param clearEdits defaults to false; indicates that mouse edits for connectors should be cleared - /// + // + // Draws an endpoint and its connections. this is the main entry point into drawing connections as well + // as endpoints, since jsPlumb is endpoint-centric under the hood. + // + // @param element element to draw (of type library specific element object) + // @param ui UI object from current library's event system. optional. + // @param timestamp timestamp for this paint cycle. used to speed things up a little by cutting down the amount of offset calculations we do. + // @param clearEdits defaults to false; indicates that mouse edits for connectors should be cleared + /// _draw = function (element, ui, timestamp, clearEdits) { if (!_suspendDrawing) { @@ -4032,9 +4289,9 @@ } }, - // - // gets an Endpoint by uuid. - // + // + // gets an Endpoint by uuid. + // _getEndpoint = function (uuid) { return endpointsByUUID[uuid]; }, @@ -4044,89 +4301,7 @@ * TODO: somehow abstract this to the adapter, because the concept of "draggable" has no * place on the server. */ - _initDraggableIfNecessary = function (element, isDraggable, dragOptions, id, fireEvent) { - // move to DragManager? - if (!jsPlumb.headless) { - var _draggable = isDraggable == null ? false : isDraggable; - if (_draggable) { - if (jsPlumb.isDragSupported(element, _currentInstance)) { - var options = dragOptions || _currentInstance.Defaults.DragOptions; - options = jsPlumb.extend({}, options); // make a copy. - if (!jsPlumb.isAlreadyDraggable(element, _currentInstance)) { - var dragEvent = jsPlumb.dragEvents.drag, - stopEvent = jsPlumb.dragEvents.stop, - startEvent = jsPlumb.dragEvents.start, - _started = false; - _manage(id, element); - - options[startEvent] = _ju.wrap(options[startEvent], function () { - _currentInstance.setHoverSuspended(true); - _currentInstance.select({source: element}).addClass(_currentInstance.elementDraggingClass + " " + _currentInstance.sourceElementDraggingClass, true); - _currentInstance.select({target: element}).addClass(_currentInstance.elementDraggingClass + " " + _currentInstance.targetElementDraggingClass, true); - _currentInstance.setConnectionBeingDragged(true); - if (options.canDrag) { - return dragOptions.canDrag(); - } - }, false); - - options[dragEvent] = _ju.wrap(options[dragEvent], function () { - var ui = _currentInstance.getUIPosition(arguments, _currentInstance.getZoom()); - if (ui != null) { - _draw(element, ui, null, true); - if (_started) { - _currentInstance.addClass(element, "jtk-dragged"); - } - _started = true; - } - }); - options[stopEvent] = _ju.wrap(options[stopEvent], function () { - var elements = arguments[0].selection, uip; - - var _one = function (_e) { - if (_e[1] != null) { - // run the reported offset through the code that takes parent containers - // into account, to adjust if necessary (issue 554) - uip = _currentInstance.getUIPosition([{ - el:_e[2].el, - pos:[_e[1].left, _e[1].top] - }]); - _draw(_e[2].el, uip); - } - _currentInstance.removeClass(_e[0], "jtk-dragged"); - _currentInstance.select({source: _e[2].el}).removeClass(_currentInstance.elementDraggingClass + " " + _currentInstance.sourceElementDraggingClass, true); - _currentInstance.select({target: _e[2].el}).removeClass(_currentInstance.elementDraggingClass + " " + _currentInstance.targetElementDraggingClass, true); - _currentInstance.getDragManager().dragEnded(_e[2].el); - }; - - for (var i = 0; i < elements.length; i++) { - _one(elements[i]); - } - - _started = false; - _currentInstance.setHoverSuspended(false); - _currentInstance.setConnectionBeingDragged(false); - }); - var elId = _getId(element); // need ID - draggableStates[elId] = true; - var draggable = draggableStates[elId]; - options.disabled = draggable == null ? false : !draggable; - _currentInstance.initDraggable(element, options); - _currentInstance.getDragManager().register(element); - if (fireEvent) { - _currentInstance.fire("elementDraggable", {el:element, options:options}); - } - } - else { - // already draggable. attach any start, drag or stop listeners to the current Drag. - if (dragOptions.force) { - _currentInstance.initDraggable(element, options); - } - } - } - } - } - }, _scopeMatch = function (e1, e2) { var s1 = e1.scope.split(/\s/), s2 = e2.scope.split(/\s/); @@ -4151,9 +4326,9 @@ return m; }, - /* - * prepares a final params object that can be passed to _newConnection, taking into account defaults, events, etc. - */ + /* + * prepares a final params object that can be passed to _newConnection, taking into account defaults, events, etc. + */ _prepareConnectionParams = function (params, referenceParams) { var _p = jsPlumb.extend({ }, params); if (referenceParams) { @@ -4314,9 +4489,9 @@ return con; }, - // - // adds the connection to the backing model, fires an event if necessary and then redraws - // + // + // adds the connection to the backing model, fires an event if necessary and then redraws + // _finaliseConnection = _currentInstance.finaliseConnection = function (jpc, params, originalEvent, doInformAnchorManager) { params = params || {}; // add to list of connections (by scope). @@ -4354,10 +4529,10 @@ } }, - /* - factory method to prepare a new endpoint. this should always be used instead of creating Endpoints - manually, since this method attaches event listeners and an id. - */ + /* + factory method to prepare a new endpoint. this should always be used instead of creating Endpoints + manually, since this method attaches event listeners and an id. + */ _newEndpoint = function (params, id) { var endpointFunc = _currentInstance.Defaults.EndpointType || jsPlumb.Endpoint; var _p = jsPlumb.extend({}, params); @@ -4379,13 +4554,13 @@ return ep; }, - /* - * performs the given function operation on all the connections found - * for the given element id; this means we find all the endpoints for - * the given element, and then for each endpoint find the connectors - * connected to it. then we pass each connection in to the given - * function. - */ + /* + * performs the given function operation on all the connections found + * for the given element id; this means we find all the endpoints for + * the given element, and then for each endpoint find the connectors + * connected to it. then we pass each connection in to the given + * function. + */ _operation = function (elId, func, endpointFunc) { var endpoints = endpointsByElement[elId]; if (endpoints && endpoints.length) { @@ -4413,16 +4588,16 @@ } }); }, - /* - * private method to do the business of hiding/showing. - * - * @param el - * either Id of the element in question or a library specific - * object for the element. - * @param state - * String specifying a value for the css 'display' property - * ('block' or 'none'). - */ + /* + * private method to do the business of hiding/showing. + * + * @param el + * either Id of the element in question or a library specific + * object for the element. + * @param state + * String specifying a value for the css 'display' property + * ('block' or 'none'). + */ _setVisible = function (el, state, alsoChangeEndpoints) { state = state === "block"; var endpointFunc = null; @@ -4446,22 +4621,6 @@ } }, endpointFunc); }, - /* - * toggles the draggable state of the given element(s). - * el is either an id, or an element object, or a list of ids/element objects. - */ - _toggleDraggable = function (el) { - var state; - jsPlumb.each(el, function (el) { - var elId = _currentInstance.getAttribute(el, "id"); - state = draggableStates[elId] == null ? false : draggableStates[elId]; - state = !state; - draggableStates[elId] = state; - _currentInstance.setDraggable(el, state); - return state; - }.bind(this)); - return state; - }, /** * private method to do the business of toggling hiding/showing. */ @@ -4479,7 +4638,7 @@ }, endpointFunc); }, - // TODO comparison performance + // TODO comparison performance _getCachedData = function (elId) { var o = offsets[elId]; if (!o) { @@ -4864,11 +5023,11 @@ params = params || {}; if (params.force || _ju.functionChain(true, false, [ - [ connection.endpoints[0], IS_DETACH_ALLOWED, [ connection ] ], - [ connection.endpoints[1], IS_DETACH_ALLOWED, [ connection ] ], - [ connection, IS_DETACH_ALLOWED, [ connection ] ], - [ _currentInstance, CHECK_CONDITION, [ BEFORE_DETACH, connection ] ] - ])) { + [ connection.endpoints[0], IS_DETACH_ALLOWED, [ connection ] ], + [ connection.endpoints[1], IS_DETACH_ALLOWED, [ connection ] ], + [ connection, IS_DETACH_ALLOWED, [ connection ] ], + [ _currentInstance, CHECK_CONDITION, [ BEFORE_DETACH, connection ] ] + ])) { connection.setHover(false); fireDetachEvent(connection, !connection.pending && params.fireEvent !== false, params.originalEvent); @@ -5003,29 +5162,6 @@ return result; }; - this.draggable = function (el, options) { - var info; - _each(function(_el) { - info = _info(_el); - if (info.el) { - _initDraggableIfNecessary(info.el, true, options, info.id, true); - } - }, el); - return _currentInstance; - }; - - this.droppable = function(el, options) { - var info; - options = options || {}; - options.allowLoopback = false; - _each(function(_el) { - info = _info(_el); - if (info.el) { - _currentInstance.initDroppable(info.el, options); - } - }, el); - return _currentInstance; - }; // helpers for select/selectEndpoints var _setOperation = function (list, func, args, selector) { @@ -5279,6 +5415,8 @@ * a static call. i just don't want to expose it to the public API). */ this.getId = _getId; + this.draw = _draw; + this.info = _info; this.appendElement = _appendElement; @@ -5299,11 +5437,6 @@ // exposed for other objects to use to get a unique id. this.idstamp = _idstamp; - // this.connectorsInitialized = false; - // this.registerConnectorType = function (connector, name) { - // connectorTypes.push([connector, name]); - // }; - // ensure that, if the current container exists, it is a DOM element and not a selector. // if it does not exist and `candidate` is supplied, the offset parent of that element will be set as the Container. // this is used to do a better default behaviour for the case that the user has not set a container: @@ -5336,6 +5469,7 @@ }; managedElements[id].info = _updateOffset({ elId: id, timestamp: _suspendedAt }); + _currentInstance.addClass(element, "jtk-managed"); if (!_transient) { _currentInstance.fire("manageElement", { id:id, info:managedElements[id].info, el:element }); } @@ -5344,8 +5478,9 @@ return managedElements[id]; }; - var _unmanage = function(id) { + var _unmanage = _currentInstance.unmanage = function(id) { if (managedElements[id]) { + _currentInstance.removeClass(managedElements[id].el, "jtk-managed"); delete managedElements[id]; _currentInstance.fire("unmanageElement", id); } @@ -5403,7 +5538,7 @@ this.updateOffset = _updateOffset; - /** + /** * callback from the current library to tell us to prepare ourselves (attach * mouse listeners etc; can't do that until the library has provided a bind method) */ @@ -5520,7 +5655,7 @@ return new root.jsPlumb.DynamicAnchor({anchors: anchors, selector: anchorSelector, elementId: null, jsPlumbInstance: _currentInstance}); }; -// --------------------- makeSource/makeTarget ---------------------------------------------- +// --------------------- makeSource/makeTarget ---------------------------------------------- this.targetEndpointDefinitions = {}; this.sourceEndpointDefinitions = {}; @@ -6335,9 +6470,6 @@ return _currentInstance; }; - // sets whether or not some element should be currently draggable. - this.setDraggable = _setDraggable; - this.deriveEndpointAndAnchorSpec = function(type, dontPrependDefault) { var bits = ((dontPrependDefault ? "" : "default ") + type).split(/[\s]/), eps = null, ep = null, a = null, as = null; for (var i = 0; i < bits.length; i++) { @@ -6475,7 +6607,6 @@ // TODO: update this method to return the current state. this.toggleVisible = _toggleVisible; - this.toggleDraggable = _toggleDraggable; this.addListener = this.bind; var floatingConnections = []; @@ -6583,7 +6714,7 @@ // --------------------- static instance + module registration ------------------------------------------- -// create static instance and assign to window if window exists. +// create static instance and assign to window if window exists. var jsPlumb = new jsPlumbInstance(); // register on 'root' (lets us run on server or browser) root.jsPlumb = jsPlumb; @@ -6620,608 +6751,10 @@ exports.jsPlumb = jsPlumb; } -// --------------------- end static instance + AMD registration ------------------------------------------- +// --------------------- end static instance + AMD registration ------------------------------------------- }).call(typeof window !== 'undefined' ? window : this); -/* - * This file contains the base functionality for DOM type adapters. - * - * Copyright (c) 2010 - 2018 jsPlumb (hello@jsplumbtoolkit.com) - * - * https://jsplumbtoolkit.com - * https://github.com/jsplumb/jsplumb - * - * Dual licensed under the MIT and GPL2 licenses. - */ -; -(function () { - - var root = this, _ju = root.jsPlumbUtil; - - var _genLoc = function (prefix, e) { - if (e == null) { - return [ 0, 0 ]; - } - var ts = _touches(e), t = _getTouch(ts, 0); - return [t[prefix + "X"], t[prefix + "Y"]]; - }, - _pageLocation = _genLoc.bind(this, "page"), - _screenLocation = _genLoc.bind(this, "screen"), - _clientLocation = _genLoc.bind(this, "client"), - _getTouch = function (touches, idx) { - return touches.item ? touches.item(idx) : touches[idx]; - }, - _touches = function (e) { - return e.touches && e.touches.length > 0 ? e.touches : - e.changedTouches && e.changedTouches.length > 0 ? e.changedTouches : - e.targetTouches && e.targetTouches.length > 0 ? e.targetTouches : - [ e ]; - }; - - /** - Manages dragging for some instance of jsPlumb. - - TODO instead of this being accessed directly, it should subscribe to events on the jsPlumb instance: every method - in here is called directly by jsPlumb. But what should happen is that we have unpublished events that this listens - to. The only trick is getting one of these instantiated with every jsPlumb instance: it needs to have a hook somehow. - Basically the general idea is to pull ALL the drag code out (prototype method registrations plus this) into a - dedicated drag script), that does not necessarily need to be included. - - - */ - var DragManager = function (_currentInstance) { - var _draggables = {}, _dlist = [], _delements = {}, _elementsWithEndpoints = {}, - // elementids mapped to the draggable to which they belong. - _draggablesForElements = {}; - - /** - register some element as draggable. right now the drag init stuff is done elsewhere, and it is - possible that will continue to be the case. - */ - this.register = function (el) { - var id = _currentInstance.getId(el), - parentOffset; - - if (!_draggables[id]) { - _draggables[id] = el; - _dlist.push(el); - _delements[id] = {}; - } - - // look for child elements that have endpoints and register them against this draggable. - var _oneLevel = function (p) { - if (p) { - for (var i = 0; i < p.childNodes.length; i++) { - if (p.childNodes[i].nodeType !== 3 && p.childNodes[i].nodeType !== 8) { - var cEl = jsPlumb.getElement(p.childNodes[i]), - cid = _currentInstance.getId(p.childNodes[i], null, true); - if (cid && _elementsWithEndpoints[cid] && _elementsWithEndpoints[cid] > 0) { - if (!parentOffset) { - parentOffset = _currentInstance.getOffset(el); - } - var cOff = _currentInstance.getOffset(cEl); - _delements[id][cid] = { - id: cid, - offset: { - left: cOff.left - parentOffset.left, - top: cOff.top - parentOffset.top - } - }; - _draggablesForElements[cid] = id; - } - _oneLevel(p.childNodes[i]); - } - } - } - }; - - _oneLevel(el); - }; - - // refresh the offsets for child elements of this element. - this.updateOffsets = function (elId, childOffsetOverrides) { - if (elId != null) { - childOffsetOverrides = childOffsetOverrides || {}; - var domEl = jsPlumb.getElement(elId), - id = _currentInstance.getId(domEl), - children = _delements[id], - parentOffset; - - if (children) { - for (var i in children) { - if (children.hasOwnProperty(i)) { - var cel = jsPlumb.getElement(i), - cOff = childOffsetOverrides[i] || _currentInstance.getOffset(cel); - - // do not update if we have a value already and we'd just be writing 0,0 - if (cel.offsetParent == null && _delements[id][i] != null) { - continue; - } - - if (!parentOffset) { - parentOffset = _currentInstance.getOffset(domEl); - } - - _delements[id][i] = { - id: i, - offset: { - left: cOff.left - parentOffset.left, - top: cOff.top - parentOffset.top - } - }; - _draggablesForElements[i] = id; - } - } - } - } - }; - - /** - notification that an endpoint was added to the given el. we go up from that el's parent - node, looking for a parent that has been registered as a draggable. if we find one, we add this - el to that parent's list of elements to update on drag (if it is not there already) - */ - this.endpointAdded = function (el, id) { - - id = id || _currentInstance.getId(el); - - var b = document.body, - p = el.parentNode; - - _elementsWithEndpoints[id] = _elementsWithEndpoints[id] ? _elementsWithEndpoints[id] + 1 : 1; - - while (p != null && p !== b) { - var pid = _currentInstance.getId(p, null, true); - if (pid && _draggables[pid]) { - var pLoc = _currentInstance.getOffset(p); - - if (_delements[pid][id] == null) { - var cLoc = _currentInstance.getOffset(el); - _delements[pid][id] = { - id: id, - offset: { - left: cLoc.left - pLoc.left, - top: cLoc.top - pLoc.top - } - }; - _draggablesForElements[id] = pid; - } - break; - } - p = p.parentNode; - } - }; - - this.endpointDeleted = function (endpoint) { - if (_elementsWithEndpoints[endpoint.elementId]) { - _elementsWithEndpoints[endpoint.elementId]--; - if (_elementsWithEndpoints[endpoint.elementId] <= 0) { - for (var i in _delements) { - if (_delements.hasOwnProperty(i) && _delements[i]) { - delete _delements[i][endpoint.elementId]; - delete _draggablesForElements[endpoint.elementId]; - } - } - } - } - }; - - this.changeId = function (oldId, newId) { - _delements[newId] = _delements[oldId]; - _delements[oldId] = {}; - _draggablesForElements[newId] = _draggablesForElements[oldId]; - _draggablesForElements[oldId] = null; - }; - - this.getElementsForDraggable = function (id) { - return _delements[id]; - }; - - this.elementRemoved = function (elementId) { - var elId = _draggablesForElements[elementId]; - if (elId) { - delete _delements[elId][elementId]; - delete _draggablesForElements[elementId]; - } - }; - - this.reset = function () { - _draggables = {}; - _dlist = []; - _delements = {}; - _elementsWithEndpoints = {}; - }; - - // - // notification drag ended. We check automatically if need to update some - // ancestor's offsets. - // - this.dragEnded = function (el) { - if (el.offsetParent != null) { - var id = _currentInstance.getId(el), - ancestor = _draggablesForElements[id]; - - if (ancestor) { - this.updateOffsets(ancestor); - } - } - }; - - this.setParent = function (el, elId, p, pId, currentChildLocation) { - var current = _draggablesForElements[elId]; - if (!_delements[pId]) { - _delements[pId] = {}; - } - var pLoc = _currentInstance.getOffset(p), - cLoc = currentChildLocation || _currentInstance.getOffset(el); - - if (current && _delements[current]) { - delete _delements[current][elId]; - } - - _delements[pId][elId] = { - id:elId, - offset : { - left: cLoc.left - pLoc.left, - top: cLoc.top - pLoc.top - } - }; - _draggablesForElements[elId] = pId; - }; - - this.clearParent = function(el, elId) { - var current = _draggablesForElements[elId]; - if (current) { - delete _delements[current][elId]; - delete _draggablesForElements[elId]; - } - }; - - this.revalidateParent = function(el, elId, childOffset) { - var current = _draggablesForElements[elId]; - if (current) { - var co = {}; - co[elId] = childOffset; - this.updateOffsets(current, co); - _currentInstance.revalidate(current); - } - }; - - this.getDragAncestor = function (el) { - var de = jsPlumb.getElement(el), - id = _currentInstance.getId(de), - aid = _draggablesForElements[id]; - - if (aid) { - return jsPlumb.getElement(aid); - } - else { - return null; - } - }; - - }; - - var trim = function (str) { - return str == null ? null : (str.replace(/^\s\s*/, '').replace(/\s\s*$/, '')); - }, - _setClassName = function (el, cn, classList) { - cn = trim(cn); - if (typeof el.className.baseVal !== "undefined") { - el.className.baseVal = cn; - } - else { - el.className = cn; - } - - // recent (i currently have 61.0.3163.100) version of chrome do not update classList when you set the base val - // of an svg element's className. in the long run we'd like to move to just using classList anyway - try { - var cl = el.classList; - if (cl != null) { - while (cl.length > 0) { - cl.remove(cl.item(0)); - } - for (var i = 0; i < classList.length; i++) { - if (classList[i]) { - cl.add(classList[i]); - } - } - } - } - catch(e) { - // not fatal - jsPlumbUtil.log("JSPLUMB: cannot set class list", e); - } - }, - _getClassName = function (el) { - return (typeof el.className.baseVal === "undefined") ? el.className : el.className.baseVal; - }, - _classManip = function (el, classesToAdd, classesToRemove) { - classesToAdd = classesToAdd == null ? [] : _ju.isArray(classesToAdd) ? classesToAdd : classesToAdd.split(/\s+/); - classesToRemove = classesToRemove == null ? [] : _ju.isArray(classesToRemove) ? classesToRemove : classesToRemove.split(/\s+/); - - var className = _getClassName(el), - curClasses = className.split(/\s+/); - - var _oneSet = function (add, classes) { - for (var i = 0; i < classes.length; i++) { - if (add) { - if (curClasses.indexOf(classes[i]) === -1) { - curClasses.push(classes[i]); - } - } - else { - var idx = curClasses.indexOf(classes[i]); - if (idx !== -1) { - curClasses.splice(idx, 1); - } - } - } - }; - - _oneSet(true, classesToAdd); - _oneSet(false, classesToRemove); - - _setClassName(el, curClasses.join(" "), curClasses); - }; - - root.jsPlumb.extend(root.jsPlumbInstance.prototype, { - - headless: false, - - pageLocation: _pageLocation, - screenLocation: _screenLocation, - clientLocation: _clientLocation, - - getDragManager:function() { - if (this.dragManager == null) { - this.dragManager = new DragManager(this); - } - - return this.dragManager; - }, - - // NEVER CALLED IN THE CURRENT JS - recalculateOffsets:function(elId) { - this.getDragManager().updateOffsets(elId); - }, - - // CONVERTED - createElement:function(tag, style, clazz, atts) { - return this.createElementNS(null, tag, style, clazz, atts); - }, - - // CONVERTED - createElementNS:function(ns, tag, style, clazz, atts) { - var e = ns == null ? document.createElement(tag) : document.createElementNS(ns, tag); - var i; - style = style || {}; - for (i in style) { - e.style[i] = style[i]; - } - - if (clazz) { - e.className = clazz; - } - - atts = atts || {}; - for (i in atts) { - e.setAttribute(i, "" + atts[i]); - } - - return e; - }, - - // CONVERTED - getAttribute: function (el, attName) { - return el.getAttribute != null ? el.getAttribute(attName) : null; - }, - - // CONVERTED - setAttribute: function (el, a, v) { - if (el.setAttribute != null) { - el.setAttribute(a, v); - } - }, - - // CONVERTED - setAttributes: function (el, atts) { - for (var i in atts) { - if (atts.hasOwnProperty(i)) { - el.setAttribute(i, atts[i]); - } - } - }, - appendToRoot: function (node) { - document.body.appendChild(node); - }, - getRenderModes: function () { - return [ "svg" ]; - }, - getClass:_getClassName, - addClass: function (el, clazz) { - jsPlumb.each(el, function (e) { - _classManip(e, clazz); - }); - }, - hasClass: function (el, clazz) { - el = jsPlumb.getElement(el); - if (el.classList) { - return el.classList.contains(clazz); - } - else { - return _getClassName(el).indexOf(clazz) !== -1; - } - }, - removeClass: function (el, clazz) { - jsPlumb.each(el, function (e) { - _classManip(e, null, clazz); - }); - }, - toggleClass:function(el, clazz) { - if (jsPlumb.hasClass(el, clazz)) { - jsPlumb.removeClass(el, clazz); - } else { - jsPlumb.addClass(el, clazz); - } - }, - updateClasses: function (el, toAdd, toRemove) { - jsPlumb.each(el, function (e) { - _classManip(e, toAdd, toRemove); - }); - }, - setClass: function (el, clazz) { - if (clazz != null) { - jsPlumb.each(el, function (e) { - _setClassName(e, clazz, clazz.split(/\s+/)); - }); - } - }, - setPosition: function (el, p) { - el.style.left = p.left + "px"; - el.style.top = p.top + "px"; - }, - getPosition: function (el) { - var _one = function (prop) { - var v = el.style[prop]; - return v ? v.substring(0, v.length - 2) : 0; - }; - return { - left: _one("left"), - top: _one("top") - }; - }, - getStyle:function(el, prop) { - if (typeof window.getComputedStyle !== 'undefined') { - return getComputedStyle(el, null).getPropertyValue(prop); - } else { - return el.currentStyle[prop]; - } - }, - getSelector: function (ctx, spec) { - var sel = null; - if (arguments.length === 1) { - sel = ctx.nodeType != null ? ctx : document.querySelectorAll(ctx); - } - else { - sel = ctx.querySelectorAll(spec); - } - - return sel; - }, - getOffset:function(el, relativeToRoot, container) { - el = jsPlumb.getElement(el); - container = container || this.getContainer(); - var out = { - left: el.offsetLeft, - top: el.offsetTop - }, - op = (relativeToRoot || (container != null && (el !== container && el.offsetParent !== container))) ? el.offsetParent : null, - _maybeAdjustScroll = function(offsetParent) { - if (offsetParent != null && offsetParent !== document.body && (offsetParent.scrollTop > 0 || offsetParent.scrollLeft > 0)) { - out.left -= offsetParent.scrollLeft; - out.top -= offsetParent.scrollTop; - } - }.bind(this); - - while (op != null) { - out.left += op.offsetLeft; - out.top += op.offsetTop; - _maybeAdjustScroll(op); - op = relativeToRoot ? op.offsetParent : - op.offsetParent === container ? null : op.offsetParent; - } - - // if container is scrolled and the element (or its offset parent) is not absolute or fixed, adjust accordingly. - if (container != null && !relativeToRoot && (container.scrollTop > 0 || container.scrollLeft > 0)) { - var pp = el.offsetParent != null ? this.getStyle(el.offsetParent, "position") : "static", - p = this.getStyle(el, "position"); - if (p !== "absolute" && p !== "fixed" && pp !== "absolute" && pp !== "fixed") { - out.left -= container.scrollLeft; - out.top -= container.scrollTop; - } - } - return out; - }, - // - // return x+y proportion of the given element's size corresponding to the location of the given event. - // - getPositionOnElement: function (evt, el, zoom) { - var box = typeof el.getBoundingClientRect !== "undefined" ? el.getBoundingClientRect() : { left: 0, top: 0, width: 0, height: 0 }, - body = document.body, - docElem = document.documentElement, - scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop, - scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft, - clientTop = docElem.clientTop || body.clientTop || 0, - clientLeft = docElem.clientLeft || body.clientLeft || 0, - pst = 0, - psl = 0, - top = box.top + scrollTop - clientTop + (pst * zoom), - left = box.left + scrollLeft - clientLeft + (psl * zoom), - cl = jsPlumb.pageLocation(evt), - w = box.width || (el.offsetWidth * zoom), - h = box.height || (el.offsetHeight * zoom), - x = (cl[0] - left) / w, - y = (cl[1] - top) / h; - - return [ x, y ]; - }, - - /** - * Gets the absolute position of some element as read from the left/top properties in its style. - * @method getAbsolutePosition - * @param {Element} el The element to retrieve the absolute coordinates from. **Note** this is a DOM element, not a selector from the underlying library. - * @return {Number[]} [left, top] pixel values. - */ - getAbsolutePosition: function (el) { - var _one = function (s) { - var ss = el.style[s]; - if (ss) { - return parseFloat(ss.substring(0, ss.length - 2)); - } - }; - return [ _one("left"), _one("top") ]; - }, - - /** - * Sets the absolute position of some element by setting the left/top properties in its style. - * @method setAbsolutePosition - * @param {Element} el The element to set the absolute coordinates on. **Note** this is a DOM element, not a selector from the underlying library. - * @param {Number[]} xy x and y coordinates - * @param {Number[]} [animateFrom] Optional previous xy to animate from. - * @param {Object} [animateOptions] Options for the animation. - */ - setAbsolutePosition: function (el, xy, animateFrom, animateOptions) { - if (animateFrom) { - this.animate(el, { - left: "+=" + (xy[0] - animateFrom[0]), - top: "+=" + (xy[1] - animateFrom[1]) - }, animateOptions); - } - else { - el.style.left = xy[0] + "px"; - el.style.top = xy[1] + "px"; - } - }, - /** - * gets the size for the element, in an array : [ width, height ]. - */ - getSize: function (el) { - return [ el.offsetWidth, el.offsetHeight ]; - }, - getWidth: function (el) { - return el.offsetWidth; - }, - getHeight: function (el) { - return el.offsetHeight; - }, - getRenderMode : function() { return "svg"; } - - }); -}).call(typeof window !== 'undefined' ? window : this); - /* * 2010 - 2018 jsPlumb (hello@jsplumbtoolkit.com) * @@ -8328,7 +7861,7 @@ dragOptions.multipleDrop = false; dragOptions.canDrag = function () { - return this.isSource || this.isTemporarySource || /*(this.isTarget && */this.connections.length > 0/*)*/; + return this.isSource || this.isTemporarySource || (this.connections.length > 0 && this.connectionsDetachable !== false); }.bind(this); _jsPlumb.initDraggable(this.canvas, dragOptions, "internal"); @@ -9024,8 +8557,8 @@ return this.endpoints; }; - this.isDetachable = function () { - return this._jsPlumb.detachable === true; + this.isDetachable = function (ep) { + return this._jsPlumb.detachable === false ? false : ep != null ? ep.connectionsDetachable === true : this._jsPlumb.detachable === true; }; this.setDetachable = function (detachable) { this._jsPlumb.detachable = detachable === true; @@ -10804,6 +10337,47 @@ maxY: Math.max(params.y1, params.y2) }; }; + + /** + * Computes the list of points on the segment that intersect the given line. + * @method lineIntersection + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @returns {Array<[number, number]>} + */ + this.lineIntersection = function(x1, y1, x2, y2) { + return []; + }; + + /** + * Computes the list of points on the segment that intersect the box with the given origin and size. + * @method boxIntersection + * @param {number} x1 + * @param {number} y1 + * @param {number} w + * @param {number} h + * @returns {Array<[number, number]>} + */ + this.boxIntersection = function(x, y, w, h) { + var a = []; + a.push.apply(a, this.lineIntersection(x, y, x + w, y)); + a.push.apply(a, this.lineIntersection(x + w, y, x + w, y + h)); + a.push.apply(a, this.lineIntersection(x + w, y + h, x, y + h)); + a.push.apply(a, this.lineIntersection(x, y + h, x, y)); + return a; + }; + + /** + * Computes the list of points on the segment that intersect the given bounding box, which is an object of the form { x:.., y:.., w:.., h:.. }. + * @method lineIntersection + * @param {BoundingRectangle} box + * @returns {Array<[number, number]>} + */ + this.boundingBoxIntersection = function(box) { + return this.boxIntersection(box.x, box.y, box.w, box.y); + }; }, Straight: function (params) { var _super = _jp.Segments.AbstractSegment.apply(this, arguments), @@ -10945,6 +10519,103 @@ out.l = fractionInSegment / length; return out; }; + + var _pointLiesBetween = function(q, p1, p2) { + return (p2 > p1) ? (p1 <= q && q <= p2) : (p1 >= q && q >= p2); + }, _plb = _pointLiesBetween; + + /** + * Calculates all intersections of the given line with this segment. + * @param _x1 + * @param _y1 + * @param _x2 + * @param _y2 + * @returns {Array} + */ + this.lineIntersection = function(_x1, _y1, _x2, _y2) { + var m2 = Math.abs(_jg.gradient({x: _x1, y: _y1}, {x: _x2, y: _y2})), + m1 = Math.abs(m), + b = m1 === Infinity ? x1 : y1 - (m1 * x1), + out = [], + b2 = m2 === Infinity ? _x1 : _y1 - (m2 * _x1); + + // if lines parallel, no intersection + if (m2 !== m1) { + // perpendicular, segment horizontal + if(m2 === Infinity && m1 === 0) { + if (_plb(_x1, x1, x2) && _plb(y1, _y1, _y2)) { + out = [ _x1, y1 ]; // we return X on the incident line and Y from the segment + } + } else if(m2 === 0 && m1 === Infinity) { + // perpendicular, segment vertical + if(_plb(_y1, y1, y2) && _plb(x1, _x1, _x2)) { + out = [x1, _y1]; // we return X on the segment and Y from the incident line + } + } else { + var X, Y; + if (m2 === Infinity) { + // test line is a vertical line. where does it cross the segment? + X = _x1; + if (_plb(X, x1, x2)) { + Y = (m1 * _x1) + b; + if (_plb(Y, _y1, _y2)) { + out = [ X, Y ]; + } + } + } else if (m2 === 0) { + Y = _y1; + // test line is a horizontal line. where does it cross the segment? + if (_plb(Y, y1, y2)) { + X = (_y1 - b) / m1; + if (_plb(X, _x1, _x2)) { + out = [ X, Y ]; + } + } + } else { + // mX + b = m2X + b2 + // mX - m2X = b2 - b + // X(m - m2) = b2 - b + // X = (b2 - b) / (m - m2) + // Y = mX + b + X = (b2 - b) / (m1 - m2); + Y = (m1 * X) + b; + if(_plb(X, x1, x2) && _plb(Y, y1, y2)) { + out = [ X, Y]; + } + } + } + } + + return out; + }; + + /** + * Calculates all intersections of the given box with this segment. By default this method simply calls `lineIntersection` with each of the four + * faces of the box; subclasses can override this if they think there's a faster way to compute the entire box at once. + * @param x X position of top left corner of box + * @param y Y position of top left corner of box + * @param w width of box + * @param h height of box + * @returns {Array} + */ + this.boxIntersection = function(x, y, w, h) { + var a = []; + a.push.apply(a, this.lineIntersection(x, y, x + w, y)); + a.push.apply(a, this.lineIntersection(x + w, y, x + w, y + h)); + a.push.apply(a, this.lineIntersection(x + w, y + h, x, y + h)); + a.push.apply(a, this.lineIntersection(x, y + h, x, y)); + return a; + }; + + /** + * Calculates all intersections of the given bounding box with this segment. By default this method simply calls `lineIntersection` with each of the four + * faces of the box; subclasses can override this if they think there's a faster way to compute the entire box at once. + * @param box Bounding box, in { x:.., y:..., w:..., h:... } format. + * @returns {Array} + */ + this.boundingBoxIntersection = function(box) { + return this.boxIntersection(box.x, box.y, box.w, box.h); + }; }, /* @@ -11103,6 +10774,8 @@ return {x: startX, y: startY}; }; + + // TODO: lineIntersection }, Bezier: function (params) { @@ -11163,6 +10836,21 @@ this.getBounds = function () { return this.bounds; }; + + this.findClosestPointOnPath = function (x, y) { + var p = root.jsBezier.nearestPointOnCurve({x:x,y:y}, this.curve); + return { + d:Math.sqrt(Math.pow(p.point.x - x, 2) + Math.pow(p.point.y - y, 2)), + x:p.point.x, + y:p.point.y, + l:p.location, + s:this + }; + }; + + this.lineIntersection = function(x1, y1, x2, y2) { + return root.jsBezier.lineIntersection(x1, y1, x2, y2, this.curve); + }; } }; @@ -11269,6 +10957,30 @@ return out; }; + this.lineIntersection = function(x1, y1, x2, y2) { + var out = []; + for (var i = 0; i < segments.length; i++) { + out.push.apply(out, segments[i].lineIntersection(x1, y1, x2, y2)); + } + return out; + }; + + this.boxIntersection = function(x, y, w, h) { + var out = []; + for (var i = 0; i < segments.length; i++) { + out.push.apply(out, segments[i].boxIntersection(x, y, w, h)); + } + return out; + }; + + this.boundingBoxIntersection = function(box) { + var out = []; + for (var i = 0; i < segments.length; i++) { + out.push.apply(out, segments[i].boundingBoxIntersection(box)); + } + return out; + }; + var _updateSegmentProportions = function () { var curLoc = 0; for (var i = 0; i < segments.length; i++) { @@ -12274,50 +11986,6 @@ }).call(typeof window !== 'undefined' ? window : this); -/* - * This file contains the base class for library adapters. - * - * Copyright (c) 2010 - 2018 jsPlumb (hello@jsplumbtoolkit.com) - * - * https://jsplumbtoolkit.com - * https://github.com/jsplumb/jsplumb - * - * Dual licensed under the MIT and GPL2 licenses. - */ -;(function() { - "use strict"; - var root = this, - _jp = root.jsPlumb; - - var _getEventManager = function(instance) { - var e = instance._mottle; - if (!e) { - e = instance._mottle = new root.Mottle(); - } - return e; - }; - - _jp.extend(root.jsPlumbInstance.prototype, { - getEventManager:function() { - return _getEventManager(this); - }, - on : function(el, event, callback) { - // TODO: here we would like to map the tap event if we know its - // an internal bind to a click. we have to know its internal because only - // then can we be sure that the UP event wont be consumed (tap is a synthesized - // event from a mousedown followed by a mouseup). - //event = { "click":"tap", "dblclick":"dbltap"}[event] || event; - this.getEventManager().on.apply(this, arguments); - return this; - }, - off : function(el, event, callback) { - this.getEventManager().off.apply(this, arguments); - return this; - } - }); - - -}).call(typeof window !== 'undefined' ? window : this); /* * Copyright (c) 2010 - 2018 jsPlumb (hello@jsplumbtoolkit.com) * @@ -12475,7 +12143,7 @@ _jsPlumb.revalidate(elId); if (!doNotFireEvent) { - var p = {group: group, el: el}; + var p = {group: group, el: el, pos:newPosition}; if (currentGroup) { p.sourceGroup = currentGroup; } @@ -14571,22 +14239,31 @@ }).call(typeof window !== 'undefined' ? window : this); /* - * This file contains the 'vanilla' adapter - having no external dependencies other than bundled libs. + * This file contains code used when jsPlumb is being rendered in a DOM. + * + * Copyright (c) 2010 - 2019 jsPlumb (hello@jsplumbtoolkit.com) * - * Copyright (c) 2010 - 2018 jsPlumb (hello@jsplumbtoolkit.com) - * * https://jsplumbtoolkit.com * https://github.com/jsplumb/jsplumb - * + * * Dual licensed under the MIT and GPL2 licenses. */ ; (function () { "use strict"; + var root = this, _jp = root.jsPlumb, _ju = root.jsPlumbUtil, _jk = root.Katavorio, _jg = root.Biltong; + var _getEventManager = function(instance) { + var e = instance._mottle; + if (!e) { + e = instance._mottle = new root.Mottle(); + } + return e; + }; + var _getDragManager = function (instance, category) { category = category || "main"; @@ -14635,6 +14312,61 @@ return k; }; + var _dragStart=function(params) { + var options = params.el._jsPlumbDragOptions; + var cont = true; + if (options.canDrag) { + cont = options.canDrag(); + } + if (cont) { + this.setHoverSuspended(true); + this.select({source: params.el}).addClass(this.elementDraggingClass + " " + this.sourceElementDraggingClass, true); + this.select({target: params.el}).addClass(this.elementDraggingClass + " " + this.targetElementDraggingClass, true); + this.setConnectionBeingDragged(true); + } + return cont; + }; + var _dragMove=function(params) { + var ui = this.getUIPosition(arguments, this.getZoom()); + if (ui != null) { + var o = params.el._jsPlumbDragOptions; + this.draw(params.el, ui, null, true); + if (o._dragging) { + this.addClass(params.el, "jtk-dragged"); + } + o._dragging = true; + } + }; + var _dragStop=function(params) { + var elements = params.selection, uip; + + var _one = function (_e) { + if (_e[1] != null) { + // run the reported offset through the code that takes parent containers + // into account, to adjust if necessary (issue 554) + uip = this.getUIPosition([{ + el:_e[2].el, + pos:[_e[1].left, _e[1].top] + }]); + this.draw(_e[2].el, uip); + } + + delete _e[0]._jsPlumbDragOptions._dragging; + + this.removeClass(_e[0], "jtk-dragged"); + this.select({source: _e[2].el}).removeClass(this.elementDraggingClass + " " + this.sourceElementDraggingClass, true); + this.select({target: _e[2].el}).removeClass(this.elementDraggingClass + " " + this.targetElementDraggingClass, true); + this.getDragManager().dragEnded(_e[2].el); + }.bind(this); + + for (var i = 0; i < elements.length; i++) { + _one(elements[i]); + } + + this.setHoverSuspended(false); + this.setConnectionBeingDragged(false); + }; + var _animProps = function (o, p) { var _one = function (pName) { if (p[pName] != null) { @@ -14654,8 +14386,666 @@ return [ _one("left"), _one("top") ]; }; - _jp.extend(root.jsPlumbInstance.prototype, { + var _genLoc = function (prefix, e) { + if (e == null) { + return [ 0, 0 ]; + } + var ts = _touches(e), t = _getTouch(ts, 0); + return [t[prefix + "X"], t[prefix + "Y"]]; + }, + _pageLocation = _genLoc.bind(this, "page"), + _screenLocation = _genLoc.bind(this, "screen"), + _clientLocation = _genLoc.bind(this, "client"), + _getTouch = function (touches, idx) { + return touches.item ? touches.item(idx) : touches[idx]; + }, + _touches = function (e) { + return e.touches && e.touches.length > 0 ? e.touches : + e.changedTouches && e.changedTouches.length > 0 ? e.changedTouches : + e.targetTouches && e.targetTouches.length > 0 ? e.targetTouches : + [ e ]; + }; + /** + Manages dragging for some instance of jsPlumb. + + TODO instead of this being accessed directly, it should subscribe to events on the jsPlumb instance: every method + in here is called directly by jsPlumb. But what should happen is that we have unpublished events that this listens + to. The only trick is getting one of these instantiated with every jsPlumb instance: it needs to have a hook somehow. + Basically the general idea is to pull ALL the drag code out (prototype method registrations plus this) into a + dedicated drag script), that does not necessarily need to be included. + + + */ + var DragManager = function (_currentInstance) { + var _draggables = {}, _dlist = [], _delements = {}, _elementsWithEndpoints = {}, + // elementids mapped to the draggable to which they belong. + _draggablesForElements = {}; + + /** + register some element as draggable. right now the drag init stuff is done elsewhere, and it is + possible that will continue to be the case. + */ + this.register = function (el) { + var id = _currentInstance.getId(el), + parentOffset; + + if (!_draggables[id]) { + _draggables[id] = el; + _dlist.push(el); + _delements[id] = {}; + } + + // look for child elements that have endpoints and register them against this draggable. + var _oneLevel = function (p) { + if (p) { + for (var i = 0; i < p.childNodes.length; i++) { + if (p.childNodes[i].nodeType !== 3 && p.childNodes[i].nodeType !== 8) { + var cEl = jsPlumb.getElement(p.childNodes[i]), + cid = _currentInstance.getId(p.childNodes[i], null, true); + if (cid && _elementsWithEndpoints[cid] && _elementsWithEndpoints[cid] > 0) { + if (!parentOffset) { + parentOffset = _currentInstance.getOffset(el); + } + var cOff = _currentInstance.getOffset(cEl); + _delements[id][cid] = { + id: cid, + offset: { + left: cOff.left - parentOffset.left, + top: cOff.top - parentOffset.top + } + }; + _draggablesForElements[cid] = id; + } + _oneLevel(p.childNodes[i]); + } + } + } + }; + + _oneLevel(el); + }; + + // refresh the offsets for child elements of this element. + this.updateOffsets = function (elId, childOffsetOverrides) { + if (elId != null) { + childOffsetOverrides = childOffsetOverrides || {}; + var domEl = jsPlumb.getElement(elId), + id = _currentInstance.getId(domEl), + children = _delements[id], + parentOffset; + + if (children) { + for (var i in children) { + if (children.hasOwnProperty(i)) { + var cel = jsPlumb.getElement(i), + cOff = childOffsetOverrides[i] || _currentInstance.getOffset(cel); + + // do not update if we have a value already and we'd just be writing 0,0 + if (cel.offsetParent == null && _delements[id][i] != null) { + continue; + } + + if (!parentOffset) { + parentOffset = _currentInstance.getOffset(domEl); + } + + _delements[id][i] = { + id: i, + offset: { + left: cOff.left - parentOffset.left, + top: cOff.top - parentOffset.top + } + }; + _draggablesForElements[i] = id; + } + } + } + } + }; + + /** + notification that an endpoint was added to the given el. we go up from that el's parent + node, looking for a parent that has been registered as a draggable. if we find one, we add this + el to that parent's list of elements to update on drag (if it is not there already) + */ + this.endpointAdded = function (el, id) { + + id = id || _currentInstance.getId(el); + + var b = document.body, + p = el.parentNode; + + _elementsWithEndpoints[id] = _elementsWithEndpoints[id] ? _elementsWithEndpoints[id] + 1 : 1; + + while (p != null && p !== b) { + var pid = _currentInstance.getId(p, null, true); + if (pid && _draggables[pid]) { + var pLoc = _currentInstance.getOffset(p); + + if (_delements[pid][id] == null) { + var cLoc = _currentInstance.getOffset(el); + _delements[pid][id] = { + id: id, + offset: { + left: cLoc.left - pLoc.left, + top: cLoc.top - pLoc.top + } + }; + _draggablesForElements[id] = pid; + } + break; + } + p = p.parentNode; + } + }; + + this.endpointDeleted = function (endpoint) { + if (_elementsWithEndpoints[endpoint.elementId]) { + _elementsWithEndpoints[endpoint.elementId]--; + if (_elementsWithEndpoints[endpoint.elementId] <= 0) { + for (var i in _delements) { + if (_delements.hasOwnProperty(i) && _delements[i]) { + delete _delements[i][endpoint.elementId]; + delete _draggablesForElements[endpoint.elementId]; + } + } + } + } + }; + + this.changeId = function (oldId, newId) { + _delements[newId] = _delements[oldId]; + _delements[oldId] = {}; + _draggablesForElements[newId] = _draggablesForElements[oldId]; + _draggablesForElements[oldId] = null; + }; + + this.getElementsForDraggable = function (id) { + return _delements[id]; + }; + + this.elementRemoved = function (elementId) { + var elId = _draggablesForElements[elementId]; + if (elId) { + delete _delements[elId][elementId]; + delete _draggablesForElements[elementId]; + } + }; + + this.reset = function () { + _draggables = {}; + _dlist = []; + _delements = {}; + _elementsWithEndpoints = {}; + }; + + // + // notification drag ended. We check automatically if need to update some + // ancestor's offsets. + // + this.dragEnded = function (el) { + if (el.offsetParent != null) { + var id = _currentInstance.getId(el), + ancestor = _draggablesForElements[id]; + + if (ancestor) { + this.updateOffsets(ancestor); + } + } + }; + + this.setParent = function (el, elId, p, pId, currentChildLocation) { + var current = _draggablesForElements[elId]; + if (!_delements[pId]) { + _delements[pId] = {}; + } + var pLoc = _currentInstance.getOffset(p), + cLoc = currentChildLocation || _currentInstance.getOffset(el); + + if (current && _delements[current]) { + delete _delements[current][elId]; + } + + _delements[pId][elId] = { + id:elId, + offset : { + left: cLoc.left - pLoc.left, + top: cLoc.top - pLoc.top + } + }; + _draggablesForElements[elId] = pId; + }; + + this.clearParent = function(el, elId) { + var current = _draggablesForElements[elId]; + if (current) { + delete _delements[current][elId]; + delete _draggablesForElements[elId]; + } + }; + + this.revalidateParent = function(el, elId, childOffset) { + var current = _draggablesForElements[elId]; + if (current) { + var co = {}; + co[elId] = childOffset; + this.updateOffsets(current, co); + _currentInstance.revalidate(current); + } + }; + + this.getDragAncestor = function (el) { + var de = jsPlumb.getElement(el), + id = _currentInstance.getId(de), + aid = _draggablesForElements[id]; + + if (aid) { + return jsPlumb.getElement(aid); + } + else { + return null; + } + }; + + }; + + var _setClassName = function (el, cn, classList) { + cn = _ju.fastTrim(cn); + if (typeof el.className.baseVal !== "undefined") { + el.className.baseVal = cn; + } + else { + el.className = cn; + } + + // recent (i currently have 61.0.3163.100) version of chrome do not update classList when you set the base val + // of an svg element's className. in the long run we'd like to move to just using classList anyway + try { + var cl = el.classList; + if (cl != null) { + while (cl.length > 0) { + cl.remove(cl.item(0)); + } + for (var i = 0; i < classList.length; i++) { + if (classList[i]) { + cl.add(classList[i]); + } + } + } + } + catch(e) { + // not fatal + _ju.log("JSPLUMB: cannot set class list", e); + } + }, + _getClassName = function (el) { + return (typeof el.className.baseVal === "undefined") ? el.className : el.className.baseVal; + }, + _classManip = function (el, classesToAdd, classesToRemove) { + classesToAdd = classesToAdd == null ? [] : _ju.isArray(classesToAdd) ? classesToAdd : classesToAdd.split(/\s+/); + classesToRemove = classesToRemove == null ? [] : _ju.isArray(classesToRemove) ? classesToRemove : classesToRemove.split(/\s+/); + + var className = _getClassName(el), + curClasses = className.split(/\s+/); + + var _oneSet = function (add, classes) { + for (var i = 0; i < classes.length; i++) { + if (add) { + if (curClasses.indexOf(classes[i]) === -1) { + curClasses.push(classes[i]); + } + } + else { + var idx = curClasses.indexOf(classes[i]); + if (idx !== -1) { + curClasses.splice(idx, 1); + } + } + } + }; + + _oneSet(true, classesToAdd); + _oneSet(false, classesToRemove); + + _setClassName(el, curClasses.join(" "), curClasses); + }; + + root.jsPlumb.extend(root.jsPlumbInstance.prototype, { + + headless: false, + + pageLocation: _pageLocation, + screenLocation: _screenLocation, + clientLocation: _clientLocation, + + getDragManager:function() { + if (this.dragManager == null) { + this.dragManager = new DragManager(this); + } + + return this.dragManager; + }, + + recalculateOffsets:function(elId) { + this.getDragManager().updateOffsets(elId); + }, + + createElement:function(tag, style, clazz, atts) { + return this.createElementNS(null, tag, style, clazz, atts); + }, + + createElementNS:function(ns, tag, style, clazz, atts) { + var e = ns == null ? document.createElement(tag) : document.createElementNS(ns, tag); + var i; + style = style || {}; + for (i in style) { + e.style[i] = style[i]; + } + + if (clazz) { + e.className = clazz; + } + + atts = atts || {}; + for (i in atts) { + e.setAttribute(i, "" + atts[i]); + } + + return e; + }, + + getAttribute: function (el, attName) { + return el.getAttribute != null ? el.getAttribute(attName) : null; + }, + + setAttribute: function (el, a, v) { + if (el.setAttribute != null) { + el.setAttribute(a, v); + } + }, + + setAttributes: function (el, atts) { + for (var i in atts) { + if (atts.hasOwnProperty(i)) { + el.setAttribute(i, atts[i]); + } + } + }, + appendToRoot: function (node) { + document.body.appendChild(node); + }, + getRenderModes: function () { + return [ "svg" ]; + }, + getClass:_getClassName, + addClass: function (el, clazz) { + jsPlumb.each(el, function (e) { + _classManip(e, clazz); + }); + }, + hasClass: function (el, clazz) { + el = jsPlumb.getElement(el); + if (el.classList) { + return el.classList.contains(clazz); + } + else { + return _getClassName(el).indexOf(clazz) !== -1; + } + }, + removeClass: function (el, clazz) { + jsPlumb.each(el, function (e) { + _classManip(e, null, clazz); + }); + }, + toggleClass:function(el, clazz) { + if (jsPlumb.hasClass(el, clazz)) { + jsPlumb.removeClass(el, clazz); + } else { + jsPlumb.addClass(el, clazz); + } + }, + updateClasses: function (el, toAdd, toRemove) { + jsPlumb.each(el, function (e) { + _classManip(e, toAdd, toRemove); + }); + }, + setClass: function (el, clazz) { + if (clazz != null) { + jsPlumb.each(el, function (e) { + _setClassName(e, clazz, clazz.split(/\s+/)); + }); + } + }, + setPosition: function (el, p) { + el.style.left = p.left + "px"; + el.style.top = p.top + "px"; + }, + getPosition: function (el) { + var _one = function (prop) { + var v = el.style[prop]; + return v ? v.substring(0, v.length - 2) : 0; + }; + return { + left: _one("left"), + top: _one("top") + }; + }, + getStyle:function(el, prop) { + if (typeof window.getComputedStyle !== 'undefined') { + return getComputedStyle(el, null).getPropertyValue(prop); + } else { + return el.currentStyle[prop]; + } + }, + getSelector: function (ctx, spec) { + var sel = null; + if (arguments.length === 1) { + sel = ctx.nodeType != null ? ctx : document.querySelectorAll(ctx); + } + else { + sel = ctx.querySelectorAll(spec); + } + + return sel; + }, + getOffset:function(el, relativeToRoot, container) { + el = jsPlumb.getElement(el); + container = container || this.getContainer(); + var out = { + left: el.offsetLeft, + top: el.offsetTop + }, + op = (relativeToRoot || (container != null && (el !== container && el.offsetParent !== container))) ? el.offsetParent : null, + _maybeAdjustScroll = function(offsetParent) { + if (offsetParent != null && offsetParent !== document.body && (offsetParent.scrollTop > 0 || offsetParent.scrollLeft > 0)) { + out.left -= offsetParent.scrollLeft; + out.top -= offsetParent.scrollTop; + } + }.bind(this); + + while (op != null) { + out.left += op.offsetLeft; + out.top += op.offsetTop; + _maybeAdjustScroll(op); + op = relativeToRoot ? op.offsetParent : + op.offsetParent === container ? null : op.offsetParent; + } + + // if container is scrolled and the element (or its offset parent) is not absolute or fixed, adjust accordingly. + if (container != null && !relativeToRoot && (container.scrollTop > 0 || container.scrollLeft > 0)) { + var pp = el.offsetParent != null ? this.getStyle(el.offsetParent, "position") : "static", + p = this.getStyle(el, "position"); + if (p !== "absolute" && p !== "fixed" && pp !== "absolute" && pp !== "fixed") { + out.left -= container.scrollLeft; + out.top -= container.scrollTop; + } + } + return out; + }, + // + // return x+y proportion of the given element's size corresponding to the location of the given event. + // + getPositionOnElement: function (evt, el, zoom) { + var box = typeof el.getBoundingClientRect !== "undefined" ? el.getBoundingClientRect() : { left: 0, top: 0, width: 0, height: 0 }, + body = document.body, + docElem = document.documentElement, + scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop, + scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft, + clientTop = docElem.clientTop || body.clientTop || 0, + clientLeft = docElem.clientLeft || body.clientLeft || 0, + pst = 0, + psl = 0, + top = box.top + scrollTop - clientTop + (pst * zoom), + left = box.left + scrollLeft - clientLeft + (psl * zoom), + cl = jsPlumb.pageLocation(evt), + w = box.width || (el.offsetWidth * zoom), + h = box.height || (el.offsetHeight * zoom), + x = (cl[0] - left) / w, + y = (cl[1] - top) / h; + + return [ x, y ]; + }, + + /** + * Gets the absolute position of some element as read from the left/top properties in its style. + * @method getAbsolutePosition + * @param {Element} el The element to retrieve the absolute coordinates from. **Note** this is a DOM element, not a selector from the underlying library. + * @return {Number[]} [left, top] pixel values. + */ + getAbsolutePosition: function (el) { + var _one = function (s) { + var ss = el.style[s]; + if (ss) { + return parseFloat(ss.substring(0, ss.length - 2)); + } + }; + return [ _one("left"), _one("top") ]; + }, + + /** + * Sets the absolute position of some element by setting the left/top properties in its style. + * @method setAbsolutePosition + * @param {Element} el The element to set the absolute coordinates on. **Note** this is a DOM element, not a selector from the underlying library. + * @param {Number[]} xy x and y coordinates + * @param {Number[]} [animateFrom] Optional previous xy to animate from. + * @param {Object} [animateOptions] Options for the animation. + */ + setAbsolutePosition: function (el, xy, animateFrom, animateOptions) { + if (animateFrom) { + this.animate(el, { + left: "+=" + (xy[0] - animateFrom[0]), + top: "+=" + (xy[1] - animateFrom[1]) + }, animateOptions); + } + else { + el.style.left = xy[0] + "px"; + el.style.top = xy[1] + "px"; + } + }, + /** + * gets the size for the element, in an array : [ width, height ]. + */ + getSize: function (el) { + return [ el.offsetWidth, el.offsetHeight ]; + }, + getWidth: function (el) { + return el.offsetWidth; + }, + getHeight: function (el) { + return el.offsetHeight; + }, + getRenderMode : function() { return "svg"; }, + draggable : function (el, options) { + var info; + el = _ju.isArray(el) || (el.length != null && !_ju.isString(el)) ? el: [ el ]; + Array.prototype.slice.call(el).forEach(function(_el) { + info = this.info(_el); + if (info.el) { + this._initDraggableIfNecessary(info.el, true, options, info.id, true); + } + }.bind(this)); + return this; + }, + initDraggable: function (el, options, category) { + _getDragManager(this, category).draggable(el, options); + el._jsPlumbDragOptions = options; + }, + destroyDraggable: function (el, category) { + _getDragManager(this, category).destroyDraggable(el); + delete el._jsPlumbDragOptions; + }, + unbindDraggable: function (el, evt, fn, category) { + _getDragManager(this, category).destroyDraggable(el, evt, fn); + }, + setDraggable : function (element, draggable) { + return jsPlumb.each(element, function (el) { + if (this.isDragSupported(el)) { + this._draggableStates[this.getAttribute(el, "id")] = draggable; + this.setElementDraggable(el, draggable); + } + }.bind(this)); + }, + _draggableStates : {}, + /* + * toggles the draggable state of the given element(s). + * el is either an id, or an element object, or a list of ids/element objects. + */ + toggleDraggable : function (el) { + var state; + jsPlumb.each(el, function (el) { + var elId = this.getAttribute(el, "id"); + state = this._draggableStates[elId] == null ? false : this._draggableStates[elId]; + state = !state; + this._draggableStates[elId] = state; + this.setDraggable(el, state); + return state; + }.bind(this)); + return state; + }, + _initDraggableIfNecessary : function (element, isDraggable, dragOptions, id, fireEvent) { + // TODO FIRST: move to DragManager. including as much of the decision to init dragging as possible. + if (!jsPlumb.headless) { + var _draggable = isDraggable == null ? false : isDraggable; + if (_draggable) { + if (jsPlumb.isDragSupported(element, this)) { + var options = dragOptions || this.Defaults.DragOptions; + options = jsPlumb.extend({}, options); // make a copy. + if (!jsPlumb.isAlreadyDraggable(element, this)) { + var dragEvent = jsPlumb.dragEvents.drag, + stopEvent = jsPlumb.dragEvents.stop, + startEvent = jsPlumb.dragEvents.start; + + this.manage(id, element); + + options[startEvent] = _ju.wrap(options[startEvent], _dragStart.bind(this)); + + options[dragEvent] = _ju.wrap(options[dragEvent], _dragMove.bind(this)); + + options[stopEvent] = _ju.wrap(options[stopEvent], _dragStop.bind(this)); + + var elId = this.getId(element); // need ID + + this._draggableStates[elId] = true; + var draggable = this._draggableStates[elId]; + + options.disabled = draggable == null ? false : !draggable; + this.initDraggable(element, options); + this.getDragManager().register(element); + if (fireEvent) { + this.fire("elementDraggable", {el:element, options:options}); + } + } + else { + // already draggable. attach any start, drag or stop listeners to the current Drag. + if (dragOptions.force) { + this.initDraggable(element, options); + } + } + } + } + } + }, animationSupported:true, getElement: function (el) { if (el == null) { @@ -14711,21 +15101,29 @@ }, step); }, // DRAG/DROP - destroyDraggable: function (el, category) { - _getDragManager(this, category).destroyDraggable(el); - }, - unbindDraggable: function (el, evt, fn, category) { - _getDragManager(this, category).destroyDraggable(el, evt, fn); - }, + + destroyDroppable: function (el, category) { _getDragManager(this, category).destroyDroppable(el); }, unbindDroppable: function (el, evt, fn, category) { _getDragManager(this, category).destroyDroppable(el, evt, fn); }, - initDraggable: function (el, options, category) { - _getDragManager(this, category).draggable(el, options); + + droppable :function(el, options) { + el = _ju.isArray(el) || (el.length != null && !_ju.isString(el)) ? el: [ el ]; + var info; + options = options || {}; + options.allowLoopback = false; + Array.prototype.slice.call(el).forEach(function(_el) { + info = this.info(_el); + if (info.el) { + this.initDroppable(info.el, options); + } + }.bind(this)); + return this; }, + initDroppable: function (el, options, category) { _getDragManager(this, category).droppable(el, options); }, @@ -14858,7 +15256,24 @@ this[key].reset(); } } + }, + getEventManager:function() { + return _getEventManager(this); + }, + on : function(el, event, callback) { + // TODO: here we would like to map the tap event if we know its + // an internal bind to a click. we have to know its internal because only + // then can we be sure that the UP event wont be consumed (tap is a synthesized + // event from a mousedown followed by a mouseup). + //event = { "click":"tap", "dblclick":"dbltap"}[event] || event; + this.getEventManager().on.apply(this, arguments); + return this; + }, + off : function(el, event, callback) { + this.getEventManager().off.apply(this, arguments); + return this; } + }); var ready = function (f) { diff --git a/libraries/panzoom.js b/libraries/panzoom.js index a1b046c58..f9caf3cda 100644 --- a/libraries/panzoom.js +++ b/libraries/panzoom.js @@ -33,11 +33,11 @@ function createPanZoom(domElement, options) { if (!panController) { if (domElement instanceof SVGElement) { - panController = makeSvgController(domElement) + panController = makeSvgController(domElement, options) } if (domElement instanceof HTMLElement) { - panController = makeDomController(domElement) + panController = makeDomController(domElement, options) } } @@ -57,7 +57,8 @@ function createPanZoom(domElement, options) { } var filterKey = typeof options.filterKey === 'function' ? options.filterKey : noop; - var realPinch = typeof options.realPinch === 'boolean' ? options.realPinch : false + // TODO: likely need to unite pinchSpeed with zoomSpeed + var pinchSpeed = typeof options.pinchSpeed === 'number' ? options.pinchSpeed : 1; var bounds = options.bounds var maxZoom = typeof options.maxZoom === 'number' ? options.maxZoom : Number.POSITIVE_INFINITY var minZoom = typeof options.minZoom === 'number' ? options.minZoom : 0 @@ -101,7 +102,7 @@ function createPanZoom(domElement, options) { var moveByAnimation var zoomToAnimation - var multitouch + var multiTouch var paused = false listenForEvents() @@ -144,7 +145,8 @@ function createPanZoom(domElement, options) { function showRectangle(rect) { // TODO: this duplicates autocenter. I think autocenter should go. - var size = transformToScreen(owner.clientWidth, owner.clientHeight) + var clientRect = owner.getBoundingClientRect() + var size = transformToScreen(clientRect.width, clientRect.height) var rectWidth = rect.right - rect.left var rectHeight = rect.bottom - rect.top @@ -503,7 +505,7 @@ function createPanZoom(domElement, options) { } else if (e.touches.length === 2) { // handleTouchMove() will care about pinch zoom. pinchZoomLength = getPinchZoomLength(e.touches[0], e.touches[1]) - multitouch = true + multiTouch = true startTouchListenerIfNeeded() } } @@ -568,25 +570,14 @@ function createPanZoom(domElement, options) { internalMoveBy(point.x, point.y) } else if (e.touches.length === 2) { // it's a zoom, let's find direction - multitouch = true + multiTouch = true var t1 = e.touches[0] var t2 = e.touches[1] var currentPinchLength = getPinchZoomLength(t1, t2) - var scaleMultiplier = 1 - - if (realPinch) { - scaleMultiplier = currentPinchLength / pinchZoomLength - } else { - var delta = 0 - if (currentPinchLength < pinchZoomLength) { - delta = 1 - } else if (currentPinchLength > pinchZoomLength) { - delta = -1 - } - - scaleMultiplier = getScaleMultiplier(delta) - } + // since the zoom speed is always based on distance from 1, we need to apply + // pinch speed only on that distance from 1: + var scaleMultiplier = 1 + (currentPinchLength / pinchZoomLength - 1) * pinchSpeed mouseX = (t1.clientX + t2.clientX)/2 mouseY = (t1.clientY + t2.clientY)/2 @@ -619,8 +610,9 @@ function createPanZoom(domElement, options) { } function getPinchZoomLength(finger1, finger2) { - return Math.sqrt((finger1.clientX - finger2.clientX) * (finger1.clientX - finger2.clientX) + - (finger1.clientY - finger2.clientY) * (finger1.clientY - finger2.clientY)) + var dx = finger1.clientX - finger2.clientX + var dy = finger1.clientY - finger2.clientY + return Math.sqrt(dx * dx + dy * dy) } function onDoubleClick(e) { @@ -630,12 +622,6 @@ function createPanZoom(domElement, options) { } function onMouseDown(e) { - if (options.onMouseDown && !options.onMouseDown(e)) { - // if they return `false` from onTouch, we don't want to stop - // events propagation. Fixes https://github.com/anvaka/panzoom/issues/46 - return - } - if (touchInProgress) { // modern browsers will fire mousedown for touch events too // we do not want this: touch is handled separately. @@ -698,7 +684,7 @@ function createPanZoom(domElement, options) { document.removeEventListener('touchend', handleTouchEnd) document.removeEventListener('touchcancel', handleTouchEnd) panstartFired = false - multitouch = false + multiTouch = false } function onMouseWheel(e) { @@ -775,8 +761,8 @@ function createPanZoom(domElement, options) { function triggerPanEnd() { if (panstartFired) { - // we should never run smooth scrolling if it was multitouch (pinch zoom animation): - if (!multitouch) smoothScroll.stop() + // we should never run smooth scrolling if it was multiTouch (pinch zoom animation): + if (!multiTouch) smoothScroll.stop() triggerEvent('panend') } } @@ -828,11 +814,13 @@ function autoRun() { if (!scripts) return; var panzoomScript; - Array.from(scripts).forEach(function(x) { + for (var i = 0; i < scripts.length; ++i) { + var x = scripts[i]; if (x.src && x.src.match(/\bpanzoom(\.min)?\.js/)) { panzoomScript = x + break; } - }) + } if (!panzoomScript) return; @@ -894,7 +882,7 @@ autoRun(); },{"./lib/domController.js":2,"./lib/kinetic.js":3,"./lib/svgController.js":4,"./lib/textSelectionInterceptor.js":5,"./lib/transform.js":6,"amator":7,"ngraph.events":9,"wheel":10}],2:[function(require,module,exports){ module.exports = makeDomController -function makeDomController(domElement) { +function makeDomController(domElement, options) { var elementValid = (domElement instanceof HTMLElement) if (!elementValid) { throw new Error('svg element is required for svg.panzoom to work') @@ -908,7 +896,10 @@ function makeDomController(domElement) { } domElement.scrollTop = 0; - owner.setAttribute('tabindex', 1); // TODO: not sure if this is really polite + + if (!options.disableKeyboardInteraction) { + owner.setAttribute('tabindex', 0); + } var api = { getBBox: getBBox, @@ -1067,7 +1058,7 @@ function kinetic(getPoint, scroll, settings) { },{}],4:[function(require,module,exports){ module.exports = makeSvgController -function makeSvgController(svgElement) { +function makeSvgController(svgElement, options) { var elementValid = (svgElement instanceof SVGElement) if (!elementValid) { throw new Error('svg element is required for svg.panzoom to work') @@ -1081,7 +1072,9 @@ function makeSvgController(svgElement) { 'As of March 2016 only FireFox supported transform on the root element') } - owner.setAttribute('tabindex', 1); // TODO: not sure if this is really polite + if (!options.disableKeyboardInteraction) { + owner.setAttribute('tabindex', 0); + } var api = { getBBox: getBBox, @@ -1315,7 +1308,7 @@ function makeAggregateRaf() { var t = backBuffer; backBuffer = frontBuffer; - frontBuffer = t; + frontBuffer = t; frontBuffer.forEach(function(callback) { callback(); @@ -1578,7 +1571,7 @@ function removeWheelListener( elem, callback, useCapture ) { // unsubscription in some browsers. But in practice, I don't think we should // worry too much about it (those browsers are on the way out) function _addWheelListener( elem, eventName, callback, useCapture ) { - elem[ _addEventListener ]( prefix + eventName, support == "wheel" ? callback : function( originalEvent ) { + elem[ _addEventListener ]( prefix + eventName, support == "wheel" ? callback : function(originalEvent ) { !originalEvent && ( originalEvent = window.event ); // create a normalized event object @@ -1620,7 +1613,10 @@ function _addWheelListener( elem, eventName, callback, useCapture ) { // it's time to fire the callback return callback( event ); - }, useCapture || false ); + }, { + capture: useCapture || false , + passive: false + }); } function _removeWheelListener( elem, eventName, callback, useCapture ) { diff --git a/src/public/javascripts/services/link.js b/src/public/javascripts/services/link.js index 43af73d18..952ee3eae 100644 --- a/src/public/javascripts/services/link.js +++ b/src/public/javascripts/services/link.js @@ -43,18 +43,19 @@ function getNotePathFromLink($link) { } function goToLink(e) { - e.preventDefault(); - const $link = $(e.target); const notePath = getNotePathFromLink($link); if (notePath) { - if (e.ctrlKey) { - noteDetailService.loadNoteDetail(notePath.split("/").pop(), { newTab: true }); + if ((e.which === 1 && e.ctrlKey) || e.which === 2) { + noteDetailService.openInTab(notePath); + } + else if (e.which === 1) { + treeService.activateNote(notePath); } else { - treeService.activateNote(notePath); + return false; } } else { @@ -64,6 +65,11 @@ function goToLink(e) { window.open(address, '_blank'); } } + + e.preventDefault(); + e.stopPropagation(); + + return true; } function addLinkToEditor(linkTitle, linkHref) { @@ -129,22 +135,24 @@ $(document).on('contextmenu', ".note-detail-render a", tabContextMenu); // when click on link popup, in case of internal link, just go the the referenced note instead of default behavior // of opening the link in new window/tab -$(document).on('click', "a[data-action='note']", goToLink); -$(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink); +$(document).on('mousedown', "a[data-action='note']", goToLink); +$(document).on('mousedown', 'div.popover-content a, div.ui-tooltip-content a', goToLink); $(document).on('dblclick', '.note-detail-text a', goToLink); -$(document).on('click', '.note-detail-text a', function (e) { +$(document).on('mousedown', '.note-detail-text a', function (e) { const notePath = getNotePathFromLink($(e.target)); - if (notePath && e.ctrlKey) { + if (notePath && ((e.which === 1 && e.ctrlKey) || e.which === 2)) { // if it's a ctrl-click, then we open on new tab, otherwise normal flow (CKEditor opens link-editing dialog) e.preventDefault(); - noteDetailService.loadNoteDetail(notePath.split("/").pop(), { newTab: true }); + noteDetailService.loadNoteDetail(notePath, { newTab: true }); + + return true; } }); -$(document).on('click', '.note-detail-render a', goToLink); -$(document).on('click', '.note-detail-text.ck-read-only a', goToLink); -$(document).on('click', 'span.ck-button__label', e => { +$(document).on('mousedown', '.note-detail-render a', goToLink); +$(document).on('mousedown', '.note-detail-text.ck-read-only a', goToLink); +$(document).on('mousedown', 'span.ck-button__label', e => { // this is a link preview dialog from CKEditor link editing // for some reason clicked element is span @@ -163,5 +171,6 @@ export default { createNoteLink, addLinkToEditor, addTextToEditor, - init + init, + goToLink }; \ No newline at end of file diff --git a/src/public/javascripts/services/note_autocomplete.js b/src/public/javascripts/services/note_autocomplete.js index cb2760d47..4da90e381 100644 --- a/src/public/javascripts/services/note_autocomplete.js +++ b/src/public/javascripts/services/note_autocomplete.js @@ -46,7 +46,8 @@ function initNoteAutocomplete($el, options) { .prop("title", "Show recent notes"); const $goToSelectedNoteButton = $("") - .addClass("input-group-text go-to-selected-note-button jam jam-arrow-right"); + .addClass("input-group-text go-to-selected-note-button jam jam-arrow-right") + .attr("data-action", "note"); const $sideButtons = $("
") .addClass("input-group-append") @@ -69,14 +70,6 @@ function initNoteAutocomplete($el, options) { return false; }); - $goToSelectedNoteButton.click(() => { - if ($el.hasClass("disabled")) { - return; - } - - treeService.activateNote($el.getSelectedPath()); - }); - $el.autocomplete({ appendTo: document.querySelector('body'), hint: false, diff --git a/src/public/javascripts/services/note_detail.js b/src/public/javascripts/services/note_detail.js index ba2adb749..bc3a946b6 100644 --- a/src/public/javascripts/services/note_detail.js +++ b/src/public/javascripts/services/note_detail.js @@ -372,11 +372,14 @@ $(tabRow.el).on('contextmenu', '.note-tab', e => { contextMenuService.initContextMenu(e, { getContextMenuItems: () => { return [ + {title: "Close all tabs", cmd: "removeAllTabs", uiIcon: "empty"}, {title: "Close all tabs except for this", cmd: "removeAllTabsExceptForThis", uiIcon: "empty"} ]; }, selectContextMenuItem: (e, cmd) => { - if (cmd === 'removeAllTabsExceptForThis') { + if (cmd === 'removeAllTabs') { + tabRow.removeAllTabs(); + } else if (cmd === 'removeAllTabsExceptForThis') { tabRow.removeAllTabsExceptForThis(tab[0]); } } diff --git a/src/public/javascripts/services/note_detail_relation_map.js b/src/public/javascripts/services/note_detail_relation_map.js index 8125976db..61e983c1a 100644 --- a/src/public/javascripts/services/note_detail_relation_map.js +++ b/src/public/javascripts/services/note_detail_relation_map.js @@ -90,6 +90,7 @@ class NoteDetailRelationMap { contextMenuWidget.initContextMenu(e, { getContextMenuItems: () => { return [ + {title: "Open in new tab", cmd: "open-in-new-tab", uiIcon: "empty"}, {title: "Remove note", cmd: "remove", uiIcon: "trash"}, {title: "Edit title", cmd: "edit-title", uiIcon: "pencil"}, ]; @@ -125,7 +126,7 @@ class NoteDetailRelationMap { this.$resetPanZoomButton.click(() => { // reset to initial pan & zoom state - this.pzInstance.zoomTo(0, 0, 1 / getZoom()); + this.pzInstance.zoomTo(0, 0, 1 / this.getZoom()); this.pzInstance.moveTo(0, 0); }); @@ -138,7 +139,10 @@ class NoteDetailRelationMap { const $title = $noteBox.find(".title a"); const noteId = this.idToNoteId($noteBox.prop("id")); - if (cmd === "remove") { + if (cmd === "open-in-new-tab") { + noteDetailService.openInTab(noteId); + } + else if (cmd === "remove") { if (!await confirmDialog.confirmDeleteNoteBoxWithNote($title.text())) { return; } @@ -310,7 +314,7 @@ class NoteDetailRelationMap { maxZoom: 2, minZoom: 0.3, smoothScroll: false, - onMouseDown: function(event) { + onMouseDown: event => { if (this.clipboard) { let {x, y} = this.getMousePosition(event); @@ -402,9 +406,6 @@ class NoteDetailRelationMap { this.jsPlumbInstance.registerConnectionType("link", { anchor:"Continuous", connector:"StateMachine", overlays: linkOverlays }); this.jsPlumbInstance.bind("connection", (info, originalEvent) => this.connectionCreatedHandler(info, originalEvent)); - - // so that canvas is not panned when clicking/dragging note box - this.$relationMapContainer.on('mousedown touchstart', '.note-box, .connection-label', e => e.stopPropagation()); } async connectionCreatedHandler(info, originalEvent) { @@ -490,10 +491,17 @@ class NoteDetailRelationMap { } async createNoteBox(noteId, title, x, y) { + const $link = await linkService.createNoteLink(noteId, title); + $link.mousedown(e => { + console.log(e); + + linkService.goToLink(e); + }); + const $noteBox = $("
") .addClass("note-box") .prop("id", this.noteIdToId(noteId)) - .append($("").addClass("title").html(await linkService.createNoteLink(noteId, title))) + .append($("").addClass("title").append($link)) .append($("
").addClass("endpoint").attr("title", "Start dragging relations from here and drop them on another note.")) .css("left", x + "px") .css("top", y + "px"); diff --git a/src/public/javascripts/services/tab_row.js b/src/public/javascripts/services/tab_row.js index eaa4e16d1..10a2c3c84 100644 --- a/src/public/javascripts/services/tab_row.js +++ b/src/public/javascripts/services/tab_row.js @@ -177,6 +177,14 @@ class TabRow { setTabCloseEventListener(tabEl) { tabEl.querySelector('.note-tab-close').addEventListener('click', _ => this.removeTab(tabEl)); + + tabEl.addEventListener('mousedown', e => { + if (e.which === 2) { + this.removeTab(tabEl); + + return true; // event has been handled + } + }); } get activeTabEl() { @@ -251,6 +259,12 @@ class TabRow { this.setVisibility(); } + async removeAllTabs() { + for (const tabEl of this.tabEls) { + await this.removeTab(tabEl); + } + } + async removeAllTabsExceptForThis(remainingTabEl) { for (const tabEl of this.tabEls) { if (remainingTabEl !== tabEl) { diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js index 35c5a0a6c..5b0ae6997 100644 --- a/src/public/javascripts/services/tree.js +++ b/src/public/javascripts/services/tree.js @@ -852,6 +852,22 @@ $(window).bind('hashchange', async function() { } }); +// fancytree doesn't support middle click so this is a way to support it +$tree.on('mousedown', '.fancytree-title', e => { + if (e.which === 2) { + const node = $.ui.fancytree.getNode(e); + + treeUtils.getNotePath(node).then(notePath => { + if (notePath) { + noteDetailService.openInTab(notePath); + } + }); + + e.stopPropagation(); + e.preventDefault(); + } +}); + utils.bindShortcut('alt+c', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument $collapseTreeButton.click(() => collapseTree()); diff --git a/src/public/stylesheets/desktop.css b/src/public/stylesheets/desktop.css index ca9d4bb5a..b8f1ff742 100644 --- a/src/public/stylesheets/desktop.css +++ b/src/public/stylesheets/desktop.css @@ -93,6 +93,7 @@ body { #context-menu-container, #context-menu-container .dropdown-menu { padding: 3px 0 0; + z-index: 1111; } #context-menu-container .dropdown-item { diff --git a/src/public/stylesheets/relation_map.css b/src/public/stylesheets/relation_map.css index 062fd01ff..312b7f469 100644 --- a/src/public/stylesheets/relation_map.css +++ b/src/public/stylesheets/relation_map.css @@ -1,9 +1,9 @@ -#note-detail-relation-map { +.note-detail-relation-map { height: 100%; overflow: hidden !important; } -#relation-map-wrapper { +.relation-map-wrapper { position: relative; height: 100%; outline: none; /* remove dotted outline on click */