From 6b67d3614f036b121fae14bc173b4559a55c718b Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Wed, 20 Jul 2022 15:01:52 +0200 Subject: [PATCH 1/2] feat: Late initialization. Add ``lazy`` argument (default: ``false``) to initialize tiptap only when the text container gets focus. This is done via an intermediate container which will be replaced when tiptap has finished it's initialization. This avoids initializing lots of tiptap instances when many text areas are shown, where most of the time none or only a few are likely to be used. A typical use case is to show a lot of comment boxes with tiptap mentioning functionality. --- src/tiptap.js | 130 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 91 insertions(+), 39 deletions(-) diff --git a/src/tiptap.js b/src/tiptap.js index 46ac2bf..f28c0a6 100644 --- a/src/tiptap.js +++ b/src/tiptap.js @@ -29,13 +29,73 @@ parser.addAlias("context-menu-link", "link-menu"); parser.addAlias("context-menu-mentions", "mentions-menu"); parser.addAlias("context-menu-tags", "tags-menu"); +parser.addArgument("lazy", false); + export default Base.extend({ name: "tiptap", trigger: ".pat-tiptap", async init() { - // Constructor - this.toolbar_el = null; + // Initialize the pattern and prepare initialization of the tiptap editor. + this.options = parser.parse(this.el, this.options); + this.is_form_el = ["TEXTAREA", "INPUT"].includes(this.el.tagName); + + // Hide original element which will be replaced with tiptap instance. + this.el.style.display = "none"; + + // Create container for tiptap. + // In case of lazy initialization the container is an intermediate container + // which will later be replaced by a tiptap container, after tiptap + // has been initialized off-canvas and be ready. + this.tiptap_container = this.create_tiptap_container({ + editable: this.options.lazy, + }); + if (this.options.lazy) { + // Display the text content before tiptap is being loaded. + this.tiptap_container.innerHTML = this.get_textarea_text() || "
"; + } + this.el.after(this.tiptap_container); + + if (this.options.lazy) { + events.add_event_listener( + this.tiptap_container, + "focus", + "tiptap--initialization", + () => this.init_editor(), + { once: true } + ); + } else { + await this.init_editor(); + } + }, + + get_textarea_text() { + // Textarea value getter + return this.is_form_el ? this.el.value : this.el.innerHTML; + }, + + set_textarea_text(value) { + // Textarea value setter + if (this.is_form_el) { + this.el.value = value; + } else { + this.el.innerHTML = value; + } + this.el.dispatchEvent(events.input_event()); + }, + + create_tiptap_container({ editable = false }) { + const tiptap_container = document.createElement("div"); + tiptap_container.setAttribute("class", "tiptap-container"); + if (editable) { + tiptap_container.setAttribute("contenteditable", true); + tiptap_container.setAttribute("tabindex", "-1"); // make selectable. + } + return tiptap_container; + }, + + async init_editor() { + // Initialize the tiptap editor itself. const TipTap = (await import("@tiptap/core")).Editor; const ExtDocument = (await import("@tiptap/extension-document")).default; @@ -44,37 +104,20 @@ export default Base.extend({ this.focus_handler = (await import("./focus-handler")).focus_handler; - this.options = parser.parse(this.el, this.options); - - // Hide element which will be replaced with tiptap instance - this.el.style.display = "none"; - // Create container for tiptap - const container = document.createElement("div"); - container.setAttribute("class", "tiptap-container"); - this.el.after(container); + this.toolbar_el = this.options.toolbarExternal + ? document.querySelector(this.options.toolbarExternal) + : null; + if (this.toolbar_el) { + const focus_handler_targets = (await import("./focus-handler")).TARGETS; // prettier-ignore + focus_handler_targets.push(this.toolbar_el); // We register the focus handler on itself. + this.focus_handler(this.toolbar_el); + } // Support for pat-autofocus and autofocus: Set focus depending on textarea's focus setting. const set_focus = this.el.classList.contains("pat-autofocus") || this.el.hasAttribute("autofocus"); - const is_form_el = ["TEXTAREA", "INPUT"].includes(this.el.tagName); - - const getText = () => { - // Textarea value getter - return is_form_el ? this.el.value : this.el.innerHTML; - }; - - const setText = (text) => { - // Textarea value setter - if (is_form_el) { - this.el.value = text; - } else { - this.el.innerHTML = text; - } - this.el.dispatchEvent(events.input_event()); - }; - const extra_extensions = [ // Allow non-paragraph line-breaks by default. (await import("@tiptap/extension-hard-break")).default.configure(), @@ -124,19 +167,19 @@ export default Base.extend({ ); } - this.toolbar_el = this.options.toolbarExternal - ? document.querySelector(this.options.toolbarExternal) - : null; - if (this.toolbar_el) { - const focus_handler_targets = (await import("./focus-handler")).TARGETS; // prettier-ignore - focus_handler_targets.push(this.toolbar_el); // We register the focus handler on itself. - this.focus_handler(this.toolbar_el); - } - const toolbar_ext = await import("./toolbar"); this.toolbar = toolbar_ext.init_pre({ app: this }); + + // Late initialization - create new element where tiptap is initialized on. + // This will replace the intermediate element where we only showed the + // content until tiptap was initialized. + const tiptap_container = this.options.lazy + ? this.create_tiptap_container({ editable: false }) + : this.tiptap_container; + + const self = this; this.editor = new TipTap({ - element: container, + element: tiptap_container, extensions: [ ExtDocument, ExtText, @@ -144,10 +187,19 @@ export default Base.extend({ ...(await toolbar_ext.init_extensions({ app: this })), ...extra_extensions, ], - content: getText(), + content: this.get_textarea_text(), + onCreate: () => { + if (this.options.lazy) { + // Late initialization - replace the intermediate tiptap container + // with the tiptap-initialized one. + this.tiptap_container.replaceWith(tiptap_container); + // We also need to set the this.tiptap_container to the new one. + this.tiptap_container = tiptap_container; + } + }, onUpdate() { // Note: ``this`` is the editor instance. - setText(this.getHTML()); + self.set_textarea_text(this.getHTML()); Registry.scan(this.view.dom); }, onFocus: async () => { From 95c8c6664e422cf58f4b6459c98762356c0b0b0b Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Wed, 20 Jul 2022 13:50:55 +0200 Subject: [PATCH 2/2] yarn upgrade. --- yarn.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7d9af46..ea9bba9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1701,10 +1701,10 @@ whybundled "^2.0.0" yarn "^1.22.19" -"@patternslib/patternslib@*": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@patternslib/patternslib/-/patternslib-8.1.0.tgz#165b07e8d53298ba98f248223d360d216b27b885" - integrity sha512-5j5l/JG2N6oGXhBq34jLRTpW+DjW0CijsBwDatIKloxI9o+V0XawQ2Rc9h3qFvXcryXXDxgcqVn0YXdS3l28uQ== +"@patternslib/patternslib@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@patternslib/patternslib/-/patternslib-9.0.0.tgz#0684c92627bb1578b7178bd9f92f1d226e59bffd" + integrity sha512-0nrvhl+8EN5hcjxQaNohBowzDVol5m1ZG+4amYMXPBjE07/6l+gtQCcYn9arPgsxVJqdYxN9wg5LTirMhR/n3w== dependencies: "@fullcalendar/adaptive" "^5.11.0" "@fullcalendar/core" "^5.11.0" @@ -1717,17 +1717,17 @@ "@stomp/stompjs" "^6.1.2" google-code-prettify "^1.0.5" imagesloaded "^4.1.4" - intersection-observer "^0.12.0" + intersection-observer "^0.12.2" jquery "^3.6.0" jquery-jcrop "^0.9.13" luxon "^2.3.2" masonry-layout "^4.2.2" - moment "^2.29.3" + moment "^2.29.4" moment-timezone "^0.5.34" photoswipe "^4.1.3" pikaday "^1.8.0" promise-polyfill "^8.2.3" - screenfull "^6.0.1" + screenfull "^6.0.2" select2 "^3.5.1" showdown "^2.1.0" showdown-prettify "^1.3.0" @@ -5233,7 +5233,7 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== -intersection-observer@^0.12.0: +intersection-observer@^0.12.2: version "0.12.2" resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.12.2.tgz#4a45349cc0cd91916682b1f44c28d7ec737dc375" integrity sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg== @@ -6504,7 +6504,7 @@ moment-timezone@^0.5.34: dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@^2.29.3: +"moment@>= 2.9.0", moment@^2.29.4: version "2.29.4" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== @@ -7820,7 +7820,7 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.0.0" -screenfull@^6.0.1: +screenfull@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-6.0.2.tgz#3dbe4b8c4f8f49fb8e33caa8f69d0bca730ab238" integrity sha512-AQdy8s4WhNvUZ6P8F6PB21tSPIYKniic+Ogx0AacBMjKP1GUHN2E9URxQHtCusiwxudnCKkdy4GrHXPPJSkCCw==