diff --git a/src/core/base.js b/src/core/base.js index e81c3fc07..4a42b3ba9 100644 --- a/src/core/base.js +++ b/src/core/base.js @@ -13,9 +13,10 @@ */ import "regenerator-runtime/runtime"; // needed for ``await`` support import $ from "jquery"; -import Registry from "./registry"; +import Registry, { PATTERN_INSTANCE_REGISTRY } from "./registry"; import logging from "./logging"; import mockupParser from "./mockup-parser"; +import utils from "./utils"; const log = logging.getLogger("Patternslib Base"); @@ -50,10 +51,15 @@ const Base = async function ($el, options, trigger) { this.options = $.extend(true, {}, this.defaults || {}, options || {}); await this.init($el, options, trigger); + this.id = utils.unique_id(); // Generate a unique id + // Store pattern instance on element this.$el.data(`pattern-${this.name}`, this); this.el[`pattern-${this.name}`] = this; + // Add Pattern instance to PATTERN_INSTANCE_REGISTRY + PATTERN_INSTANCE_REGISTRY.push(this); + this.emit("init"); }; diff --git a/src/core/events.js b/src/core/events.js index 09103de2d..0512986a0 100644 --- a/src/core/events.js +++ b/src/core/events.js @@ -14,9 +14,19 @@ const event_listener_map = {}; * @param {string} id - A unique id under which the event is registered. * @param {function} cb - The event handler / callback function. * @param {Object} opts - Options for the addEventListener API. - * + * @param {Function} remove_condition - If this function evaluates to true, + * the event listener will be deregistered and not called. + * Defaults to always return ``false``, so no check is actually done. + * Can be used to unregister event handlers automatically. */ -const add_event_listener = (el, event_type, id, cb, opts = {}) => { +const add_event_listener = ( + el, + event_type, + id, + cb, + opts = {}, + remove_condition = () => false +) => { if (!el?.addEventListener) { return; // nothing to do. } @@ -26,7 +36,15 @@ const add_event_listener = (el, event_type, id, cb, opts = {}) => { event_listener_map[el] = {}; } event_listener_map[el][id] = [event_type, cb, opts.capture ? opts : undefined]; // prettier-ignore - el.addEventListener(event_type, cb, opts); + const _cb = () => { + if (remove_condition()) { + remove_event_listener(el, id); + } else { + cb(); + } + }; + + el.addEventListener(event_type, _cb, opts); }; /** diff --git a/src/core/registry.js b/src/core/registry.js index d4a7c3819..cb9f0d2a4 100644 --- a/src/core/registry.js +++ b/src/core/registry.js @@ -54,8 +54,23 @@ if (typeof window.__patternslib_registry_initialized === "undefined") { window.__patternslib_registry_initialized = false; } +/** + * Global pattern instance registry. + * + * If your Pattern uses the base pattern base class, any concrete instance of + * the pattern will be stored in this registry. + * When the element on which the pattern is defined is removed, we call the + * ``destroy`` method on the pattern instance and remove the pattern instance + * from this registry. + */ +if (typeof window.__patternslib_instance_registry === "undefined") { + window.__patternslib_instance_registry = []; +} +export const PATTERN_INSTANCE_REGISTRY = window.__patternslib_instance_registry; + const registry = { patterns: PATTERN_REGISTRY, // reference to global patterns registry + patterns_instances: PATTERN_INSTANCE_REGISTRY, // reference to global pattern instance registry // as long as the registry is not initialized, pattern // registration just registers a pattern. Once init is called, // the DOM is scanned. After that registering a new pattern @@ -69,6 +84,19 @@ const registry = { window.__patternslib_registry_initialized = true; log.debug("Loaded: " + Object.keys(registry.patterns).sort().join(", ")); registry.scan(document.body); + + // Call the Pattern instance's destroy method when a Pattern element + // is removed. + const remove_observer = new MutationObserver((mutations, observer) => { + registry.pattern_instances.forEach((instance, idx) => { + if (!document.body.contains(instance.el)) { + instance?.destroy(); + delete registry.pattern_instances[idx]; + } + }); + }); + remove_observer.observe(document.body, { childList: true }); + log.debug("Finished initial scan."); }); }, @@ -79,6 +107,9 @@ const registry = { for (const name in registry.patterns) { delete registry.patterns[name]; } + registry.pattern_instances.forEach((instance, idx) => { + delete registry.pattern_instances[idx]; + }); }, transformPattern(name, content) { diff --git a/src/core/utils.js b/src/core/utils.js index dabb624b7..7104b3031 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -642,6 +642,15 @@ const is_iso_date_time = (value, optional_time = false) => { return re_date_time.test(value); }; +/** + * Generate a unique id. + * + * @return {String} - A unique string. + */ +const unique_id = () => { + return Math.floor((1 + Math.random()) * 0x1000000000000).toString(16); +}; + var utils = { // pattern pimping - own module? jqueryPlugin: jqueryPlugin, @@ -673,6 +682,7 @@ var utils = { escape_html: escape_html, unescape_html: unescape_html, is_iso_date_time: is_iso_date_time, + unique_id: unique_id, getCSSValue: dom.get_css_value, // BBB: moved to dom. TODO: Remove in upcoming version. };