From d6dbeaaab1731e1e67f7b0733f53fd4ffa44c573 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:22:19 +0200 Subject: [PATCH 1/8] fix: sync props (state) with attributes and propagate changes with bubbles Refs: https://github.com/cheminfo/openchemlib-js/issues/229 --- .../init/canvas_editor_element.js | 137 ++++++++++++++++-- 1 file changed, 122 insertions(+), 15 deletions(-) diff --git a/lib/canvas_editor/init/canvas_editor_element.js b/lib/canvas_editor/init/canvas_editor_element.js index e8c8c86b..71dfa46d 100644 --- a/lib/canvas_editor/init/canvas_editor_element.js +++ b/lib/canvas_editor/init/canvas_editor_element.js @@ -16,16 +16,59 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { 'readonly', ]); - /* --- custom element attributes --- */ - /** @type {string} */ idcode = ''; - /** @type {boolean} */ fragment = false; - /** @type {'molecule' | 'reaction'} */ mode = - CanvasEditorElement.MODE.MOLECULE; - /** @type {boolean} */ readonly = false; + /** + * @type {{mode: 'molecule' | 'reaction', fragment: boolean, idcode: string, readonly: boolean}} + */ + #state; + + get idcode() { + return this.#state.idcode; + } + + set idcode(value) { + this.#state.idcode = String(value); + this.setAttribute('idcode', this.#state.idcode); + } + + get fragment() { + return this.#state.fragment; + } + + set fragment(value) { + this.#state.fragment = Boolean(value); + if (this.#state.fragment) { + this.setAttribute('fragment', ''); + } else { + this.removeAttribute('fragment'); + } + } + + get mode() { + return this.#state.mode; + } + + set mode(value) { + this.#state.mode = String(value); + this.setAttribute('mode', this.#state.mode); + } + + get readonly() { + return this.#state.readonly; + } + + set readonly(value) { + this.#state.readonly = Boolean(value); + if (this.#state.readonly) { + this.setAttribute('readonly', ''); + } else { + this.removeAttribute('readonly'); + } + } /* --- custom element api --- */ /** * @param {Molecule} molecule + * @this {CanvasEditorElement} */ setMolecule(molecule) { this.fragment = molecule.isFragment(); @@ -36,6 +79,7 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { /** * @return {Molecule} + * @this {CanvasEditorElement} */ getMolecule() { return this.#editor.getMolecule(); @@ -43,6 +87,7 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { /** * @param {Reaction} reaction + * @this {CanvasEditorElement} */ setReaction(reaction) { this.fragment = reaction.isFragment(); @@ -53,11 +98,15 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { /** * @return {Reaction} + * @this {CanvasEditorElement} */ getReaction() { return this.#editor.getReaction(); } + /** + * @this {CanvasEditorElement} + */ clearAll() { this.#editor.clearAll(); this.idcode = ''; @@ -78,12 +127,15 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { requestIdleCallback(() => this.#initIdCode()); } + /** + * @this {CanvasEditorElement} + */ #initIdCode() { switch (this.mode) { - case this.constructor.MODE.MOLECULE: { + case CanvasEditorElement.MODE.MOLECULE: { return this.#initMolecule(); } - case this.constructor.MODE.REACTION: { + case CanvasEditorElement.MODE.REACTION: { return this.#initReaction(); } default: @@ -91,6 +143,9 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { } } + /** + * @this {CanvasEditorElement} + */ #initMolecule() { const molecule = Molecule.fromIDCode(this.idcode); molecule.setFragment(this.fragment); @@ -98,6 +153,9 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { this.#editor.setMolecule(molecule); } + /** + * @this {CanvasEditorElement} + */ #initReaction() { const reaction = ReactionEncoder.decode(this.idcode); reaction.setFragment(this.fragment); @@ -105,10 +163,48 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { this.#editor.setReaction(reaction); } + #ignoreAttributeChange = false; + /** + * @param {() => void} fn + * @this {CanvasEditorElement} + */ + #wrapIgnoreAttributeChange(fn) { + this.#ignoreAttributeChange = true; + + try { + fn(); + } finally { + this.#ignoreAttributeChange = false; + } + } + #handleChange = (editorEventOnChange) => { + // update internal state from editor change + this.#wrapIgnoreAttributeChange(() => { + if (editorEventOnChange.mode !== 'molecule') return; + + switch (this.mode) { + case CanvasEditorElement.MODE.MOLECULE: { + const molecule = this.getMolecule(); + this.idcode = molecule.getIDCode(); + this.fragment = molecule.isFragment(); + break; + } + case CanvasEditorElement.MODE.REACTION: { + const reaction = this.getReaction(); + this.idcode = ReactionEncoder.encode(reaction); + this.fragment = reaction.isFragment(); + break; + } + default: + throw new Error(`Unsupported mode ${this.mode}`); + } + }); + + // propagate editor changes to parent const domEvent = new CustomEvent('change', { detail: editorEventOnChange, - composed: true, + bubbles: true, }); this.dispatchEvent(domEvent); }; @@ -130,6 +226,13 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { * Custom element added to page. */ connectedCallback() { + this.#state = { + idcode: this.getAttribute('idcode') || '', + fragment: this.hasAttribute('fragment'), + mode: this.getAttribute('mode') || CanvasEditorElement.MODE.MOLECULE, + readonly: this.hasAttribute('fragment'), + }; + this.#initEditor(); } @@ -148,32 +251,36 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { /** * Attribute ${name} has changed from ${oldValue} to ${newValue} + * + * Sync attribute changes to internal state. + * propagate changes to editor. */ attributeChangedCallback(name, oldValue, newValue) { + if (!this.#editor) return; + if (this.#ignoreAttributeChange) return; + const mutatorHandler = (() => { switch (name) { case 'idcode': { - this.idcode = String(newValue); + this.#state.idcode = String(newValue); return () => this.#initIdCode(); } case 'fragment': { - this.fragment = newValue !== null; + this.#state.fragment = newValue !== null; return () => this.#initIdCode(); } case 'mode': { - this.mode = String(newValue); + this.#state.mode = String(newValue); return () => this.#resetEditor(); } case 'readonly': { - this.readonly = newValue !== null; + this.#state.readonly = newValue !== null; return () => this.#resetEditor(); } default: throw new Error('unsupported attribute change'); } })(); - - if (!this.#editor) return; mutatorHandler(); } } From 41fecf98b95d273b94ca06f3029837893bc03b01 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:02:24 +0200 Subject: [PATCH 2/8] feat: vaadin events feels wrong to put them in this repository Refs: https://github.com/cheminfo/openchemlib-js/issues/229#issuecomment-2399397382 --- .../init/canvas_editor_element.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/canvas_editor/init/canvas_editor_element.js b/lib/canvas_editor/init/canvas_editor_element.js index 71dfa46d..19f71e31 100644 --- a/lib/canvas_editor/init/canvas_editor_element.js +++ b/lib/canvas_editor/init/canvas_editor_element.js @@ -115,6 +115,9 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { /* --- internals --- */ /** @type {CanvasEditor} */ #editor; + /** + * @this {CanvasEditorElement} + */ #initEditor() { if (this.#editor) return; @@ -179,6 +182,9 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { } #handleChange = (editorEventOnChange) => { + const idcode = this.idcode; + const fragment = this.fragment; + // update internal state from editor change this.#wrapIgnoreAttributeChange(() => { if (editorEventOnChange.mode !== 'molecule') return; @@ -207,6 +213,24 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { bubbles: true, }); this.dispatchEvent(domEvent); + + if (editorEventOnChange.mode !== 'molecule') return; + + // propagate vaadin events + if (this.idcode !== idcode) { + const idcodeChangeEvent = new CustomEvent('idcode-changed', { + detail: this.idcode, + bubbles: true, + }); + this.dispatchEvent(idcodeChangeEvent); + } + if (this.fragment !== fragment) { + const fragmentChangeEvent = new CustomEvent('fragment-changed', { + detail: this.fragment, + bubbles: true, + }); + this.dispatchEvent(fragmentChangeEvent); + } }; #destroyEditor() { From 1f4f81bd1de63ac68bfd44af6b915929fbca56fe Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:46:38 +0200 Subject: [PATCH 3/8] feat: copy/paste --- lib/canvas_editor/create_editor.js | 31 ++++++++++++++++++- lib/canvas_editor/events.js | 4 +-- .../init/canvas_editor_element.js | 5 +-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/lib/canvas_editor/create_editor.js b/lib/canvas_editor/create_editor.js index bd1cca95..580db155 100644 --- a/lib/canvas_editor/create_editor.js +++ b/lib/canvas_editor/create_editor.js @@ -37,7 +37,10 @@ function createEditor( webkitUserSelect: 'none', }); - const shadowRoot = rootElement.attachShadow({ mode: 'closed' }); + const shadowRoot = rootElement.attachShadow({ + mode: 'closed', + delegatesFocus: true, + }); shadowRoot.adoptedStyleSheets = [getEditorStylesheet()]; @@ -125,12 +128,38 @@ function createEditor( ); } + /** + * @param {ClipboardEvent} event + */ + function onPaste(event) { + const idcode = event.clipboardData.getData('text/plain'); + if (!idcode) return; + + const molecule = Molecule.fromIDCode(idcode); + molecule.setFragment(initialFragment); + + editorArea.setMolecule(molecule); + } + + function onCopy(event) { + const idcode = editorArea.getMolecule().getIDCode(); + event.clipboardData.setData('text/plain', idcode); + } + + if (!readOnly) { + rootElement.addEventListener('paste', onPaste); + } + rootElement.addEventListener('copy', onCopy); + function destroy() { rootElement.remove(); resizeObserver.disconnect(); removePointerListeners?.(); removeKeyboardListeners?.(); removeToolbarPointerListeners?.(); + + rootElement.removeEventListener('paste', onPaste); + rootElement.removeEventListener('copy', onCopy); } return { editorArea, toolbar, uiHelper, destroy }; diff --git a/lib/canvas_editor/events.js b/lib/canvas_editor/events.js index e1bf8b27..eae5fac7 100644 --- a/lib/canvas_editor/events.js +++ b/lib/canvas_editor/events.js @@ -74,8 +74,8 @@ function addKeyboardListeners(canvasElement, editorArea, JavaEditorArea) { function fireKeyEvent(what, ev) { const key = getKeyFromEvent(ev, JavaEditorArea); if (key === null) return; - ev.stopPropagation(); - ev.preventDefault(); + // ev.stopPropagation(); + // ev.preventDefault(); editorArea.fireKeyEvent( what, key, diff --git a/lib/canvas_editor/init/canvas_editor_element.js b/lib/canvas_editor/init/canvas_editor_element.js index 19f71e31..5e7f1fb4 100644 --- a/lib/canvas_editor/init/canvas_editor_element.js +++ b/lib/canvas_editor/init/canvas_editor_element.js @@ -270,8 +270,9 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { /** * Custom element moved to new page. */ - // eslint-disable-next-line no-empty-function - adoptedCallback() {} + adoptedCallback() { + this.connectedCallback(); + } /** * Attribute ${name} has changed from ${oldValue} to ${newValue} From 78816154743c905d3dc24278fc02818ea9d9c715 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:56:18 +0200 Subject: [PATCH 4/8] fix: readonly state from attribute init --- lib/canvas_editor/init/canvas_editor_element.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/canvas_editor/init/canvas_editor_element.js b/lib/canvas_editor/init/canvas_editor_element.js index 5e7f1fb4..8e2b1a3f 100644 --- a/lib/canvas_editor/init/canvas_editor_element.js +++ b/lib/canvas_editor/init/canvas_editor_element.js @@ -254,7 +254,7 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { idcode: this.getAttribute('idcode') || '', fragment: this.hasAttribute('fragment'), mode: this.getAttribute('mode') || CanvasEditorElement.MODE.MOLECULE, - readonly: this.hasAttribute('fragment'), + readonly: this.hasAttribute('readonly'), }; this.#initEditor(); From 0d9a56c8dfa2ddffe98ecd99ab6dc36580becc37 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:16:19 +0200 Subject: [PATCH 5/8] fix: `#handleChange` filter --- lib/canvas_editor/init/canvas_editor_element.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/canvas_editor/init/canvas_editor_element.js b/lib/canvas_editor/init/canvas_editor_element.js index 8e2b1a3f..94ded72b 100644 --- a/lib/canvas_editor/init/canvas_editor_element.js +++ b/lib/canvas_editor/init/canvas_editor_element.js @@ -181,13 +181,19 @@ function initCanvasEditorElement(CanvasEditor, Molecule, ReactionEncoder) { } } + /** + * @param {{ + * type: 'molecule' | 'selection' | 'highlight-atom' | 'highlight-bond'; + * isUserEvent: boolean; + * }} editorEventOnChange + */ #handleChange = (editorEventOnChange) => { const idcode = this.idcode; const fragment = this.fragment; // update internal state from editor change this.#wrapIgnoreAttributeChange(() => { - if (editorEventOnChange.mode !== 'molecule') return; + if (editorEventOnChange.type !== 'molecule') return; switch (this.mode) { case CanvasEditorElement.MODE.MOLECULE: { From 248fc1bc1a5b586b20ca264845ae054323d0ecd1 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:22:34 +0200 Subject: [PATCH 6/8] fix: paste fragment --- lib/canvas_editor/create_editor.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/canvas_editor/create_editor.js b/lib/canvas_editor/create_editor.js index 580db155..0b268b08 100644 --- a/lib/canvas_editor/create_editor.js +++ b/lib/canvas_editor/create_editor.js @@ -37,10 +37,7 @@ function createEditor( webkitUserSelect: 'none', }); - const shadowRoot = rootElement.attachShadow({ - mode: 'closed', - delegatesFocus: true, - }); + const shadowRoot = rootElement.attachShadow({ mode: 'closed' }); shadowRoot.adoptedStyleSheets = [getEditorStylesheet()]; @@ -136,8 +133,6 @@ function createEditor( if (!idcode) return; const molecule = Molecule.fromIDCode(idcode); - molecule.setFragment(initialFragment); - editorArea.setMolecule(molecule); } From fa5594cec7714c8b64895fed00ac2bf467c0a06f Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:14:31 +0200 Subject: [PATCH 7/8] wip: support copy/paste on reaction --- lib/canvas_editor/create_editor.js | 45 ++++++++++++++++++++++--- lib/canvas_editor/init/canvas_editor.js | 2 ++ lib/canvas_editor/init/index.js | 1 + 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/lib/canvas_editor/create_editor.js b/lib/canvas_editor/create_editor.js index 0b268b08..b245dbe4 100644 --- a/lib/canvas_editor/create_editor.js +++ b/lib/canvas_editor/create_editor.js @@ -15,6 +15,7 @@ function createEditor( JavaUIHelper, Molecule, Reaction, + ReactionEncoder, ) { const { readOnly = false, @@ -72,6 +73,16 @@ function createEditor( new EditorArea(editorCanvas, onChange), uiHelper, ); + + /** + * @return {'reaction' | 'molecule'} + */ + function getMode() { + const mode = editorArea.getMode(); + const isReaction = mode & (JavaEditorArea.MODE_REACTION !== 0); + return isReaction ? 'reaction' : 'molecule'; + } + if (initialFragment) { if (initialMode === 'molecule') { const fragmentMolecule = new Molecule(0, 0); @@ -132,13 +143,39 @@ function createEditor( const idcode = event.clipboardData.getData('text/plain'); if (!idcode) return; - const molecule = Molecule.fromIDCode(idcode); - editorArea.setMolecule(molecule); + switch (getMode()) { + case 'molecule': { + const molecule = Molecule.fromIDCode(idcode); + editorArea.setMolecule(molecule); + break; + } + case 'reaction': { + const reaction = ReactionEncoder.decode(idcode); + editorArea.setReaction(reaction); + break; + } + default: { + throw new Error(`Mode ${getMode()} is not supported`); + } + } } function onCopy(event) { - const idcode = editorArea.getMolecule().getIDCode(); - event.clipboardData.setData('text/plain', idcode); + switch (getMode()) { + case 'molecule': { + const idcode = editorArea.getMolecule().getIDCode(); + event.clipboardData.setData('text/plain', idcode); + break; + } + case 'reaction': { + const idcode = ReactionEncoder.encode(editorArea.getReaction()); + event.clipboardData.setData('text/plain', idcode); + break; + } + default: { + throw new Error(`Mode ${getMode()} is not supported`); + } + } } if (!readOnly) { diff --git a/lib/canvas_editor/init/canvas_editor.js b/lib/canvas_editor/init/canvas_editor.js index 26ddb005..8b37ba96 100644 --- a/lib/canvas_editor/init/canvas_editor.js +++ b/lib/canvas_editor/init/canvas_editor.js @@ -8,6 +8,7 @@ function initCanvasEditor( JavaUIHelper, Molecule, Reaction, + ReactionEncoder, ) { class CanvasEditor { #isReadOnly; @@ -32,6 +33,7 @@ function initCanvasEditor( JavaUIHelper, Molecule, Reaction, + ReactionEncoder, ); this.#isReadOnly = readOnly; this.#editorArea = editorArea; diff --git a/lib/canvas_editor/init/index.js b/lib/canvas_editor/init/index.js index c0dcf715..c2f3ef72 100644 --- a/lib/canvas_editor/init/index.js +++ b/lib/canvas_editor/init/index.js @@ -19,6 +19,7 @@ function init(OCL) { JavaUIHelper, Molecule, Reaction, + ReactionEncoder, ); function registerCustomElement() { From 5d1f3ff5c53dee6f09d3cf7f7ed5f75bb9826c2f Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:26:09 +0200 Subject: [PATCH 8/8] feat: add clipboard listener --- .run/Build java.run.xml | 2 +- lib/canvas_editor/clipboard_handler.js | 11 +- lib/canvas_editor/create_editor.js | 72 ++------ lib/canvas_editor/events.js | 58 ++++++- lib/canvas_editor/init/canvas_editor.js | 2 - lib/canvas_editor/init/index.js | 1 - .../gui/editor/GenericEditorArea.java | 163 ++++++++++++++++-- .../gui/generic/GenericClipboardEvent.java | 34 ++++ .../gui/generic/JSClipboardEventHandler.java | 10 ++ .../gwt/gui/generic/JSClipboardHandler.java | 64 +++---- .../gwt/gui/generic/JSEditorArea.java | 12 ++ 11 files changed, 309 insertions(+), 120 deletions(-) create mode 100644 src/com/actelion/research/gwt/chemlib/com/actelion/research/gui/generic/GenericClipboardEvent.java create mode 100644 src/com/actelion/research/gwt/gui/generic/JSClipboardEventHandler.java diff --git a/.run/Build java.run.xml b/.run/Build java.run.xml index a5d0699c..d0e8654f 100644 --- a/.run/Build java.run.xml +++ b/.run/Build java.run.xml @@ -1,6 +1,6 @@ -