blob: f9d0e1e1651db98840288a1fa5f938e84dd0749c [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of template_binding;
/** Extensions to [Element]s that behave as templates. */
class TemplateBindExtension extends _ElementExtension {
var _model;
BindingDelegate _bindingDelegate;
_TemplateIterator _iterator;
bool _setModelScheduled = false;
Element _templateInstanceRef;
// Note: only used if `this is! TemplateElement`
DocumentFragment _content;
bool _templateIsDecorated;
HtmlDocument _stagingDocument;
_InstanceBindingMap _bindingMap;
Node _refContent;
TemplateBindExtension._(Element node) : super(node);
Element get _node => super._node;
TemplateBindExtension get _self => super._node is TemplateBindExtension
? _node : this;
Bindable bind(String name, value, {bool oneTime: false}) {
if (name != 'ref') return super.bind(name, value, oneTime: oneTime);
var ref = oneTime ? value : value.open((ref) {
_node.attributes['ref'] = ref;
_refChanged();
});
_node.attributes['ref'] = ref;
_refChanged();
if (oneTime) return null;
return _updateBindings('ref', value);
}
_TemplateIterator _processBindingDirectives(_TemplateBindingMap directives) {
if (_iterator != null) _iterator._closeDependencies();
if (directives._if == null &&
directives._bind == null &&
directives._repeat == null) {
if (_iterator != null) {
_iterator.close();
_iterator = null;
}
return null;
}
if (_iterator == null) {
_iterator = new _TemplateIterator(this);
}
_iterator._updateDependencies(directives, model);
_templateObserver.observe(_node,
attributes: true, attributeFilter: ['ref']);
return _iterator;
}
/**
* Creates an instance of the template, using the provided [model] and
* optional binding [delegate].
*
* If [instanceBindings] is supplied, each [Bindable] in the returned
* instance will be added to the list. This makes it easy to close all of the
* bindings without walking the tree. This is not normally necesssary, but is
* used internally by the system.
*/
DocumentFragment createInstance([model, BindingDelegate delegate]) {
if (delegate == null) delegate = _bindingDelegate;
if (_refContent == null) _refContent = templateBind(_ref).content;
var content = _refContent;
if (content.firstChild == null) return _emptyInstance;
final map = _getInstanceBindingMap(content, delegate);
final staging = _getTemplateStagingDocument();
final instance = _stagingDocument.createDocumentFragment();
final instanceExt = new _InstanceExtension();
_instanceExtension[instance] = instanceExt
.._templateCreator = _node
.._protoContent = content;
final instanceRecord = new TemplateInstance(model);
nodeBindFallback(instance)._templateInstance = instanceRecord;
var i = 0;
bool collectTerminator = false;
for (var c = content.firstChild; c != null; c = c.nextNode, i++) {
// The terminator of the instance is the clone of the last child of the
// content. If the last child is an active template, it may produce
// instances as a result of production, so simply collecting the last
// child of the instance after it has finished producing may be wrong.
if (c.nextNode == null) collectTerminator = true;
final childMap = map != null ? map.getChild(i) : null;
var clone = _cloneAndBindInstance(c, instance, _stagingDocument,
childMap, model, delegate, instanceExt._bindings);
nodeBindFallback(clone)._templateInstance = instanceRecord;
if (collectTerminator) instanceExt._terminator = clone;
}
instanceRecord._firstNode = instance.firstChild;
instanceRecord._lastNode = instance.lastChild;
instanceExt._protoContent = null;
instanceExt._templateCreator = null;
return instance;
}
/** The data model which is inherited through the tree. */
get model => _model;
void set model(value) {
_model = value;
_ensureSetModelScheduled();
}
static Node _deepCloneIgnoreTemplateContent(Node node, stagingDocument) {
var clone = stagingDocument.importNode(node, false);
if (isSemanticTemplate(clone)) return clone;
for (var c = node.firstChild; c != null; c = c.nextNode) {
clone.append(_deepCloneIgnoreTemplateContent(c, stagingDocument));
}
return clone;
}
/**
* The binding delegate which is inherited through the tree. It can be used
* to configure custom syntax for `{{bindings}}` inside this template.
*/
BindingDelegate get bindingDelegate => _bindingDelegate;
void set bindingDelegate(BindingDelegate value) {
if (_bindingDelegate != null) {
throw new StateError('Template must be cleared before a new '
'bindingDelegate can be assigned');
}
_bindingDelegate = value;
// Clear cached state based on the binding delegate.
_bindingMap = null;
if (_iterator != null) {
_iterator._initPrepareFunctions = false;
_iterator._instanceModelFn = null;
_iterator._instancePositionChangedFn = null;
}
}
_ensureSetModelScheduled() {
if (_setModelScheduled) return;
_decorate();
_setModelScheduled = true;
scheduleMicrotask(_setModel);
}
void _setModel() {
_setModelScheduled = false;
var map = _getBindings(_node, _bindingDelegate);
_processBindings(_node, map, _model);
}
_refChanged() {
if (_iterator == null || _refContent == templateBind(_ref).content) return;
_refContent = null;
_iterator._valueChanged(null);
_iterator._updateIteratedValue(null);
}
void clear() {
_model = null;
_bindingDelegate = null;
if (bindings != null) {
var ref = bindings.remove('ref');
if (ref != null) ref.close();
}
_refContent = null;
if (_iterator == null) return;
_iterator._valueChanged(null);
_iterator.close();
_iterator = null;
}
/** Gets the template this node refers to. */
Element get _ref {
_decorate();
var ref = _searchRefId(_node, _node.attributes['ref']);
if (ref == null) {
ref = _templateInstanceRef;
if (ref == null) return _node;
}
var nextRef = templateBindFallback(ref)._ref;
return nextRef != null ? nextRef : ref;
}
/**
* Gets the content of this template.
*/
DocumentFragment get content {
_decorate();
return _content != null ? _content : (_node as TemplateElement).content;
}
/**
* Ensures proper API and content model for template elements.
*
* [instanceRef] can be used to set the [Element.ref] property of [template],
* and use the ref's content will be used as source when createInstance() is
* invoked.
*
* Returns true if this template was just decorated, or false if it was
* already decorated.
*/
static bool decorate(Element template, [Element instanceRef]) =>
templateBindFallback(template)._decorate(instanceRef);
bool _decorate([Element instanceRef]) {
// == true check because it starts as a null field.
if (_templateIsDecorated == true) return false;
_injectStylesheet();
_globalBaseUriWorkaround();
var templateElementExt = this;
_templateIsDecorated = true;
var isNativeHtmlTemplate = _node is TemplateElement;
final bootstrapContents = isNativeHtmlTemplate;
final liftContents = !isNativeHtmlTemplate;
var liftRoot = false;
if (!isNativeHtmlTemplate) {
if (_isAttributeTemplate(_node)) {
if (instanceRef != null) {
// Dart note: this is just an assert in JS.
throw new ArgumentError('instanceRef should not be supplied for '
'attribute templates.');
}
templateElementExt = templateBind(
_extractTemplateFromAttributeTemplate(_node));
templateElementExt._templateIsDecorated = true;
isNativeHtmlTemplate = templateElementExt._node is TemplateElement;
liftRoot = true;
} else if (_isSvgTemplate(_node)) {
templateElementExt = templateBind(
_extractTemplateFromSvgTemplate(_node));
templateElementExt._templateIsDecorated = true;
isNativeHtmlTemplate = templateElementExt._node is TemplateElement;
}
}
if (!isNativeHtmlTemplate) {
var doc = _getOrCreateTemplateContentsOwner(templateElementExt._node);
templateElementExt._content = doc.createDocumentFragment();
}
if (instanceRef != null) {
// template is contained within an instance, its direct content must be
// empty
templateElementExt._templateInstanceRef = instanceRef;
} else if (liftContents) {
_liftNonNativeChildrenIntoContent(templateElementExt, _node, liftRoot);
} else if (bootstrapContents) {
bootstrap(templateElementExt.content);
}
return true;
}
static final _contentsOwner = new Expando();
static final _ownerStagingDocument = new Expando();
// http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#dfn-template-contents-owner
static HtmlDocument _getOrCreateTemplateContentsOwner(Element template) {
var doc = template.ownerDocument;
if (doc.window == null) return doc;
var d = _contentsOwner[doc];
if (d == null) {
// TODO(arv): This should either be a Document or HTMLDocument depending
// on doc.
d = doc.implementation.createHtmlDocument('');
while (d.lastChild != null) {
d.lastChild.remove();
}
_contentsOwner[doc] = d;
}
return d;
}
HtmlDocument _getTemplateStagingDocument() {
if (_stagingDocument == null) {
var owner = _node.ownerDocument;
var doc = _ownerStagingDocument[owner];
if (doc == null) {
doc = owner.implementation.createHtmlDocument('');
_isStagingDocument[doc] = true;
_baseUriWorkaround(doc);
_ownerStagingDocument[owner] = doc;
}
_stagingDocument = doc;
}
return _stagingDocument;
}
// For non-template browsers, the parser will disallow <template> in certain
// locations, so we allow "attribute templates" which combine the template
// element with the top-level container node of the content, e.g.
//
// <tr template repeat="{{ foo }}"" class="bar"><td>Bar</td></tr>
//
// becomes
//
// <template repeat="{{ foo }}">
// + #document-fragment
// + <tr class="bar">
// + <td>Bar</td>
//
static Element _extractTemplateFromAttributeTemplate(Element el) {
var template = el.ownerDocument.createElement('template');
el.parentNode.insertBefore(template, el);
for (var name in el.attributes.keys.toList()) {
switch (name) {
case 'template':
el.attributes.remove(name);
break;
case 'repeat':
case 'bind':
case 'ref':
template.attributes[name] = el.attributes.remove(name);
break;
}
}
return template;
}
static Element _extractTemplateFromSvgTemplate(Element el) {
var template = el.ownerDocument.createElement('template');
el.parentNode.insertBefore(template, el);
template.attributes.addAll(el.attributes);
el.attributes.clear();
el.remove();
return template;
}
static void _liftNonNativeChildrenIntoContent(TemplateBindExtension template,
Element el, bool useRoot) {
var content = template.content;
if (useRoot) {
content.append(el);
return;
}
var child;
while ((child = el.firstChild) != null) {
content.append(child);
}
}
/**
* This used to decorate recursively all templates from a given node.
*
* By default [decorate] will be called on templates lazily when certain
* properties such as [model] are accessed, but it can be run eagerly to
* decorate an entire tree recursively.
*/
// TODO(rafaelw): Review whether this is the right public API.
static void bootstrap(Node content) {
void _bootstrap(template) {
if (!TemplateBindExtension.decorate(template)) {
bootstrap(templateBind(template).content);
}
}
// Need to do this first as the contents may get lifted if |node| is
// template.
// TODO(jmesserly): content is DocumentFragment or Element
var descendents =
(content as dynamic).querySelectorAll(_allTemplatesSelectors);
if (isSemanticTemplate(content)) {
_bootstrap(content);
}
descendents.forEach(_bootstrap);
}
static final String _allTemplatesSelectors =
'template, ' +
_SEMANTIC_TEMPLATE_TAGS.keys.map((k) => "$k[template]").join(", ");
static bool _initStyles;
// This is to replicate template_element.css
// TODO(jmesserly): move this to an opt-in CSS file?
static void _injectStylesheet() {
if (_initStyles == true) return;
_initStyles = true;
var style = new StyleElement()
..text = '$_allTemplatesSelectors { display: none; }';
document.head.append(style);
}
static bool _initBaseUriWorkaround;
static void _globalBaseUriWorkaround() {
if (_initBaseUriWorkaround == true) return;
_initBaseUriWorkaround = true;
var t = document.createElement('template');
if (t is TemplateElement) {
var d = t.content.ownerDocument;
if (d.documentElement == null) {
d.append(d.createElement('html')).append(d.createElement('head'));
}
// don't patch this if TemplateBinding.js already has.
if (d.head.querySelector('base') == null) {
_baseUriWorkaround(d);
}
}
}
// TODO(rafaelw): Remove when fix for
// https://codereview.chromium.org/164803002/
// makes it to Chrome release.
static void _baseUriWorkaround(HtmlDocument doc) {
BaseElement base = doc.createElement('base');
base.href = document.baseUri;
doc.head.append(base);
}
static final _templateObserver = new MutationObserver((records, _) {
for (MutationRecord record in records) {
templateBindFallback(record.target)._refChanged();
}
});
}
final DocumentFragment _emptyInstance = () {
var empty = new DocumentFragment();
_instanceExtension[empty] = new _InstanceExtension();
return empty;
}();
// TODO(jmesserly): if we merged with wtih TemplateInstance, it seems like it
// would speed up some operations (e.g. _getInstanceRoot wouldn't need to walk
// the parent chain).
class _InstanceExtension {
final List _bindings = [];
Node _terminator;
Element _templateCreator;
DocumentFragment _protoContent;
}
// TODO(jmesserly): this is private in JS but public for us because pkg:polymer
// uses it.
List getTemplateInstanceBindings(DocumentFragment fragment) {
var ext = _instanceExtension[fragment];
return ext != null ? ext._bindings : ext;
}
/// Gets the root of the current node's parent chain
_getFragmentRoot(Node node) {
var p;
while ((p = node.parentNode) != null) {
node = p;
}
return node;
}
Node _searchRefId(Node node, String id) {
if (id == null || id == '') return null;
final selector = '#$id';
while (true) {
node = _getFragmentRoot(node);
Node ref = null;
_InstanceExtension instance = _instanceExtension[node];
if (instance != null && instance._protoContent != null) {
ref = instance._protoContent.querySelector(selector);
} else if (_hasGetElementById(node)) {
ref = (node as dynamic).getElementById(id);
}
if (ref != null) return ref;
if (instance == null) return null;
node = instance._templateCreator;
if (node == null) return null;
}
}
_getInstanceRoot(node) {
while (node.parentNode != null) {
node = node.parentNode;
}
_InstanceExtension instance = _instanceExtension[node];
return instance != null && instance._templateCreator != null ? node : null;
}
final Expando<_InstanceExtension> _instanceExtension = new Expando();
final _isStagingDocument = new Expando();