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 () => { 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==