|
@@ -55,13 +55,6 @@ export default class Template {
|
|
|
constructor( def ) {
|
|
constructor( def ) {
|
|
|
Object.assign( this, normalize( clone( def ) ) );
|
|
Object.assign( this, normalize( clone( def ) ) );
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * A template used by {@link #revert} method.
|
|
|
|
|
- *
|
|
|
|
|
- * @member {ui/template~Template}
|
|
|
|
|
- */
|
|
|
|
|
- this._revertData = null;
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* Tag of this template, i.e. `div`, indicating that the instance will render
|
|
* Tag of this template, i.e. `div`, indicating that the instance will render
|
|
|
* to an HTMLElement.
|
|
* to an HTMLElement.
|
|
@@ -98,6 +91,18 @@ export default class Template {
|
|
|
*
|
|
*
|
|
|
* @member {Object} #eventListeners
|
|
* @member {Object} #eventListeners
|
|
|
*/
|
|
*/
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Data used by {@link #revert} method to restore a node
|
|
|
|
|
+ * to its original state.
|
|
|
|
|
+ *
|
|
|
|
|
+ * See: {@link #apply}.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @protected
|
|
|
|
|
+ * @readonly
|
|
|
|
|
+ * @member {ui/template~Template}
|
|
|
|
|
+ */
|
|
|
|
|
+ this._revertData = null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -108,7 +113,9 @@ export default class Template {
|
|
|
* @returns {HTMLElement|Text}
|
|
* @returns {HTMLElement|Text}
|
|
|
*/
|
|
*/
|
|
|
render() {
|
|
render() {
|
|
|
- return this._renderNode( undefined, true );
|
|
|
|
|
|
|
+ return this._renderNode( {
|
|
|
|
|
+ intoFragment: true
|
|
|
|
|
+ } );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -135,9 +142,12 @@ export default class Template {
|
|
|
*
|
|
*
|
|
|
* element.outerHTML == "<div id="first-div" class="my-div">Div text.</div>"
|
|
* element.outerHTML == "<div id="first-div" class="my-div">Div text.</div>"
|
|
|
*
|
|
*
|
|
|
|
|
+ * **Note:** The process of applying a template can be reverted using
|
|
|
|
|
+ * {@link module:ui/template~Template#revert} method.
|
|
|
|
|
+ *
|
|
|
* @see module:ui/template~Template#render
|
|
* @see module:ui/template~Template#render
|
|
|
* @see module:ui/template~Template#revert
|
|
* @see module:ui/template~Template#revert
|
|
|
- * @param {Node} element Root element for the template to apply.
|
|
|
|
|
|
|
+ * @param {Node} node Root node for the template to apply.
|
|
|
*/
|
|
*/
|
|
|
apply( node ) {
|
|
apply( node ) {
|
|
|
if ( !node ) {
|
|
if ( !node ) {
|
|
@@ -149,18 +159,22 @@ export default class Template {
|
|
|
throw new CKEditorError( 'ui-template-wrong-node: No DOM Node specified.' );
|
|
throw new CKEditorError( 'ui-template-wrong-node: No DOM Node specified.' );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- this._revertData = {};
|
|
|
|
|
|
|
+ this._revertData = getEmptyRevertData();
|
|
|
|
|
|
|
|
- this._renderNode( node, null, this._revertData );
|
|
|
|
|
|
|
+ this._renderNode( {
|
|
|
|
|
+ node: node,
|
|
|
|
|
+ isApplying: true,
|
|
|
|
|
+ revertData: this._revertData
|
|
|
|
|
+ } );
|
|
|
|
|
|
|
|
return node;
|
|
return node;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Reverts the template from an existing DOM Node, either `HTMLElement` or `Text`.
|
|
|
|
|
|
|
+ * Reverts a template {@link module:ui/template~Template#apply applied} to an existing DOM Node.
|
|
|
*
|
|
*
|
|
|
- * @see module:ui/template~Template#apply
|
|
|
|
|
- * @param {Node} element Root element for the template to revert.
|
|
|
|
|
|
|
+ * @param {Node} node Root node for the template to revert. Must be the same node
|
|
|
|
|
+ * that {@link module:ui/template~Template#apply} has used.
|
|
|
*/
|
|
*/
|
|
|
revert( node ) {
|
|
revert( node ) {
|
|
|
if ( !node ) {
|
|
if ( !node ) {
|
|
@@ -279,15 +293,12 @@ export default class Template {
|
|
|
* Renders a DOM Node (either `HTMLElement` or `Text`) out of the template.
|
|
* Renders a DOM Node (either `HTMLElement` or `Text`) out of the template.
|
|
|
*
|
|
*
|
|
|
* @protected
|
|
* @protected
|
|
|
- * @param {Node} applyNode If specified, this template will be applied to an existing DOM Node.
|
|
|
|
|
- * @param {Boolean} intoFragment If set, children are rendered into `DocumentFragment`.
|
|
|
|
|
- * @returns {HTMLElement|Text} A rendered Node.
|
|
|
|
|
- * @param {Object} revertData
|
|
|
|
|
|
|
+ * @param {module:ui/template~RenderConfig} config Rendering config.
|
|
|
*/
|
|
*/
|
|
|
- _renderNode( applyNode, intoFragment, revertData ) {
|
|
|
|
|
|
|
+ _renderNode( config ) {
|
|
|
let isInvalid;
|
|
let isInvalid;
|
|
|
|
|
|
|
|
- if ( applyNode ) {
|
|
|
|
|
|
|
+ if ( config.node ) {
|
|
|
// When applying, a definition cannot have "tag" and "text" at the same time.
|
|
// When applying, a definition cannot have "tag" and "text" at the same time.
|
|
|
isInvalid = this.tag && this.text;
|
|
isInvalid = this.tag && this.text;
|
|
|
} else {
|
|
} else {
|
|
@@ -305,60 +316,47 @@ export default class Template {
|
|
|
throw new CKEditorError( 'ui-template-wrong-syntax: Node definition must have either "tag" or "text" when rendering new Node.' );
|
|
throw new CKEditorError( 'ui-template-wrong-syntax: Node definition must have either "tag" or "text" when rendering new Node.' );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return this.text ?
|
|
|
|
|
- this._renderText( applyNode, !!applyNode, revertData )
|
|
|
|
|
- :
|
|
|
|
|
- this._renderElement( applyNode, intoFragment, revertData );
|
|
|
|
|
|
|
+ if ( this.text ) {
|
|
|
|
|
+ return this._renderText( config );
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return this._renderElement( config );
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Renders an `HTMLElement` out of the template.
|
|
* Renders an `HTMLElement` out of the template.
|
|
|
*
|
|
*
|
|
|
* @protected
|
|
* @protected
|
|
|
- * @param {HTMLElement} applyElement If specified, this template will be applied to an existing `HTMLElement`.
|
|
|
|
|
- * @param {Boolean} intoFragment If set, children are rendered into `DocumentFragment`.
|
|
|
|
|
- * @returns {HTMLElement} A rendered `HTMLElement`.
|
|
|
|
|
- * @param {Object} revertData
|
|
|
|
|
|
|
+ * @param {module:ui/template~RenderConfig} config Rendering config.
|
|
|
*/
|
|
*/
|
|
|
- _renderElement( applyElement, intoFragment, revertData ) {
|
|
|
|
|
- const el = applyElement ||
|
|
|
|
|
- document.createElementNS( this.ns || xhtmlNs, this.tag );
|
|
|
|
|
-
|
|
|
|
|
- this._renderAttributes( el, !!applyElement, revertData );
|
|
|
|
|
-
|
|
|
|
|
- // Invoke children recursively.
|
|
|
|
|
- if ( intoFragment ) {
|
|
|
|
|
- const docFragment = document.createDocumentFragment();
|
|
|
|
|
|
|
+ _renderElement( config ) {
|
|
|
|
|
+ let node = config.node;
|
|
|
|
|
|
|
|
- this._renderElementChildren( el, docFragment );
|
|
|
|
|
-
|
|
|
|
|
- el.appendChild( docFragment );
|
|
|
|
|
- } else {
|
|
|
|
|
- this._renderElementChildren( el, el, !!applyElement, revertData );
|
|
|
|
|
|
|
+ if ( !node ) {
|
|
|
|
|
+ node = config.node = document.createElementNS( this.ns || xhtmlNs, this.tag );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Setup DOM bindings event listeners.
|
|
|
|
|
- this._setUpListeners( el, revertData );
|
|
|
|
|
|
|
+ this._renderAttributes( config );
|
|
|
|
|
+ this._renderElementChildren( config );
|
|
|
|
|
+ this._setUpListeners( config );
|
|
|
|
|
|
|
|
- return el;
|
|
|
|
|
|
|
+ return node;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Renders a `Text` node out of {@link module:ui/template~Template#text}.
|
|
* Renders a `Text` node out of {@link module:ui/template~Template#text}.
|
|
|
*
|
|
*
|
|
|
* @protected
|
|
* @protected
|
|
|
- * @param {HTMLElement} textNode If specified, this template instance will be applied to an existing `Text` Node.
|
|
|
|
|
- * @returns {Text} A rendered `Text` node in DOM.
|
|
|
|
|
- * @param {Object} revertData
|
|
|
|
|
|
|
+ * @param {module:ui/template~RenderConfig} config Rendering config.
|
|
|
*/
|
|
*/
|
|
|
- _renderText( textNode, shouldApply, revertData ) {
|
|
|
|
|
- if ( !textNode ) {
|
|
|
|
|
- textNode = document.createTextNode( '' );
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ _renderText( config ) {
|
|
|
|
|
+ let node = config.node;
|
|
|
|
|
|
|
|
// Save the original textContent to revert it in #revert().
|
|
// Save the original textContent to revert it in #revert().
|
|
|
- if ( shouldApply ) {
|
|
|
|
|
- revertData.text = textNode.textContent;
|
|
|
|
|
|
|
+ if ( node ) {
|
|
|
|
|
+ config.revertData.text = node.textContent;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ node = config.node = document.createTextNode( '' );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Check if this Text Node is bound to Observable. Cases:
|
|
// Check if this Text Node is bound to Observable. Cases:
|
|
@@ -366,58 +364,53 @@ export default class Template {
|
|
|
// { text: [ 'foo', Template.bind( ... ).to( ... ), ... ] }
|
|
// { text: [ 'foo', Template.bind( ... ).to( ... ), ... ] }
|
|
|
if ( hasTemplateBinding( this.text ) ) {
|
|
if ( hasTemplateBinding( this.text ) ) {
|
|
|
// Preserve the original content of the text node.
|
|
// Preserve the original content of the text node.
|
|
|
- if ( shouldApply && textNode.textContent ) {
|
|
|
|
|
- this.text.unshift( textNode.textContent );
|
|
|
|
|
|
|
+ if ( config.revertData && node.textContent ) {
|
|
|
|
|
+ this.text.unshift( node.textContent );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- this._bindToObservable(
|
|
|
|
|
- this.text,
|
|
|
|
|
- textNode,
|
|
|
|
|
- getTextUpdater( textNode ),
|
|
|
|
|
- revertData
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ this._bindToObservable( this.text, getTextUpdater( node ), config );
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
// Simply set text. Cases:
|
|
// Simply set text. Cases:
|
|
|
// { text: [ 'all', 'are', 'static' ] }
|
|
// { text: [ 'all', 'are', 'static' ] }
|
|
|
// { text: [ 'foo' ] }
|
|
// { text: [ 'foo' ] }
|
|
|
else {
|
|
else {
|
|
|
- textNode.textContent += this.text.join( '' );
|
|
|
|
|
|
|
+ node.textContent += this.text.join( '' );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return textNode;
|
|
|
|
|
|
|
+ return node;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Renders an `HTMLElement` attributes out of {@link module:ui/template~Template#attributes}.
|
|
* Renders an `HTMLElement` attributes out of {@link module:ui/template~Template#attributes}.
|
|
|
*
|
|
*
|
|
|
* @protected
|
|
* @protected
|
|
|
- * @param {HTMLElement} el `HTMLElement` which attributes are to be rendered.
|
|
|
|
|
- * @param {Object} revertData
|
|
|
|
|
|
|
+ * @param {module:ui/template~RenderConfig} config Rendering config.
|
|
|
*/
|
|
*/
|
|
|
- _renderAttributes( el, shouldApply, revertData ) {
|
|
|
|
|
- let attrName, attrValue, domValue, attrNs;
|
|
|
|
|
|
|
+ _renderAttributes( config ) {
|
|
|
|
|
+ let attrName, attrValue, domAttrValue, attrNs;
|
|
|
|
|
|
|
|
if ( !this.attributes ) {
|
|
if ( !this.attributes ) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if ( shouldApply ) {
|
|
|
|
|
- revertData.attributes = {};
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const node = config.node;
|
|
|
|
|
+ const revertData = config.revertData;
|
|
|
|
|
|
|
|
for ( attrName in this.attributes ) {
|
|
for ( attrName in this.attributes ) {
|
|
|
- domValue = el.getAttribute( attrName );
|
|
|
|
|
|
|
+ // Current attribute value in DOM.
|
|
|
|
|
+ domAttrValue = node.getAttribute( attrName );
|
|
|
|
|
+
|
|
|
|
|
+ // The value to be set.
|
|
|
attrValue = this.attributes[ attrName ];
|
|
attrValue = this.attributes[ attrName ];
|
|
|
|
|
|
|
|
// Save revert data.
|
|
// Save revert data.
|
|
|
- if ( shouldApply ) {
|
|
|
|
|
- revertData.attributes[ attrName ] = domValue;
|
|
|
|
|
|
|
+ if ( revertData ) {
|
|
|
|
|
+ revertData.attributes[ attrName ] = domAttrValue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Detect custom namespace:
|
|
// Detect custom namespace:
|
|
|
// { class: { ns: 'abc', value: Template.bind( ... ).to( ... ) } }
|
|
// { class: { ns: 'abc', value: Template.bind( ... ).to( ... ) } }
|
|
|
- attrNs = isObject( attrValue[ 0 ] ) && attrValue[ 0 ].ns ? attrValue[ 0 ].ns : null;
|
|
|
|
|
|
|
+ attrNs = ( isObject( attrValue[ 0 ] ) && attrValue[ 0 ].ns ) ? attrValue[ 0 ].ns : null;
|
|
|
|
|
|
|
|
// Activate binding if one is found. Cases:
|
|
// Activate binding if one is found. Cases:
|
|
|
// { class: [ Template.bind( ... ).to( ... ) ] }
|
|
// { class: [ Template.bind( ... ).to( ... ) ] }
|
|
@@ -426,19 +419,14 @@ export default class Template {
|
|
|
if ( hasTemplateBinding( attrValue ) ) {
|
|
if ( hasTemplateBinding( attrValue ) ) {
|
|
|
// Normalize attributes with additional data like namespace:
|
|
// Normalize attributes with additional data like namespace:
|
|
|
// { class: { ns: 'abc', value: [ ... ] } }
|
|
// { class: { ns: 'abc', value: [ ... ] } }
|
|
|
- const attrValueToBind = attrNs ? attrValue[ 0 ].value : attrValue;
|
|
|
|
|
|
|
+ const toBind = attrNs ? attrValue[ 0 ].value : attrValue;
|
|
|
|
|
|
|
|
- if ( shouldApply ) {
|
|
|
|
|
- // Preserve the original value.
|
|
|
|
|
- attrValueToBind.unshift( domValue );
|
|
|
|
|
|
|
+ // Preserve the original value.
|
|
|
|
|
+ if ( revertData ) {
|
|
|
|
|
+ toBind.unshift( domAttrValue );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- this._bindToObservable(
|
|
|
|
|
- attrValueToBind,
|
|
|
|
|
- el,
|
|
|
|
|
- getAttributeUpdater( el, attrName, attrNs ),
|
|
|
|
|
- revertData
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ this._bindToObservable( toBind, getAttributeUpdater( node, attrName, attrNs ), config );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Style attribute could be an Object so it needs to be parsed in a specific way.
|
|
// Style attribute could be an Object so it needs to be parsed in a specific way.
|
|
@@ -447,7 +435,7 @@ export default class Template {
|
|
|
// height: Template.bind( ... ).to( ... )
|
|
// height: Template.bind( ... ).to( ... )
|
|
|
// }
|
|
// }
|
|
|
else if ( attrName == 'style' && typeof attrValue[ 0 ] !== 'string' ) {
|
|
else if ( attrName == 'style' && typeof attrValue[ 0 ] !== 'string' ) {
|
|
|
- this._renderStyleAttribute( attrValue[ 0 ], el, revertData );
|
|
|
|
|
|
|
+ this._renderStyleAttribute( attrValue[ 0 ], config );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Otherwise simply set the static attribute.
|
|
// Otherwise simply set the static attribute.
|
|
@@ -455,8 +443,9 @@ export default class Template {
|
|
|
// { class: [ 'all', 'are', 'static' ] }
|
|
// { class: [ 'all', 'are', 'static' ] }
|
|
|
// { class: [ { ns: 'abc', value: [ 'foo' ] } ] }
|
|
// { class: [ { ns: 'abc', value: [ 'foo' ] } ] }
|
|
|
else {
|
|
else {
|
|
|
- if ( domValue ) {
|
|
|
|
|
- attrValue.unshift( domValue );
|
|
|
|
|
|
|
+ // Preserve the original value.
|
|
|
|
|
+ if ( domAttrValue ) {
|
|
|
|
|
+ attrValue.unshift( domAttrValue );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
attrValue = attrValue
|
|
attrValue = attrValue
|
|
@@ -468,7 +457,7 @@ export default class Template {
|
|
|
.reduce( arrayValueReducer, '' );
|
|
.reduce( arrayValueReducer, '' );
|
|
|
|
|
|
|
|
if ( !isFalsy( attrValue ) ) {
|
|
if ( !isFalsy( attrValue ) ) {
|
|
|
- el.setAttributeNS( attrNs, attrName, attrValue );
|
|
|
|
|
|
|
+ node.setAttributeNS( attrNs, attrName, attrValue );
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -498,10 +487,11 @@ export default class Template {
|
|
|
*
|
|
*
|
|
|
* @private
|
|
* @private
|
|
|
* @param {Object} styles module:ui/template~TemplateDefinition.attributes.styles Styles definition.
|
|
* @param {Object} styles module:ui/template~TemplateDefinition.attributes.styles Styles definition.
|
|
|
- * @param {HTMLElement} el `HTMLElement` which `style` attribute is rendered.
|
|
|
|
|
- * @param {Object} revertData
|
|
|
|
|
|
|
+ * @param {module:ui/template~RenderConfig} config Rendering config.
|
|
|
*/
|
|
*/
|
|
|
- _renderStyleAttribute( styles, el, revertData ) {
|
|
|
|
|
|
|
+ _renderStyleAttribute( styles, config ) {
|
|
|
|
|
+ const node = config.node;
|
|
|
|
|
+
|
|
|
for ( let styleName in styles ) {
|
|
for ( let styleName in styles ) {
|
|
|
const styleValue = styles[ styleName ];
|
|
const styleValue = styles[ styleName ];
|
|
|
|
|
|
|
@@ -509,19 +499,14 @@ export default class Template {
|
|
|
// color: bind.to( 'attribute' )
|
|
// color: bind.to( 'attribute' )
|
|
|
// }
|
|
// }
|
|
|
if ( hasTemplateBinding( styleValue ) ) {
|
|
if ( hasTemplateBinding( styleValue ) ) {
|
|
|
- this._bindToObservable(
|
|
|
|
|
- [ styleValue ],
|
|
|
|
|
- el,
|
|
|
|
|
- getStyleUpdater( el, styleName ),
|
|
|
|
|
- revertData
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ this._bindToObservable( [ styleValue ], getStyleUpdater( node, styleName ), config );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// style: {
|
|
// style: {
|
|
|
// color: 'red'
|
|
// color: 'red'
|
|
|
// }
|
|
// }
|
|
|
else {
|
|
else {
|
|
|
- el.style[ styleName ] = styleValue;
|
|
|
|
|
|
|
+ node.style[ styleName ] = styleValue;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -530,68 +515,70 @@ export default class Template {
|
|
|
* Recursively renders `HTMLElement` children from {@link module:ui/template~Template#children}.
|
|
* Recursively renders `HTMLElement` children from {@link module:ui/template~Template#children}.
|
|
|
*
|
|
*
|
|
|
* @protected
|
|
* @protected
|
|
|
- * @param {HTMLElement} element The element which is being rendered.
|
|
|
|
|
- * @param {HTMLElement|DocumentFragment} container `HTMLElement` or `DocumentFragment`
|
|
|
|
|
- * into which children are being rendered. If `shouldApply == true`, then `container === element`.
|
|
|
|
|
- * @param {Boolean} shouldApply Traverse existing DOM structure only, don't modify DOM.
|
|
|
|
|
- * @param {Object} revertData
|
|
|
|
|
|
|
+ * @param {module:ui/template~RenderConfig} config Rendering config.
|
|
|
*/
|
|
*/
|
|
|
- _renderElementChildren( element, container, shouldApply, revertData ) {
|
|
|
|
|
|
|
+ _renderElementChildren( config ) {
|
|
|
|
|
+ const node = config.node;
|
|
|
|
|
+ const container = config.intoFragment ? document.createDocumentFragment() : node;
|
|
|
|
|
+ const isApplying = config.isApplying;
|
|
|
let childIndex = 0;
|
|
let childIndex = 0;
|
|
|
|
|
|
|
|
for ( let child of this.children ) {
|
|
for ( let child of this.children ) {
|
|
|
if ( isViewCollection( child ) ) {
|
|
if ( isViewCollection( child ) ) {
|
|
|
- if ( !shouldApply ) {
|
|
|
|
|
- child.setParent( element );
|
|
|
|
|
|
|
+ if ( !isApplying ) {
|
|
|
|
|
+ child.setParent( node );
|
|
|
|
|
|
|
|
for ( let view of child ) {
|
|
for ( let view of child ) {
|
|
|
container.appendChild( view.element );
|
|
container.appendChild( view.element );
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
} else if ( isView( child ) ) {
|
|
} else if ( isView( child ) ) {
|
|
|
- if ( !shouldApply ) {
|
|
|
|
|
|
|
+ if ( !isApplying ) {
|
|
|
container.appendChild( child.element );
|
|
container.appendChild( child.element );
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- if ( shouldApply ) {
|
|
|
|
|
- const childRevertData = {};
|
|
|
|
|
-
|
|
|
|
|
- if ( !revertData.children ) {
|
|
|
|
|
- revertData.children = [];
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if ( isApplying ) {
|
|
|
|
|
+ const revertData = config.revertData;
|
|
|
|
|
+ const childRevertData = getEmptyRevertData();
|
|
|
|
|
|
|
|
revertData.children.push( childRevertData );
|
|
revertData.children.push( childRevertData );
|
|
|
|
|
|
|
|
- child._renderNode( container.childNodes[ childIndex++ ], null, childRevertData );
|
|
|
|
|
|
|
+ child._renderNode( {
|
|
|
|
|
+ node: container.childNodes[ childIndex++ ],
|
|
|
|
|
+ isApplying: true,
|
|
|
|
|
+ revertData: childRevertData
|
|
|
|
|
+ } );
|
|
|
} else {
|
|
} else {
|
|
|
container.appendChild( child.render() );
|
|
container.appendChild( child.render() );
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ if ( config.intoFragment ) {
|
|
|
|
|
+ node.appendChild( container );
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Activates ~Template#on listeners on a passed `HTMLElement`.
|
|
|
|
|
|
|
+ * Activates {@link module:ui/template~Template#on} listeners on a passed `HTMLElement`.
|
|
|
*
|
|
*
|
|
|
* @protected
|
|
* @protected
|
|
|
- * @param {HTMLElement} el `HTMLElement` which is being rendered.
|
|
|
|
|
- * @param {Object} revertData
|
|
|
|
|
|
|
+ * @param {module:ui/template~RenderConfig} config Rendering config.
|
|
|
*/
|
|
*/
|
|
|
- _setUpListeners( el, revertData ) {
|
|
|
|
|
|
|
+ _setUpListeners( config ) {
|
|
|
if ( !this.eventListeners ) {
|
|
if ( !this.eventListeners ) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if ( revertData && !revertData.bindings ) {
|
|
|
|
|
- revertData.bindings = [];
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
for ( let key in this.eventListeners ) {
|
|
for ( let key in this.eventListeners ) {
|
|
|
- const reverts = this.eventListeners[ key ]
|
|
|
|
|
- .map( schemaItem => schemaItem.activateDomEventListener( el, ...key.split( '@' ) ) );
|
|
|
|
|
|
|
+ const revertBindings = this.eventListeners[ key ].map( schemaItem => {
|
|
|
|
|
+ const [ domEvtName, domSelector ] = key.split( '@' );
|
|
|
|
|
|
|
|
- if ( revertData ) {
|
|
|
|
|
- revertData.bindings.push( reverts );
|
|
|
|
|
|
|
+ return schemaItem.activateDomEventListener( domEvtName, domSelector, config );
|
|
|
|
|
+ } );
|
|
|
|
|
+
|
|
|
|
|
+ if ( config.revertData ) {
|
|
|
|
|
+ config.revertData.bindings.push( revertBindings );
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -603,16 +590,17 @@ export default class Template {
|
|
|
* Note: {@link module:ui/template~TemplateValueSchema} can be for HTMLElement attributes or Text Node `textContent`.
|
|
* Note: {@link module:ui/template~TemplateValueSchema} can be for HTMLElement attributes or Text Node `textContent`.
|
|
|
*
|
|
*
|
|
|
* @protected
|
|
* @protected
|
|
|
- * @param {module:ui/template~TemplateValueSchema} valueSchema
|
|
|
|
|
- * @param {Node} node DOM Node to be updated when {@link module:utils/observablemixin~ObservableMixin} changes.
|
|
|
|
|
- * @param {Function} domUpdater A function which updates DOM (like attribute or text).
|
|
|
|
|
- * @param {Object} revertData
|
|
|
|
|
|
|
+ * @param {module:ui/template~TemplateValueSchema} schema
|
|
|
|
|
+ * @param {Function} updater A function which updates DOM (like attribute or text).
|
|
|
|
|
+ * @param {module:ui/template~RenderConfig} config Rendering config.
|
|
|
*/
|
|
*/
|
|
|
- _bindToObservable( valueSchema, node, domUpdater, revertData ) {
|
|
|
|
|
|
|
+ _bindToObservable( schema, updater, config ) {
|
|
|
|
|
+ const revertData = config.revertData;
|
|
|
|
|
+
|
|
|
// Set initial values.
|
|
// Set initial values.
|
|
|
syncValueSchemaValue( ...arguments );
|
|
syncValueSchemaValue( ...arguments );
|
|
|
|
|
|
|
|
- const reverts = valueSchema
|
|
|
|
|
|
|
+ const revertBindings = schema
|
|
|
// Filter "falsy" (false, undefined, null, '') value schema components out.
|
|
// Filter "falsy" (false, undefined, null, '') value schema components out.
|
|
|
.filter( item => !isFalsy( item ) )
|
|
.filter( item => !isFalsy( item ) )
|
|
|
// Filter inactive bindings from schema, like static strings ('foo'), numbers (42), etc.
|
|
// Filter inactive bindings from schema, like static strings ('foo'), numbers (42), etc.
|
|
@@ -623,14 +611,19 @@ export default class Template {
|
|
|
.map( templateBinding => templateBinding.activateAttributeListener( ...arguments ) );
|
|
.map( templateBinding => templateBinding.activateAttributeListener( ...arguments ) );
|
|
|
|
|
|
|
|
if ( revertData ) {
|
|
if ( revertData ) {
|
|
|
- if ( !revertData.bindings ) {
|
|
|
|
|
- revertData.bindings = [];
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- revertData.bindings.push( reverts );
|
|
|
|
|
|
|
+ revertData.bindings.push( revertBindings );
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Reverts a node to the original state using information stored
|
|
|
|
|
+ * in {@link module:ui/template~RenderConfig#revertData}.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @protected
|
|
|
|
|
+ * @param {HTMLElement|Text} node A node to be reverted.
|
|
|
|
|
+ * @param {module:ui/template~RenderConfig#revertData} revertData An information
|
|
|
|
|
+ * about desired node state after revert.
|
|
|
|
|
+ */
|
|
|
_revertNode( node, revertData ) {
|
|
_revertNode( node, revertData ) {
|
|
|
if ( revertData.text ) {
|
|
if ( revertData.text ) {
|
|
|
node.textContent = revertData.text;
|
|
node.textContent = revertData.text;
|
|
@@ -719,10 +712,10 @@ export class TemplateBinding {
|
|
|
* @param {Node} [node] A native DOM node, passed to the custom {@link module:ui/template~TemplateBinding#callback}.
|
|
* @param {Node} [node] A native DOM node, passed to the custom {@link module:ui/template~TemplateBinding#callback}.
|
|
|
* @returns {*} The value of {@link module:ui/template~TemplateBinding#attribute} in {@link module:ui/template~TemplateBinding#observable}.
|
|
* @returns {*} The value of {@link module:ui/template~TemplateBinding#attribute} in {@link module:ui/template~TemplateBinding#observable}.
|
|
|
*/
|
|
*/
|
|
|
- getValue( domNode ) {
|
|
|
|
|
|
|
+ getValue( node ) {
|
|
|
const value = this.observable[ this.attribute ];
|
|
const value = this.observable[ this.attribute ];
|
|
|
|
|
|
|
|
- return this.callback ? this.callback( value, domNode ) : value;
|
|
|
|
|
|
|
+ return this.callback ? this.callback( value, node ) : value;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -733,13 +726,13 @@ export class TemplateBinding {
|
|
|
* For instance, the `class` attribute of the `Template` element can be be bound to
|
|
* For instance, the `class` attribute of the `Template` element can be be bound to
|
|
|
* the observable `foo` attribute in `ObservableMixin` instance.
|
|
* the observable `foo` attribute in `ObservableMixin` instance.
|
|
|
*
|
|
*
|
|
|
- * @param {module:ui/template~TemplateValueSchema} valueSchema A full schema to generate an attribute or text in DOM.
|
|
|
|
|
- * @param {Node} node A native DOM node, which attribute or text is to be updated.
|
|
|
|
|
|
|
+ * @param {module:ui/template~TemplateValueSchema} schema A full schema to generate an attribute or text in DOM.
|
|
|
* @param {Function} updater A DOM updater function used to update native DOM attribute or text.
|
|
* @param {Function} updater A DOM updater function used to update native DOM attribute or text.
|
|
|
- * @returns {Function} TODO
|
|
|
|
|
|
|
+ * @param {module:ui/template~RenderConfig} config Rendering config.
|
|
|
|
|
+ * @returns {Function} A function to sever the listener binding.
|
|
|
*/
|
|
*/
|
|
|
- activateAttributeListener( valueSchema, node, updater ) {
|
|
|
|
|
- const callback = () => syncValueSchemaValue( valueSchema, node, updater );
|
|
|
|
|
|
|
+ activateAttributeListener( schema, updater, config ) {
|
|
|
|
|
+ const callback = () => syncValueSchemaValue( schema, updater, config );
|
|
|
|
|
|
|
|
this.emitter.listenTo( this.observable, 'change:' + this.attribute, callback );
|
|
this.emitter.listenTo( this.observable, 'change:' + this.attribute, callback );
|
|
|
|
|
|
|
@@ -765,12 +758,12 @@ export class TemplateToBinding extends TemplateBinding {
|
|
|
* Activates the listener for the native DOM event, which when fired, is propagated by
|
|
* Activates the listener for the native DOM event, which when fired, is propagated by
|
|
|
* the {@link module:ui/template~TemplateBinding#emitter}.
|
|
* the {@link module:ui/template~TemplateBinding#emitter}.
|
|
|
*
|
|
*
|
|
|
- * @param {HTMLElement} element An element on which listening to the native DOM event.
|
|
|
|
|
* @param {String} domEvtName A name of the native DOM event.
|
|
* @param {String} domEvtName A name of the native DOM event.
|
|
|
- * @param {String} [domSelector] A selector in DOM to filter delegated events.
|
|
|
|
|
- * @returns {Function} TODO
|
|
|
|
|
|
|
+ * @param {String} domSelector A selector in DOM to filter delegated events.
|
|
|
|
|
+ * @param {module:ui/template~RenderConfig} config Rendering config.
|
|
|
|
|
+ * @returns {Function} A function to sever the listener binding.
|
|
|
*/
|
|
*/
|
|
|
- activateDomEventListener( el, domEvtName, domSelector ) {
|
|
|
|
|
|
|
+ activateDomEventListener( domEvtName, domSelector, config ) {
|
|
|
const callback = ( evt, domEvt ) => {
|
|
const callback = ( evt, domEvt ) => {
|
|
|
if ( !domSelector || domEvt.target.matches( domSelector ) ) {
|
|
if ( !domSelector || domEvt.target.matches( domSelector ) ) {
|
|
|
if ( typeof this.eventNameOrFunction == 'function' ) {
|
|
if ( typeof this.eventNameOrFunction == 'function' ) {
|
|
@@ -781,11 +774,11 @@ export class TemplateToBinding extends TemplateBinding {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- this.emitter.listenTo( el, domEvtName, callback );
|
|
|
|
|
|
|
+ this.emitter.listenTo( config.node, domEvtName, callback );
|
|
|
|
|
|
|
|
// Allows revert of the listener.
|
|
// Allows revert of the listener.
|
|
|
return () => {
|
|
return () => {
|
|
|
- this.emitter.stopListening( el, domEvtName, callback );
|
|
|
|
|
|
|
+ this.emitter.stopListening( config.node, domEvtName, callback );
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -800,8 +793,8 @@ export class TemplateIfBinding extends TemplateBinding {
|
|
|
/**
|
|
/**
|
|
|
* @inheritDoc
|
|
* @inheritDoc
|
|
|
*/
|
|
*/
|
|
|
- getValue( domNode ) {
|
|
|
|
|
- const value = super.getValue( domNode );
|
|
|
|
|
|
|
+ getValue( node ) {
|
|
|
|
|
+ const value = super.getValue( node );
|
|
|
|
|
|
|
|
return isFalsy( value ) ? false : ( this.valueIfTrue || true );
|
|
return isFalsy( value ) ? false : ( this.valueIfTrue || true );
|
|
|
}
|
|
}
|
|
@@ -817,22 +810,22 @@ export class TemplateIfBinding extends TemplateBinding {
|
|
|
// Checks whether given {@link module:ui/template~TemplateValueSchema} contains a
|
|
// Checks whether given {@link module:ui/template~TemplateValueSchema} contains a
|
|
|
// {@link module:ui/template~TemplateBinding}.
|
|
// {@link module:ui/template~TemplateBinding}.
|
|
|
//
|
|
//
|
|
|
-// @param {module:ui/template~TemplateValueSchema} valueSchema
|
|
|
|
|
|
|
+// @param {module:ui/template~TemplateValueSchema} schema
|
|
|
// @returns {Boolean}
|
|
// @returns {Boolean}
|
|
|
-function hasTemplateBinding( valueSchema ) {
|
|
|
|
|
- if ( !valueSchema ) {
|
|
|
|
|
|
|
+function hasTemplateBinding( schema ) {
|
|
|
|
|
+ if ( !schema ) {
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Normalize attributes with additional data like namespace:
|
|
// Normalize attributes with additional data like namespace:
|
|
|
// class: { ns: 'abc', value: [ ... ] }
|
|
// class: { ns: 'abc', value: [ ... ] }
|
|
|
- if ( valueSchema.value ) {
|
|
|
|
|
- valueSchema = valueSchema.value;
|
|
|
|
|
|
|
+ if ( schema.value ) {
|
|
|
|
|
+ schema = schema.value;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if ( Array.isArray( valueSchema ) ) {
|
|
|
|
|
- return valueSchema.some( hasTemplateBinding );
|
|
|
|
|
- } else if ( valueSchema instanceof TemplateBinding ) {
|
|
|
|
|
|
|
+ if ( Array.isArray( schema ) ) {
|
|
|
|
|
+ return schema.some( hasTemplateBinding );
|
|
|
|
|
+ } else if ( schema instanceof TemplateBinding ) {
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -843,14 +836,14 @@ function hasTemplateBinding( valueSchema ) {
|
|
|
// an Array. Each entry of an Array corresponds to one of {@link module:ui/template~TemplateValueSchema}
|
|
// an Array. Each entry of an Array corresponds to one of {@link module:ui/template~TemplateValueSchema}
|
|
|
// items.
|
|
// items.
|
|
|
//
|
|
//
|
|
|
-// @param {module:ui/template~TemplateValueSchema} valueSchema
|
|
|
|
|
|
|
+// @param {module:ui/template~TemplateValueSchema} schema
|
|
|
// @param {Node} node DOM Node updated when {@link module:utils/observablemixin~ObservableMixin} changes.
|
|
// @param {Node} node DOM Node updated when {@link module:utils/observablemixin~ObservableMixin} changes.
|
|
|
// @return {Array}
|
|
// @return {Array}
|
|
|
-function getValueSchemaValue( valueSchema, domNode ) {
|
|
|
|
|
- return valueSchema.map( schemaItem => {
|
|
|
|
|
|
|
+function getValueSchemaValue( schema, node ) {
|
|
|
|
|
+ return schema.map( schemaItem => {
|
|
|
// Process {@link module:ui/template~TemplateBinding} bindings.
|
|
// Process {@link module:ui/template~TemplateBinding} bindings.
|
|
|
if ( schemaItem instanceof TemplateBinding ) {
|
|
if ( schemaItem instanceof TemplateBinding ) {
|
|
|
- return schemaItem.getValue( domNode );
|
|
|
|
|
|
|
+ return schemaItem.getValue( node );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// All static values like strings, numbers, and "falsy" values (false, null, undefined, '', etc.) just pass.
|
|
// All static values like strings, numbers, and "falsy" values (false, null, undefined, '', etc.) just pass.
|
|
@@ -861,24 +854,24 @@ function getValueSchemaValue( valueSchema, domNode ) {
|
|
|
// A function executed each time bound Observable attribute changes, which updates DOM with a value
|
|
// A function executed each time bound Observable attribute changes, which updates DOM with a value
|
|
|
// constructed from {@link module:ui/template~TemplateValueSchema}.
|
|
// constructed from {@link module:ui/template~TemplateValueSchema}.
|
|
|
//
|
|
//
|
|
|
-// @param {module:ui/template~TemplateValueSchema} valueSchema
|
|
|
|
|
|
|
+// @param {module:ui/template~TemplateValueSchema} schema
|
|
|
|
|
+// @param {Function} updater A function which updates DOM (like attribute or text).
|
|
|
// @param {Node} node DOM Node updated when {@link module:utils/observablemixin~ObservableMixin} changes.
|
|
// @param {Node} node DOM Node updated when {@link module:utils/observablemixin~ObservableMixin} changes.
|
|
|
-// @param {Function} domUpdater A function which updates DOM (like attribute or text).
|
|
|
|
|
-function syncValueSchemaValue( valueSchema, domNode, domUpdater ) {
|
|
|
|
|
- let value = getValueSchemaValue( valueSchema, domNode );
|
|
|
|
|
|
|
+function syncValueSchemaValue( schema, updater, { node } ) {
|
|
|
|
|
+ let value = getValueSchemaValue( schema, node );
|
|
|
|
|
|
|
|
- // Check if valueSchema is a single Template.bind.if, like:
|
|
|
|
|
|
|
+ // Check if schema is a single Template.bind.if, like:
|
|
|
// { class: Template.bind.if( 'foo' ) }
|
|
// { class: Template.bind.if( 'foo' ) }
|
|
|
- if ( valueSchema.length == 1 && valueSchema[ 0 ] instanceof TemplateIfBinding ) {
|
|
|
|
|
|
|
+ if ( schema.length == 1 && schema[ 0 ] instanceof TemplateIfBinding ) {
|
|
|
value = value[ 0 ];
|
|
value = value[ 0 ];
|
|
|
} else {
|
|
} else {
|
|
|
value = value.reduce( arrayValueReducer, '' );
|
|
value = value.reduce( arrayValueReducer, '' );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if ( isFalsy( value ) ) {
|
|
if ( isFalsy( value ) ) {
|
|
|
- domUpdater.remove();
|
|
|
|
|
|
|
+ updater.remove();
|
|
|
} else {
|
|
} else {
|
|
|
- domUpdater.set( value );
|
|
|
|
|
|
|
+ updater.set( value );
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1248,6 +1241,14 @@ function isViewCollection( item ) {
|
|
|
return item instanceof ViewCollection;
|
|
return item instanceof ViewCollection;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function getEmptyRevertData() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ children: [],
|
|
|
|
|
+ bindings: [],
|
|
|
|
|
+ attributes: {}
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* A definition of {@link module:ui/template~Template}.
|
|
* A definition of {@link module:ui/template~Template}.
|
|
|
* See: {@link module:ui/template~TemplateValueSchema}.
|
|
* See: {@link module:ui/template~TemplateValueSchema}.
|
|
@@ -1453,3 +1454,37 @@ function isViewCollection( item ) {
|
|
|
* @param {Function} [callback] Allows processing of the value. Accepts `Node` and `value` as arguments.
|
|
* @param {Function} [callback] Allows processing of the value. Accepts `Node` and `value` as arguments.
|
|
|
* @return {module:ui/template~TemplateBinding}
|
|
* @return {module:ui/template~TemplateBinding}
|
|
|
*/
|
|
*/
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * The {@link module:ui/template~Template#_renderNode} config.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @interface module:ui/template~RenderConfig
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Tells {@link module:ui/template~Template#_renderNode} to render
|
|
|
|
|
+ * children into DocumentFragment first and then append the fragment
|
|
|
|
|
+ * to the parent element.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @member {Boolean} #intoFragment
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * A node which is rendered.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @member {HTMLElement|Text} #node
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Indicates whether the {@module:ui/template~RenderNodeOptions#node} has
|
|
|
|
|
+ * been provided by {@module:ui/template~Template#apply}.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @member {Node} #isApplying
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * An object storing the data to help {@module:ui/template~Template#revert}
|
|
|
|
|
+ * bring back an element is initial state.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @member {Node} #revertData
|
|
|
|
|
+ */
|