| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| UI.Fragment = class { |
| /** |
| * @param {!Element} element |
| */ |
| constructor(element) { |
| this._element = element; |
| |
| /** @type {!Map<string, !Array<!UI.Fragment._State>>} */ |
| this._states = new Map(); |
| |
| /** @type {!Map<string, !Element>} */ |
| this._elementsById = new Map(); |
| } |
| |
| /** |
| * @return {!Element} |
| */ |
| element() { |
| return this._element; |
| } |
| |
| /** |
| * @param {string} elementId |
| * @return {!Element} |
| */ |
| $(elementId) { |
| return this._elementsById.get(elementId); |
| } |
| |
| /** |
| * @param {string} name |
| * @param {boolean} toggled |
| */ |
| setState(name, toggled) { |
| const list = this._states.get(name); |
| if (list === undefined) { |
| console.error('Unknown state ' + name); |
| return; |
| } |
| for (const state of list) { |
| if (state.toggled === toggled) |
| continue; |
| state.toggled = toggled; |
| const value = state.attributeValue; |
| state.attributeValue = state.element.getAttribute(state.attributeName); |
| if (value === null) |
| state.element.removeAttribute(state.attributeName); |
| else |
| state.element.setAttribute(state.attributeName, value); |
| } |
| } |
| |
| /** |
| * @param {!Array<string>} strings |
| * @param {...*} vararg |
| * @return {!UI.Fragment} |
| */ |
| static build(strings, vararg) { |
| const values = Array.prototype.slice.call(arguments, 1); |
| return UI.Fragment._render(UI.Fragment._template(strings), values); |
| } |
| |
| /** |
| * @param {!Array<string>} strings |
| * @param {...*} vararg |
| * @return {!UI.Fragment} |
| */ |
| static cached(strings, vararg) { |
| const values = Array.prototype.slice.call(arguments, 1); |
| let template = UI.Fragment._templateCache.get(strings); |
| if (!template) { |
| template = UI.Fragment._template(strings); |
| UI.Fragment._templateCache.set(strings, template); |
| } |
| return UI.Fragment._render(template, values); |
| } |
| |
| /** |
| * @param {!Array<string>} strings |
| * @return {!UI.Fragment._Template} |
| * @suppressGlobalPropertiesCheck |
| */ |
| static _template(strings) { |
| let html = ''; |
| let insideText = false; |
| for (let i = 0; i < strings.length - 1; i++) { |
| html += strings[i]; |
| const close = strings[i].lastIndexOf('>'); |
| if (close !== -1) { |
| if (strings[i].indexOf('<', close + 1) === -1) |
| insideText = true; |
| else |
| insideText = false; |
| } |
| html += insideText ? UI.Fragment._textMarker : UI.Fragment._attributeMarker(i); |
| } |
| html += strings[strings.length - 1]; |
| |
| const template = window.document.createElement('template'); |
| template.innerHTML = html; |
| const walker = template.ownerDocument.createTreeWalker( |
| template.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, null, false); |
| let valueIndex = 0; |
| const emptyTextNodes = []; |
| const binds = []; |
| const nodesToMark = []; |
| while (walker.nextNode()) { |
| const node = walker.currentNode; |
| if (node.nodeType === Node.ELEMENT_NODE && node.hasAttributes()) { |
| if (node.hasAttribute('$')) { |
| nodesToMark.push(node); |
| binds.push({elementId: node.getAttribute('$')}); |
| node.removeAttribute('$'); |
| } |
| |
| const attributesToRemove = []; |
| for (let i = 0; i < node.attributes.length; i++) { |
| let name = node.attributes[i].name; |
| |
| if (name.startsWith('s-')) { |
| attributesToRemove.push(name); |
| name = name.substring(2); |
| const state = name.substring(0, name.indexOf('-')); |
| const attr = name.substring(state.length + 1); |
| nodesToMark.push(node); |
| binds.push({state: {name: state, attribute: attr, value: node.attributes[i].value}}); |
| continue; |
| } |
| |
| if (!UI.Fragment._attributeMarkerRegex.test(name) && |
| !UI.Fragment._attributeMarkerRegex.test(node.attributes[i].value)) |
| continue; |
| |
| attributesToRemove.push(name); |
| nodesToMark.push(node); |
| const bind = {attr: {index: valueIndex}}; |
| bind.attr.names = name.split(UI.Fragment._attributeMarkerRegex); |
| valueIndex += bind.attr.names.length - 1; |
| bind.attr.values = node.attributes[i].value.split(UI.Fragment._attributeMarkerRegex); |
| valueIndex += bind.attr.values.length - 1; |
| binds.push(bind); |
| } |
| for (let i = 0; i < attributesToRemove.length; i++) |
| node.removeAttribute(attributesToRemove[i]); |
| } |
| |
| if (node.nodeType === Node.TEXT_NODE && node.data.indexOf(UI.Fragment._textMarker) !== -1) { |
| const texts = node.data.split(UI.Fragment._textMarkerRegex); |
| node.data = texts[texts.length - 1]; |
| for (let i = 0; i < texts.length - 1; i++) { |
| if (texts[i]) |
| node.parentNode.insertBefore(createTextNode(texts[i]), node); |
| const nodeToReplace = createElement('span'); |
| nodesToMark.push(nodeToReplace); |
| binds.push({replaceNodeIndex: valueIndex++}); |
| node.parentNode.insertBefore(nodeToReplace, node); |
| } |
| } |
| |
| if (node.nodeType === Node.TEXT_NODE && |
| (!node.previousSibling || node.previousSibling.nodeType === Node.ELEMENT_NODE) && |
| (!node.nextSibling || node.nextSibling.nodeType === Node.ELEMENT_NODE) && /^\s*$/.test(node.data)) |
| emptyTextNodes.push(node); |
| } |
| |
| for (let i = 0; i < nodesToMark.length; i++) |
| nodesToMark[i].classList.add(UI.Fragment._class(i)); |
| |
| for (const emptyTextNode of emptyTextNodes) |
| emptyTextNode.remove(); |
| return {template: template, binds: binds}; |
| } |
| |
| /** |
| * @param {!UI.Fragment._Template} template |
| * @param {!Array<*>} values |
| * @return {!UI.Fragment} |
| */ |
| static _render(template, values) { |
| const content = template.template.ownerDocument.importNode(template.template.content, true); |
| const resultElement = |
| /** @type {!Element} */ (content.firstChild === content.lastChild ? content.firstChild : content); |
| const result = new UI.Fragment(resultElement); |
| |
| const idByElement = new Map(); |
| const boundElements = []; |
| for (let i = 0; i < template.binds.length; i++) { |
| const className = UI.Fragment._class(i); |
| const element = /** @type {!Element} */ (content.querySelector('.' + className)); |
| element.classList.remove(className); |
| boundElements.push(element); |
| } |
| |
| for (let bindIndex = 0; bindIndex < template.binds.length; bindIndex++) { |
| const bind = template.binds[bindIndex]; |
| const element = boundElements[bindIndex]; |
| if ('elementId' in bind) { |
| result._elementsById.set(/** @type {string} */ (bind.elementId), element); |
| idByElement.set(element, bind.elementId); |
| } else if ('replaceNodeIndex' in bind) { |
| const value = values[/** @type {number} */ (bind.replaceNodeIndex)]; |
| let node = null; |
| if (value instanceof Node) |
| node = value; |
| else if (value instanceof UI.Fragment) |
| node = value._element; |
| else |
| node = createTextNode('' + value); |
| |
| element.parentNode.insertBefore(node, element); |
| element.remove(); |
| } else if ('state' in bind) { |
| const list = result._states.get(bind.state.name) || []; |
| list.push( |
| {attributeName: bind.state.attribute, attributeValue: bind.state.value, element: element, toggled: false}); |
| result._states.set(bind.state.name, list); |
| } else if ('attr' in bind) { |
| if (bind.attr.names.length === 2 && bind.attr.values.length === 1 && |
| typeof values[bind.attr.index] === 'function') { |
| values[bind.attr.index].call(null, element); |
| } else { |
| let name = bind.attr.names[0]; |
| for (let i = 1; i < bind.attr.names.length; i++) { |
| name += values[bind.attr.index + i - 1]; |
| name += bind.attr.names[i]; |
| } |
| if (name) { |
| let value = bind.attr.values[0]; |
| for (let i = 1; i < bind.attr.values.length; i++) { |
| value += values[bind.attr.index + bind.attr.names.length - 1 + i - 1]; |
| value += bind.attr.values[i]; |
| } |
| element.setAttribute(name, value); |
| } |
| } |
| } else { |
| throw 'Unexpected bind'; |
| } |
| } |
| |
| // We do this after binds so that querySelector works. |
| const shadows = result._element.querySelectorAll('x-shadow'); |
| for (const shadow of shadows) { |
| if (!shadow.parentElement) |
| throw 'There must be a parent element here'; |
| const shadowRoot = UI.createShadowRootWithCoreStyles(shadow.parentElement); |
| if (shadow.parentElement.tagName === 'X-WIDGET') |
| shadow.parentElement._shadowRoot = shadowRoot; |
| const children = []; |
| while (shadow.lastChild) { |
| children.push(shadow.lastChild); |
| shadow.lastChild.remove(); |
| } |
| for (let i = children.length - 1; i >= 0; i--) |
| shadowRoot.appendChild(children[i]); |
| const id = idByElement.get(shadow); |
| if (id) |
| result._elementsById.set(id, /** @type {!Element} */ (/** @type {!Node} */ (shadowRoot))); |
| shadow.remove(); |
| } |
| |
| return result; |
| } |
| }; |
| |
| /** |
| * @typedef {!{ |
| * template: !Element, |
| * binds: !Array<!UI.Fragment._Bind> |
| * }} |
| */ |
| UI.Fragment._Template; |
| |
| /** |
| * @typedef {!{ |
| * attributeName: string, |
| * attributeValue: string, |
| * element: !Element, |
| * toggled: boolean |
| * }} |
| */ |
| UI.Fragment._State; |
| |
| /** |
| * @typedef {!{ |
| * elementId: (string|undefined), |
| * |
| * state: (!{ |
| * name: string, |
| * attribute: string, |
| * value: string |
| * }|undefined), |
| * |
| * attr: (!{ |
| * index: number, |
| * names: !Array<string>, |
| * values: !Array<string> |
| * }|undefined), |
| * |
| * replaceNodeIndex: (number|undefined) |
| * }} |
| */ |
| UI.Fragment._Bind; |
| |
| UI.Fragment._textMarker = '{{template-text}}'; |
| UI.Fragment._textMarkerRegex = /{{template-text}}/; |
| |
| UI.Fragment._attributeMarker = index => 'template-attribute' + index; |
| UI.Fragment._attributeMarkerRegex = /template-attribute\d+/; |
| |
| UI.Fragment._class = index => 'template-class-' + index; |
| |
| UI.Fragment._templateCache = new Map(); |