diff --git a/src/core/basepattern.js b/src/core/basepattern.js index 733e556d9..d935c1402 100644 --- a/src/core/basepattern.js +++ b/src/core/basepattern.js @@ -21,6 +21,7 @@ class BasePattern { parser_group_options = true; parser_multiple = undefined; parser_inherit = true; + init_lazy = false; constructor(el, options = {}) { // Make static variables available on instance. @@ -59,45 +60,62 @@ class BasePattern { // Both limitations are gone in next tick. // window.setTimeout(async () => { - if (typeof this.el[`pattern-${this.name}`] !== "undefined") { - // Do not reinstantiate - log.debug(`Not reinstatiating the pattern ${this.name}.`, this.el); - - // Notify that not instantiated - this.el.dispatchEvent( - new Event(`not-init.${this.name}.patterns`, { - bubbles: true, - cancelable: false, - }) - ); - return; + if (this.init_lazy === true) { + const observer = new IntersectionObserver(async (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + await this.pattern_init(options); + observer.unobserve(entry.target); + } + } + }); + + observer.observe(this.el); + } else { + await this.pattern_init(options); } + }, 0); + } - // Create the options object by parsing the element and using the - // optional options as default. - this.options = - this.parser?.parse( - this.el, - options, - this.parser_multiple, - this.parser_inherit, - this.parser_group_options - ) ?? options; - - // Store pattern instance on element - this.el[`pattern-${this.name}`] = this; - - // Initialize the pattern - await this.init(); + async pattern_init(options) { + if (typeof this.el[`pattern-${this.name}`] !== "undefined") { + // Do not reinstantiate + log.debug(`Not reinstatiating the pattern ${this.name}.`, this.el); - // Notify that now ready + // Notify that not instantiated this.el.dispatchEvent( - new Event(`init.${this.name}.patterns`, { + new Event(`not-init.${this.name}.patterns`, { bubbles: true, - cancelable: true, + cancelable: false, }) ); - }, 0); + return; + } + + // Create the options object by parsing the element and using the + // optional options as default. + this.options = + this.parser?.parse( + this.el, + options, + this.parser_multiple, + this.parser_inherit, + this.parser_group_options + ) ?? options; + + // Store pattern instance on element + this.el[`pattern-${this.name}`] = this; + + // Initialize the pattern + await this.init(); + + // Notify that now ready + this.el.dispatchEvent( + new Event(`init.${this.name}.patterns`, { + bubbles: true, + cancelable: true, + }) + ); } async init() { @@ -110,7 +128,7 @@ class BasePattern { dom: this.el, action: action, ...options, - } + }; this.el.dispatchEvent(events.update_event(options)); } diff --git a/src/core/registry.js b/src/core/registry.js index aef82d554..95aa1d48a 100644 --- a/src/core/registry.js +++ b/src/core/registry.js @@ -60,7 +60,13 @@ const registry = { // registration just registers a pattern. Once init is called, // the DOM is scanned. After that registering a new pattern // results in rescanning the DOM only for this pattern. - init() { + init(patterns) { + // Extend this registries patterns object. + // This is a way to inject lazy loading patterns. + if (typeof patterns === "object") { + this.patterns = { ...this.patterns, ...patterns }; + } + dom.document_ready(() => { if (window.__patternslib_registry_initialized) { // Do not reinitialize a already initialized registry. @@ -108,27 +114,46 @@ const registry = { /* Initialize the pattern with the provided name and in the context * of the passed in DOM element. */ - const $el = $(el); const pattern = registry.patterns[name]; - const plog = logging.getLogger(`pat.${name}`); if (el.matches(pattern.trigger)) { - plog.debug("Initialising.", el); - try { - if (pattern.init) { - // old style initialisation - pattern.init($el, null, trigger); - } else { - // class based pattern initialisation - new pattern($el, null, trigger); - } + if (pattern.importer) { + const observer = new IntersectionObserver(async (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + const _pattern = await pattern.importer(); + this.patterns[name] = _pattern; + this.initPattern__initializer(_pattern, name, el, trigger); + observer.unobserve(el); + } + } + }); - plog.debug("done."); - } catch (e) { - if (dont_catch) { - throw e; - } - plog.error("Caught error:", e); + observer.observe(el); + } else { + this.initPattern__initializer(pattern, name, el, trigger); + } + } + }, + + initPattern__initializer(pattern, name, el, trigger) { + const $el = $(el); + const logger = logging.getLogger(`pat.${name}`); + logger.debug("Initialising.", el); + try { + if (pattern.init) { + // old style initialisation + pattern.init($el, null, trigger); + } else { + // class based pattern initialisation + new pattern($el, null, trigger); + } + + logger.debug("done."); + } catch (e) { + if (dont_catch) { + throw e; } + logger.error("Caught error:", e); } }, @@ -177,20 +202,17 @@ const registry = { // Clean up selectors: // - Remove whitespace, // - Remove trailing commas, - // - Join to selecto string. - const selector_string = selectors.map( - (selector) => selector.trim().replace(/,$/, "") - ).join(","); + // - Join to selector string. + const selector_string = selectors + .map((selector) => selector.trim().replace(/,$/, "")) + .join(","); // Exit, if no selector. if (!selector_string) { return; } - let matches = dom.querySelectorAllAndMe( - content, - selector_string - ); + let matches = dom.querySelectorAllAndMe(content, selector_string); matches = matches.filter((el) => { // Filter out patterns: // - with class ``.disable-patterns`` or wrapped within. diff --git a/src/pat/inject/inject.js b/src/pat/inject/inject.js index 0e75b6873..cfc96cb4c 100644 --- a/src/pat/inject/inject.js +++ b/src/pat/inject/inject.js @@ -38,6 +38,7 @@ parser.addArgument("class"); // Add a class to the injected content. parser.addArgument("history", "none", ["none", "record"]); parser.addArgument("push-marker"); parser.addArgument("scroll"); +parser.addArgument("remove-tags", "script", [], true); // Note: this should not be here but the parser would bail on unknown // parameters and expand/collapsible need to pass the url to us. @@ -826,14 +827,28 @@ const inject = { elementbefore: "before", }[cfg.action]; - // Inject the content HERE! target[method](...source_nodes); + if (! cfg.removeTags?.includes("script")) { + // Find and execute scripts + for (const node of source_nodes) { + const scripts = node.querySelectorAll?.("script") || []; + for (const script of scripts) { + const new_script = document.createElement("script"); + for (const attr of [...script.attributes]) { + new_script.setAttribute(attr.name, attr.value) + } + new_script.textContent = script.textContent; + script.replaceWith(new_script); + } + } + } + return true; }, - _sourcesFromHtml(html, url, sources) { - const $html = this._parseRawHtml(html, url); + _sourcesFromHtml(html, url, sources, cfg) { + const $html = this._parseRawHtml(html, url, cfg); return sources.map((source) => { if (source === "body") { source = "#__original_body"; @@ -971,17 +986,21 @@ const inject = { return page.innerHTML.trim(); }, - _parseRawHtml(html, url = "") { + _parseRawHtml(html, url = "", cfg = {}) { // remove script tags and head and replace body by a div const title = html.match(/\(.*)\<\/title\>/); let clean_html = html - .replace(/)<[^<]*)*<\/script>/gi, "") .replace(/)<[^<]*)*<\/head>/gi, "") .replace(/]*?)>/gi, "") .replace(/<\/html([^>]*?)>/gi, "") .replace(/]*?)>/gi, '
') .replace(/<\/body([^>]*?)>/gi, "
"); + for (const tag of cfg.removeTags || []) { + const re = RegExp(String.raw`<${tag}\b[^<]*(?:(?!<\/${tag}>)<[^<]*)*<\/${tag}>`, "gi") + clean_html = clean_html.replace(re, ""); + } + if (title && title.length == 2) { clean_html = title[0] + clean_html; } @@ -1122,7 +1141,7 @@ const inject = { sources(cfgs, data) { const sources = cfgs.map((cfg) => cfg.source); sources.push("title"); - const result = this._sourcesFromHtml(data, cfgs[0].url, sources); + const result = this._sourcesFromHtml(data, cfgs[0].url, sources, cfgs[0]); return result; }, }, diff --git a/src/patterns.js b/src/patterns.js index 7537e47df..aca849273 100644 --- a/src/patterns.js +++ b/src/patterns.js @@ -5,70 +5,256 @@ // Import base import registry from "./core/registry"; -// Import all used patterns for the bundle to be generated +//// Import all used patterns for the bundle to be generated import "./core/push_kit"; -import "./pat/ajax/ajax"; -import "./pat/auto-scale/auto-scale"; -import "./pat/auto-submit/auto-submit"; -import "./pat/auto-suggest/auto-suggest"; -import "./pat/autofocus/autofocus"; -import "./pat/breadcrumbs/breadcrumbs"; -import "./pat/bumper/bumper"; -import "./pat/calendar/calendar"; -import "./pat/carousel/carousel"; -import "./pat/checklist/checklist"; -import "./pat/clone/clone"; -import "./pat/clone-code/clone-code"; -import "./pat/collapsible/collapsible"; -import "./pat/colour-picker/colour-picker"; -import "./pat/date-picker/date-picker"; -import "./pat/datetime-picker/datetime-picker"; -import "./pat/depends/depends"; -import "./pat/display-time/display-time"; -import "./pat/equaliser/equaliser"; -import "./pat/expandable-tree/expandable-tree"; import "./pat/focus/focus"; -import "./pat/form-state/form-state"; -import "./pat/forward/forward"; -import "./pat/fullscreen/fullscreen-close"; -import "./pat/fullscreen/fullscreen"; -import "./pat/gallery/gallery"; -import "./pat/image-crop/image-crop"; -import "./pat/inject/inject"; -import "./pat/legend/legend"; // NOTE: Transforms tags to

for styling reasons. -import "./pat/markdown/markdown"; -import "./pat/masonry/masonry"; -import "./pat/menu/menu"; -import "./pat/modal/modal"; -import "./pat/navigation/navigation"; -import "./pat/notification/notification"; -import "./pat/push/push"; -import "./pat/scroll-box/scroll-box"; -import "./pat/scroll/scroll"; -import "./pat/scroll-marker/scroll-marker"; -import "./pat/selectbox/selectbox"; -import "./pat/slides/slides"; -import "./pat/sortable/sortable"; -import "./pat/stacks/stacks"; -import "./pat/subform/subform"; -import "./pat/switch/switch"; -import "./pat/syntax-highlight/syntax-highlight"; -import "./pat/tabs/tabs"; -import "./pat/toggle/toggle"; -import "./pat/tooltip/tooltip"; -import "./pat/validation/validation"; -import "./pat/zoom/zoom"; - -// External patterns -import "@patternslib/pat-content-mirror"; -import "@patternslib/pat-doclock"; -import "@patternslib/pat-shopping-cart"; -import "@patternslib/pat-sortable-table"; -import "@patternslib/pat-tiptap"; -import "@patternslib/pat-upload"; // Importing pattern styles in JavaScript // Set to ``true`` to include core styles via JavaScript //window.__patternslib_import_styles = false; -registry.init(); +const lazy_patterns = { + "inject": { + trigger: ".pat-inject", + importer: async () => (await import("./pat/inject/inject")).default, + }, + "validation": { + trigger: "form.pat-validation", + importer: async () => (await import("./pat/validation/validation")).default, + }, + + "ajax": { + trigger: ".pat-ajax", + importer: async () => (await import("./pat/ajax/ajax")).default, + }, + "autoscale": { + trigger: ".pat-auto-scale", + importer: async () => (await import("./pat/auto-scale/auto-scale")).default, + }, + "autosubmit": { + trigger: ".pat-autosubmit, .pat-auto-submit", + importer: async () => (await import("./pat/auto-submit/auto-submit")).default, + }, + "autosuggest": { + trigger: ".pat-autosuggest,.pat-auto-suggest", + importer: async () => (await import("./pat/auto-suggest/auto-suggest")).default, + }, + "autofocus": { + trigger: ` + input.pat-autofocus, + input[autofocus], + select.pat-autofocus, + select[autofocus], + textarea.pat-autofocus, + textarea[autofocus], + button.pat-autofocus, + button[autofocus] + `, + importer: async () => (await import("./pat/autofocus/autofocus")).default, + }, + "breadcrumbs": { + trigger: "nav.pat-breadcrumbs", + importer: async () => (await import("./pat/breadcrumbs/breadcrumbs")).default, + }, + "bumper": { + trigger: ".pat-bumper", + importer: async () => (await import("./pat/bumper/bumper")).default, + }, + "calendar": { + trigger: ".pat-calendar", + importer: async () => (await import("./pat/calendar/calendar")).default, + }, + "carousel": { + trigger: ".pat-carousel", + importer: async () => (await import("./pat/carousel/carousel")).default, + }, + "checklist": { + trigger: ".pat-checklist", + importer: async () => (await import("./pat/checklist/checklist")).default, + }, + "clone": { + trigger: ".pat-clone", + importer: async () => (await import("./pat/clone/clone")).default, + }, + "clone-code": { + trigger: ".pat-clone-code", + importer: async () => (await import("./pat/clone-code/clone-code")).default, + }, + "collapsible": { + trigger: ".pat-collapsible", + importer: async () => (await import("./pat/collapsible/collapsible")).default, + }, + "polyfill-color": { + trigger: "input.pat-colour-picker,input.pat-color-picker", + importer: async () => + (await import("./pat/colour-picker/colour-picker")).default, + }, + "date-picker": { + trigger: ".pat-date-picker", + importer: async () => (await import("./pat/date-picker/date-picker")).default, + }, + "datetime-picker": { + trigger: ".pat-datetime-picker", + importer: async () => + (await import("./pat/datetime-picker/datetime-picker")).default, + }, + "depends": { + trigger: ".pat-depends", + importer: async () => (await import("./pat/depends/depends")).default, + }, + "display-time": { + trigger: ".pat-display-time", + importer: async () => (await import("./pat/display-time/display-time")).default, + }, + "equaliser": { + trigger: ".pat-equaliser, .pat-equalizer", + importer: async () => (await import("./pat/equaliser/equaliser")).default, + }, + "expandable": { + trigger: "ul.pat-expandable", + importer: async () => + (await import("./pat/expandable-tree/expandable-tree")).default, + }, + "form-state": { + trigger: "form.pat-form-state", + importer: async () => (await import("./pat/form-state/form-state")).default, + }, + "forward": { + trigger: ".pat-forward", + importer: async () => (await import("./pat/forward/forward")).default, + }, + "fullscreen-close": { + trigger: ".close-fullscreen", + importer: async () => + (await import("./pat/fullscreen/fullscreen-close")).default, + }, + "fullscreen": { + trigger: ".pat-fullscreen", + importer: async () => (await import("./pat/fullscreen/fullscreen")).default, + }, + "gallery": { + trigger: ".pat-gallery", + importer: async () => (await import("./pat/gallery/gallery")).default, + }, + "image-crop": { + trigger: "img.pat-image-crop", + importer: async () => (await import("./pat/image-crop/image-crop")).default, + }, + "legend": { + trigger: "legend", + importer: async () => (await import("./pat/legend/legend")).default, + }, + "markdown": { + trigger: ".pat-markdown", + importer: async () => (await import("./pat/markdown/markdown")).default, + }, + "masonry": { + trigger: ".pat-masonry", + importer: async () => (await import("./pat/masonry/masonry")).default, + }, + "menu": { + trigger: ".pat-menu", + importer: async () => (await import("./pat/menu/menu")).default, + }, + "modal": { + trigger: "div.pat-modal, a.pat-modal, form.pat-modal, .pat-modal.pat-subform", + importer: async () => (await import("./pat/modal/modal")).default, + }, + "navigation": { + trigger: ".pat-navigation", + importer: async () => (await import("./pat/navigation/navigation")).default, + }, + "notification": { + trigger: ".pat-notification", + importer: async () => (await import("./pat/notification/notification")).default, + }, + "push": { + trigger: ".pat-push", + importer: async () => (await import("./pat/push/push")).default, + }, + "scroll": { + trigger: ".pat-scroll", + importer: async () => (await import("./pat/scroll/scroll")).default, + }, + "scroll-box": { + trigger: ".pat-scroll-box", + importer: async () => (await import("./pat/scroll-box/scroll-box")).default, + }, + "scroll-marker": { + trigger: ".pat-scroll-marker", + importer: async () => + (await import("./pat/scroll-marker/scroll-marker")).default, + }, + "selectbox": { + trigger: ".pat-selectbox", + importer: async () => (await import("./pat/selectbox/selectbox")).default, + }, + "slides": { + trigger: ".pat-slides", + importer: async () => (await import("./pat/slides/slides")).default, + }, + "sortable": { + trigger: ".pat-sortable", + importer: async () => (await import("./pat/sortable/sortable")).default, + }, + "stacks": { + trigger: ".pat-stacks", + importer: async () => (await import("./pat/stacks/stacks")).default, + }, + "subform": { + trigger: ".pat-subform", + importer: async () => (await import("./pat/subform/subform")).default, + }, + "switch": { + trigger: ".pat-switch", + importer: async () => (await import("./pat/switch/switch")).default, + }, + "syntax-highlight": { + trigger: ".pat-syntax-highlight", + importer: async () => + (await import("./pat/syntax-highlight/syntax-highlight")).default, + }, + "tabs": { + trigger: ".pat-tabs", + importer: async () => (await import("./pat/tabs/tabs")).default, + }, + "toggle": { + trigger: ".pat-toggle", + importer: async () => (await import("./pat/toggle/toggle")).default, + }, + "tooltip": { + trigger: ".pat-tooltip, .pat-tooltip-ng", + importer: async () => (await import("./pat/tooltip/tooltip")).default, + }, + "zoom": { + trigger: ".pat-zoom", + importer: async () => (await import("./pat/zoom/zoom")).default, + }, + // External patterns + "content-mirror": { + trigger: ".pat-content-mirror", + importer: async () => (await import("@patternslib/pat-content-mirror")).default, + }, + "doclock": { + trigger: ".pat-doclock", + importer: async () => (await import("@patternslib/pat-doclock")).default, + }, + "shopping-cart": { + trigger: ".pat-shopping-cart", + importer: async () => (await import("@patternslib/pat-shopping-cart")).default, + }, + "sortable-table": { + trigger: ".pat-sortable-table", + importer: async () => (await import("@patternslib/pat-sortable-table")).default, + }, + "tiptap": { + trigger: ".pat-tiptap", + importer: async () => (await import("@patternslib/pat-tiptap")).default, + }, + "upload": { + trigger: ".pat-upload", + importer: async () => (await import("@patternslib/pat-upload")).default, + }, +}; + +registry.init(lazy_patterns);