UNPKG

@awcrotwell/motion

Version:

Motion allows you to build reactive, real-time frontend UI components in your Amber application using pure Crystal that are reusable, testable & encapsulated. For brevity, we will call them MotionComponents.

128 lines (127 loc) 4.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const BindingManager_1 = __importDefault(require("./BindingManager")); class AttributeTracker { constructor(attribute, createManager) { this.attribute = attribute; this.createManager = createManager; this._managers = new Map(); this._attributeSelector = `[${attribute}]`; this._mutationObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { this._processMutation(mutation); } }); } attachRoot(element) { this._forEachMatchingUnder(element, (match) => this._detect(match)); this._mutationObserver.observe(element, { attributes: true, attributeFilter: [this.attribute], childList: true, subtree: true, }); } shutdown() { this._mutationObserver.disconnect(); for (const manager of this._managers.values()) { if (manager) { this._errorBoundry(() => manager.shutdown()); } } this._managers.clear(); } getManager(element) { return this._managers.get(element); } _detect(element) { let manager = null; this._errorBoundry(() => { if (this._managers.has(element)) { throw new Error('Double detect in AttributeTracker'); } manager = this.createManager(element); }); if (manager) this._managers.set(element, manager); } _update(element) { const manager = this._managers.get(element); if (manager && manager instanceof BindingManager_1.default) { this._errorBoundry(() => manager.update()); } else { this._remove(element); this._detect(element); } } _remove(element) { const manager = this._managers.get(element); if (manager && manager.shutdown) { this._errorBoundry(() => manager.shutdown()); } this._managers.delete(element); } _processMutation(mutation) { if (mutation.type === 'childList') { this._processChildListMutation(mutation); } else if (mutation.type === 'attributes') { this._processAttributesMutation(mutation); } } _processChildListMutation({ removedNodes, addedNodes }) { this._forEachMatchingIn(removedNodes, (match) => this._remove(match)); this._forEachMatchingIn(addedNodes, (match) => this._detect(match)); } _processAttributesMutation({ target }) { if (this._managers.has(target)) { this._processAttributeUpdateToTracked(target); } else { this._processAttributeUpdateToUntracked(target); } } _processAttributeUpdateToTracked(element) { if (element.hasAttribute(this.attribute)) { this._update(element); } else { this._remove(element); } } _processAttributeUpdateToUntracked(element) { if (element.hasAttribute(this.attribute)) { this._detect(element); } } _forEachMatchingIn(nodes, callback) { for (const node of nodes) { this._forEachMatchingUnder(node, callback); } } _forEachMatchingUnder(node, callback) { if (node.hasAttribute && node.hasAttribute(this.attribute)) { callback(node); } if (node.querySelectorAll) { for (const match of node.querySelectorAll(this._attributeSelector)) { callback(match); } } } // eslint-disable-next-line class-methods-use-this _errorBoundry(callback) { try { callback(); } catch (error) { // eslint-disable-next-line no-console console.error('[Motion] An internal error occurred:', error); } } } exports.default = AttributeTracker;