From 30ecd19128caa094cec8647ffcb2a1e47e67cf21 Mon Sep 17 00:00:00 2001 From: Nico Rehwaldt Date: Thu, 4 Oct 2018 16:34:47 +0200 Subject: [PATCH] WIP: experiment with native browser focus handling Related to #281 --- assets/diagram-js.css | 16 ++++- lib/features/context-pad/ContextPad.js | 18 +++-- .../interaction-events/InteractionEvents.js | 50 +++++++++++++- lib/features/outline/Outline.js | 6 +- lib/features/selection/Selection.js | 43 ++++++++++-- lib/features/selection/SelectionBehavior.js | 68 +++++++++++++++---- lib/features/selection/SelectionVisuals.js | 33 +++++++-- package.json | 2 +- 8 files changed, 196 insertions(+), 40 deletions(-) diff --git a/assets/diagram-js.css b/assets/diagram-js.css index 7c6f320dd..1d425eb18 100644 --- a/assets/diagram-js.css +++ b/assets/diagram-js.css @@ -3,12 +3,13 @@ */ .djs-outline { - fill: none; + fill: #CCCCCC66; visibility: hidden; } .djs-element.hover .djs-outline, -.djs-element.selected .djs-outline { +.djs-element.selected .djs-outline, +.djs-element.focussed .djs-outline { visibility: visible; shape-rendering: crispEdges; stroke-dasharray: 3,3; @@ -24,6 +25,11 @@ stroke-width: 1px; } +.djs-element.focussed .djs-outline { + stroke: #8888FF; + stroke-width: 2px; +} + .djs-shape.connect-ok .djs-visual > :nth-child(1) { fill: #DCFECC /* light-green */ !important; } @@ -170,10 +176,12 @@ svg.new-parent { */ .djs-shape .djs-hit { pointer-events: all; + outline: none; } .djs-connection .djs-hit { pointer-events: stroke; + outline: none; } /** @@ -358,6 +366,10 @@ svg.new-parent { background-color: #FEFEFE; box-shadow: 0 0 2px 1px #FEFEFE; pointer-events: all; + + border: none; + outline: none; + padding: 0; } .djs-context-pad .entry:before { diff --git a/lib/features/context-pad/ContextPad.js b/lib/features/context-pad/ContextPad.js index 3bc057927..53cf35d7d 100644 --- a/lib/features/context-pad/ContextPad.js +++ b/lib/features/context-pad/ContextPad.js @@ -22,20 +22,22 @@ var entrySelector = '.entry'; * * @param {EventBus} eventBus * @param {Overlays} overlays + * @param {Canvas} canvas */ -export default function ContextPad(eventBus, overlays) { +export default function ContextPad(eventBus, overlays, canvas) { this._providers = []; this._eventBus = eventBus; this._overlays = overlays; + this._canvas = canvas; this._current = null; this._init(); } -ContextPad.$inject = [ 'eventBus', 'overlays' ]; +ContextPad.$inject = [ 'eventBus', 'overlays', 'canvas' ]; /** @@ -44,15 +46,19 @@ ContextPad.$inject = [ 'eventBus', 'overlays' ]; ContextPad.prototype._init = function() { var eventBus = this._eventBus; + var canvas = this._canvas; var self = this; eventBus.on('selection.changed', function(e) { - var selection = e.newSelection; + var selection = e.newSelection, + focussed = e.focussed; - if (selection.length === 1) { - self.open(selection[0]); + var target = selection.length === 1 && (focussed || selection[0]); + + if (target && canvas.getRootElement() !== target) { + self.open(target); } else { self.close(); } @@ -179,7 +185,7 @@ ContextPad.prototype._updateAndOpen = function(element) { forEach(entries, function(entry, id) { var grouping = entry.group || 'default', - control = domify(entry.html || '
'), + control = domify(entry.html || ''), container; domAttr(control, 'data-action', id); diff --git a/lib/features/interaction-events/InteractionEvents.js b/lib/features/interaction-events/InteractionEvents.js index 34bcf2f11..42ac5e326 100644 --- a/lib/features/interaction-events/InteractionEvents.js +++ b/lib/features/interaction-events/InteractionEvents.js @@ -47,7 +47,8 @@ export default function InteractionEvents(eventBus, elementRegistry, styles) { var HIT_STYLE = styles.cls('djs-hit', [ 'no-fill', 'no-border' ], { stroke: 'white', - strokeWidth: 15 + strokeWidth: 15, + tabindex: 0 }); /** @@ -117,7 +118,9 @@ export default function InteractionEvents(eventBus, elementRegistry, styles) { dblclick: 'element.dblclick', mousedown: 'element.mousedown', mouseup: 'element.mouseup', - contextmenu: 'element.contextmenu' + contextmenu: 'element.contextmenu', + focus: 'element.focus', + blur: 'element.blur' }; var ignoredFilters = { @@ -219,6 +222,27 @@ export default function InteractionEvents(eventBus, elementRegistry, styles) { svgAppend(gfx, hit); }); + eventBus.on('selection.changed', 500, function(event) { + + var newSelection = event.newSelection; + + var element = newSelection[0]; + + if (!element) { + return; + } + + var gfx = elementRegistry.getGraphics(element); + + var hit = domQuery('.djs-hit', gfx); + + if (newSelection.length) { + if (document.activeElement !== hit) { + // hit.focus(); + } + } + }); + // Update djs-hit on change. // A low priortity is necessary, because djs-hit of labels has to be updated // after the label bounds have been updated in the renderer. @@ -340,4 +364,26 @@ InteractionEvents.$inject = [ * @property {djs.model.Base} element * @property {SVGElement} gfx * @property {Event} originalEvent + */ + +/** + * An event indicating that the element received user focus. + * + * @event element.focus + * + * @type {Object} + * @property {djs.model.Base} element + * @property {SVGElement} gfx + * @property {Event} originalEvent + */ + +/** + * An event indicating that the element lost user focus. + * + * @event element.blur + * + * @type {Object} + * @property {djs.model.Base} element + * @property {SVGElement} gfx + * @property {Event} originalEvent */ \ No newline at end of file diff --git a/lib/features/outline/Outline.js b/lib/features/outline/Outline.js index 11053658c..4020586ed 100644 --- a/lib/features/outline/Outline.js +++ b/lib/features/outline/Outline.js @@ -3,7 +3,7 @@ import { getBBox } from '../../util/Elements'; var LOW_PRIORITY = 500; import { - append as svgAppend, + prepend as svgPrepend, attr as svgAttr, create as svgCreate } from 'tiny-svg'; @@ -31,7 +31,7 @@ export default function Outline(eventBus, styles, elementRegistry) { this.offset = 6; - var OUTLINE_STYLE = styles.cls('djs-outline', [ 'no-fill' ]); + var OUTLINE_STYLE = styles.cls('djs-outline'); var self = this; @@ -45,7 +45,7 @@ export default function Outline(eventBus, styles, elementRegistry) { height: 100 }, OUTLINE_STYLE)); - svgAppend(gfx, outline); + svgPrepend(gfx, outline); return outline; } diff --git a/lib/features/selection/Selection.js b/lib/features/selection/Selection.js index e65de0dee..dbdce046e 100644 --- a/lib/features/selection/Selection.js +++ b/lib/features/selection/Selection.js @@ -36,6 +36,8 @@ Selection.$inject = [ 'eventBus' ]; Selection.prototype.deselect = function(element) { var selectedElements = this._selectedElements; + console.log(`deselect #${element.id}`); + var idx = selectedElements.indexOf(element); if (idx !== -1) { @@ -69,8 +71,9 @@ Selection.prototype.isSelected = function(element) { * @param {boolean} [add] whether the element(s) should be appended to the current selection, defaults to false */ Selection.prototype.select = function(elements, add) { - var selectedElements = this._selectedElements, - oldSelection = selectedElements.slice(); + + var oldSelection = this._selectedElements, + newSelection = oldSelection; if (!isArray(elements)) { elements = elements ? [ elements ] : []; @@ -80,16 +83,44 @@ Selection.prototype.select = function(elements, add) { // to the method if (add) { forEach(elements, function(element) { - if (selectedElements.indexOf(element) !== -1) { + if (newSelection.indexOf(element) !== -1) { // already selected return; } else { - selectedElements.push(element); + newSelection = newSelection.concat(element); } }); } else { - this._selectedElements = selectedElements = elements.slice(); + newSelection = elements.slice(); } - this._eventBus.fire('selection.changed', { oldSelection: oldSelection, newSelection: selectedElements }); + // only emit selection changed on actual changes + if (newSelection !== oldSelection) { + + console.log(`select #${elements.map(e => e.id).join(', ')} (add=${add})`); + + this._selectedElements = newSelection; + + this._eventBus.fire('selection.changed', { + oldSelection: oldSelection, + newSelection: newSelection, + selection: newSelection, + focussed: this._focussedElement + }); + } }; + + +Selection.prototype.setFocussed = function(element) { + + const oldFocussed = this._focussedElement; + + this._focussedElement = element; + + this._eventBus.fire('selection.focusChanged', { + oldFocussed: oldFocussed, + newFocussed: element, + focussed: element, + selection: this._selectedElements + }); +}; \ No newline at end of file diff --git a/lib/features/selection/SelectionBehavior.js b/lib/features/selection/SelectionBehavior.js index c7ac6e426..d6778752d 100644 --- a/lib/features/selection/SelectionBehavior.js +++ b/lib/features/selection/SelectionBehavior.js @@ -1,5 +1,6 @@ import { - hasPrimaryModifier + hasPrimaryModifier, + hasSecondaryModifier } from '../../util/Mouse'; import { @@ -50,32 +51,69 @@ export default function SelectionBehavior( var element = event.element; - // do not select the root element - // or connections + // don't select root element if (element === canvas.getRootElement()) { element = null; } - var isSelected = selection.isSelected(element), - isMultiSelect = selection.get().length > 1; + console.log(`click #${element && element.id}`); - // mouse-event: SELECTION_KEY - var add = hasPrimaryModifier(event); + // SHIFT + CLICK : add element to selection + var forceAdd = hasSecondaryModifier(event); - // select OR deselect element in multi selection - if (isSelected && isMultiSelect) { - if (add) { + if (forceAdd) { + return selection.select(element, true); + } + + var isSelected = selection.isSelected(element); + + // CTRL + CLICK : toggle element selection + var toggleAdd = hasPrimaryModifier(event); + + if (toggleAdd) { + if (isSelected) { return selection.deselect(element); } else { - return selection.select(element); + return selection.select(element, true); } - } else + } + + // CLICK : select element if (!isSelected) { - selection.select(element, add); - } else { - selection.deselect(element); + selection.select(element); } }); + + eventBus.on('element.focus', function(event) { + var element = event.element; + + console.log(`focus #${element.id}`); + + selection.setFocussed(element); + + const timeout = setTimeout(function() { + + console.log('FOCUS WITHOUT CLICK'); + + if (selection.isSelected(element)) { + return; + } + + selection.select(element, false); + }, 100); + + eventBus.on('element.click', function() { + clearTimeout(timeout); + }); + }); + + eventBus.on('element.blur', function(event) { + var element = event.element; + + console.log(`blur #${element.id}`); + + selection.setFocussed(false); + }); } SelectionBehavior.$inject = [ diff --git a/lib/features/selection/SelectionVisuals.js b/lib/features/selection/SelectionVisuals.js index 6d60ebe81..529a51d43 100644 --- a/lib/features/selection/SelectionVisuals.js +++ b/lib/features/selection/SelectionVisuals.js @@ -3,7 +3,8 @@ import { } from 'min-dash'; var MARKER_HOVER = 'hover', - MARKER_SELECTED = 'selected'; + MARKER_SELECTED = 'selected', + MARKER_FOCUSSED = 'focussed'; /** @@ -40,11 +41,11 @@ export default function SelectionVisuals(events, canvas, selection, styles) { events.on('selection.changed', function(event) { - function deselect(s) { + function unmarkSelected(s) { removeMarker(s, MARKER_SELECTED); } - function select(s) { + function markSelected(s) { addMarker(s, MARKER_SELECTED); } @@ -53,16 +54,38 @@ export default function SelectionVisuals(events, canvas, selection, styles) { forEach(oldSelection, function(e) { if (newSelection.indexOf(e) === -1) { - deselect(e); + unmarkSelected(e); } }); forEach(newSelection, function(e) { if (oldSelection.indexOf(e) === -1) { - select(e); + markSelected(e); } }); }); + + events.on('selection.focusChanged', function(event) { + + function unmarkFocussed(s) { + removeMarker(s, MARKER_FOCUSSED); + } + + function markFocussed(s) { + addMarker(s, MARKER_FOCUSSED); + } + + var oldFocussed = event.oldFocussed, + newFocussed = event.newFocussed; + + if (oldFocussed) { + unmarkFocussed(oldFocussed); + } + + if (newFocussed) { + markFocussed(newFocussed); + } + }); } SelectionVisuals.$inject = [ diff --git a/package.json b/package.json index 9934bd326..7e07ebd38 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,6 @@ "min-dom": "^3.0.0", "object-refs": "^0.3.0", "path-intersection": "^1.0.2", - "tiny-svg": "^2.0.0" + "tiny-svg": "^2.2.0" } }