From a177faf91828e5e55063f6f324f9565f36cfb895 Mon Sep 17 00:00:00 2001 From: barsdeveloper Date: Tue, 15 Mar 2022 20:05:20 +0100 Subject: [PATCH] Links fixed --- dist/ueblueprint.js | 2237 +++++++++++++++-------------- js/Blueprint.js | 18 +- js/element/LinkElement.js | 11 +- js/element/PinElement.js | 2 + js/entity/PinEntity.js | 4 +- js/input/mouse/MouseCreateLink.js | 2 +- js/serialization/ISerializer.js | 2 +- js/template/NodeTemplate.js | 1 + js/template/PinTemplate.js | 17 +- 9 files changed, 1192 insertions(+), 1102 deletions(-) diff --git a/dist/ueblueprint.js b/dist/ueblueprint.js index 7bdf119..3298d59 100755 --- a/dist/ueblueprint.js +++ b/dist/ueblueprint.js @@ -129,28 +129,28 @@ class Configuration { */ const html = String.raw; -/** - * @typedef {import("../element/IElement").default} IElement - */ -class ITemplate { - - /** - * Computes the html content of the target element. - * @param {IElement} entity Element of the graph - * @returns The result html - */ - render(entity) { - return "" - } - - /** - * Applies the style to the element. - * @param {IElement} element Element of the graph - */ - apply(element) { - // TODO replace with the safer element.setHTML(...) when it will be available - element.innerHTML = this.render(element); - } +/** + * @typedef {import("../element/IElement").default} IElement + */ +class ITemplate { + + /** + * Computes the html content of the target element. + * @param {IElement} entity Element of the graph + * @returns The result html + */ + render(entity) { + return "" + } + + /** + * Applies the style to the element. + * @param {IElement} element Element of the graph + */ + apply(element) { + // TODO replace with the safer element.setHTML(...) when it will be available + element.innerHTML = this.render(element); + } } document.createElement("div"); @@ -474,50 +474,50 @@ class FastSelectionModel { } -/** - * @typedef {import("../Blueprint").default} Blueprint - * @typedef {import("../entity/IEntity").default} IEntity - * @typedef {import("../input/IContext").default} IContext - * @typedef {import("../template/ITemplate").default} ITemplate - */ - -class IElement extends HTMLElement { - - static tagName = "" - - /** - * @param {IEntity} entity The entity containing blueprint related data for this graph element - * @param {ITemplate} template The template to render this node - */ - constructor(entity, template) { - super(); - /** @type {Blueprint} */ - this.blueprint = null; - /** @type {IEntity} */ - this.entity = entity; - /** @type {ITemplate} */ - this.template = template; - /** @type {IContext[]} */ - this.inputObjects = []; - } - - getTemplate() { - return this.template - } - - connectedCallback() { - this.blueprint = this.closest("ueb-blueprint"); - this.template.apply(this); - this.inputObjects = this.createInputObjects(); - } - - disconnectedCallback() { - this.inputObjects.forEach(v => v.unlistenDOMElement()); - } - - createInputObjects() { - return [] - } +/** + * @typedef {import("../Blueprint").default} Blueprint + * @typedef {import("../entity/IEntity").default} IEntity + * @typedef {import("../input/IContext").default} IContext + * @typedef {import("../template/ITemplate").default} ITemplate + */ + +class IElement extends HTMLElement { + + static tagName = "" + + /** + * @param {IEntity} entity The entity containing blueprint related data for this graph element + * @param {ITemplate} template The template to render this node + */ + constructor(entity, template) { + super(); + /** @type {Blueprint} */ + this.blueprint = null; + /** @type {IEntity} */ + this.entity = entity; + /** @type {ITemplate} */ + this.template = template; + /** @type {IContext[]} */ + this.inputObjects = []; + } + + getTemplate() { + return this.template + } + + connectedCallback() { + this.blueprint = this.closest("ueb-blueprint"); + this.template.apply(this); + this.inputObjects = this.createInputObjects(); + } + + disconnectedCallback() { + this.inputObjects.forEach(v => v.unlistenDOMElement()); + } + + createInputObjects() { + return [] + } } /** @@ -567,41 +567,41 @@ class SelectorTemplate extends ITemplate { } } -class SelectorElement extends IElement { - - static tagName = "ueb-selector" - - constructor() { - super({}, new SelectorTemplate()); - this.selectionModel = null; - /** @type {SelectorTemplate} */ - this.template; - } - - /** - * Create a selection rectangle starting from the specified position - * @param {number[]} initialPosition - Selection rectangle initial position (relative to the .ueb-grid element) - */ - startSelecting(initialPosition) { - this.template.applyStartSelecting(this, initialPosition); - this.selectionModel = new FastSelectionModel(initialPosition, this.blueprint.getNodes(), this.blueprint.nodeBoundariesSupplier, this.blueprint.nodeSelectToggleFunction); - } - - /** - * Move selection rectagle to the specified final position. The initial position was specified by startSelecting() - * @param {number[]} finalPosition - Selection rectangle final position (relative to the .ueb-grid element) - */ - doSelecting(finalPosition) { - this.template.applyDoSelecting(this, finalPosition); - this.selectionModel.selectTo(finalPosition); - } - - finishSelecting() { - this.template.applyFinishSelecting(this); - this.selectionModel = null; - } -} - +class SelectorElement extends IElement { + + static tagName = "ueb-selector" + + constructor() { + super({}, new SelectorTemplate()); + this.selectionModel = null; + /** @type {SelectorTemplate} */ + this.template; + } + + /** + * Create a selection rectangle starting from the specified position + * @param {number[]} initialPosition - Selection rectangle initial position (relative to the .ueb-grid element) + */ + startSelecting(initialPosition) { + this.template.applyStartSelecting(this, initialPosition); + this.selectionModel = new FastSelectionModel(initialPosition, this.blueprint.getNodes(), this.blueprint.nodeBoundariesSupplier, this.blueprint.nodeSelectToggleFunction); + } + + /** + * Move selection rectagle to the specified final position. The initial position was specified by startSelecting() + * @param {number[]} finalPosition - Selection rectangle final position (relative to the .ueb-grid element) + */ + doSelecting(finalPosition) { + this.template.applyDoSelecting(this, finalPosition); + this.selectionModel.selectTo(finalPosition); + } + + finishSelecting() { + this.template.applyFinishSelecting(this); + this.selectionModel = null; + } +} + customElements.define(SelectorElement.tagName, SelectorElement); /** @typedef {import("../Blueprint").default} Blueprint */ @@ -725,35 +725,35 @@ class BlueprintTemplate extends ITemplate { } } -class IContext { - - constructor(target, blueprint, options) { - /** @type {HTMLElement} */ - this.target = target; - /** @type {import("../Blueprint").default}" */ - this.blueprint = blueprint; - this.options = options; - let self = this; - this.blueprintFocusHandler = _ => self.listenEvents(); - this.blueprintUnfocusHandler = _ => self.unlistenEvents(); - if (options?.wantsFocusCallback ?? false) { - this.blueprint.addEventListener("blueprint-focus", this.blueprintFocusHandler); - this.blueprint.addEventListener("blueprint-unfocus", this.blueprintUnfocusHandler); - } - } - - unlistenDOMElement() { - this.unlistenEvents(); - this.blueprint.removeEventListener("blueprint-focus", this.blueprintFocusHandler); - this.blueprint.removeEventListener("blueprint-unfocus", this.blueprintUnfocusHandler); - } - - /* Subclasses will probabily override the following methods */ - listenEvents() { - } - - unlistenEvents() { - } +class IContext { + + constructor(target, blueprint, options) { + /** @type {HTMLElement} */ + this.target = target; + /** @type {import("../Blueprint").default}" */ + this.blueprint = blueprint; + this.options = options; + let self = this; + this.blueprintFocusHandler = _ => self.listenEvents(); + this.blueprintUnfocusHandler = _ => self.unlistenEvents(); + if (options?.wantsFocusCallback ?? false) { + this.blueprint.addEventListener("blueprint-focus", this.blueprintFocusHandler); + this.blueprint.addEventListener("blueprint-unfocus", this.blueprintUnfocusHandler); + } + } + + unlistenDOMElement() { + this.unlistenEvents(); + this.blueprint.removeEventListener("blueprint-focus", this.blueprintFocusHandler); + this.blueprint.removeEventListener("blueprint-unfocus", this.blueprintUnfocusHandler); + } + + /* Subclasses will probabily override the following methods */ + listenEvents() { + } + + unlistenEvents() { + } } class TypeInitialization { @@ -899,56 +899,56 @@ class Utility { } } -class IEntity { - - constructor(options = {}) { - /** - * @param {String[]} prefix - * @param {Object} target - * @param {Object} properties - */ - const defineAllAttributes = (prefix, target, properties) => { - let fullKey = prefix.concat(""); - const last = fullKey.length - 1; - for (let property in properties) { - fullKey[last] = property; - // Not instanceof because all objects are instenceof Object, exact match needed - if (properties[property]?.constructor === Object) { - target[property] = {}; - defineAllAttributes(fullKey, target[property], properties[property]); - continue - } - /* - * The value can either be: - * - Array: can contain multiple values, its property is assigned multiple times like (X=1, X=4, X="Hello World") - * - TypeInitialization: contains the maximum amount of information about the attribute. - * - A type: the default value will be default constructed object without arguments. - * - A proper value. - */ - const value = Utility.objectGet(options, fullKey); - if (value !== null) { - target[property] = value; - continue - } - let defaultValue = properties[property]; - if (defaultValue instanceof TypeInitialization) { - if (!defaultValue.showDefault) { - continue - } - defaultValue = defaultValue.value; - } - if (defaultValue instanceof Array) { - target[property] = []; - continue - } - if (defaultValue instanceof Function) { - defaultValue = TypeInitialization.sanitize(new defaultValue()); - } - target[property] = TypeInitialization.sanitize(defaultValue); - } - }; - defineAllAttributes([], this, this.constructor.attributes); - } +class IEntity { + + constructor(options = {}) { + /** + * @param {String[]} prefix + * @param {Object} target + * @param {Object} properties + */ + const defineAllAttributes = (prefix, target, properties) => { + let fullKey = prefix.concat(""); + const last = fullKey.length - 1; + for (let property in properties) { + fullKey[last] = property; + // Not instanceof because all objects are instenceof Object, exact match needed + if (properties[property]?.constructor === Object) { + target[property] = {}; + defineAllAttributes(fullKey, target[property], properties[property]); + continue + } + /* + * The value can either be: + * - Array: can contain multiple values, its property is assigned multiple times like (X=1, X=4, X="Hello World") + * - TypeInitialization: contains the maximum amount of information about the attribute. + * - A type: the default value will be default constructed object without arguments. + * - A proper value. + */ + const value = Utility.objectGet(options, fullKey); + if (value !== null) { + target[property] = value; + continue + } + let defaultValue = properties[property]; + if (defaultValue instanceof TypeInitialization) { + if (!defaultValue.showDefault) { + continue + } + defaultValue = defaultValue.value; + } + if (defaultValue instanceof Array) { + target[property] = []; + continue + } + if (defaultValue instanceof Function) { + defaultValue = TypeInitialization.sanitize(new defaultValue()); + } + target[property] = TypeInitialization.sanitize(defaultValue); + } + }; + defineAllAttributes([], this, this.constructor.attributes); + } } class ObjectReferenceEntity extends IEntity { @@ -1113,13 +1113,13 @@ class PinEntity extends IEntity { linkTo(targetObjectName, targetPinEntity) { /** @type {PinReferenceEntity[]} */ this.LinkedTo; - const pinExists = !this.LinkedTo.find( + const linkExists = this.LinkedTo.find( /** @type {PinReferenceEntity} */ pinReferenceEntity => { return pinReferenceEntity.objectName == targetObjectName && pinReferenceEntity.pinGuid == targetPinEntity.PinId }); - if (pinExists) { + if (!linkExists) { this.LinkedTo.push(new PinReferenceEntity({ objectName: targetObjectName, pinGuid: targetPinEntity.PinId @@ -1381,82 +1381,82 @@ class SerializerFactory { } } -class ISerializer { - - static grammar = Parsimmon.createLanguage(new Grammar()) - - constructor(entityType, prefix, separator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter) { - this.entityType = entityType; - this.prefix = prefix ?? ""; - this.separator = separator ?? ","; - this.trailingSeparator = trailingSeparator ?? false; - this.attributeValueConjunctionSign = attributeValueConjunctionSign ?? "="; - this.attributeKeyPrinter = attributeKeyPrinter ?? (k => k.join(".")); - } - - writeValue(value) { - if (value === null) { - return "()" - } - const serialize = v => SerializerFactory.getSerializer(Utility.getType(v)).write(v); - // This is an exact match (and not instanceof) to hit also primitive types (by accessing value.constructor they are converted to objects automatically) - switch (value?.constructor) { - case Function: - return this.writeValue(value()) - case Boolean: - return Utility.FirstCapital(value.toString()) - case Number: - return value.toString() - case String: - return `"${value}"` - } - if (value instanceof Array) { - return `(${value.map(v => serialize(v) + ",")})` - } - if (value instanceof IEntity) { - return serialize(value) - } - } - - /** - * @param {String[]} key - * @param {Object} object - * @returns {String} - */ - subWrite(key, object) { - let result = ""; - let fullKey = key.concat(""); - const last = fullKey.length - 1; - for (const property in object) { - fullKey[last] = property; - const value = object[property]; - if (object[property]?.constructor === Object) { - // Recursive call when finding an object - result += (result.length ? this.separator : "") - + this.subWrite(fullKey, value); - } else if (this.showProperty(fullKey, value)) { - result += (result.length ? this.separator : "") - + this.prefix - + this.attributeKeyPrinter(fullKey) - + this.attributeValueConjunctionSign - + this.writeValue(value); - } - } - if (this.trailingSeparator && result.length && fullKey.length === 0) { - // append separator at the end if asked and there was printed content - result += this.separator; - } - return result - } - - showProperty(attributeKey, attributeValue) { - const attributes = this.entityType.attributes; - const attribute = Utility.objectGet(attributes, attributeKey); - if (attribute instanceof TypeInitialization) { - return !Utility.equals(attribute.value, attributeValue) || attribute.showDefault - } - return true - } +class ISerializer { + + static grammar = Parsimmon.createLanguage(new Grammar()) + + constructor(entityType, prefix, separator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter) { + this.entityType = entityType; + this.prefix = prefix ?? ""; + this.separator = separator ?? ","; + this.trailingSeparator = trailingSeparator ?? false; + this.attributeValueConjunctionSign = attributeValueConjunctionSign ?? "="; + this.attributeKeyPrinter = attributeKeyPrinter ?? (k => k.join(".")); + } + + writeValue(value) { + if (value === null) { + return "()" + } + const serialize = v => SerializerFactory.getSerializer(Utility.getType(v)).write(v); + // This is an exact match (and not instanceof) to hit also primitive types (by accessing value.constructor they are converted to objects automatically) + switch (value?.constructor) { + case Function: + return this.writeValue(value()) + case Boolean: + return Utility.FirstCapital(value.toString()) + case Number: + return value.toString() + case String: + return `"${value}"` + } + if (value instanceof Array) { + return `(${value.map(v => serialize(v) + ",").join("")})` + } + if (value instanceof IEntity) { + return serialize(value) + } + } + + /** + * @param {String[]} key + * @param {Object} object + * @returns {String} + */ + subWrite(key, object) { + let result = ""; + let fullKey = key.concat(""); + const last = fullKey.length - 1; + for (const property in object) { + fullKey[last] = property; + const value = object[property]; + if (object[property]?.constructor === Object) { + // Recursive call when finding an object + result += (result.length ? this.separator : "") + + this.subWrite(fullKey, value); + } else if (this.showProperty(fullKey, value)) { + result += (result.length ? this.separator : "") + + this.prefix + + this.attributeKeyPrinter(fullKey) + + this.attributeValueConjunctionSign + + this.writeValue(value); + } + } + if (this.trailingSeparator && result.length && fullKey.length === 0) { + // append separator at the end if asked and there was printed content + result += this.separator; + } + return result + } + + showProperty(attributeKey, attributeValue) { + const attributes = this.entityType.attributes; + const attribute = Utility.objectGet(attributes, attributeKey); + if (attribute instanceof TypeInitialization) { + return !Utility.equals(attribute.value, attributeValue) || attribute.showDefault + } + return true + } } class ObjectSerializer extends ISerializer { @@ -1538,87 +1538,87 @@ class Copy extends IContext { } } -let P = Parsimmon; - -class KeyGrammar { - - // Creates a grammar where each alternative is the string from ModifierKey mapped to a number for bit or use - ModifierKey = r => P.alt(...Configuration.ModifierKeys.map((v, i) => P.string(v).map(_ => 1 << i))) - Key = r => P.alt(...Object.keys(Configuration.Keys).map(v => P.string(v))).map(v => Configuration.Keys[v]) - KeyboardShortcut = r => P.alt( - P.seqMap( - P.seqMap(r.ModifierKey, P.optWhitespace, P.string(Configuration.keysSeparator), (v, _, __) => v) - .atLeast(1) - .map(v => v.reduce((acc, cur) => acc | cur)), - P.optWhitespace, - r.Key, - (modifierKeysFlag, _, key) => ({ - key: key, - ctrlKey: Boolean(modifierKeysFlag & (1 << Configuration.ModifierKeys.indexOf("Ctrl"))), - shiftKey: Boolean(modifierKeysFlag & (1 << Configuration.ModifierKeys.indexOf("Shift"))), - altKey: Boolean(modifierKeysFlag & (1 << Configuration.ModifierKeys.indexOf("Alt"))), - metaKey: Boolean(modifierKeysFlag & (1 << Configuration.ModifierKeys.indexOf("Meta"))) - }) - ), - r.Key.map(v => ({ key: v })) - ) - .trim(P.optWhitespace) -} - -class IKeyboardShortcut extends IContext { - - static keyGrammar = P.createLanguage(new KeyGrammar()) - - constructor(target, blueprint, options = {}) { - options.wantsFocusCallback = true; - super(target, blueprint, options); - - /** @type {String[]} */ - this.key = this.options.key; - this.ctrlKey = options.ctrlKey ?? false; - this.shiftKey = options.shiftKey ?? false; - this.altKey = options.altKey ?? false; - this.metaKey = options.metaKey ?? false; - - let self = this; - this.keyDownHandler = e => { - if ( - e.code == self.key - && e.ctrlKey === self.ctrlKey - && e.shiftKey === self.shiftKey - && e.altKey === self.altKey - && e.metaKey === self.metaKey - ) { - self.fire(); - e.preventDefault(); - return true - } - return false - }; - } - - /** - * @param {String} keyString - * @returns {Object} - */ - static keyOptionsParse(options, keyString) { - options = { - ...options, - ...IKeyboardShortcut.keyGrammar.KeyboardShortcut.parse(keyString).value - }; - return options - } - - listenEvents() { - document.addEventListener("keydown", this.keyDownHandler); - } - - unlistenEvents() { - document.removeEventListener("keydown", this.keyDownHandler); - } - - fire() { - } +let P = Parsimmon; + +class KeyGrammar { + + // Creates a grammar where each alternative is the string from ModifierKey mapped to a number for bit or use + ModifierKey = r => P.alt(...Configuration.ModifierKeys.map((v, i) => P.string(v).map(_ => 1 << i))) + Key = r => P.alt(...Object.keys(Configuration.Keys).map(v => P.string(v))).map(v => Configuration.Keys[v]) + KeyboardShortcut = r => P.alt( + P.seqMap( + P.seqMap(r.ModifierKey, P.optWhitespace, P.string(Configuration.keysSeparator), (v, _, __) => v) + .atLeast(1) + .map(v => v.reduce((acc, cur) => acc | cur)), + P.optWhitespace, + r.Key, + (modifierKeysFlag, _, key) => ({ + key: key, + ctrlKey: Boolean(modifierKeysFlag & (1 << Configuration.ModifierKeys.indexOf("Ctrl"))), + shiftKey: Boolean(modifierKeysFlag & (1 << Configuration.ModifierKeys.indexOf("Shift"))), + altKey: Boolean(modifierKeysFlag & (1 << Configuration.ModifierKeys.indexOf("Alt"))), + metaKey: Boolean(modifierKeysFlag & (1 << Configuration.ModifierKeys.indexOf("Meta"))) + }) + ), + r.Key.map(v => ({ key: v })) + ) + .trim(P.optWhitespace) +} + +class IKeyboardShortcut extends IContext { + + static keyGrammar = P.createLanguage(new KeyGrammar()) + + constructor(target, blueprint, options = {}) { + options.wantsFocusCallback = true; + super(target, blueprint, options); + + /** @type {String[]} */ + this.key = this.options.key; + this.ctrlKey = options.ctrlKey ?? false; + this.shiftKey = options.shiftKey ?? false; + this.altKey = options.altKey ?? false; + this.metaKey = options.metaKey ?? false; + + let self = this; + this.keyDownHandler = e => { + if ( + e.code == self.key + && e.ctrlKey === self.ctrlKey + && e.shiftKey === self.shiftKey + && e.altKey === self.altKey + && e.metaKey === self.metaKey + ) { + self.fire(); + e.preventDefault(); + return true + } + return false + }; + } + + /** + * @param {String} keyString + * @returns {Object} + */ + static keyOptionsParse(options, keyString) { + options = { + ...options, + ...IKeyboardShortcut.keyGrammar.KeyboardShortcut.parse(keyString).value + }; + return options + } + + listenEvents() { + document.addEventListener("keydown", this.keyDownHandler); + } + + unlistenEvents() { + document.removeEventListener("keydown", this.keyDownHandler); + } + + fire() { + } } class KeyvoardCanc extends IKeyboardShortcut { @@ -1818,367 +1818,374 @@ class LinkTemplate extends ITemplate { } } -/** - * @typedef {import("./PinElement").default} PinElement - * @typedef {import("./LinkMessageElement").default} LinkMessageElement - */ -class LinkElement extends IElement { - - static tagName = "ueb-link" - /** @type {PinElement} */ - #source - /** @type {PinElement} */ - #destination - #nodeDeleteHandler - #nodeDragSourceHandler - #nodeDragDestinatonHandler - sourceLocation = [0, 0] - /** @type {SVGPathElement} */ - pathElement - /** @type {LinkMessageElement} */ - linkMessageElement - originatesFromInput = false - destinationLocation = [0, 0] - - /** - * @param {?PinElement} source - * @param {?PinElement} destination - */ - constructor(source, destination) { - super({}, new LinkTemplate()); - /** @type {import("../template/LinkTemplate").default} */ - this.template; - const self = this; - this.#nodeDeleteHandler = _ => self.remove(); - this.#nodeDragSourceHandler = e => self.addSourceLocation(e.detail.value); - this.#nodeDragDestinatonHandler = e => self.addDestinationLocation(e.detail.value); - if (source) { - this.setSourcePin(source); - } - if (destination) { - this.setDestinationPin(destination); - } - if (source && destination) { - this.#linkPins(); - } - } - - #linkPins() { - this.#source.linkTo(this.#destination); - this.#destination.linkTo(this.#source); - } - - #unlinkPins() { - this.#source.unlinkFrom(this.#destination); - this.#destination.unlinkFrom(this.#source); - } - - /** - * @returns {Number[]} - */ - getSourceLocation() { - return this.sourceLocation - } - - /** - * @param {Number[]} offset - */ - addSourceLocation(offset) { - const location = [ - this.sourceLocation[0] + offset[0], - this.sourceLocation[1] + offset[1] - ]; - this.sourceLocation = location; - this.template.applyFullLocation(this); - } - - /** - * @param {Number[]} location - */ - setSourceLocation(location) { - if (location == null) { - location = this.#source.template.getLinkLocation(this.#source); - } - this.sourceLocation = location; - this.template.applySourceLocation(this); - } - - /** - * @returns {Number[]} - */ - getDestinationLocation() { - return this.destinationLocation - } - - /** - * @param {Number[]} offset - */ - addDestinationLocation(offset) { - const location = [ - this.destinationLocation[0] + offset[0], - this.destinationLocation[1] + offset[1] - ]; - this.setDestinationLocation(location); - } - - /** - * @param {Number[]} location - */ - setDestinationLocation(location) { - if (location == null) { - location = this.#destination.template.getLinkLocation(this.#destination); - } - this.destinationLocation = location; - this.template.applyFullLocation(this); - } - - /** - * @returns {PinElement} - */ - getSourcePin() { - return this.#source - } - - /** - * @param {PinElement} pin - */ - setSourcePin(pin) { - if (this.#source) { - const nodeElement = this.#source.getNodeElement(); - nodeElement.removeEventListener(Configuration.nodeDeleteEventName, this.#nodeDeleteHandler); - nodeElement.removeEventListener(Configuration.nodeDragLocalEventName, this.#nodeDragSourceHandler); - if (this.#destination) { - this.#unlinkPins(); - } - } - this.#source = pin; - if (this.#source) { - const nodeElement = this.#source.getNodeElement(); - this.originatesFromInput = pin.isInput(); - nodeElement.addEventListener(Configuration.nodeDeleteEventName, this.#nodeDeleteHandler); - nodeElement.addEventListener(Configuration.nodeDragLocalEventName, this.#nodeDragSourceHandler); - this.setSourceLocation(); - if (this.#destination) { - this.#linkPins(); - } - } - } - - /** - * @returns {PinElement} - */ - getDestinationPin() { - return this.#destination - } - - /** - * @param {PinElement} pin - */ - setDestinationPin(pin) { - if (this.#destination) { - const nodeElement = this.#destination.getNodeElement(); - nodeElement.removeEventListener(Configuration.nodeDeleteEventName, this.#nodeDeleteHandler); - nodeElement.removeEventListener(Configuration.nodeDragLocalEventName, this.#nodeDragDestinatonHandler); - if (this.#source) { - this.#unlinkPins(); - } - } - this.#destination = pin; - if (this.#destination) { - const nodeElement = this.#destination.getNodeElement(); - nodeElement.addEventListener(Configuration.nodeDeleteEventName, this.#nodeDeleteHandler); - nodeElement.addEventListener(Configuration.nodeDragLocalEventName, this.#nodeDragDestinatonHandler); - this.setDestinationLocation(); - if (this.#source) { - this.#linkPins(); - } - } - } - - /** - * @param {LinkMessageElement} linkMessage - */ - setLinkMessage(linkMessage) { - if (linkMessage) { - this.template.applyLinkMessage(this, linkMessage); - } else if (this.linkMessageElement) { - this.linkMessageElement.remove(); - this.linkMessageElement = null; - } - } - - startDragging() { - this.template.applyStartDragging(this); - } - - finishDragging() { - this.template.applyFinishDragging(this); - } -} - +/** + * @typedef {import("./PinElement").default} PinElement + * @typedef {import("./LinkMessageElement").default} LinkMessageElement + */ +class LinkElement extends IElement { + + static tagName = "ueb-link" + /** @type {PinElement} */ + #source + /** @type {PinElement} */ + #destination + #nodeDeleteHandler + #nodeDragSourceHandler + #nodeDragDestinatonHandler + sourceLocation = [0, 0] + /** @type {SVGPathElement} */ + pathElement + /** @type {LinkMessageElement} */ + linkMessageElement + originatesFromInput = false + destinationLocation = [0, 0] + + /** + * @param {?PinElement} source + * @param {?PinElement} destination + */ + constructor(source, destination) { + super({}, new LinkTemplate()); + /** @type {import("../template/LinkTemplate").default} */ + this.template; + const self = this; + this.#nodeDeleteHandler = _ => self.remove(); + this.#nodeDragSourceHandler = e => self.addSourceLocation(e.detail.value); + this.#nodeDragDestinatonHandler = e => self.addDestinationLocation(e.detail.value); + if (source) { + this.setSourcePin(source); + } + if (destination) { + this.setDestinationPin(destination); + } + if (source && destination) { + this.#linkPins(); + } + } + + #linkPins() { + this.#source.linkTo(this.#destination); + this.#destination.linkTo(this.#source); + } + + #unlinkPins() { + if (this.#source && this.#destination) { + this.#source.unlinkFrom(this.#destination); + this.#destination.unlinkFrom(this.#source); + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.#unlinkPins(); + } + + /** + * @returns {Number[]} + */ + getSourceLocation() { + return this.sourceLocation + } + + /** + * @param {Number[]} offset + */ + addSourceLocation(offset) { + const location = [ + this.sourceLocation[0] + offset[0], + this.sourceLocation[1] + offset[1] + ]; + this.sourceLocation = location; + this.template.applyFullLocation(this); + } + + /** + * @param {Number[]} location + */ + setSourceLocation(location) { + if (location == null) { + location = this.#source.template.getLinkLocation(this.#source); + } + this.sourceLocation = location; + this.template.applySourceLocation(this); + } + + /** + * @returns {Number[]} + */ + getDestinationLocation() { + return this.destinationLocation + } + + /** + * @param {Number[]} offset + */ + addDestinationLocation(offset) { + const location = [ + this.destinationLocation[0] + offset[0], + this.destinationLocation[1] + offset[1] + ]; + this.setDestinationLocation(location); + } + + /** + * @param {Number[]} location + */ + setDestinationLocation(location) { + if (location == null) { + location = this.#destination.template.getLinkLocation(this.#destination); + } + this.destinationLocation = location; + this.template.applyFullLocation(this); + } + + /** + * @returns {PinElement} + */ + getSourcePin() { + return this.#source + } + + /** + * @param {PinElement} pin + */ + setSourcePin(pin) { + if (this.#source) { + const nodeElement = this.#source.getNodeElement(); + nodeElement.removeEventListener(Configuration.nodeDeleteEventName, this.#nodeDeleteHandler); + nodeElement.removeEventListener(Configuration.nodeDragLocalEventName, this.#nodeDragSourceHandler); + if (this.#destination) { + this.#unlinkPins(); + } + } + this.#source = pin; + if (this.#source) { + const nodeElement = this.#source.getNodeElement(); + this.originatesFromInput = pin.isInput(); + nodeElement.addEventListener(Configuration.nodeDeleteEventName, this.#nodeDeleteHandler); + nodeElement.addEventListener(Configuration.nodeDragLocalEventName, this.#nodeDragSourceHandler); + this.setSourceLocation(); + if (this.#destination) { + this.#linkPins(); + } + } + } + + /** + * @returns {PinElement} + */ + getDestinationPin() { + return this.#destination + } + + /** + * @param {PinElement} pin + */ + setDestinationPin(pin) { + if (this.#destination) { + const nodeElement = this.#destination.getNodeElement(); + nodeElement.removeEventListener(Configuration.nodeDeleteEventName, this.#nodeDeleteHandler); + nodeElement.removeEventListener(Configuration.nodeDragLocalEventName, this.#nodeDragDestinatonHandler); + if (this.#source) { + this.#unlinkPins(); + } + } + this.#destination = pin; + if (this.#destination) { + const nodeElement = this.#destination.getNodeElement(); + nodeElement.addEventListener(Configuration.nodeDeleteEventName, this.#nodeDeleteHandler); + nodeElement.addEventListener(Configuration.nodeDragLocalEventName, this.#nodeDragDestinatonHandler); + this.setDestinationLocation(); + if (this.#source) { + this.#linkPins(); + } + } + } + + /** + * @param {LinkMessageElement} linkMessage + */ + setLinkMessage(linkMessage) { + if (linkMessage) { + this.template.applyLinkMessage(this, linkMessage); + } else if (this.linkMessageElement) { + this.linkMessageElement.remove(); + this.linkMessageElement = null; + } + } + + startDragging() { + this.template.applyStartDragging(this); + } + + finishDragging() { + this.template.applyFinishDragging(this); + } +} + customElements.define(LinkElement.tagName, LinkElement); -class IPointing extends IContext { - - constructor(target, blueprint, options) { - super(target, blueprint, options); - this.movementSpace = this.blueprint?.getGridDOMElement() ?? document.documentElement; - } - - /** - * @param {MouseEvent} mouseEvent - */ - locationFromEvent(mouseEvent) { - return this.blueprint.compensateTranslation( - Utility.convertLocation( - [mouseEvent.clientX, mouseEvent.clientY], - this.movementSpace)) - } +class IPointing extends IContext { + + constructor(target, blueprint, options) { + super(target, blueprint, options); + this.movementSpace = this.blueprint?.getGridDOMElement() ?? document.documentElement; + } + + /** + * @param {MouseEvent} mouseEvent + */ + locationFromEvent(mouseEvent) { + return this.blueprint.compensateTranslation( + Utility.convertLocation( + [mouseEvent.clientX, mouseEvent.clientY], + this.movementSpace)) + } } -/** - * This class manages the ui gesture of mouse click and drag. Tha actual operations are implemented by the subclasses. - */ -class IMouseClickDrag extends IPointing { - - /** @type {(e: MouseEvent) => void} */ - #mouseDownHandler - - /** @type {(e: MouseEvent) => void} */ - #mouseStartedMovingHandler - - /** @type {(e: MouseEvent) => void} */ - #mouseMoveHandler - - /** @type {(e: MouseEvent) => void} */ - #mouseUpHandler - - #trackingMouse = false - - started = false - - constructor(target, blueprint, options) { - super(target, blueprint, options); - this.clickButton = options?.clickButton ?? 0; - this.exitAnyButton = options?.exitAnyButton ?? true; - this.moveEverywhere = options?.moveEverywhere ?? false; - this.looseTarget = options?.looseTarget ?? false; - this.consumeClickEvent = options?.consumeClickEvent ?? true; - this.clickedPosition = [0, 0]; - - const movementListenedElement = this.moveEverywhere ? document.documentElement : this.movementSpace; - 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 - if (self.looseTarget || e.target == e.currentTarget) { - e.preventDefault(); - if (this.consumeClickEvent) { - e.stopImmediatePropagation(); // Captured, don't call anyone else - } - // Attach the listeners - movementListenedElement.addEventListener("mousemove", self.#mouseStartedMovingHandler); - self.clickedPosition = self.locationFromEvent(e); - self.clicked(self.clickedPosition); - } - break - default: - if (!self.exitAnyButton) { - self.#mouseUpHandler(e); - } - break - } - }; - - this.#mouseStartedMovingHandler = e => { - e.preventDefault(); - if (this.consumeClickEvent) { - e.stopImmediatePropagation(); // Captured, don't call anyone else - } - // Delegate from now on to self.#mouseMoveHandler - movementListenedElement.removeEventListener("mousemove", self.#mouseStartedMovingHandler); - movementListenedElement.addEventListener("mousemove", self.#mouseMoveHandler); - document.addEventListener("mouseup", self.#mouseUpHandler); - // Handler calls e.preventDefault() when it receives the event, this means dispatchEvent returns false - const dragEvent = self.getEvent(Configuration.trackingMouseEventName.begin); - self.#trackingMouse = this.target.dispatchEvent(dragEvent) == false; - // Do actual actions - self.startDrag(); - self.started = true; - }; - - this.#mouseMoveHandler = e => { - e.preventDefault(); - if (this.consumeClickEvent) { - e.stopImmediatePropagation(); // Captured, don't call anyone else - } - const location = self.locationFromEvent(e); - const movement = [e.movementX, e.movementY]; - self.dragTo(location, movement); - if (self.#trackingMouse) { - self.blueprint.entity.mousePosition = self.locationFromEvent(e); - } - }; - - this.#mouseUpHandler = e => { - if (!self.exitAnyButton || e.button == self.clickButton) { - e.preventDefault(); - if (this.consumeClickEvent) { - e.stopImmediatePropagation(); // Captured, don't call anyone else - } - // Remove the handlers of "mousemove" and "mouseup" - movementListenedElement.removeEventListener("mousemove", self.#mouseStartedMovingHandler); - movementListenedElement.removeEventListener("mousemove", self.#mouseMoveHandler); - document.removeEventListener("mouseup", self.#mouseUpHandler); - self.endDrag(); - if (self.#trackingMouse) { - const dragEvent = self.getEvent(Configuration.trackingMouseEventName.end); - this.target.dispatchEvent(dragEvent); - self.#trackingMouse = false; - } - self.started = false; - } - }; - - this.target.addEventListener("mousedown", this.#mouseDownHandler); - if (this.clickButton == 2) { - this.target.addEventListener("contextmenu", e => e.preventDefault()); - } - } - - getEvent(eventName) { - return new CustomEvent(eventName, { - detail: { - tracker: this - }, - bubbles: true, - cancelable: true - }) - } - - unlistenDOMElement() { - super.unlistenDOMElement(); - this.target.removeEventListener("mousedown", this.#mouseDownHandler); - if (this.clickButton == 2) { - this.target.removeEventListener("contextmenu", e => e.preventDefault()); - } - } - - /* Subclasses will override the following methods */ - clicked(location) { - } - - startDrag(location) { - } - - dragTo(location, movement) { - } - - endDrag() { - } +/** + * This class manages the ui gesture of mouse click and drag. Tha actual operations are implemented by the subclasses. + */ +class IMouseClickDrag extends IPointing { + + /** @type {(e: MouseEvent) => void} */ + #mouseDownHandler + + /** @type {(e: MouseEvent) => void} */ + #mouseStartedMovingHandler + + /** @type {(e: MouseEvent) => void} */ + #mouseMoveHandler + + /** @type {(e: MouseEvent) => void} */ + #mouseUpHandler + + #trackingMouse = false + + started = false + + constructor(target, blueprint, options) { + super(target, blueprint, options); + this.clickButton = options?.clickButton ?? 0; + this.exitAnyButton = options?.exitAnyButton ?? true; + this.moveEverywhere = options?.moveEverywhere ?? false; + this.looseTarget = options?.looseTarget ?? false; + this.consumeClickEvent = options?.consumeClickEvent ?? true; + this.clickedPosition = [0, 0]; + + const movementListenedElement = this.moveEverywhere ? document.documentElement : this.movementSpace; + 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 + if (self.looseTarget || e.target == e.currentTarget) { + e.preventDefault(); + if (this.consumeClickEvent) { + e.stopImmediatePropagation(); // Captured, don't call anyone else + } + // Attach the listeners + movementListenedElement.addEventListener("mousemove", self.#mouseStartedMovingHandler); + self.clickedPosition = self.locationFromEvent(e); + self.clicked(self.clickedPosition); + } + break + default: + if (!self.exitAnyButton) { + self.#mouseUpHandler(e); + } + break + } + }; + + this.#mouseStartedMovingHandler = e => { + e.preventDefault(); + if (this.consumeClickEvent) { + e.stopImmediatePropagation(); // Captured, don't call anyone else + } + // Delegate from now on to self.#mouseMoveHandler + movementListenedElement.removeEventListener("mousemove", self.#mouseStartedMovingHandler); + movementListenedElement.addEventListener("mousemove", self.#mouseMoveHandler); + document.addEventListener("mouseup", self.#mouseUpHandler); + // Handler calls e.preventDefault() when it receives the event, this means dispatchEvent returns false + const dragEvent = self.getEvent(Configuration.trackingMouseEventName.begin); + self.#trackingMouse = this.target.dispatchEvent(dragEvent) == false; + // Do actual actions + self.startDrag(); + self.started = true; + }; + + this.#mouseMoveHandler = e => { + e.preventDefault(); + if (this.consumeClickEvent) { + e.stopImmediatePropagation(); // Captured, don't call anyone else + } + const location = self.locationFromEvent(e); + const movement = [e.movementX, e.movementY]; + self.dragTo(location, movement); + if (self.#trackingMouse) { + self.blueprint.entity.mousePosition = self.locationFromEvent(e); + } + }; + + this.#mouseUpHandler = e => { + if (!self.exitAnyButton || e.button == self.clickButton) { + e.preventDefault(); + if (this.consumeClickEvent) { + e.stopImmediatePropagation(); // Captured, don't call anyone else + } + // Remove the handlers of "mousemove" and "mouseup" + movementListenedElement.removeEventListener("mousemove", self.#mouseStartedMovingHandler); + movementListenedElement.removeEventListener("mousemove", self.#mouseMoveHandler); + document.removeEventListener("mouseup", self.#mouseUpHandler); + self.endDrag(); + if (self.#trackingMouse) { + const dragEvent = self.getEvent(Configuration.trackingMouseEventName.end); + this.target.dispatchEvent(dragEvent); + self.#trackingMouse = false; + } + self.started = false; + } + }; + + this.target.addEventListener("mousedown", this.#mouseDownHandler); + if (this.clickButton == 2) { + this.target.addEventListener("contextmenu", e => e.preventDefault()); + } + } + + getEvent(eventName) { + return new CustomEvent(eventName, { + detail: { + tracker: this + }, + bubbles: true, + cancelable: true + }) + } + + unlistenDOMElement() { + super.unlistenDOMElement(); + this.target.removeEventListener("mousedown", this.#mouseDownHandler); + if (this.clickButton == 2) { + this.target.removeEventListener("contextmenu", e => e.preventDefault()); + } + } + + /* Subclasses will override the following methods */ + clicked(location) { + } + + startDrag(location) { + } + + dragTo(location, movement) { + } + + endDrag() { + } } class MouseScrollGraph extends IMouseClickDrag { @@ -2305,181 +2312,181 @@ class MouseMoveNodes extends IMouseClickDrag { } } -/** @typedef {import("../template/SelectableDraggableTemplate").default} SelectableDraggableTemplate */ - -class ISelectableDraggableElement extends IElement { - - constructor(...args) { - super(...args); - this.dragObject = null; - this.location = [0, 0]; - this.selected = false; - /** @type {SelectableDraggableTemplate} */ - this.template; - - let self = this; - this.dragHandler = (e) => { - self.addLocation(e.detail.value); - }; - } - - createInputObjects() { - return [ - new MouseMoveNodes(this, this.blueprint, { - looseTarget: true - }), - ] - } - - setLocation(value = [0, 0]) { - const d = [value[0] - this.location[0], value[1] - this.location[1]]; - const dragLocalEvent = new CustomEvent(Configuration.nodeDragLocalEventName, { - detail: { - value: d - }, - bubbles: false, - cancelable: true - }); - this.location = value; - this.template.applyLocation(this); - this.dispatchEvent(dragLocalEvent); - } - - addLocation(value) { - this.setLocation([this.location[0] + value[0], this.location[1] + value[1]]); - } - - setSelected(value = true) { - if (this.selected == value) { - return - } - this.selected = value; - if (this.selected) { - this.blueprint.addEventListener(Configuration.nodeDragEventName, this.dragHandler); - } else { - this.blueprint.removeEventListener(Configuration.nodeDragEventName, this.dragHandler); - } - this.template.applySelected(this); - } - - dispatchDragEvent(value) { - if (!this.selected) { - this.blueprint.unselectAll(); - this.setSelected(true); - } - const dragEvent = new CustomEvent(Configuration.nodeDragEventName, { - detail: { - value: value - }, - bubbles: true, - cancelable: true - }); - this.dispatchEvent(dragEvent); - } - - snapToGrid() { - let snappedLocation = this.blueprint.snapToGrid(this.location); - if (this.location[0] != snappedLocation[0] || this.location[1] != snappedLocation[1]) { - this.setLocation(snappedLocation); - } - } +/** @typedef {import("../template/SelectableDraggableTemplate").default} SelectableDraggableTemplate */ + +class ISelectableDraggableElement extends IElement { + + constructor(...args) { + super(...args); + this.dragObject = null; + this.location = [0, 0]; + this.selected = false; + /** @type {SelectableDraggableTemplate} */ + this.template; + + let self = this; + this.dragHandler = (e) => { + self.addLocation(e.detail.value); + }; + } + + createInputObjects() { + return [ + new MouseMoveNodes(this, this.blueprint, { + looseTarget: true + }), + ] + } + + setLocation(value = [0, 0]) { + const d = [value[0] - this.location[0], value[1] - this.location[1]]; + const dragLocalEvent = new CustomEvent(Configuration.nodeDragLocalEventName, { + detail: { + value: d + }, + bubbles: false, + cancelable: true + }); + this.location = value; + this.template.applyLocation(this); + this.dispatchEvent(dragLocalEvent); + } + + addLocation(value) { + this.setLocation([this.location[0] + value[0], this.location[1] + value[1]]); + } + + setSelected(value = true) { + if (this.selected == value) { + return + } + this.selected = value; + if (this.selected) { + this.blueprint.addEventListener(Configuration.nodeDragEventName, this.dragHandler); + } else { + this.blueprint.removeEventListener(Configuration.nodeDragEventName, this.dragHandler); + } + this.template.applySelected(this); + } + + dispatchDragEvent(value) { + if (!this.selected) { + this.blueprint.unselectAll(); + this.setSelected(true); + } + const dragEvent = new CustomEvent(Configuration.nodeDragEventName, { + detail: { + value: value + }, + bubbles: true, + cancelable: true + }); + this.dispatchEvent(dragEvent); + } + + snapToGrid() { + let snappedLocation = this.blueprint.snapToGrid(this.location); + if (this.location[0] != snappedLocation[0] || this.location[1] != snappedLocation[1]) { + this.setLocation(snappedLocation); + } + } } -/** - * @typedef {import("../element/LinkMessageElement").default} LinkMessageElement - */ -class LinkMessageTemplate extends ITemplate { - - /** - * Computes the html content of the target element. - * @param {LinkMessageElement} linkMessage - */ - render(linkMessage) { - return html` - - - ` - } - - /** - * Applies the style to the element. - * @param {LinkMessageElement} linkMessage - */ - apply(linkMessage) { - super.apply(linkMessage); - const linkMessageSetup = _ => linkMessage.querySelector(".ueb-link-message").innerText = linkMessage.message( - linkMessage.linkElement.getSourcePin(), - linkMessage.linkElement.getDestinationPin() - ); - linkMessage.linkElement = linkMessage.closest(LinkElement.tagName); - if (linkMessage.linkElement) { - linkMessageSetup(); - } else { - window.customElements.whenDefined(linkMessage.constructor.tagName).then(linkMessage); - } - } - -} - -/** - * @typedef {import("./PinElement").default} PinElement - * @typedef {import("./LinkElement").default} LinkElement - * @typedef {(sourcePin: PinElement, sourcePin: PinElement) => String} LinkRetrieval - */ -class LinkMessageElement extends IElement { - - static tagName = "ueb-link-message" - static convertType = _ => new LinkMessageElement( - "ueb-icon-conver-type", - /** @type {LinkRetrieval} */ - (s, d) => `Convert ${s.getType()} to ${d.getType()}.` - ) - static correct = _ => new LinkMessageElement( - "ueb-icon-correct", - /** @type {LinkRetrieval} */ - (s, d) => "" - ) - static directionsIncompatible = _ => new LinkMessageElement( - "ueb-icon-directions-incompatible", - /** @type {LinkRetrieval} */ - (s, d) => "Directions are not compatbile." - ) - static placeNode = _ => new LinkMessageElement( - "ueb-icon-place-node", - /** @type {LinkRetrieval} */ - (s, d) => "Place a new node." - ) - static replaceLink = _ => new LinkMessageElement( - "ueb-icon-replace-link", - /** @type {LinkRetrieval} */ - (s, d) => "Replace existing input connections." - ) - static sameNode = _ => new LinkMessageElement( - "ueb-icon-same-node", - /** @type {LinkRetrieval} */ - (s, d) => "Both are on the same node." - ) - static typesIncompatible = _ => new LinkMessageElement( - "ueb-icon-types-incompatible", - /** @type {LinkRetrieval} */ - (s, d) => `${s.getType()} is not compatible with ${d.getType()}.` - ) - - /** @type {String} */ - icon - /** @type {String} */ - message - /** @type {LinkElement} */ - linkElement - - constructor(icon, message) { - super({}, new LinkMessageTemplate()); - this.icon = icon; - this.message = message; - } - +/** + * @typedef {import("../element/LinkMessageElement").default} LinkMessageElement + */ +class LinkMessageTemplate extends ITemplate { + + /** + * Computes the html content of the target element. + * @param {LinkMessageElement} linkMessage + */ + render(linkMessage) { + return html` + + + ` + } + + /** + * Applies the style to the element. + * @param {LinkMessageElement} linkMessage + */ + apply(linkMessage) { + super.apply(linkMessage); + const linkMessageSetup = _ => linkMessage.querySelector(".ueb-link-message").innerText = linkMessage.message( + linkMessage.linkElement.getSourcePin(), + linkMessage.linkElement.getDestinationPin() + ); + linkMessage.linkElement = linkMessage.closest(LinkElement.tagName); + if (linkMessage.linkElement) { + linkMessageSetup(); + } else { + window.customElements.whenDefined(linkMessage.constructor.tagName).then(linkMessage); + } + } + } +/** + * @typedef {import("./PinElement").default} PinElement + * @typedef {import("./LinkElement").default} LinkElement + * @typedef {(sourcePin: PinElement, sourcePin: PinElement) => String} LinkRetrieval + */ +class LinkMessageElement extends IElement { + + static tagName = "ueb-link-message" + static convertType = _ => new LinkMessageElement( + "ueb-icon-conver-type", + /** @type {LinkRetrieval} */ + (s, d) => `Convert ${s.getType()} to ${d.getType()}.` + ) + static correct = _ => new LinkMessageElement( + "ueb-icon-correct", + /** @type {LinkRetrieval} */ + (s, d) => "" + ) + static directionsIncompatible = _ => new LinkMessageElement( + "ueb-icon-directions-incompatible", + /** @type {LinkRetrieval} */ + (s, d) => "Directions are not compatbile." + ) + static placeNode = _ => new LinkMessageElement( + "ueb-icon-place-node", + /** @type {LinkRetrieval} */ + (s, d) => "Place a new node." + ) + static replaceLink = _ => new LinkMessageElement( + "ueb-icon-replace-link", + /** @type {LinkRetrieval} */ + (s, d) => "Replace existing input connections." + ) + static sameNode = _ => new LinkMessageElement( + "ueb-icon-same-node", + /** @type {LinkRetrieval} */ + (s, d) => "Both are on the same node." + ) + static typesIncompatible = _ => new LinkMessageElement( + "ueb-icon-types-incompatible", + /** @type {LinkRetrieval} */ + (s, d) => `${s.getType()} is not compatible with ${d.getType()}.` + ) + + /** @type {String} */ + icon + /** @type {String} */ + message + /** @type {LinkElement} */ + linkElement + + constructor(icon, message) { + super({}, new LinkMessageTemplate()); + this.icon = icon; + this.message = message; + } + +} + customElements.define(LinkMessageElement.tagName, LinkMessageElement); /** @@ -2544,6 +2551,7 @@ class MouseCreateLink extends IMouseClickDrag { startDrag() { this.link = new LinkElement(this.target, null); + this.blueprint.nodesContainerElement.prepend(this.link); this.setLinkMessage(LinkMessageElement.placeNode()); this.#listenedPins = this.blueprint.querySelectorAll(this.target.constructor.tagName); this.#listenedPins.forEach(pin => { @@ -2581,7 +2589,6 @@ class MouseCreateLink extends IMouseClickDrag { setLinkMessage(linkMessage) { this.link.setLinkMessage(linkMessage); - this.blueprint.nodesContainerElement.prepend(this.link); } } @@ -2621,10 +2628,15 @@ class PinTemplate extends ITemplate { "ueb-pin-" + sanitizeText(pin.getType()) ); pin.clickableElement = pin; - pin.nodeElement = pin.closest(NodeElement.tagName); - if (!pin.nodeElement) { - window.customElements.whenDefined(linkMessage.constructor.tagName).then(linkMessage); - } + window.customElements.whenDefined(NodeElement.tagName).then(pin.nodeElement = pin.closest(NodeElement.tagName)); + pin.getLinks().forEach(pinReference => { + const targetPin = pin.blueprint.getPin(pinReference.pinGuid); + if (linkedToPin) { + const [sourcePin, destinationPin] = pin.isOutput() ? [pin, targetPin] : [targetPin, pin]; + pin.blueprint.addGraphElement(new LinkElement(sourcePin, destinationPin)); + } + }); + } /** @@ -2650,109 +2662,123 @@ class PinTemplate extends ITemplate { } } -/** - * @typedef {import("./NodeElement").default} NodeElement - */ -class PinElement extends IElement { - - static tagName = "ueb-pin" - - /** @type {NodeElement} */ - nodeElement - - /** @type {HTMLElement} */ - clickableElement - - /** @type {String} */ - #color - - constructor(entity) { - super(entity, new PinTemplate()); - /** @type {import("../entity/PinEntity").default} */ - this.entity; - /** @type {PinTemplate} */ - this.template; - } - - connectedCallback() { - super.connectedCallback(); - this.#color = window.getComputedStyle(this).getPropertyValue("--ueb-pin-color"); - } - - createInputObjects() { - return [ - new MouseCreateLink(this.clickableElement, this.blueprint, { - moveEverywhere: true, - looseTarget: true - }), - ] - } - - /** - * @returns {String} - */ - getPinName() { - return this.entity.PinName - } - - /** - * @returns {String} - */ - getPinDisplayName() { - return this.entity.PinName - } - - isInput() { - return this.entity.isInput() - } - - isOutput() { - return this.entity.isOutput() - } - - isConnected() { - return this.entity.isConnected() - } - - getType() { - return this.entity.getType() - } - - getClickableElement() { - return this.clickableElement - } - - getColor() { - return this.#color - } - - /** - * Returns The exact location where the link originates from or arrives at. - * @returns {Number[]} The location array - */ - getLinkLocation() { - return this.template.getLinkLocation(this) - } - - getNodeElement() { - return this.closest("ueb-node") - } - - /** - * @param {PinElement} targetPinElement - */ - linkTo(targetPinElement) { - this.entity.linkTo(targetPinElement.nodeElement.getNodeName(), targetPinElement.entity); - } - - /** - * @param {PinElement} targetPinElement - */ - unlinkFrom(targetPinElement) { - this.entity.unlinkFrom(targetPinElement.nodeElement.getNodeName(), targetPinElement.entity); - } -} - +/** + * @typedef {import("./NodeElement").default} NodeElement + * @typedef {import("../entity/GuidEntity").default} GuidEntity + */ +class PinElement extends IElement { + + static tagName = "ueb-pin" + + /** @type {NodeElement} */ + nodeElement + + /** @type {HTMLElement} */ + clickableElement + + /** @type {String} */ + #color + + constructor(entity) { + super(entity, new PinTemplate()); + /** @type {import("../entity/PinEntity").default} */ + this.entity; + /** @type {PinTemplate} */ + this.template; + } + + connectedCallback() { + super.connectedCallback(); + this.#color = window.getComputedStyle(this).getPropertyValue("--ueb-pin-color"); + } + + createInputObjects() { + return [ + new MouseCreateLink(this.clickableElement, this.blueprint, { + moveEverywhere: true, + looseTarget: true + }), + ] + } + + /** @type {GuidEntity} */ + GetPinId() { + return this.entity.PinId + } + + /** + * @returns {String} + */ + getPinName() { + return this.entity.PinName + } + + /** + * @returns {String} + */ + getPinDisplayName() { + return this.entity.PinName + } + + isInput() { + return this.entity.isInput() + } + + isOutput() { + return this.entity.isOutput() + } + + isConnected() { + return this.entity.isConnected() + } + + getType() { + return this.entity.getType() + } + + getClickableElement() { + return this.clickableElement + } + + getColor() { + return this.#color + } + + /** + * Returns The exact location where the link originates from or arrives at. + * @returns {Number[]} The location array + */ + getLinkLocation() { + return this.template.getLinkLocation(this) + } + + getNodeElement() { + return this.closest("ueb-node") + } + + getLinks() { + return this.entity.LinkedTo.map(pinReference => + pinReference + ) + } + + /** + * @param {PinElement} targetPinElement + */ + linkTo(targetPinElement) { + this.entity.linkTo(targetPinElement.nodeElement.getNodeName(), targetPinElement.entity); + this.template.applyConnected(this); + } + + /** + * @param {PinElement} targetPinElement + */ + unlinkFrom(targetPinElement) { + this.entity.unlinkFrom(targetPinElement.nodeElement.getNodeName(), targetPinElement.entity); + this.template.applyConnected(this); + } +} + customElements.define(PinElement.tagName, PinElement); /** @@ -2830,65 +2856,79 @@ class NodeTemplate extends SelectableDraggableTemplate { pins.filter(v => v.isInput()).forEach(v => inputContainer.appendChild(new PinElement(v))); pins.filter(v => v.isOutput()).forEach(v => outputContainer.appendChild(new PinElement(v))); } + + /** + * @param {NodeElement} node + * @returns {NodeListOf} + */ + getPinElements(node) { + return node.querySelectorAll(PinElement.tagName) + } } -class NodeElement extends ISelectableDraggableElement { - - static tagName = "ueb-node" - - /** - * @param {ObjectEntity} entity - */ - constructor(entity) { - super(entity, new NodeTemplate()); - /** @type {ObjectEntity} */ - this.entity; - this.dragLinkObjects = []; - super.setLocation([this.entity.NodePosX, this.entity.NodePosY]); - } - - static fromSerializedObject(str) { - let entity = SerializerFactory.getSerializer(ObjectEntity).read(str); - return new NodeElement(entity) - } - - disconnectedCallback() { - super.disconnectedCallback(); - this.dispatchDeleteEvent(); - } - - getNodeName() { - return this.entity.getName() - } - - /** - * @returns {PinEntity[]} - */ - getPinEntities() { - return this.entity.CustomProperties.filter(v => v instanceof PinEntity) - } - - connectedCallback() { - this.getAttribute("type")?.trim(); - super.connectedCallback(); - } - - setLocation(value = [0, 0]) { - let nodeType = this.entity.NodePosX.constructor; - this.entity.NodePosX = new nodeType(value[0]); - this.entity.NodePosY = new nodeType(value[1]); - super.setLocation(value); - } - - dispatchDeleteEvent(value) { - let deleteEvent = new CustomEvent(Configuration.nodeDeleteEventName, { - bubbles: true, - cancelable: true, - }); - this.dispatchEvent(deleteEvent); - } -} - +class NodeElement extends ISelectableDraggableElement { + + static tagName = "ueb-node" + + /** + * @param {ObjectEntity} entity + */ + constructor(entity) { + super(entity, new NodeTemplate()); + /** @type {ObjectEntity} */ + this.entity; + /** @type {NodeTemplate} */ + this.template; + this.dragLinkObjects = []; + super.setLocation([this.entity.NodePosX, this.entity.NodePosY]); + } + + static fromSerializedObject(str) { + let entity = SerializerFactory.getSerializer(ObjectEntity).read(str); + return new NodeElement(entity) + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.dispatchDeleteEvent(); + } + + getNodeName() { + return this.entity.getName() + } + + getPinElements() { + return this.template.getPinElements(this) + } + + /** + * @returns {PinEntity[]} + */ + getPinEntities() { + return this.entity.CustomProperties.filter(v => v instanceof PinEntity) + } + + connectedCallback() { + this.getAttribute("type")?.trim(); + super.connectedCallback(); + } + + setLocation(value = [0, 0]) { + let nodeType = this.entity.NodePosX.constructor; + this.entity.NodePosX = new nodeType(value[0]); + this.entity.NodePosY = new nodeType(value[1]); + super.setLocation(value); + } + + dispatchDeleteEvent(value) { + let deleteEvent = new CustomEvent(Configuration.nodeDeleteEventName, { + bubbles: true, + cancelable: true, + }); + this.dispatchEvent(deleteEvent); + } +} + customElements.define(NodeElement.tagName, NodeElement); class Paste extends IContext { @@ -3001,50 +3041,50 @@ class Unfocus extends IContext { } } -class IMouseWheel extends IPointing { - - /** @type {(e: WheelEvent) => void} */ - #mouseWheelHandler - - /** @type {(e: WheelEvent) => void} */ - #mouseParentWheelHandler - - /** - * @param {HTMLElement} target - * @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; - - this.#mouseWheelHandler = e => { - e.preventDefault(); - const location = self.locationFromEvent(e); - self.wheel(Math.sign(e.deltaY), location); - }; - this.#mouseParentWheelHandler = e => e.preventDefault(); - - if (this.blueprint.focused) { - this.movementSpace.addEventListener("wheel", this.#mouseWheelHandler, false); - } - } - - listenEvents() { - this.movementSpace.addEventListener("wheel", this.#mouseWheelHandler, false); - this.movementSpace.parentElement?.addEventListener("wheel", this.#mouseParentWheelHandler); - } - - unlistenEvents() { - this.movementSpace.removeEventListener("wheel", this.#mouseWheelHandler, false); - this.movementSpace.parentElement?.removeEventListener("wheel", this.#mouseParentWheelHandler); - } - - /* Subclasses will override the following method */ - wheel(variation, location) { - } +class IMouseWheel extends IPointing { + + /** @type {(e: WheelEvent) => void} */ + #mouseWheelHandler + + /** @type {(e: WheelEvent) => void} */ + #mouseParentWheelHandler + + /** + * @param {HTMLElement} target + * @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; + + this.#mouseWheelHandler = e => { + e.preventDefault(); + const location = self.locationFromEvent(e); + self.wheel(Math.sign(e.deltaY), location); + }; + this.#mouseParentWheelHandler = e => e.preventDefault(); + + if (this.blueprint.focused) { + this.movementSpace.addEventListener("wheel", this.#mouseWheelHandler, false); + } + } + + listenEvents() { + this.movementSpace.addEventListener("wheel", this.#mouseWheelHandler, false); + this.movementSpace.parentElement?.addEventListener("wheel", this.#mouseParentWheelHandler); + } + + unlistenEvents() { + this.movementSpace.removeEventListener("wheel", this.#mouseWheelHandler, false); + this.movementSpace.parentElement?.removeEventListener("wheel", this.#mouseParentWheelHandler); + } + + /* Subclasses will override the following method */ + wheel(variation, location) { + } } class Zoom extends IMouseWheel { @@ -3056,9 +3096,15 @@ class Zoom extends IMouseWheel { } } +/** + * @typedef {import("./entity/GuidEntity").default} GuidEntity + * @typedef {import("./element/PinElement").default} PinElement + */ class Blueprint extends IElement { static tagName = "ueb-blueprint" + /** @type {WeakMap} */ + #pinGuidMap = new WeakMap() /** @type {number} */ gridSize = Configuration.gridSize /** @type {NodeElement[]}" */ @@ -3328,7 +3374,6 @@ class Blueprint extends IElement { /** * Returns the list of nodes in this blueprint. It can filter the list providing just the selected ones. - * @returns {NodeElement[]} Nodes */ getNodes(selected = false) { if (selected) { @@ -3340,6 +3385,13 @@ class Blueprint extends IElement { } } + /** + * @param {GuidEntity} guid + */ + getPin(guid) { + return this.#pinGuidMap[guid] + } + /** * Returns the list of links in this blueprint. * @returns {LinkElement[]} Nodes @@ -3375,22 +3427,35 @@ class Blueprint extends IElement { * @param {...IElement} graphElements */ addGraphElement(...graphElements) { + const intoArray = element => { + if (element instanceof NodeElement) { + this.nodes.push(element); + element.getPinElements().forEach( + pinElement => this.#pinGuidMap[pinElement.GetPinId()] = pinElement + ); + } else if (element instanceof LinkElement) { + this.links.push(element); + } + }; if (this.nodesContainerElement) { graphElements.forEach(element => { if (element.closest(Blueprint.tagName) != this) { + // If not already the in target DOM position this.nodesContainerElement.appendChild(element); + intoArray(element); } - this.nodes = [...this.querySelectorAll(NodeElement.tagName)]; - this.links = [...this.querySelectorAll(LinkElement.tagName)]; }); } else { - graphElements.forEach(element => { - if (element instanceof NodeElement) { - this.nodes.push(element); - } else if (element instanceof LinkElement) { - this.links.push(element); - } - }); + graphElements + .filter(element => { + if (element instanceof NodeElement) { + return !this.nodes.includes(element) + } else if (element instanceof LinkElement) { + return !this.links.includes(element) + } + return false + }) + .forEach(intoArray); } } diff --git a/js/Blueprint.js b/js/Blueprint.js index ce67d5b..3bb1848 100755 --- a/js/Blueprint.js +++ b/js/Blueprint.js @@ -15,10 +15,15 @@ import Unfocus from "./input/mouse/Unfocus" import Utility from "./Utility" import Zoom from "./input/mouse/Zoom" +/** + * @typedef {import("./entity/GuidEntity").default} GuidEntity + * @typedef {import("./element/PinElement").default} PinElement + */ export default class Blueprint extends IElement { static tagName = "ueb-blueprint" - #pinGuidMap = new Map() + /** @type {WeakMap} */ + #pinGuidMap = new WeakMap() /** @type {number} */ gridSize = Configuration.gridSize /** @type {NodeElement[]}" */ @@ -288,7 +293,6 @@ export default class Blueprint extends IElement { /** * Returns the list of nodes in this blueprint. It can filter the list providing just the selected ones. - * @returns {NodeElement[]} Nodes */ getNodes(selected = false) { if (selected) { @@ -300,6 +304,13 @@ export default class Blueprint extends IElement { } } + /** + * @param {GuidEntity} guid + */ + getPin(guid) { + return this.#pinGuidMap[guid] + } + /** * Returns the list of links in this blueprint. * @returns {LinkElement[]} Nodes @@ -339,8 +350,7 @@ export default class Blueprint extends IElement { if (element instanceof NodeElement) { this.nodes.push(element) element.getPinElements().forEach( - pinElement => this.#pinGuidMap[ - pinElement.] + pinElement => this.#pinGuidMap[pinElement.GetPinId()] = pinElement ) } else if (element instanceof LinkElement) { this.links.push(element) diff --git a/js/element/LinkElement.js b/js/element/LinkElement.js index 3e7c482..9a0d6aa 100644 --- a/js/element/LinkElement.js +++ b/js/element/LinkElement.js @@ -53,8 +53,15 @@ export default class LinkElement extends IElement { } #unlinkPins() { - this.#source.unlinkFrom(this.#destination) - this.#destination.unlinkFrom(this.#source) + if (this.#source && this.#destination) { + this.#source.unlinkFrom(this.#destination) + this.#destination.unlinkFrom(this.#source) + } + } + + disconnectedCallback() { + super.disconnectedCallback() + this.#unlinkPins() } /** diff --git a/js/element/PinElement.js b/js/element/PinElement.js index 08e8fc6..59ca5cf 100644 --- a/js/element/PinElement.js +++ b/js/element/PinElement.js @@ -107,6 +107,7 @@ export default class PinElement extends IElement { */ linkTo(targetPinElement) { this.entity.linkTo(targetPinElement.nodeElement.getNodeName(), targetPinElement.entity) + this.template.applyConnected(this) } /** @@ -114,6 +115,7 @@ export default class PinElement extends IElement { */ unlinkFrom(targetPinElement) { this.entity.unlinkFrom(targetPinElement.nodeElement.getNodeName(), targetPinElement.entity) + this.template.applyConnected(this) } } diff --git a/js/entity/PinEntity.js b/js/entity/PinEntity.js index 539c671..ebe5c4a 100755 --- a/js/entity/PinEntity.js +++ b/js/entity/PinEntity.js @@ -71,13 +71,13 @@ export default class PinEntity extends IEntity { linkTo(targetObjectName, targetPinEntity) { /** @type {PinReferenceEntity[]} */ this.LinkedTo - const pinExists = !this.LinkedTo.find( + const linkExists = this.LinkedTo.find( /** @type {PinReferenceEntity} */ pinReferenceEntity => { return pinReferenceEntity.objectName == targetObjectName && pinReferenceEntity.pinGuid == targetPinEntity.PinId }) - if (pinExists) { + if (!linkExists) { this.LinkedTo.push(new PinReferenceEntity({ objectName: targetObjectName, pinGuid: targetPinEntity.PinId diff --git a/js/input/mouse/MouseCreateLink.js b/js/input/mouse/MouseCreateLink.js index 7a6654f..7ad5abe 100755 --- a/js/input/mouse/MouseCreateLink.js +++ b/js/input/mouse/MouseCreateLink.js @@ -64,6 +64,7 @@ export default class MouseCreateLink extends IMouseClickDrag { startDrag() { this.link = new LinkElement(this.target, null) + this.blueprint.nodesContainerElement.prepend(this.link) this.setLinkMessage(LinkMessageElement.placeNode()) this.#listenedPins = this.blueprint.querySelectorAll(this.target.constructor.tagName) this.#listenedPins.forEach(pin => { @@ -101,6 +102,5 @@ export default class MouseCreateLink extends IMouseClickDrag { setLinkMessage(linkMessage) { this.link.setLinkMessage(linkMessage) - this.blueprint.nodesContainerElement.prepend(this.link) } } diff --git a/js/serialization/ISerializer.js b/js/serialization/ISerializer.js index 38519a2..83a350d 100644 --- a/js/serialization/ISerializer.js +++ b/js/serialization/ISerializer.js @@ -35,7 +35,7 @@ export default class ISerializer { return `"${value}"` } if (value instanceof Array) { - return `(${value.map(v => serialize(v) + ",")})` + return `(${value.map(v => serialize(v) + ",").join("")})` } if (value instanceof IEntity) { return serialize(value) diff --git a/js/template/NodeTemplate.js b/js/template/NodeTemplate.js index 6ab97ec..f4b6fda 100755 --- a/js/template/NodeTemplate.js +++ b/js/template/NodeTemplate.js @@ -54,6 +54,7 @@ export default class NodeTemplate extends SelectableDraggableTemplate { /** * @param {NodeElement} node + * @returns {NodeListOf} */ getPinElements(node) { return node.querySelectorAll(PinElement.tagName) diff --git a/js/template/PinTemplate.js b/js/template/PinTemplate.js index 08f3f73..baf383b 100755 --- a/js/template/PinTemplate.js +++ b/js/template/PinTemplate.js @@ -1,8 +1,9 @@ import html from "./html" import ITemplate from "./ITemplate" +import LinkElement from "../element/LinkElement" +import NodeElement from "../element/NodeElement" import sanitizeText from "./sanitizeText" import Utility from "../Utility" -import NodeElement from "../element/NodeElement" /** * @typedef {import("../element/NodeElement").default} NodeElement @@ -40,11 +41,15 @@ export default class PinTemplate extends ITemplate { "ueb-pin-" + sanitizeText(pin.getType()) ) pin.clickableElement = pin - pin.nodeElement = pin.closest(NodeElement.tagName) - if (!pin.nodeElement) { - window.customElements.whenDefined(linkMessage.constructor.tagName).then(linkMessage) - } - pin.getLin + window.customElements.whenDefined(NodeElement.tagName).then(pin.nodeElement = pin.closest(NodeElement.tagName)) + pin.getLinks().forEach(pinReference => { + const targetPin = pin.blueprint.getPin(pinReference.pinGuid) + if (linkedToPin) { + const [sourcePin, destinationPin] = pin.isOutput() ? [pin, targetPin] : [targetPin, pin] + pin.blueprint.addGraphElement(new LinkElement(sourcePin, destinationPin)) + } + }) + } /**