From ceb07688f2c8788a6dbc3f7dc6b69ab88ba28630 Mon Sep 17 00:00:00 2001 From: barsdeveloper Date: Sun, 31 Oct 2021 17:01:56 +0100 Subject: [PATCH] Paste event handling WIP --- css/ueblueprint-style.css | 2 +- dist/ueblueprint.js | 480 +++++++++++++++------------ js/Blueprint.js | 4 + js/entity/ObjectEntity.js | 8 + js/entity/PinEntity.js | 8 + js/input/MouseClickDrag.js | 8 +- js/input/MouseWheel.js | 2 +- js/input/Paste.js | 29 ++ js/serialization/ObjectSerializer.js | 14 + js/template/NodeTemplate.js | 16 +- js/template/Template.js | 2 +- 11 files changed, 352 insertions(+), 221 deletions(-) create mode 100644 js/input/Paste.js diff --git a/css/ueblueprint-style.css b/css/ueblueprint-style.css index 9ca2294..7d64267 100644 --- a/css/ueblueprint-style.css +++ b/css/ueblueprint-style.css @@ -252,7 +252,7 @@ u-blueprint { } .ueb-node-inputs { - margin-right: 2em; + margin-right: auto; } .ueb-node-value-icon { diff --git a/dist/ueblueprint.js b/dist/ueblueprint.js index 25d00a3..176ca24 100644 --- a/dist/ueblueprint.js +++ b/dist/ueblueprint.js @@ -323,6 +323,14 @@ class PinEntity extends Entity { return PinEntity.attributes } + /** + * + * @returns {String} + */ + getPinDisplayName() { + return this.PinName + } + isOutput() { if (this.Direction === "EGPD_Output") { return true @@ -361,6 +369,14 @@ class ObjectEntity extends Entity { getAttributes() { return ObjectEntity.attributes } + + /** + * + * @returns {String} The name of the node + */ + getNodeDisplayName() { + return this.Name + } } var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; @@ -681,6 +697,20 @@ class ObjectSerializer extends Serializer { return parseResult.value } + /** + * + * @param {String} value + * @returns {ObjectEntity[]} + */ + readMultiple(value) { + const parseResult = Serializer.grammar.MultipleObject.parse(value); + if (!parseResult.status) { + console.error("Error when trying to parse the object."); + return parseResult + } + return parseResult.value + } + /** * * @param {ObjectEntity} object @@ -708,7 +738,7 @@ class Template { * @returns The computed html */ render(entity) { - return `` + return "" } /** @@ -804,7 +834,7 @@ class MouseClickDrag extends Pointing { const movementListenedElement = this.moveEverywhere ? document.documentElement : this.movementSpace; let self = this; - this.mouseDownHandler = function (e) { + this.mouseDownHandler = e => { switch (e.button) { case self.clickButton: // Either doesn't matter or consider the click only when clicking on the parent, not descandants @@ -826,7 +856,7 @@ class MouseClickDrag extends Pointing { } }; - this.mouseStartedMovingHandler = function (e) { + this.mouseStartedMovingHandler = e => { e.preventDefault(); e.stopPropagation(); @@ -839,7 +869,7 @@ class MouseClickDrag extends Pointing { self.started = true; }; - this.mouseMoveHandler = function (e) { + this.mouseMoveHandler = e => { e.preventDefault(); e.stopPropagation(); const location = self.getLocation(e); @@ -847,7 +877,7 @@ class MouseClickDrag extends Pointing { self.dragTo(location, movement); }; - this.mouseUpHandler = function (e) { + this.mouseUpHandler = e => { if (!self.exitAnyButton || e.button == self.clickButton) { // Remove the handlers of "mousemove" and "mouseup" movementListenedElement.removeEventListener('mousemove', self.mouseStartedMovingHandler); @@ -1322,7 +1352,7 @@ class MouseWheel extends Pointing { this.looseTarget = options?.looseTarget ?? true; let self = this; - this.mouseWheelHandler = function (e) { + this.mouseWheelHandler = e => { e.preventDefault(); const location = self.getLocation(e); self.wheel(Math.sign(e.deltaY), location); @@ -1361,6 +1391,239 @@ class BlueprintData { } } +/** + * @typedef {import("../entity/ObjectEntity").default} ObjectEntity + */ +class NodeTemplate extends Template { + + /** + * Computes the html content of the target element. + * @param {ObjectEntity} entity Entity representing the element + * @returns The computed html + */ + header(entity) { + return ` +
+ + + ${entity.getNodeDisplayName()} + +
+ ` + } + + /** + * Computes the html content of the target element. + * @param {ObjectEntity} entity Entity representing the element + * @returns The computed html + */ + body(entity) { + /** @type {PinEntity[]} */ + let inputs = entity.CustomProperties.filter(v => v instanceof PinEntity); + let outputs = inputs.filter(v => v.isOutput()); + inputs = inputs.filter(v => !v.isOutput()); + return ` +
+
+ ${inputs.map((input, index) => ` +
+ + ${input.getPinDisplayName()} +
+ `).join("") ?? ""} +
+
+ ${outputs.map((output, index) => ` +
+ ${output.getPinDisplayName()} + +
+ `).join("") ?? ''} +
+
+ ` + } + + /** + * Computes the html content of the target element. + * @param {ObjectEntity} entity Entity representing the element + * @returns The computed html + */ + render(entity) { + return ` +
+
+ ${this.header(entity)} + ${this.body(entity)} +
+
+ ` + } +} + +class Drag extends MouseClickDrag { + constructor(target, blueprint, options) { + super(target, blueprint, options); + this.stepSize = parseInt(options?.stepSize); + this.mousePosition = [0, 0]; + } + + snapToGrid(location) { + return [ + this.stepSize * Math.round(location[0] / this.stepSize), + this.stepSize * Math.round(location[1] / this.stepSize) + ] + } + + startDrag() { + if (isNaN(this.stepSize) || this.stepSize <= 0) { + this.stepSize = parseInt(getComputedStyle(this.target).getPropertyValue('--ueb-grid-snap')); + if (isNaN(this.stepSize) || this.stepSize <= 0) { + this.stepSize = 1; + } + } + // Get the current mouse position + this.mousePosition = this.stepSize != 1 ? this.snapToGrid(this.clickedPosition) : this.clickedPosition; + } + + dragTo(location, movement) { + const mousePosition = this.stepSize != 1 ? this.snapToGrid(location) : location; + const d = [mousePosition[0] - this.mousePosition[0], mousePosition[1] - this.mousePosition[1]]; + + if (d[0] == 0 && d[1] == 0) { + return + } + + this.target.dragDispatch(d); + + // Reassign the position of mouse + this.mousePosition = mousePosition; + } +} + +class SelectableDraggable extends GraphElement { + + constructor(...args) { + super(...args); + this.dragObject = null; + this.location = [0, 0]; + this.selected = false; + + let self = this; + this.dragHandler = (e) => { + self.addLocation(e.detail.value); + }; + } + + connectedCallback() { + super.connectedCallback(); + this.dragObject = new Drag(this, null, { // UDrag doesn't need blueprint + looseTarget: true + }); + } + + disconnectedCallback() { + this.dragObject.unlistenDOMElement(); + } + + setLocation(value = [0, 0]) { + this.location = value; + this.style.setProperty('--ueb-position-x', this.location[0]); + this.style.setProperty('--ueb-position-y', this.location[1]); + } + + addLocation(value) { + this.setLocation([this.location[0] + value[0], this.location[1] + value[1]]); + } + + dragDispatch(value) { + if (!this.selected) { + this.blueprint.unselectAll(); + this.setSelected(true); + } + let dragEvent = new CustomEvent('uDragSelected', { + detail: { + instigator: this, + value: value + }, + bubbles: false, + cancelable: true, + composed: false, + }); + this.blueprint.dispatchEvent(dragEvent); + } + + setSelected(value = true) { + if (this.selected == value) { + return + } + this.selected = value; + if (this.selected) { + this.classList.add('ueb-selected'); + this.blueprint.addEventListener('uDragSelected', this.dragHandler); + } else { + this.classList.remove('ueb-selected'); + this.blueprint.removeEventListener('uDragSelected', this.dragHandler); + } + } + +} + +class GraphNode extends SelectableDraggable { + + static fromSerializedObject(str) { + let entity = SerializerFactory.getSerializer(ObjectEntity).read(str); + return new GraphNode(entity) + } + + constructor(entity) { + super(entity, new NodeTemplate()); + this.graphNodeName = 'n/a'; + this.inputs = []; + this.outputs = []; + } + + connectedCallback() { + this.getAttribute('type')?.trim(); + super.connectedCallback(); + this.classList.add('ueb-node'); + if (this.selected) { + this.classList.add('ueb-selected'); + } + this.style.setProperty('--ueb-position-x', this.location[0]); + this.style.setProperty('--ueb-position-y', this.location[1]); + } +} + +customElements.define('u-node', GraphNode); + +class Paste { + + 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); + } + + pasted(value) { + let nodes = this.serializer.readMultiple(value).map(entity => new GraphNode(entity)); + this.blueprint.addNode(...nodes); + } + + unlistenDOMElement() { + this.target.removeEventListener("paste", this.pasteHandler); + } + +} + /** @typedef {import("./graph/GraphNode").default} GraphNode */ class Blueprint extends GraphElement { @@ -1438,6 +1701,8 @@ class Blueprint extends GraphElement { moveEverywhere: true, exitAnyButton: true }); + + this.pasteObject = new Paste(this.getGridDOMElement(), this); } getGridDOMElement() { @@ -1448,6 +1713,7 @@ class Blueprint extends GraphElement { super.disconnectedCallback(); this.dragObject.unlistenDOMElement(); this.selectObject.unlistenDOMElement(); + this.pasteObject.unlistenDOMElement(); } getScroll() { @@ -1666,208 +1932,6 @@ class Blueprint extends GraphElement { customElements.define('u-blueprint', Blueprint); -class NodeTemplate extends Template { - - /** - * Computes the html content of the target element. - * @param {HTMLElement} entity Entity representing the element - * @returns The computed html - */ - header(entity) { - return ` -
- - - ${entity.graphNodeName} - -
- ` - } - - /** - * Computes the html content of the target element. - * @param {import("../entity/ObjectEntity").default} entity Entity representing the element - * @returns The computed html - */ - body(entity) { - let inputs = entity.CustomProperties.filter(v => v instanceof PinEntity); - let outputs = inputs.filter(v => v.isOutput()); - inputs = inputs.filter(v => !v.isOutput()); - return ` -
-
- ${inputs.map((input, index) => ` -
- - ${input.name} -
- `).join("") ?? ""} -
-
- ${outputs.map((output, index) => ` -
- ${output.name} - -
- `).join("") ?? ''} -
-
- ` - } - - /** - * Computes the html content of the target element. - * @param {HTMLElement} entity Entity representing the element - * @returns The computed html - */ - render(entity) { - return ` -
-
- ${this.header(entity)} - ${this.body(entity)} -
-
- ` - } -} - -class Drag extends MouseClickDrag { - constructor(target, blueprint, options) { - super(target, blueprint, options); - this.stepSize = parseInt(options?.stepSize); - this.mousePosition = [0, 0]; - } - - snapToGrid(location) { - return [ - this.stepSize * Math.round(location[0] / this.stepSize), - this.stepSize * Math.round(location[1] / this.stepSize) - ] - } - - startDrag() { - if (isNaN(this.stepSize) || this.stepSize <= 0) { - this.stepSize = parseInt(getComputedStyle(this.target).getPropertyValue('--ueb-grid-snap')); - if (isNaN(this.stepSize) || this.stepSize <= 0) { - this.stepSize = 1; - } - } - // Get the current mouse position - this.mousePosition = this.stepSize != 1 ? this.snapToGrid(this.clickedPosition) : this.clickedPosition; - } - - dragTo(location, movement) { - const mousePosition = this.stepSize != 1 ? this.snapToGrid(location) : location; - const d = [mousePosition[0] - this.mousePosition[0], mousePosition[1] - this.mousePosition[1]]; - - if (d[0] == 0 && d[1] == 0) { - return - } - - this.target.dragDispatch(d); - - // Reassign the position of mouse - this.mousePosition = mousePosition; - } -} - -class SelectableDraggable extends GraphElement { - - constructor(...args) { - super(...args); - this.dragObject = null; - this.location = [0, 0]; - this.selected = false; - - let self = this; - this.dragHandler = (e) => { - self.addLocation(e.detail.value); - }; - } - - connectedCallback() { - super.connectedCallback(); - this.dragObject = new Drag(this, null, { // UDrag doesn't need blueprint - looseTarget: true - }); - } - - disconnectedCallback() { - this.dragObject.unlistenDOMElement(); - } - - setLocation(value = [0, 0]) { - this.location = value; - this.style.setProperty('--ueb-position-x', this.location[0]); - this.style.setProperty('--ueb-position-y', this.location[1]); - } - - addLocation(value) { - this.setLocation([this.location[0] + value[0], this.location[1] + value[1]]); - } - - dragDispatch(value) { - if (!this.selected) { - this.blueprint.unselectAll(); - this.setSelected(true); - } - let dragEvent = new CustomEvent('uDragSelected', { - detail: { - instigator: this, - value: value - }, - bubbles: false, - cancelable: true, - composed: false, - }); - this.blueprint.dispatchEvent(dragEvent); - } - - setSelected(value = true) { - if (this.selected == value) { - return - } - this.selected = value; - if (this.selected) { - this.classList.add('ueb-selected'); - this.blueprint.addEventListener('uDragSelected', this.dragHandler); - } else { - this.classList.remove('ueb-selected'); - this.blueprint.removeEventListener('uDragSelected', this.dragHandler); - } - } - -} - -class GraphNode extends SelectableDraggable { - - static fromSerializedObject(str) { - let entity = SerializerFactory.getSerializer(ObjectEntity).read(str); - return new GraphNode(entity) - } - - constructor(entity) { - super(entity, new NodeTemplate()); - this.graphNodeName = 'n/a'; - this.inputs = []; - this.outputs = []; - } - - connectedCallback() { - this.getAttribute('type')?.trim(); - super.connectedCallback(); - this.classList.add('ueb-node'); - if (this.selected) { - this.classList.add('ueb-selected'); - } - this.style.setProperty('--ueb-position-x', this.location[0]); - this.style.setProperty('--ueb-position-y', this.location[1]); - } -} - -customElements.define('u-node', GraphNode); - class GraphLink extends GraphElement { /** diff --git a/js/Blueprint.js b/js/Blueprint.js index 2ded613..b72f5ad 100755 --- a/js/Blueprint.js +++ b/js/Blueprint.js @@ -6,6 +6,7 @@ import Select from "./input/Select" import Utility from "./Utility" import Zoom from "./input/Zoom" import BlueprintData from "./BlueprintData" +import Paste from "./input/Paste" /** @typedef {import("./graph/GraphNode").default} GraphNode */ export default class Blueprint extends GraphElement { @@ -84,6 +85,8 @@ export default class Blueprint extends GraphElement { moveEverywhere: true, exitAnyButton: true }) + + this.pasteObject = new Paste(this.getGridDOMElement(), this) } getGridDOMElement() { @@ -94,6 +97,7 @@ export default class Blueprint extends GraphElement { super.disconnectedCallback() this.dragObject.unlistenDOMElement() this.selectObject.unlistenDOMElement() + this.pasteObject.unlistenDOMElement() } getScroll() { diff --git a/js/entity/ObjectEntity.js b/js/entity/ObjectEntity.js index d0205c1..ba0a17e 100755 --- a/js/entity/ObjectEntity.js +++ b/js/entity/ObjectEntity.js @@ -24,4 +24,12 @@ export default class ObjectEntity extends Entity { getAttributes() { return ObjectEntity.attributes } + + /** + * + * @returns {String} The name of the node + */ + getNodeDisplayName() { + return this.Name + } } diff --git a/js/entity/PinEntity.js b/js/entity/PinEntity.js index c51e8d3..542d9fc 100755 --- a/js/entity/PinEntity.js +++ b/js/entity/PinEntity.js @@ -41,6 +41,14 @@ export default class PinEntity extends Entity { return PinEntity.attributes } + /** + * + * @returns {String} + */ + getPinDisplayName() { + return this.PinName + } + isOutput() { if (this.Direction === "EGPD_Output") { return true diff --git a/js/input/MouseClickDrag.js b/js/input/MouseClickDrag.js index b70f4f8..d42ee12 100755 --- a/js/input/MouseClickDrag.js +++ b/js/input/MouseClickDrag.js @@ -15,7 +15,7 @@ export default class MouseClickDrag extends Pointing { const movementListenedElement = this.moveEverywhere ? document.documentElement : this.movementSpace let self = this - this.mouseDownHandler = function (e) { + this.mouseDownHandler = e => { switch (e.button) { case self.clickButton: // Either doesn't matter or consider the click only when clicking on the parent, not descandants @@ -37,7 +37,7 @@ export default class MouseClickDrag extends Pointing { } } - this.mouseStartedMovingHandler = function (e) { + this.mouseStartedMovingHandler = e => { e.preventDefault() e.stopPropagation() @@ -50,7 +50,7 @@ export default class MouseClickDrag extends Pointing { self.started = true } - this.mouseMoveHandler = function (e) { + this.mouseMoveHandler = e => { e.preventDefault() e.stopPropagation() const location = self.getLocation(e) @@ -58,7 +58,7 @@ export default class MouseClickDrag extends Pointing { self.dragTo(location, movement) } - this.mouseUpHandler = function (e) { + this.mouseUpHandler = e => { if (!self.exitAnyButton || e.button == self.clickButton) { // Remove the handlers of "mousemove" and "mouseup" movementListenedElement.removeEventListener('mousemove', self.mouseStartedMovingHandler) diff --git a/js/input/MouseWheel.js b/js/input/MouseWheel.js index 7097365..ea95d83 100755 --- a/js/input/MouseWheel.js +++ b/js/input/MouseWheel.js @@ -13,7 +13,7 @@ export default class MouseWheel extends Pointing { this.looseTarget = options?.looseTarget ?? true let self = this - this.mouseWheelHandler = function (e) { + this.mouseWheelHandler = e => { e.preventDefault() const location = self.getLocation(e) self.wheel(Math.sign(e.deltaY), location) diff --git a/js/input/Paste.js b/js/input/Paste.js new file mode 100644 index 0000000..c9db9cd --- /dev/null +++ b/js/input/Paste.js @@ -0,0 +1,29 @@ +import GraphNode from "../graph/GraphNode" +import ObjectSerializer from "../serialization/ObjectSerializer" + +export default class Paste { + + 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) + } + + pasted(value) { + 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/serialization/ObjectSerializer.js b/js/serialization/ObjectSerializer.js index 2b3e082..231f84e 100755 --- a/js/serialization/ObjectSerializer.js +++ b/js/serialization/ObjectSerializer.js @@ -29,6 +29,20 @@ export default class ObjectSerializer extends Serializer { return parseResult.value } + /** + * + * @param {String} value + * @returns {ObjectEntity[]} + */ + readMultiple(value) { + const parseResult = Serializer.grammar.MultipleObject.parse(value) + if (!parseResult.status) { + console.error("Error when trying to parse the object.") + return parseResult + } + return parseResult.value + } + /** * * @param {ObjectEntity} object diff --git a/js/template/NodeTemplate.js b/js/template/NodeTemplate.js index a04b727..a9af786 100755 --- a/js/template/NodeTemplate.js +++ b/js/template/NodeTemplate.js @@ -1,11 +1,14 @@ import PinEntity from "../entity/PinEntity" import Template from "./Template" +/** + * @typedef {import("../entity/ObjectEntity").default} ObjectEntity + */ export default class NodeTemplate extends Template { /** * Computes the html content of the target element. - * @param {HTMLElement} entity Entity representing the element + * @param {ObjectEntity} entity Entity representing the element * @returns The computed html */ header(entity) { @@ -13,7 +16,7 @@ export default class NodeTemplate extends Template {
- ${entity.graphNodeName} + ${entity.getNodeDisplayName()}
` @@ -21,10 +24,11 @@ export default class NodeTemplate extends Template { /** * Computes the html content of the target element. - * @param {import("../entity/ObjectEntity").default} entity Entity representing the element + * @param {ObjectEntity} entity Entity representing the element * @returns The computed html */ body(entity) { + /** @type {PinEntity[]} */ let inputs = entity.CustomProperties.filter(v => v instanceof PinEntity) let outputs = inputs.filter(v => v.isOutput()) inputs = inputs.filter(v => !v.isOutput()) @@ -34,14 +38,14 @@ export default class NodeTemplate extends Template { ${inputs.map((input, index) => `
- ${input.name} + ${input.getPinDisplayName()}
`).join("") ?? ""}
${outputs.map((output, index) => `
- ${output.name} + ${output.getPinDisplayName()}
`).join("") ?? ''} @@ -52,7 +56,7 @@ export default class NodeTemplate extends Template { /** * Computes the html content of the target element. - * @param {HTMLElement} entity Entity representing the element + * @param {ObjectEntity} entity Entity representing the element * @returns The computed html */ render(entity) { diff --git a/js/template/Template.js b/js/template/Template.js index d0f89cc..ab6b88b 100755 --- a/js/template/Template.js +++ b/js/template/Template.js @@ -9,7 +9,7 @@ export default class Template { * @returns The computed html */ render(entity) { - return `` + return "" } /**