UNPKG

aurelia-templating

Version:

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

423 lines (347 loc) 11.2 kB
import {DOM} from 'aurelia-pal'; import {_isAllWhitespace} from './util'; let noNodes = Object.freeze([]); export class SlotCustomAttribute { static inject() { return [DOM.Element]; } constructor(element) { this.element = element; this.element.auSlotAttribute = this; } valueChanged(newValue, oldValue) { //console.log('au-slot', newValue); } } export class PassThroughSlot { constructor(anchor, name, destinationName, fallbackFactory) { this.anchor = anchor; this.anchor.viewSlot = this; this.name = name; this.destinationName = destinationName; this.fallbackFactory = fallbackFactory; this.destinationSlot = null; this.projections = 0; this.contentView = null; let attr = new SlotCustomAttribute(this.anchor); attr.value = this.destinationName; } get needsFallbackRendering() { return this.fallbackFactory && this.projections === 0; } renderFallbackContent(view, nodes, projectionSource, index) { if (this.contentView === null) { this.contentView = this.fallbackFactory.create(this.ownerView.container); this.contentView.bind(this.ownerView.bindingContext, this.ownerView.overrideContext); let slots = Object.create(null); slots[this.destinationSlot.name] = this.destinationSlot; ShadowDOM.distributeView(this.contentView, slots, projectionSource, index, this.destinationSlot.name); } } passThroughTo(destinationSlot) { this.destinationSlot = destinationSlot; } addNode(view, node, projectionSource, index) { if (this.contentView !== null) { this.contentView.removeNodes(); this.contentView.detached(); this.contentView.unbind(); this.contentView = null; } if (node.viewSlot instanceof PassThroughSlot) { node.viewSlot.passThroughTo(this); return; } this.projections++; this.destinationSlot.addNode(view, node, projectionSource, index); } removeView(view, projectionSource) { this.projections--; this.destinationSlot.removeView(view, projectionSource); if (this.needsFallbackRendering) { this.renderFallbackContent(null, noNodes, projectionSource); } } removeAll(projectionSource) { this.projections = 0; this.destinationSlot.removeAll(projectionSource); if (this.needsFallbackRendering) { this.renderFallbackContent(null, noNodes, projectionSource); } } projectFrom(view, projectionSource) { this.destinationSlot.projectFrom(view, projectionSource); } created(ownerView) { this.ownerView = ownerView; } bind(view) { if (this.contentView) { this.contentView.bind(view.bindingContext, view.overrideContext); } } attached() { if (this.contentView) { this.contentView.attached(); } } detached() { if (this.contentView) { this.contentView.detached(); } } unbind() { if (this.contentView) { this.contentView.unbind(); } } } export class ShadowSlot { constructor(anchor, name, fallbackFactory) { this.anchor = anchor; this.anchor.isContentProjectionSource = true; this.anchor.viewSlot = this; this.name = name; this.fallbackFactory = fallbackFactory; this.contentView = null; this.projections = 0; this.children = []; this.projectFromAnchors = null; this.destinationSlots = null; } get needsFallbackRendering() { return this.fallbackFactory && this.projections === 0; } addNode(view, node, projectionSource, index, destination) { if (this.contentView !== null) { this.contentView.removeNodes(); this.contentView.detached(); this.contentView.unbind(); this.contentView = null; } if (node.viewSlot instanceof PassThroughSlot) { node.viewSlot.passThroughTo(this); return; } if (this.destinationSlots !== null) { ShadowDOM.distributeNodes(view, [node], this.destinationSlots, this, index); } else { node.auOwnerView = view; node.auProjectionSource = projectionSource; node.auAssignedSlot = this; let anchor = this._findAnchor(view, node, projectionSource, index); let parent = anchor.parentNode; parent.insertBefore(node, anchor); this.children.push(node); this.projections++; } } removeView(view, projectionSource) { if (this.destinationSlots !== null) { ShadowDOM.undistributeView(view, this.destinationSlots, this); } else if (this.contentView && this.contentView.hasSlots) { ShadowDOM.undistributeView(view, this.contentView.slots, projectionSource); } else { let found = this.children.find(x => x.auSlotProjectFrom === projectionSource); if (found) { let children = found.auProjectionChildren; for (let i = 0, ii = children.length; i < ii; ++i) { let child = children[i]; if (child.auOwnerView === view) { children.splice(i, 1); view.fragment.appendChild(child); i--; ii--; this.projections--; } } if (this.needsFallbackRendering) { this.renderFallbackContent(view, noNodes, projectionSource); } } } } removeAll(projectionSource) { if (this.destinationSlots !== null) { ShadowDOM.undistributeAll(this.destinationSlots, this); } else if (this.contentView && this.contentView.hasSlots) { ShadowDOM.undistributeAll(this.contentView.slots, projectionSource); } else { let found = this.children.find(x => x.auSlotProjectFrom === projectionSource); if (found) { let children = found.auProjectionChildren; for (let i = 0, ii = children.length; i < ii; ++i) { let child = children[i]; child.auOwnerView.fragment.appendChild(child); this.projections--; } found.auProjectionChildren = []; if (this.needsFallbackRendering) { this.renderFallbackContent(null, noNodes, projectionSource); } } } } _findAnchor(view, node, projectionSource, index) { if (projectionSource) { //find the anchor associated with the projected view slot let found = this.children.find(x => x.auSlotProjectFrom === projectionSource); if (found) { if (index !== undefined) { let children = found.auProjectionChildren; let viewIndex = -1; let lastView; for (let i = 0, ii = children.length; i < ii; ++i) { let current = children[i]; if (current.auOwnerView !== lastView) { viewIndex++; lastView = current.auOwnerView; if (viewIndex >= index && lastView !== view) { children.splice(i, 0, node); return current; } } } } found.auProjectionChildren.push(node); return found; } } return this.anchor; } projectTo(slots) { this.destinationSlots = slots; } projectFrom(view, projectionSource) { let anchor = DOM.createComment('anchor'); let parent = this.anchor.parentNode; anchor.auSlotProjectFrom = projectionSource; anchor.auOwnerView = view; anchor.auProjectionChildren = []; parent.insertBefore(anchor, this.anchor); this.children.push(anchor); if (this.projectFromAnchors === null) { this.projectFromAnchors = []; } this.projectFromAnchors.push(anchor); } renderFallbackContent(view, nodes, projectionSource, index) { if (this.contentView === null) { this.contentView = this.fallbackFactory.create(this.ownerView.container); this.contentView.bind(this.ownerView.bindingContext, this.ownerView.overrideContext); this.contentView.insertNodesBefore(this.anchor); } if (this.contentView.hasSlots) { let slots = this.contentView.slots; let projectFromAnchors = this.projectFromAnchors; if (projectFromAnchors !== null) { for (let slotName in slots) { let slot = slots[slotName]; for (let i = 0, ii = projectFromAnchors.length; i < ii; ++i) { let anchor = projectFromAnchors[i]; slot.projectFrom(anchor.auOwnerView, anchor.auSlotProjectFrom); } } } this.fallbackSlots = slots; ShadowDOM.distributeNodes(view, nodes, slots, projectionSource, index); } } created(ownerView) { this.ownerView = ownerView; } bind(view) { if (this.contentView) { this.contentView.bind(view.bindingContext, view.overrideContext); } } attached() { if (this.contentView) { this.contentView.attached(); } } detached() { if (this.contentView) { this.contentView.detached(); } } unbind() { if (this.contentView) { this.contentView.unbind(); } } } export class ShadowDOM { static defaultSlotKey = '__au-default-slot-key__'; static getSlotName(node) { if (node.auSlotAttribute === undefined) { return ShadowDOM.defaultSlotKey; } return node.auSlotAttribute.value; } static distributeView(view, slots, projectionSource, index, destinationOverride) { let nodes; if (view === null) { nodes = noNodes; } else { let childNodes = view.fragment.childNodes; let ii = childNodes.length; nodes = new Array(ii); for (let i = 0; i < ii; ++i) { nodes[i] = childNodes[i]; } } ShadowDOM.distributeNodes( view, nodes, slots, projectionSource, index, destinationOverride ); } static undistributeView(view, slots, projectionSource) { for (let slotName in slots) { slots[slotName].removeView(view, projectionSource); } } static undistributeAll(slots, projectionSource) { for (let slotName in slots) { slots[slotName].removeAll(projectionSource); } } static distributeNodes(view, nodes, slots, projectionSource, index, destinationOverride) { for (let i = 0, ii = nodes.length; i < ii; ++i) { let currentNode = nodes[i]; let nodeType = currentNode.nodeType; if (currentNode.isContentProjectionSource) { currentNode.viewSlot.projectTo(slots); for (let slotName in slots) { slots[slotName].projectFrom(view, currentNode.viewSlot); } nodes.splice(i, 1); ii--; i--; } else if (nodeType === 1 || nodeType === 3 || currentNode.viewSlot instanceof PassThroughSlot) { //project only elements and text if (nodeType === 3 && _isAllWhitespace(currentNode)) { nodes.splice(i, 1); ii--; i--; } else { let found = slots[destinationOverride || ShadowDOM.getSlotName(currentNode)]; if (found) { found.addNode(view, currentNode, projectionSource, index); nodes.splice(i, 1); ii--; i--; } } } else { nodes.splice(i, 1); ii--; i--; } } for (let slotName in slots) { let slot = slots[slotName]; if (slot.needsFallbackRendering) { slot.renderFallbackContent(view, nodes, projectionSource, index); } } } }