UNPKG

aurelia-templating

Version:

An extensible HTML templating engine supporting databinding, custom elements, attached behaviors and more.

282 lines (225 loc) 7.62 kB
import {DOM} from 'aurelia-pal'; import {metadata} from 'aurelia-metadata'; import {HtmlBehaviorResource} from './html-behavior'; function createChildObserverDecorator(selectorOrConfig, all) { return function(target, key, descriptor) { let actualTarget = typeof key === 'string' ? target.constructor : target; //is it on a property or a class? let r = metadata.getOrCreateOwn(metadata.resource, HtmlBehaviorResource, actualTarget); if (typeof selectorOrConfig === 'string') { selectorOrConfig = { selector: selectorOrConfig, name: key }; } if (descriptor) { descriptor.writable = true; descriptor.configurable = true; } selectorOrConfig.all = all; r.addChildBinding(new ChildObserver(selectorOrConfig)); }; } /** * Creates a behavior property that references an array of immediate content child elements that matches the provided selector. */ export function children(selectorOrConfig: string | Object): any { return createChildObserverDecorator(selectorOrConfig, true); } /** * Creates a behavior property that references an immediate content child element that matches the provided selector. */ export function child(selectorOrConfig: string | Object): any { return createChildObserverDecorator(selectorOrConfig, false); } class ChildObserver { constructor(config) { this.name = config.name; this.changeHandler = config.changeHandler || this.name + 'Changed'; this.selector = config.selector; this.all = config.all; } create(viewHost, viewModel, controller) { return new ChildObserverBinder(this.selector, viewHost, this.name, viewModel, controller, this.changeHandler, this.all); } } const noMutations = []; function trackMutation(groupedMutations, binder, record) { let mutations = groupedMutations.get(binder); if (!mutations) { mutations = []; groupedMutations.set(binder, mutations); } mutations.push(record); } function onChildChange(mutations, observer) { let binders = observer.binders; let bindersLength = binders.length; let groupedMutations = new Map(); for (let i = 0, ii = mutations.length; i < ii; ++i) { let record = mutations[i]; let added = record.addedNodes; let removed = record.removedNodes; for (let j = 0, jj = removed.length; j < jj; ++j) { let node = removed[j]; if (node.nodeType === 1) { for (let k = 0; k < bindersLength; ++k) { let binder = binders[k]; if (binder.onRemove(node)) { trackMutation(groupedMutations, binder, record); } } } } for (let j = 0, jj = added.length; j < jj; ++j) { let node = added[j]; if (node.nodeType === 1) { for (let k = 0; k < bindersLength; ++k) { let binder = binders[k]; if (binder.onAdd(node)) { trackMutation(groupedMutations, binder, record); } } } } } groupedMutations.forEach((value, key) => { if (key.changeHandler !== null) { key.viewModel[key.changeHandler](value); } }); } class ChildObserverBinder { constructor(selector, viewHost, property, viewModel, controller, changeHandler, all) { this.selector = selector; this.viewHost = viewHost; this.property = property; this.viewModel = viewModel; this.controller = controller; this.changeHandler = changeHandler in viewModel ? changeHandler : null; this.usesShadowDOM = controller.behavior.usesShadowDOM; this.all = all; if (!this.usesShadowDOM && controller.view && controller.view.contentView) { this.contentView = controller.view.contentView; } else { this.contentView = null; } } matches(element) { if (element.matches(this.selector)) { if (this.contentView === null) { return true; } let contentView = this.contentView; let assignedSlot = element.auAssignedSlot; if (assignedSlot && assignedSlot.projectFromAnchors) { let anchors = assignedSlot.projectFromAnchors; for (let i = 0, ii = anchors.length; i < ii; ++i) { if (anchors[i].auOwnerView === contentView) { return true; } } return false; } return element.auOwnerView === contentView; } return false; } bind(source) { let viewHost = this.viewHost; let viewModel = this.viewModel; let observer = viewHost.__childObserver__; if (!observer) { observer = viewHost.__childObserver__ = DOM.createMutationObserver(onChildChange); let options = { childList: true, subtree: !this.usesShadowDOM }; observer.observe(viewHost, options); observer.binders = []; } observer.binders.push(this); if (this.usesShadowDOM) { //if using shadow dom, the content is already present, so sync the items let current = viewHost.firstElementChild; if (this.all) { let items = viewModel[this.property]; if (!items) { items = viewModel[this.property] = []; } else { // The existing array may alread be observed in other bindings // Setting length to 0 will not work properly, unless we intercept it items.splice(0); } while (current) { if (this.matches(current)) { items.push(current.au && current.au.controller ? current.au.controller.viewModel : current); } current = current.nextElementSibling; } if (this.changeHandler !== null) { this.viewModel[this.changeHandler](noMutations); } } else { while (current) { if (this.matches(current)) { let value = current.au && current.au.controller ? current.au.controller.viewModel : current; this.viewModel[this.property] = value; if (this.changeHandler !== null) { this.viewModel[this.changeHandler](value); } break; } current = current.nextElementSibling; } } } } onRemove(element) { if (this.matches(element)) { let value = element.au && element.au.controller ? element.au.controller.viewModel : element; if (this.all) { let items = (this.viewModel[this.property] || (this.viewModel[this.property] = [])); let index = items.indexOf(value); if (index !== -1) { items.splice(index, 1); } return true; } return false; } return false; } onAdd(element) { if (this.matches(element)) { let value = element.au && element.au.controller ? element.au.controller.viewModel : element; if (this.all) { let items = (this.viewModel[this.property] || (this.viewModel[this.property] = [])); if (this.selector === '*') { items.push(value); return true; } let index = 0; let prev = element.previousElementSibling; while (prev) { if (this.matches(prev)) { index++; } prev = prev.previousElementSibling; } items.splice(index, 0, value); return true; } this.viewModel[this.property] = value; if (this.changeHandler !== null) { this.viewModel[this.changeHandler](value); } } return false; } unbind() { if (this.viewHost.__childObserver__) { this.viewHost.__childObserver__.disconnect(); this.viewHost.__childObserver__ = null; this.viewModel[this.property] = null; } } }