From afa27bf42c0b0873307f99e6b980894058deff7e Mon Sep 17 00:00:00 2001 From: barsdeveloper Date: Sat, 6 Nov 2021 20:59:00 +0100 Subject: [PATCH] Blueprint focusable --- css/ueblueprint-style.css | 18 +++- dist/ueblueprint.js | 180 ++++++++++++++++++++++++++------ js/Blueprint.js | 27 +++-- js/graph/SelectableDraggable.js | 4 +- js/input/Context.js | 23 ++++ js/input/Drag.js | 3 +- js/input/KeyboardShortcut.js | 46 ++++++++ js/input/MouseClickDrag.js | 2 + js/input/MouseWheel.js | 19 +++- js/input/Paste.js | 29 ++--- js/input/Pointing.js | 8 +- js/input/Unfocus.js | 35 +++++++ js/input/Zoom.js | 3 +- 13 files changed, 318 insertions(+), 79 deletions(-) create mode 100644 js/input/Context.js create mode 100644 js/input/KeyboardShortcut.js create mode 100644 js/input/Unfocus.js diff --git a/css/ueblueprint-style.css b/css/ueblueprint-style.css index 7d64267..7a272b7 100644 --- a/css/ueblueprint-style.css +++ b/css/ueblueprint-style.css @@ -55,10 +55,14 @@ u-blueprint { position : relative; height : var(--ueb-viewport-height); width : var(--ueb-viewport-width); - overflow : scroll; + overflow : hidden; scrollbar-width: 0; } +u-blueprint[data-focused="true"] .ueb-viewport-body { + overflow: scroll; +} + .ueb-grid { --ueb-grid-line-actual-width: calc(var(--ueb-grid-line-width) / var(--ueb-scale)); position : absolute; @@ -297,10 +301,10 @@ u-blueprint { display : block; position : absolute; visibility: hidden; - top : min(var(--ueb-select-from-y) * 1px, var(--ueb-select-to-y) * 1px); - left : min(var(--ueb-select-from-x) * 1px, var(--ueb-select-to-x) * 1px); - width : calc(max(var(--ueb-select-from-x) - var(--ueb-select-to-x), var(--ueb-select-to-x) - var(--ueb-select-from-x)) * 1px); - height : calc(max(var(--ueb-select-from-y) - var(--ueb-select-to-y), var(--ueb-select-to-y) - var(--ueb-select-from-y)) * 1px); + top : 0; + left : 0; + width : 0; + height : 0; background-image: /* Top */ repeating-linear-gradient(90deg, @@ -389,4 +393,8 @@ u-blueprint { .ueb-selector[data-selecting="true"] { visibility: visible; + top : min(var(--ueb-select-from-y) * 1px, var(--ueb-select-to-y) * 1px); + left : min(var(--ueb-select-from-x) * 1px, var(--ueb-select-to-x) * 1px); + width : calc(max(var(--ueb-select-from-x) - var(--ueb-select-to-x), var(--ueb-select-to-x) - var(--ueb-select-from-x)) * 1px); + height : calc(max(var(--ueb-select-from-y) - var(--ueb-select-to-y), var(--ueb-select-to-y) - var(--ueb-select-from-y)) * 1px); } \ No newline at end of file diff --git a/dist/ueblueprint.js b/dist/ueblueprint.js index 176ca24..fdb0d5b 100644 --- a/dist/ueblueprint.js +++ b/dist/ueblueprint.js @@ -798,13 +798,34 @@ class BlueprintTemplate extends Template { } } -class Pointing { +class Context { + + constructor(target, blueprint, options) { + /** @type {HTMLElement} */ + this.target = target; + /** @type {import("../Blueprint").default}" */ + this.blueprint = blueprint; + this.options = options; + if (options?.wantsFocusCallback ?? false) { + let self = this; + this.blueprint.addEventListener("blueprintfocus", _ => self.blueprintFocused()); + this.blueprint.addEventListener("blueprintunfocus", _ => self.blueprintUnfocused()); + } + } + + blueprintFocused() { + console.log("focused"); + } + + blueprintUnfocused() { + } + +} + +class Pointing extends Context { constructor(target, blueprint, options) { - /** @type {HTMLElement} */ - this.target = target; - /** @type {import("../Blueprint").Blueprint}" */ - this.blueprint = blueprint; + super(target, blueprint, options); this.movementSpace = this.blueprint?.getGridDOMElement() ?? document.documentElement; } @@ -835,6 +856,7 @@ class MouseClickDrag extends Pointing { let self = this; this.mouseDownHandler = e => { + this.blueprint.setFocused(true); switch (e.button) { case self.clickButton: // Either doesn't matter or consider the click only when clicking on the parent, not descandants @@ -1344,10 +1366,11 @@ class MouseWheel extends Pointing { /** * * @param {HTMLElement} target - * @param {import("../Blueprint").Blueprint} blueprint + * @param {import("../Blueprint").default} blueprint * @param {Object} options */ constructor(target, blueprint, options) { + options.wantsFocusCallback = true; super(target, blueprint, options); this.looseTarget = options?.looseTarget ?? true; let self = this; @@ -1357,15 +1380,25 @@ class MouseWheel extends Pointing { const location = self.getLocation(e); self.wheel(Math.sign(e.deltaY), location); }; + this.mouseParentWheelHandler = e => e.preventDefault(); + if (this.blueprint.focused) { + this.movementSpace.addEventListener('wheel', this.mouseWheelHandler, false); + } + } + + blueprintFocused() { this.movementSpace.addEventListener('wheel', this.mouseWheelHandler, false); - // Prevent movement space from being scrolled - this.movementSpace.parentElement?.addEventListener('wheel', e => e.preventDefault()); + this.movementSpace.parentElement?.addEventListener('wheel', this.mouseParentWheelHandler); + } + + blueprintUnfocused() { + this.movementSpace.removeEventListener('wheel', this.mouseWheelHandler, false); + this.movementSpace.parentElement?.removeEventListener('wheel', this.mouseParentWheelHandler); } /* Subclasses will override the following method */ wheel(variation, location) { - } } @@ -1494,7 +1527,7 @@ class Drag extends MouseClickDrag { return } - this.target.dragDispatch(d); + this.target.dispatchDragEvent(d); // Reassign the position of mouse this.mousePosition = mousePosition; @@ -1517,7 +1550,7 @@ class SelectableDraggable extends GraphElement { connectedCallback() { super.connectedCallback(); - this.dragObject = new Drag(this, null, { // UDrag doesn't need blueprint + this.dragObject = new Drag(this, this.blueprint, { // UDrag doesn't need blueprint looseTarget: true }); } @@ -1536,7 +1569,7 @@ class SelectableDraggable extends GraphElement { this.setLocation([this.location[0] + value[0], this.location[1] + value[1]]); } - dragDispatch(value) { + dispatchDragEvent(value) { if (!this.selected) { this.blueprint.unselectAll(); this.setSelected(true); @@ -1597,40 +1630,103 @@ class GraphNode extends SelectableDraggable { customElements.define('u-node', GraphNode); -class Paste { +class KeyboardShortcut extends Context { constructor(target, blueprint, options) { - /** @type {HTMLElement} */ - this.target = target; - /** @type {import("../Blueprint").default}" */ - this.blueprint = blueprint; - this.serializer = new ObjectSerializer(); + super(target, blueprint, options); + /** @type {String[]} */ + this.keys = this.options?.keys ?? []; + /** @type Numeric */ + this.currentKey = 0; + let self = this; - this.pasteHandler = e => { - self.pasted(e.clipboardData.getData("text")); + this.keyDownHandler = e => { + e.preventDefault(); + if (Utility.equals(e.keys[self.currentKey], e.key)) { + if (++self.currentKey == this.keys.length) { + self.fire(); + } + } }; - this.target.addEventListener("paste", this.pasteHandler); - } - - pasted(value) { - let nodes = this.serializer.readMultiple(value).map(entity => new GraphNode(entity)); - this.blueprint.addNode(...nodes); + this.keyUpHandler = e => { + e.preventDefault(); + for (let i = 0; i < self.currentKey; ++i) { + if (Utility.equals(e.keys[self.currentKey], e.key)) { + self.currentKey = i; + break + } + } + }; + if (this.keys.length > 0) { + this.target.addEventListener("keydown", this.keyDownHandler); + this.target.addEventListener("keyup", this.keyUpHandler); + } } unlistenDOMElement() { - this.target.removeEventListener("paste", this.pasteHandler); + this.target.removeEventListener('keydown', this.keyDownHandler); + this.target.removeEventListener('keyup', this.keyUpHandler); + } + + fire() { } } +class Paste extends KeyboardShortcut { + + constructor(target, blueprint) { + super(target, blueprint, { + keys: ["Control", "C"] + }); + } + + fire() { + let value = navigator.clipboard.readText(); + let nodes = this.serializer.readMultiple(value).map(entity => new GraphNode(entity)); + this.blueprint.addNode(...nodes); + } + +} + +class Unfocus extends Context { + + constructor(target, blueprint, options = {}) { + options.wantsFocusCallback = true; + super(target, blueprint, options); + + let self = this; + this.clickHandler = e => self.clickedSomewhere(e); + if (this.blueprint.focuse) { + document.addEventListener("click", this.clickHandler); + } + } + + /** + * + * @param {MouseEvent} e + */ + clickedSomewhere(e) { + // If target is inside the blueprint grid + if (e.target.closest("u-blueprint")) { + return + } + this.blueprint.setFocused(false); + } + + blueprintFocused() { + document.addEventListener("click", this.clickHandler); + } + + blueprintUnfocused() { + document.removeEventListener("click", this.clickHandler); + } +} + /** @typedef {import("./graph/GraphNode").default} GraphNode */ class Blueprint extends GraphElement { - insertChildren() { - this.querySelector('[data-nodes]').append(...this.entity.nodes); - } - constructor() { super(new BlueprintData(), new BlueprintTemplate()); /** @type {HTMLElement} */ @@ -1649,8 +1745,9 @@ class Blueprint extends GraphElement { this.zoom = 0; /** @type {HTMLElement} */ this.headerElement = null; + this.focused = false; /** @type {(node: GraphNode) => BoundariesInfo} */ - this.nodeBoundariesSupplier = (node) => { + this.nodeBoundariesSupplier = node => { let rect = node.getBoundingClientRect(); let gridRect = this.nodesContainerElement.getBoundingClientRect(); const scaleCorrection = 1 / this.getScale(); @@ -1684,7 +1781,9 @@ class Blueprint extends GraphElement { this.nodesContainerElement = this.querySelector('[data-nodes]'); console.assert(this.nodesContainerElement, "Nodes container element not provided by the template."); this.nodesContainerElement.append(this.selectorElement); - this.insertChildren(); + this.querySelector('[data-nodes]').append(...this.entity.nodes); + + this.pasteObject = new Paste(this.getGridDOMElement(), this); this.dragObject = new DragScroll(this.getGridDOMElement(), this, { clickButton: 2, @@ -1702,7 +1801,7 @@ class Blueprint extends GraphElement { exitAnyButton: true }); - this.pasteObject = new Paste(this.getGridDOMElement(), this); + this.unfocusObject = new Unfocus(this.getGridDOMElement(), this); } getGridDOMElement() { @@ -1928,6 +2027,19 @@ class Blueprint extends GraphElement { this.nodesContainerElement.append(...graphNodes); } } + + setFocused(value = true) { + if (this.focused == value) { + return; + } + let event = new CustomEvent(value ? "blueprintfocus" : "blueprintunfocus"); + this.focused = value; + this.dataset.focused = this.focused; + if (!this.focused) { + this.unselectAll(); + } + this.dispatchEvent(event); + } } customElements.define('u-blueprint', Blueprint); diff --git a/js/Blueprint.js b/js/Blueprint.js index b72f5ad..fae734e 100755 --- a/js/Blueprint.js +++ b/js/Blueprint.js @@ -7,14 +7,11 @@ import Utility from "./Utility" import Zoom from "./input/Zoom" import BlueprintData from "./BlueprintData" import Paste from "./input/Paste" +import Unfocus from "./input/Unfocus" /** @typedef {import("./graph/GraphNode").default} GraphNode */ export default class Blueprint extends GraphElement { - insertChildren() { - this.querySelector('[data-nodes]').append(...this.entity.nodes) - } - constructor() { super(new BlueprintData(), new BlueprintTemplate()) /** @type {HTMLElement} */ @@ -33,8 +30,9 @@ export default class Blueprint extends GraphElement { this.zoom = 0 /** @type {HTMLElement} */ this.headerElement = null + this.focused = false /** @type {(node: GraphNode) => BoundariesInfo} */ - this.nodeBoundariesSupplier = (node) => { + this.nodeBoundariesSupplier = node => { let rect = node.getBoundingClientRect() let gridRect = this.nodesContainerElement.getBoundingClientRect() const scaleCorrection = 1 / this.getScale() @@ -68,7 +66,9 @@ export default class Blueprint extends GraphElement { this.nodesContainerElement = this.querySelector('[data-nodes]') console.assert(this.nodesContainerElement, "Nodes container element not provided by the template.") this.nodesContainerElement.append(this.selectorElement) - this.insertChildren() + this.querySelector('[data-nodes]').append(...this.entity.nodes) + + this.pasteObject = new Paste(this.getGridDOMElement(), this) this.dragObject = new DragScroll(this.getGridDOMElement(), this, { clickButton: 2, @@ -86,7 +86,7 @@ export default class Blueprint extends GraphElement { exitAnyButton: true }) - this.pasteObject = new Paste(this.getGridDOMElement(), this) + this.unfocusObject = new Unfocus(this.getGridDOMElement(), this) } getGridDOMElement() { @@ -312,6 +312,19 @@ export default class Blueprint extends GraphElement { this.nodesContainerElement.append(...graphNodes) } } + + setFocused(value = true) { + if (this.focused == value) { + return; + } + let event = new CustomEvent(value ? "blueprintfocus" : "blueprintunfocus") + this.focused = value + this.dataset.focused = this.focused + if (!this.focused) { + this.unselectAll() + } + this.dispatchEvent(event) + } } customElements.define('u-blueprint', Blueprint) diff --git a/js/graph/SelectableDraggable.js b/js/graph/SelectableDraggable.js index f394c34..ee0251b 100755 --- a/js/graph/SelectableDraggable.js +++ b/js/graph/SelectableDraggable.js @@ -17,7 +17,7 @@ export default class SelectableDraggable extends GraphElement { connectedCallback() { super.connectedCallback() - this.dragObject = new Drag(this, null, { // UDrag doesn't need blueprint + this.dragObject = new Drag(this, this.blueprint, { // UDrag doesn't need blueprint looseTarget: true }) } @@ -36,7 +36,7 @@ export default class SelectableDraggable extends GraphElement { this.setLocation([this.location[0] + value[0], this.location[1] + value[1]]) } - dragDispatch(value) { + dispatchDragEvent(value) { if (!this.selected) { this.blueprint.unselectAll() this.setSelected(true) diff --git a/js/input/Context.js b/js/input/Context.js new file mode 100644 index 0000000..73cb793 --- /dev/null +++ b/js/input/Context.js @@ -0,0 +1,23 @@ +export default class Context { + + constructor(target, blueprint, options) { + /** @type {HTMLElement} */ + this.target = target + /** @type {import("../Blueprint").default}" */ + this.blueprint = blueprint + this.options = options + if (options?.wantsFocusCallback ?? false) { + let self = this + this.blueprint.addEventListener("blueprintfocus", _ => self.blueprintFocused()) + this.blueprint.addEventListener("blueprintunfocus", _ => self.blueprintUnfocused()) + } + } + + blueprintFocused() { + console.log("focused") + } + + blueprintUnfocused() { + } + +} diff --git a/js/input/Drag.js b/js/input/Drag.js index d5875e3..97d1cb0 100755 --- a/js/input/Drag.js +++ b/js/input/Drag.js @@ -1,6 +1,7 @@ import MouseClickDrag from "./MouseClickDrag" export default class Drag extends MouseClickDrag { + constructor(target, blueprint, options) { super(target, blueprint, options) this.stepSize = parseInt(options?.stepSize) @@ -33,7 +34,7 @@ export default class Drag extends MouseClickDrag { return } - this.target.dragDispatch(d) + this.target.dispatchDragEvent(d) // Reassign the position of mouse this.mousePosition = mousePosition diff --git a/js/input/KeyboardShortcut.js b/js/input/KeyboardShortcut.js new file mode 100644 index 0000000..8868eff --- /dev/null +++ b/js/input/KeyboardShortcut.js @@ -0,0 +1,46 @@ +import Context from "./Context" +import Utility from "../Utility" + +export default class KeyboardShortcut extends Context { + + constructor(target, blueprint, options) { + super(target, blueprint, options) + /** @type {String[]} */ + this.keys = this.options?.keys ?? [] + /** @type Numeric */ + this.currentKey = 0 + + + let self = this + this.keyDownHandler = e => { + e.preventDefault() + if (Utility.equals(e.keys[self.currentKey], e.key)) { + if (++self.currentKey == this.keys.length) { + self.fire() + } + } + } + this.keyUpHandler = e => { + e.preventDefault() + for (let i = 0; i < self.currentKey; ++i) { + if (Utility.equals(e.keys[self.currentKey], e.key)) { + self.currentKey = i + break + } + } + } + if (this.keys.length > 0) { + this.target.addEventListener("keydown", this.keyDownHandler) + this.target.addEventListener("keyup", this.keyUpHandler) + } + } + + unlistenDOMElement() { + this.target.removeEventListener('keydown', this.keyDownHandler) + this.target.removeEventListener('keyup', this.keyUpHandler) + } + + fire() { + } + +} diff --git a/js/input/MouseClickDrag.js b/js/input/MouseClickDrag.js index d42ee12..7cce94c 100755 --- a/js/input/MouseClickDrag.js +++ b/js/input/MouseClickDrag.js @@ -4,6 +4,7 @@ import Pointing from "./Pointing" * This class manages the ui gesture of mouse click and drag. Tha actual operations are implemented by the subclasses. */ export default class MouseClickDrag extends Pointing { + constructor(target, blueprint, options) { super(target, blueprint, options) this.clickButton = options?.clickButton ?? 0 @@ -16,6 +17,7 @@ export default class MouseClickDrag extends Pointing { let self = this this.mouseDownHandler = e => { + this.blueprint.setFocused(true) switch (e.button) { case self.clickButton: // Either doesn't matter or consider the click only when clicking on the parent, not descandants diff --git a/js/input/MouseWheel.js b/js/input/MouseWheel.js index ea95d83..79a66ee 100755 --- a/js/input/MouseWheel.js +++ b/js/input/MouseWheel.js @@ -5,10 +5,11 @@ export default class MouseWheel extends Pointing { /** * * @param {HTMLElement} target - * @param {import("../Blueprint").Blueprint} blueprint + * @param {import("../Blueprint").default} blueprint * @param {Object} options */ constructor(target, blueprint, options) { + options.wantsFocusCallback = true super(target, blueprint, options) this.looseTarget = options?.looseTarget ?? true let self = this @@ -18,14 +19,24 @@ export default class MouseWheel extends Pointing { const location = self.getLocation(e) self.wheel(Math.sign(e.deltaY), location) } + this.mouseParentWheelHandler = e => e.preventDefault() + if (this.blueprint.focused) { + this.movementSpace.addEventListener('wheel', this.mouseWheelHandler, false) + } + } + + blueprintFocused() { this.movementSpace.addEventListener('wheel', this.mouseWheelHandler, false) - // Prevent movement space from being scrolled - this.movementSpace.parentElement?.addEventListener('wheel', e => e.preventDefault()) + this.movementSpace.parentElement?.addEventListener('wheel', this.mouseParentWheelHandler) + } + + blueprintUnfocused() { + this.movementSpace.removeEventListener('wheel', this.mouseWheelHandler, false) + this.movementSpace.parentElement?.removeEventListener('wheel', this.mouseParentWheelHandler) } /* Subclasses will override the following method */ wheel(variation, location) { - } } diff --git a/js/input/Paste.js b/js/input/Paste.js index c9db9cd..04be027 100644 --- a/js/input/Paste.js +++ b/js/input/Paste.js @@ -1,29 +1,18 @@ import GraphNode from "../graph/GraphNode" -import ObjectSerializer from "../serialization/ObjectSerializer" +import KeyboardShortcut from "./KeyboardShortcut" -export default class Paste { +export default class Paste extends KeyboardShortcut { - constructor(target, blueprint, options) { - /** @type {HTMLElement} */ - this.target = target - /** @type {import("../Blueprint").default}" */ - this.blueprint = blueprint - this.serializer = new ObjectSerializer() - - let self = this - this.pasteHandler = e => { - self.pasted(e.clipboardData.getData("text")) - } - this.target.addEventListener("paste", this.pasteHandler) + constructor(target, blueprint) { + super(target, blueprint, { + keys: ["Control", "C"] + }) } - pasted(value) { + fire() { + let value = navigator.clipboard.readText() let nodes = this.serializer.readMultiple(value).map(entity => new GraphNode(entity)) this.blueprint.addNode(...nodes) } - unlistenDOMElement() { - this.target.removeEventListener("paste", this.pasteHandler) - } - -} \ No newline at end of file +} diff --git a/js/input/Pointing.js b/js/input/Pointing.js index d738130..569f92e 100755 --- a/js/input/Pointing.js +++ b/js/input/Pointing.js @@ -1,12 +1,10 @@ +import Context from "./Context" import Utility from "../Utility" -export default class Pointing { +export default class Pointing extends Context { constructor(target, blueprint, options) { - /** @type {HTMLElement} */ - this.target = target - /** @type {import("../Blueprint").Blueprint}" */ - this.blueprint = blueprint + super(target, blueprint, options) this.movementSpace = this.blueprint?.getGridDOMElement() ?? document.documentElement } diff --git a/js/input/Unfocus.js b/js/input/Unfocus.js new file mode 100644 index 0000000..b370569 --- /dev/null +++ b/js/input/Unfocus.js @@ -0,0 +1,35 @@ +import Context from "./Context"; + +export default class Unfocus extends Context { + + constructor(target, blueprint, options = {}) { + options.wantsFocusCallback = true + super(target, blueprint, options) + + let self = this + this.clickHandler = e => self.clickedSomewhere(e) + if (this.blueprint.focuse) { + document.addEventListener("click", this.clickHandler) + } + } + + /** + * + * @param {MouseEvent} e + */ + clickedSomewhere(e) { + // If target is inside the blueprint grid + if (e.target.closest("u-blueprint")) { + return + } + this.blueprint.setFocused(false) + } + + blueprintFocused() { + document.addEventListener("click", this.clickHandler) + } + + blueprintUnfocused() { + document.removeEventListener("click", this.clickHandler) + } +} diff --git a/js/input/Zoom.js b/js/input/Zoom.js index 3987f42..2d62460 100755 --- a/js/input/Zoom.js +++ b/js/input/Zoom.js @@ -1,6 +1,7 @@ -import MouseWheel from "./MouseWheel"; +import MouseWheel from "./MouseWheel" export default class Zoom extends MouseWheel { + wheel(variation, location) { let zoomLevel = this.blueprint.getZoom() zoomLevel -= variation