From 89cdb2e5749f1a3ac8f9a59a80dac5795013c3d3 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Sun, 1 Jan 2023 18:42:54 +0100 Subject: [PATCH] #1535 - Keyboard shortcuts do not work when focus is in certain editors - Add a forwarding of keyboard events from within the iframe to the outside document --- .../src/main/ts/src/ExternalEditorFactory.ts | 184 ++++++++++-------- 1 file changed, 105 insertions(+), 79 deletions(-) diff --git a/inception/inception-external-editor/src/main/ts/src/ExternalEditorFactory.ts b/inception/inception-external-editor/src/main/ts/src/ExternalEditorFactory.ts index 2a313ee8ecf..51d44b78ac5 100644 --- a/inception/inception-external-editor/src/main/ts/src/ExternalEditorFactory.ts +++ b/inception/inception-external-editor/src/main/ts/src/ExternalEditorFactory.ts @@ -2,166 +2,192 @@ * Licensed to the Technische Universität Darmstadt under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information - * regarding copyright ownership. The Technische Universität Darmstadt + * regarding copyright ownership. The Technische Universität Darmstadt * licenses this file to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -import { AnnotationEditor, AnnotationEditorFactory, AnnotationEditorProperties, DiamClientFactory } from "@inception-project/inception-js-api"; +import { AnnotationEditor, AnnotationEditorFactory, AnnotationEditorProperties, DiamClientFactory } from '@inception-project/inception-js-api' -const PROP_EDITOR = "__editor__"; +const PROP_EDITOR = '__editor__' export class ExternalEditorFactory implements AnnotationEditorFactory { - async getOrInitialize(element: Node, diam: DiamClientFactory, props: AnnotationEditorProperties): Promise { + async getOrInitialize (element: Node, diam: DiamClientFactory, props: AnnotationEditorProperties): Promise { if (element[PROP_EDITOR] != null) { - return element[PROP_EDITOR]; + return element[PROP_EDITOR] } if (element instanceof HTMLIFrameElement) { - const iframe = element as HTMLIFrameElement; + const iframe = element as HTMLIFrameElement // Hiding editor because loading the editor resources and in particular CSS files for large // documents can take a while. This might cause the browser to render the document first // without CSS and then re-render with CSS - which causes an undesired "flickering" - iframe.style.visibility = "hidden"; + iframe.style.visibility = 'hidden' element[PROP_EDITOR] = await this.loadIFrameContent(iframe) .then(win => this.loadEditorResources(win, props)) + .then(win => this.installKeyEventForwarding(win, element.ownerDocument.defaultView)) .then(win => { if (this.isDocumentJavascriptCapable(win.document)) { // On HTML documents provide the body element as target to the editor - return this.initEditor(win, win.document.getElementsByTagName("body")[0], diam, props); + return this.initEditor(win, win.document.getElementsByTagName('body')[0], diam, props) } // On XML documents, provide the document root as target to the editor - return this.initEditor(window, win.document, diam, props); - }); + return this.initEditor(window, win.document, diam, props) + }) - // Restoring visibility - iframe.style.visibility = "visible"; - - return element[PROP_EDITOR]; + // Restoring visibility + iframe.style.visibility = 'visible' + + return element[PROP_EDITOR] } element[PROP_EDITOR] = await this.loadEditorResources(window, props) .then(win => { - return this.initEditor(win, element as HTMLElement, diam, props); - }); + return this.initEditor(win, element as HTMLElement, diam, props) + }) - return element[PROP_EDITOR]; + return element[PROP_EDITOR] } - loadIFrameContent(iframe: HTMLIFrameElement): Promise { + loadIFrameContent (iframe: HTMLIFrameElement): Promise { return new Promise(resolve => { - const iframeUrl = iframe.src; - iframe.src = 'about:blank'; + const iframeUrl = iframe.src + iframe.src = 'about:blank' const eventHandler = () => { - iframe.removeEventListener('load', eventHandler); - let content = iframe.contentDocument || iframe.contentWindow?.document; - console.debug(`IFrame has loaded: ${content?.location}`); - resolve(iframe.contentWindow); - }; - console.debug("Waiting for IFrame content to load..."); - iframe.addEventListener('load', eventHandler); - iframe.src = iframeUrl; - }); + iframe.removeEventListener('load', eventHandler) + const content = iframe.contentDocument || iframe.contentWindow?.document + console.debug(`IFrame has loaded: ${content?.location}`) + resolve(iframe.contentWindow) + } + console.debug('Waiting for IFrame content to load...') + iframe.addEventListener('load', eventHandler) + iframe.src = iframeUrl + }) } - loadEditorResources(win: Window, props: AnnotationEditorProperties): Promise { + installKeyEventForwarding (source: Window, target: Window): Promise { return new Promise(resolve => { - console.debug("Preparing to load editor resources..."); + console.debug('Installing key event forwarding...') + + if (source !== target) { + source.addEventListener('keydown', event => { + console.debug(`Forwarding keydown event: ${event.key}`) + target.document.dispatchEvent(new KeyboardEvent('keydown', event)) + }) + + source.addEventListener('keyup', event => { + console.debug(`Forwarding keyup event: ${event.key}`) + target.document.dispatchEvent(new KeyboardEvent('keyup', event)) + }) + + source.addEventListener('keypress', event => { + console.debug(`Forwarding keypress event: ${event.key}`) + target.document.dispatchEvent(new KeyboardEvent('kekeypressyup', event)) + }) + } + + resolve(source) + }) + } + + loadEditorResources (win: Window, props: AnnotationEditorProperties): Promise { + return new Promise(resolve => { + console.debug('Preparing to load editor resources...') // Make sure body is accessible via body property - seems the browser does not always ensure // this... if (win.document instanceof HTMLDocument) { if (!document.body) { - win.document.body = win.document.getElementsByTagName("body")[0]; + win.document.body = win.document.getElementsByTagName('body')[0] } } - let target = this.isDocumentJavascriptCapable(win.document) ? win.document : document; - let allPromises: Promise[] = []; + const target = this.isDocumentJavascriptCapable(win.document) ? win.document : document + const allPromises: Promise[] = [] if (this.isDocumentStylesheetCapable(win.document) && props.stylesheetSources) { - allPromises.push(...props.stylesheetSources.map(src => this.loadStylesheet(target, src))); + allPromises.push(...props.stylesheetSources.map(src => this.loadStylesheet(target, src))) } if (props.scriptSources) { - allPromises.push(...props.scriptSources.map(src => this.loadScript(target, src))); + allPromises.push(...props.scriptSources.map(src => this.loadScript(target, src))) } Promise.all(allPromises).then(() => { - console.info(`${allPromises.length} editor resources loaded`); - resolve(win); - }); - }); + console.info(`${allPromises.length} editor resources loaded`) + resolve(win) + }) + }) } - initEditor(contextWindow: Window, targetElement: HTMLElement | Document, diam: DiamClientFactory, props: AnnotationEditorProperties): Promise { + initEditor (contextWindow: Window, targetElement: HTMLElement | Document, diam: DiamClientFactory, props: AnnotationEditorProperties): Promise { return new Promise(resolve => { - console.debug("Preparing to initialize editor..."); + console.debug('Preparing to initialize editor...') - const editorFactory = (contextWindow as any).eval(props.editorFactory) as AnnotationEditorFactory; + const editorFactory = (contextWindow as any).eval(props.editorFactory) as AnnotationEditorFactory editorFactory.getOrInitialize(targetElement, diam, props).then(editor => { - console.info("Editor initialized"); - resolve(editor); - }); - }); + console.info('Editor initialized') + resolve(editor) + }) + }) } - isDocumentJavascriptCapable(document: Document): boolean { - return document instanceof HTMLDocument || document.documentElement.constructor.name === "HTMLHtmlElement"; + isDocumentJavascriptCapable (document: Document): boolean { + return document instanceof HTMLDocument || document.documentElement.constructor.name === 'HTMLHtmlElement' } - isDocumentStylesheetCapable(document: Document): boolean { - // We could theoretically also inject an XML-stylesheet processing instruction into the + isDocumentStylesheetCapable (document: Document): boolean { + // We could theoretically also inject an XML-stylesheet processing instruction into the // IFrame, but probably we could not wait for the stylesheet to load as we do in HTML - return document instanceof HTMLDocument || document.documentElement.constructor.name === "HTMLHtmlElement"; + return document instanceof HTMLDocument || document.documentElement.constructor.name === 'HTMLHtmlElement' } - loadStylesheet(document: Document, styleSheetSource: string): Promise { + loadStylesheet (document: Document, styleSheetSource: string): Promise { return new Promise(resolve => { - console.debug(`Preparing to load stylesheet: ${styleSheetSource} ...`); + console.debug(`Preparing to load stylesheet: ${styleSheetSource} ...`) - var css = document.createElement("link"); - css.rel = "stylesheet"; - css.type = "text/css"; - css.href = styleSheetSource; + const css = document.createElement('link') + css.rel = 'stylesheet' + css.type = 'text/css' + css.href = styleSheetSource css.onload = () => { - console.info(`Loaded stylesheet: ${styleSheetSource}`); - css.onload = null; - resolve(); - }; - document.getElementsByTagName('head')[0].appendChild(css); - }); + console.info(`Loaded stylesheet: ${styleSheetSource}`) + css.onload = null + resolve() + } + document.getElementsByTagName('head')[0].appendChild(css) + }) } - loadScript(document: Document, scriptSource: string): Promise { + loadScript (document: Document, scriptSource: string): Promise { return new Promise(resolve => { - console.debug(`Preparing to load script: ${scriptSource} ...`); + console.debug(`Preparing to load script: ${scriptSource} ...`) - var script = document.createElement("script"); - script.src = scriptSource; + const script = document.createElement('script') + script.src = scriptSource script.onload = () => { - console.info(`Loaded script: ${scriptSource}`); - script.onload = null; - resolve(); - }; - document.getElementsByTagName('head')[0].appendChild(script); - }); + console.info(`Loaded script: ${scriptSource}`) + script.onload = null + resolve() + } + document.getElementsByTagName('head')[0].appendChild(script) + }) } - destroy(element: Node): void { + destroy (element: Node): void { if (element[PROP_EDITOR] != null) { - element[PROP_EDITOR].destroy(); - console.info('Destroyed editor'); + element[PROP_EDITOR].destroy() + console.info('Destroyed editor') } } }