| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269 |
- /**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /**
- * @module ui/template
- */
- /* global document */
- import CKEditorError from '../utils/ckeditorerror.js';
- import mix from '../utils/mix.js';
- import EmitterMixin from '../utils/emittermixin.js';
- import Collection from '../utils/collection.js';
- import View from './view.js';
- import ViewCollection from './viewcollection.js';
- import cloneDeepWith from '../utils/lib/lodash/cloneDeepWith.js';
- import isObject from '../utils/lib/lodash/isObject.js';
- const xhtmlNs = 'http://www.w3.org/1999/xhtml';
- /**
- * A basic Template class. It renders DOM HTMLElement or Text from {@link module:ui/template~TemplateDefinition} and supports
- * element attributes, children, bindings to {@link module:utils/observablemixin~ObservableMixin} instances and DOM events
- * propagation. For example:
- *
- * new Template( {
- * tag: 'p',
- * attributes: {
- * class: 'foo',
- * style: {
- * backgroundColor: 'yellow'
- * }
- * },
- * children: [
- * 'A paragraph.'
- * ]
- * } ).render();
- *
- * will render the following HTMLElement:
- *
- * <p class="foo" style="background-color: yellow;">A paragraph.</p>
- *
- * See {@link module:ui/template~TemplateDefinition} to know more about templates and complex template definitions.
- *
- * @mixes module:utils/emittermixin~EmitterMixin
- */
- export default class Template {
- /**
- * Creates an instance of the {@link ~Template} class.
- *
- * @param {module:ui/template~TemplateDefinition} def The definition of the template.
- */
- constructor( def ) {
- Object.assign( this, normalize( clone( def ) ) );
- /**
- * Tag of this template, i.e. `div`, indicating that the instance will render
- * to an HTMLElement.
- *
- * @member {String} #tag
- */
- /**
- * Text of this template, indicating that the instance will render to a DOM Text.
- *
- * @member {Array.<String|module:ui/template~TemplateValueSchema>} #text
- */
- /**
- * Attributes of this template, i.e. `{ id: [ 'ck-id' ] }`, corresponding with
- * HTML attributes on HTMLElement.
- *
- * Note: Only when {@link #tag} is defined.
- *
- * @member {Object} #attributes
- */
- /**
- * Children of this template; sub–templates. Each one is an independent
- * instance of {@link ~Template}.
- *
- * Note: Only when {@link #tag} is defined.
- *
- * @member {module:utils/collection~Collection.<module:ui/template~Template>} #children
- */
- /**
- * DOM event listeners of this template.
- *
- * @member {Object} #eventListeners
- */
- }
- /**
- * Renders a DOM Node (`HTMLElement` or `Text`) out of the template.
- *
- * @see #apply
- *
- * @returns {HTMLElement|Text}
- */
- render() {
- return this._renderNode( undefined, true );
- }
- /**
- * Applies the template to an existing DOM Node, either `HTMLElement` or `Text`.
- *
- * **Note:** No new DOM nodes (HTMLElement or Text) will be created. Applying extends attributes
- * ({@link module:ui/template~TemplateDefinition attributes}) and listeners ({@link module:ui/template~TemplateDefinition on}) only.
- *
- * const element = document.createElement( 'div' );
- * const bind = Template.bind( observableInstance, emitterInstance );
- *
- * new Template( {
- * attrs: {
- * id: 'first-div',
- * class: bind.to( 'divClass' )
- * },
- * on: {
- * click: bind( 'elementClicked' ) // Will be fired by the observableInstance.
- * }
- * children: [
- * 'Div text.'
- * ]
- * } ).apply( element );
- *
- * element.outerHTML == "<div id="first-div" class="my-div">Div text.</div>"
- *
- * @see module:ui/template~Template#render
- * @param {Node} element Root element for the template to apply.
- */
- apply( node ) {
- if ( !node ) {
- /**
- * No DOM Node specified.
- *
- * @error ui-template-wrong-syntax
- */
- throw new CKEditorError( 'ui-template-wrong-node: No DOM Node specified.' );
- }
- return this._renderNode( node );
- }
- /**
- * An entry point to the interface which allows binding DOM nodes to {@link module:utils/observablemixin~ObservableMixin}.
- * There are two types of bindings:
- *
- * * `HTMLElement` attributes or Text Node `textContent` can be synchronized with {@link module:utils/observablemixin~ObservableMixin}
- * instance attributes. See {@link module:ui/template~Template.bind.to} and {@link module:ui/template~Template.bind.if}.
- *
- * * DOM events fired on `HTMLElement` can be propagated through {@link module:utils/observablemixin~ObservableMixin}.
- * See {@link module:ui/template~Template.bind.to}.
- *
- * @param {module:utils/observablemixin~ObservableMixin} observable An instance of ObservableMixin class.
- * @param {module:utils/emittermixin~EmitterMixin} emitter An instance of `Emitter` class. It listens
- * to `observable` attribute changes and DOM Events, depending on the binding. Usually {@link module:ui/view~View} instance.
- * @returns {Object}
- */
- static bind( observable, emitter ) {
- return {
- /**
- * Binds {@link module:utils/observablemixin~ObservableMixin} instance to:
- * * HTMLElement attribute or Text Node `textContent` so remains in sync with the Observable when it changes:
- * * HTMLElement DOM event, so the DOM events are propagated through Observable.
- *
- * const bind = Template.bind( observableInstance, emitterInstance );
- *
- * new Template( {
- * tag: 'p',
- * attributes: {
- * // class="..." attribute gets bound to `observableInstance#a`
- * 'class': bind.to( 'a' )
- * },
- * children: [
- * // <p>...</p> gets bound to `observableInstance#b`; always `toUpperCase()`.
- * { text: bind.to( 'b', ( value, node ) => value.toUpperCase() ) }
- * ],
- * on: {
- * click: [
- * // "clicked" event will be fired on `observableInstance` when "click" fires in DOM.
- * bind.to( 'clicked' ),
- *
- * // A custom callback function will be executed when "click" fires in DOM.
- * bind.to( () => {
- * ...
- * } )
- * ]
- * }
- * } ).render();
- *
- * const bind = Template.bind( observableInstance, emitterInstance );
- *
- * new Template( {
- * tag: 'p',
- * } ).render();
- *
- * @function
- * @param {String|Function} eventNameOrFunctionOrAttribute An attribute name of {@link module:utils/observablemixin~ObservableMixin} or a
- * DOM
- * event name or an event callback.
- * @param {Function} [callback] Allows processing of the value. Accepts `Node` and `value` as arguments.
- * @return {module:ui/template~TemplateBinding}
- */
- to( eventNameOrFunctionOrAttribute, callback ) {
- return new TemplateToBinding( {
- eventNameOrFunction: eventNameOrFunctionOrAttribute,
- attribute: eventNameOrFunctionOrAttribute,
- observable, emitter, callback
- } );
- },
- /**
- * Binds {@link module:utils/observablemixin~ObservableMixin} to HTMLElement attribute or Text Node `textContent`
- * so remains in sync with the Model when it changes. Unlike {@link module:ui/template~Template.bind.to},
- * it controls the presence of the attribute/`textContent` depending on the "falseness" of
- * {@link module:utils/observablemixin~ObservableMixin} attribute.
- *
- * const bind = Template.bind( observableInstance, emitterInstance );
- *
- * new Template( {
- * tag: 'input',
- * attributes: {
- * // <input checked> when `observableInstance#a` is not undefined/null/false/''
- * // <input> when `observableInstance#a` is undefined/null/false
- * checked: bind.if( 'a' )
- * },
- * children: [
- * {
- * // <input>"b-is-not-set"</input> when `observableInstance#b` is undefined/null/false/''
- * // <input></input> when `observableInstance#b` is not "falsy"
- * text: bind.if( 'b', 'b-is-not-set', ( value, node ) => !value )
- * }
- * ]
- * } ).render();
- *
- * @function
- * @param {String} attribute An attribute name of {@link module:utils/observablemixin~ObservableMixin} used in the binding.
- * @param {String} [valueIfTrue] Value set when {@link module:utils/observablemixin~ObservableMixin} attribute is not
- * undefined/null/false/''.
- * @param {Function} [callback] Allows processing of the value. Accepts `Node` and `value` as arguments.
- * @return {module:ui/template~TemplateBinding}
- */
- if( attribute, valueIfTrue, callback ) {
- return new TemplateIfBinding( {
- observable, emitter, attribute, valueIfTrue, callback
- } );
- }
- };
- }
- /**
- * Extends {@link module:ui/template~Template} instance with additional content from {@link module:ui/template~TemplateDefinition}.
- *
- * const bind = Template.bind( observable, emitterInstance );
- * const instance = new Template( {
- * tag: 'p',
- * attributes: {
- * class: 'a',
- * data-x: bind.to( 'foo' )
- * },
- * children: [
- * {
- * tag: 'span',
- * attributes: {
- * class: 'b'
- * },
- * children: [
- * 'Span'
- * ]
- * }
- * ]
- * } );
- *
- * // Instance-level extension.
- * Template.extend( instance, {
- * attributes: {
- * class: 'b',
- * data-x: bind.to( 'bar' )
- * },
- * children: [
- * {
- * attributes: {
- * class: 'c'
- * }
- * }
- * ]
- * } );
- *
- * // Child extension.
- * Template.extend( instance.children.get( 0 ), {
- * attributes: {
- * class: 'd'
- * }
- * } );
- *
- * the `instance.render().outerHTML` is
- *
- * <p class="a b" data-x="{ observable.foo } { observable.bar }">
- * <span class="b c d">Span</span>
- * </p>
- *
- * @param {module:ui/template~Template} template Existing Template instance to be extended.
- * @param {module:ui/template~TemplateDefinition} def An extension to existing an template instance.
- */
- static extend( template, def ) {
- extendTemplate( template, normalize( clone( def ) ) );
- }
- /**
- * Renders a DOM Node (either `HTMLElement` or `Text`) out of the template.
- *
- * @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.
- */
- _renderNode( applyNode, intoFragment ) {
- let isInvalid;
- if ( applyNode ) {
- // When applying, a definition cannot have "tag" and "text" at the same time.
- isInvalid = this.tag && this.text;
- } else {
- // When rendering, a definition must have either "tag" or "text": XOR( this.tag, this.text ).
- isInvalid = this.tag ? this.text : !this.text;
- }
- if ( isInvalid ) {
- /**
- * Node definition cannot have "tag" and "text" properties at the same time.
- * Node definition must have either "tag" or "text" when rendering new Node.
- *
- * @error ui-template-wrong-syntax
- */
- 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 ) : this._renderElement( applyNode, intoFragment );
- }
- /**
- * Renders an `HTMLElement` out of the template.
- *
- * @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`.
- */
- _renderElement( applyElement, intoFragment ) {
- const el = applyElement ||
- document.createElementNS( this.ns || xhtmlNs, this.tag );
- this._renderAttributes( el );
- // Invoke children recursively.
- if ( intoFragment ) {
- const docFragment = document.createDocumentFragment();
- this._renderElementChildren( el, docFragment );
- el.appendChild( docFragment );
- } else {
- this._renderElementChildren( el, el, !!applyElement );
- }
- // Setup DOM bindings event listeners.
- this._setUpListeners( el );
- return el;
- }
- /**
- * Renders a `Text` node out of {@link module:ui/template~Template#text}.
- *
- * @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.
- */
- _renderText( textNode = document.createTextNode( '' ) ) {
- // Check if this Text Node is bound to Observable. Cases:
- // { text: [ Template.bind( ... ).to( ... ) ] }
- // { text: [ 'foo', Template.bind( ... ).to( ... ), ... ] }
- if ( hasTemplateBinding( this.text ) ) {
- this._bindToObservable( this.text, textNode, getTextUpdater( textNode ) );
- }
- // Simply set text. Cases:
- // { text: [ 'all', 'are', 'static' ] }
- // { text: [ 'foo' ] }
- else {
- textNode.textContent = this.text.join( '' );
- }
- return textNode;
- }
- /**
- * Renders an `HTMLElement` attributes out of {@link module:ui/template~Template#attributes}.
- *
- * @protected
- * @param {HTMLElement} el `HTMLElement` which attributes are to be rendered.
- */
- _renderAttributes( el ) {
- let attrName, attrValue, attrNs;
- if ( !this.attributes ) {
- return;
- }
- for ( attrName in this.attributes ) {
- attrValue = this.attributes[ attrName ];
- // Detect custom namespace:
- // { class: { ns: 'abc', value: Template.bind( ... ).to( ... ) } }
- attrNs = isObject( attrValue[ 0 ] ) && attrValue[ 0 ].ns ? attrValue[ 0 ].ns : null;
- // Activate binding if one is found. Cases:
- // { class: [ Template.bind( ... ).to( ... ) ] }
- // { class: [ 'bar', Template.bind( ... ).to( ... ), 'baz' ] }
- // { class: { ns: 'abc', value: Template.bind( ... ).to( ... ) } }
- if ( hasTemplateBinding( attrValue ) ) {
- this._bindToObservable(
- // Normalize attributes with additional data like namespace:
- // { class: { ns: 'abc', value: [ ... ] } }
- attrNs ? attrValue[ 0 ].value : attrValue,
- el,
- getAttributeUpdater( el, attrName, attrNs )
- );
- }
- // Style attribute could be an Object so it needs to be parsed in a specific way.
- // style: {
- // width: '100px',
- // height: Template.bind( ... ).to( ... )
- // }
- else if ( attrName == 'style' && typeof attrValue[ 0 ] !== 'string' ) {
- this._renderStyleAttribute( attrValue[ 0 ], el );
- }
- // Otherwise simply set the static attribute.
- // { class: [ 'foo' ] }
- // { class: [ 'all', 'are', 'static' ] }
- // { class: [ { ns: 'abc', value: [ 'foo' ] } ] }
- else {
- attrValue = attrValue
- // Retrieve "values" from { class: [ { ns: 'abc', value: [ ... ] } ] }
- .map( v => v ? ( v.value || v ) : v )
- // Flatten the array.
- .reduce( ( p, n ) => p.concat( n ), [] )
- // Convert into string.
- .reduce( arrayValueReducer, '' );
- if ( !isFalsy( attrValue ) ) {
- el.setAttributeNS( attrNs, attrName, attrValue );
- }
- }
- }
- }
- /**
- * Renders `style` attribute of an `HTMLElement` based on {@link module:ui/template~Template#attributes}.
- *
- * Style attribute is an {Object} with static values:
- *
- * attributes: {
- * style: {
- * color: 'red'
- * }
- * }
- *
- * or values bound to {@link module:ui/model~Model} properties:
- *
- * attributes: {
- * style: {
- * color: bind.to( ... )
- * }
- * }
- *
- * Note: `style` attribute is rendered without setting the namespace. It does not seem to be
- * needed.
- *
- * @private
- * @param {Object} styles module:ui/template~TemplateDefinition.attributes.styles Styles definition.
- * @param {HTMLElement} el `HTMLElement` which `style` attribute is rendered.
- */
- _renderStyleAttribute( styles, el ) {
- for ( let styleName in styles ) {
- const styleValue = styles[ styleName ];
- // style: {
- // color: bind.to( 'attribute' )
- // }
- if ( hasTemplateBinding( styleValue ) ) {
- this._bindToObservable( [ styleValue ], el, getStyleUpdater( el, styleName ) );
- }
- // style: {
- // color: 'red'
- // }
- else {
- el.style[ styleName ] = styleValue;
- }
- }
- }
- /**
- * Recursively renders `HTMLElement` children from {@link module:ui/template~Template#children}.
- *
- * @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.
- */
- _renderElementChildren( element, container, shouldApply ) {
- let childIndex = 0;
- for ( let child of this.children ) {
- if ( isViewCollection( child ) ) {
- if ( !shouldApply ) {
- child.setParent( element );
- for ( let view of child ) {
- container.appendChild( view.element );
- }
- }
- } else if ( isView( child ) ) {
- if ( !shouldApply ) {
- container.appendChild( child.element );
- }
- } else {
- if ( shouldApply ) {
- child._renderNode( container.childNodes[ childIndex++ ] );
- } else {
- container.appendChild( child.render() );
- }
- }
- }
- }
- /**
- * Activates ~Template#on listeners on a passed `HTMLElement`.
- *
- * @protected
- * @param {HTMLElement} el `HTMLElement` which is being rendered.
- */
- _setUpListeners( el ) {
- if ( !this.eventListeners ) {
- return;
- }
- for ( let key in this.eventListeners ) {
- const [ domEvtName, domSelector ] = key.split( '@' );
- this.eventListeners[ key ].forEach( schemaItem => {
- schemaItem.activateDomEventListener( el, domEvtName, domSelector );
- } );
- }
- }
- /**
- * For given {@link module:ui/template~TemplateValueSchema} containing {@link module:ui/template~TemplateBinding} it activates the
- * binding and sets its initial value.
- *
- * Note: {@link module:ui/template~TemplateValueSchema} can be for HTMLElement attributes or Text Node `textContent`.
- *
- * @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).
- */
- _bindToObservable( valueSchema ) {
- valueSchema
- // Filter "falsy" (false, undefined, null, '') value schema components out.
- .filter( item => !isFalsy( item ) )
- // Filter inactive bindings from schema, like static strings ('foo'), numbers (42), etc.
- .filter( item => item.observable )
- // Once only the actual binding are left, let the emitter listen to observable change:attribute event.
- // TODO: Reduce the number of listeners attached as many bindings may listen
- // to the same observable attribute.
- .forEach( templateBinding => templateBinding.activateAttributeListener( ...arguments ) );
- // Set initial values.
- syncValueSchemaValue( ...arguments );
- }
- }
- mix( Template, EmitterMixin );
- /**
- * Describes a binding created by {@link module:ui/template~Template.bind} interface.
- *
- * @protected
- */
- export class TemplateBinding {
- /**
- * Creates an instance of the {@link module:ui/template~TemplateBinding} class.
- *
- * @param {module:ui/template~TemplateDefinition} def The definition of the binding.
- */
- constructor( def ) {
- Object.assign( this, def );
- /**
- * An observable instance of the binding. It provides the attribute
- * with the value or passes the event when a corresponding DOM event is fired.
- *
- * @member {module:utils/observablemixin~ObservableMixin} module:ui/template~TemplateBinding#observable
- */
- /**
- * An {@link module:utils/emittermixin~EmitterMixin} instance used by the binding
- * to (either):
- *
- * * listen to the attribute change in the {@link module:ui/template~TemplateBinding#observable},
- * * listen to the event in the DOM.
- *
- * @member {module:utils/emittermixin~EmitterMixin} module:ui/template~TemplateBinding#emitter
- */
- /**
- * The name of the attribute of {@link module:ui/template~TemplateBinding#observable} which is observed.
- *
- * @member {String} module:ui/template~TemplateBinding#attribute
- */
- /**
- * A custom function to process the value of {@link module:ui/template~TemplateBinding#attribute}.
- *
- * @member {Function} [module:ui/template~TemplateBinding#callback]
- */
- }
- /**
- * Returns the value of the binding, which is the value of {@link module:ui/template~TemplateBinding#attribute} in
- * {@link module:ui/template~TemplateBinding#observable}.
- *
- * @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}.
- */
- getValue( domNode ) {
- const value = this.observable[ this.attribute ];
- return this.callback ? this.callback( value, domNode ) : value;
- }
- /**
- * Activates the listener for the changes of {@link module:ui/template~TemplateBinding#attribute} in
- * {@link module:ui/template~TemplateBinding#observable}, which then updates the DOM with the aggregated
- * value of {@link module:ui/template~TemplateValueSchema}.
- *
- * For instance, the `class` attribute of the `Template` element can be be bound to
- * 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 {Function} updater A DOM updater function used to update native DOM attribute or text.
- */
- activateAttributeListener( valueSchema, node, updater ) {
- this.emitter.listenTo( this.observable, 'change:' + this.attribute, () => {
- syncValueSchemaValue( valueSchema, node, updater );
- } );
- }
- }
- /**
- * Describes either
- *
- * * a binding to {@link module:utils/observablemixin~ObservableMixin}
- * * or a native DOM event binding
- *
- * created by {@link module:ui/template~Template.bind.to} method.
- *
- * @protected
- */
- export class TemplateToBinding extends TemplateBinding {
- /**
- * Activates the listener for the native DOM event, which when fired, is propagated by
- * 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} [domSelector] A selector in DOM to filter delegated events.
- */
- activateDomEventListener( el, domEvtName, domSelector ) {
- this.emitter.listenTo( el, domEvtName, ( evt, domEvt ) => {
- if ( !domSelector || domEvt.target.matches( domSelector ) ) {
- if ( typeof this.eventNameOrFunction == 'function' ) {
- this.eventNameOrFunction( domEvt );
- } else {
- this.observable.fire( this.eventNameOrFunction, domEvt );
- }
- }
- } );
- }
- }
- /**
- * Describes a binding to {@link module:utils/observablemixin~ObservableMixin} created by {@link module:ui/template~Template.bind.if}
- * method.
- *
- * @protected
- */
- export class TemplateIfBinding extends TemplateBinding {
- /**
- * @inheritDoc
- */
- getValue( domNode ) {
- const value = super.getValue( domNode );
- return isFalsy( value ) ? false : ( this.valueIfTrue || true );
- }
- /**
- * The value of the DOM attribute/text to be set if the {@link module:ui/template~TemplateBinding#attribute} in
- * {@link module:ui/template~TemplateBinding#observable} is `true`.
- *
- * @member {String} [module:ui/template~TemplateIfBinding#valueIfTrue]
- */
- }
- // Checks whether given {@link module:ui/template~TemplateValueSchema} contains a
- // {@link module:ui/template~TemplateBinding}.
- //
- // @param {module:ui/template~TemplateValueSchema} valueSchema
- // @returns {Boolean}
- function hasTemplateBinding( valueSchema ) {
- if ( !valueSchema ) {
- return false;
- }
- // Normalize attributes with additional data like namespace:
- // class: { ns: 'abc', value: [ ... ] }
- if ( valueSchema.value ) {
- valueSchema = valueSchema.value;
- }
- if ( Array.isArray( valueSchema ) ) {
- return valueSchema.some( hasTemplateBinding );
- } else if ( valueSchema instanceof TemplateBinding ) {
- return true;
- }
- return false;
- }
- // Assembles the value using {@link module:ui/template~TemplateValueSchema} and stores it in a form of
- // an Array. Each entry of an Array corresponds to one of {@link module:ui/template~TemplateValueSchema}
- // items.
- //
- // @param {module:ui/template~TemplateValueSchema} valueSchema
- // @param {Node} node DOM Node updated when {@link module:utils/observablemixin~ObservableMixin} changes.
- // @return {Array}
- function getValueSchemaValue( valueSchema, domNode ) {
- return valueSchema.map( schemaItem => {
- // Process {@link module:ui/template~TemplateBinding} bindings.
- if ( schemaItem instanceof TemplateBinding ) {
- return schemaItem.getValue( domNode );
- }
- // All static values like strings, numbers, and "falsy" values (false, null, undefined, '', etc.) just pass.
- return schemaItem;
- } );
- }
- // A function executed each time bound Observable attribute changes, which updates DOM with a value
- // constructed from {@link module:ui/template~TemplateValueSchema}.
- //
- // @param {module:ui/template~TemplateValueSchema} valueSchema
- // @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 );
- // Check if valueSchema is a single Template.bind.if, like:
- // { class: Template.bind.if( 'foo' ) }
- if ( valueSchema.length == 1 && valueSchema[ 0 ] instanceof TemplateIfBinding ) {
- value = value[ 0 ];
- } else {
- value = value.reduce( arrayValueReducer, '' );
- }
- if ( isFalsy( value ) ) {
- domUpdater.remove();
- } else {
- domUpdater.set( value );
- }
- }
- // Returns an object consisting of `set` and `remove` functions, which
- // can be used in the context of DOM Node to set or reset `textContent`.
- // @see module:ui/view~View#_bindToObservable
- //
- // @param {Node} node DOM Node to be modified.
- // @returns {Object}
- function getTextUpdater( node ) {
- return {
- set( value ) {
- node.textContent = value;
- },
- remove() {
- node.textContent = '';
- }
- };
- }
- // Returns an object consisting of `set` and `remove` functions, which
- // can be used in the context of DOM Node to set or reset an attribute.
- // @see module:ui/view~View#_bindToObservable
- //
- // @param {Node} node DOM Node to be modified.
- // @param {String} attrName Name of the attribute to be modified.
- // @param {String} [ns=null] Namespace to use.
- // @returns {Object}
- function getAttributeUpdater( el, attrName, ns ) {
- return {
- set( value ) {
- el.setAttributeNS( ns, attrName, value );
- },
- remove() {
- el.removeAttributeNS( ns, attrName );
- }
- };
- }
- // Returns an object consisting of `set` and `remove` functions, which
- // can be used in the context of CSSStyleDeclaration to set or remove a style.
- // @see module:ui/view~View#_bindToObservable
- //
- // @param {Node} node DOM Node to be modified.
- // @param {String} styleName Name of the style to be modified.
- // @returns {Object}
- function getStyleUpdater( el, styleName ) {
- return {
- set( value ) {
- el.style[ styleName ] = value;
- },
- remove() {
- el.style[ styleName ] = null;
- }
- };
- }
- // Clones definition of the template.
- //
- // @param {module:ui/template~TemplateDefinition} def
- // @returns {module:ui/template~TemplateDefinition}
- function clone( def ) {
- const clone = cloneDeepWith( def, value => {
- // Don't clone the `Template.bind`* bindings because of the references to Observable
- // and DomEmitterMixin instances inside, which would also be traversed and cloned by greedy
- // cloneDeepWith algorithm. There's no point in cloning Observable/DomEmitterMixins
- // along with the definition.
- //
- // Also don't clone View instances if provided as a child of the Template. The template
- // instance will be extracted from the View during the normalization and there's no need
- // to clone it.
- if ( value && ( value instanceof TemplateBinding || isView( value ) || isViewCollection( value ) ) ) {
- return value;
- }
- } );
- return clone;
- }
- // Normalizes given {@link module:ui/template~TemplateDefinition}.
- //
- // See:
- // * {@link normalizeAttributes}
- // * {@link normalizeListeners}
- // * {@link normalizePlainTextDefinition}
- // * {@link normalizeTextDefinition}
- //
- // @param {module:ui/template~TemplateDefinition} def
- // @returns {module:ui/template~TemplateDefinition} Normalized definition.
- function normalize( def ) {
- if ( typeof def == 'string' ) {
- def = normalizePlainTextDefinition( def );
- } else if ( def.text ) {
- normalizeTextDefinition( def );
- }
- if ( def.on ) {
- def.eventListeners = normalizeListeners( def.on );
- // Template mixes EmitterMixin, so delete #on to avoid collision.
- delete def.on;
- }
- if ( !def.text ) {
- if ( def.attributes ) {
- normalizeAttributes( def.attributes );
- }
- const children = new Collection();
- if ( def.children ) {
- if ( isViewCollection( def.children ) ) {
- children.add( def.children );
- } else {
- for ( let child of def.children ) {
- if ( isView( child ) ) {
- children.add( child );
- } else {
- children.add( new Template( child ) );
- }
- }
- }
- }
- def.children = children;
- }
- return def;
- }
- // Normalizes "attributes" section of {@link module:ui/template~TemplateDefinition}.
- //
- // attributes: {
- // a: 'bar',
- // b: {@link module:ui/template~TemplateBinding},
- // c: {
- // value: 'bar'
- // }
- // }
- //
- // becomes
- //
- // attributes: {
- // a: [ 'bar' ],
- // b: [ {@link module:ui/template~TemplateBinding} ],
- // c: {
- // value: [ 'bar' ]
- // }
- // }
- //
- // @param {Object} attrs
- function normalizeAttributes( attrs ) {
- for ( let a in attrs ) {
- if ( attrs[ a ].value ) {
- attrs[ a ].value = [].concat( attrs[ a ].value );
- }
- arrayify( attrs, a );
- }
- }
- // Normalizes "on" section of {@link module:ui/template~TemplateDefinition}.
- //
- // on: {
- // a: 'bar',
- // b: {@link module:ui/template~TemplateBinding},
- // c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
- // }
- //
- // becomes
- //
- // on: {
- // a: [ 'bar' ],
- // b: [ {@link module:ui/template~TemplateBinding} ],
- // c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
- // }
- //
- // @param {Object} listeners
- // @returns {Object} Object containing normalized listeners.
- function normalizeListeners( listeners ) {
- for ( let l in listeners ) {
- arrayify( listeners, l );
- }
- return listeners;
- }
- // Normalizes "string" {@link module:ui/template~TemplateDefinition}.
- //
- // "foo"
- //
- // becomes
- //
- // { text: [ 'foo' ] },
- //
- // @param {String} def
- // @returns {module:ui/template~TemplateDefinition} Normalized template definition.
- function normalizePlainTextDefinition( def ) {
- return {
- text: [ def ]
- };
- }
- // Normalizes text {@link module:ui/template~TemplateDefinition}.
- //
- // children: [
- // { text: 'def' },
- // { text: {@link module:ui/template~TemplateBinding} }
- // ]
- //
- // becomes
- //
- // children: [
- // { text: [ 'def' ] },
- // { text: [ {@link module:ui/template~TemplateBinding} ] }
- // ]
- //
- // @param {module:ui/template~TemplateDefinition} def
- function normalizeTextDefinition( def ) {
- if ( !Array.isArray( def.text ) ) {
- def.text = [ def.text ];
- }
- }
- // Wraps an entry in Object in an Array, if not already one.
- //
- // {
- // x: 'y',
- // a: [ 'b' ]
- // }
- //
- // becomes
- //
- // {
- // x: [ 'y' ],
- // a: [ 'b' ]
- // }
- //
- // @param {Object} obj
- // @param {String} key
- function arrayify( obj, key ) {
- if ( !Array.isArray( obj[ key ] ) ) {
- obj[ key ] = [ obj[ key ] ];
- }
- }
- // A helper which concatenates the value avoiding unwanted
- // leading white spaces.
- //
- // @param {String} prev
- // @param {String} cur
- // @returns {String}
- function arrayValueReducer( prev, cur ) {
- if ( isFalsy( cur ) ) {
- return prev;
- } else if ( isFalsy( prev ) ) {
- return cur;
- } else {
- return `${prev} ${cur}`;
- }
- }
- // Extends one object defined in the following format:
- //
- // {
- // key1: [Array1],
- // key2: [Array2],
- // ...
- // keyN: [ArrayN]
- // }
- //
- // with another object of the same data format.
- //
- // @param {Object} obj Base object.
- // @param {Object} ext Object extending base.
- // @returns {String}
- function extendObjectValueArray( obj, ext ) {
- for ( let a in ext ) {
- if ( obj[ a ] ) {
- obj[ a ].push( ...ext[ a ] );
- } else {
- obj[ a ] = ext[ a ];
- }
- }
- }
- // A helper for {@link module:ui/template~Template#extend}. Recursively extends {@link module:ui/template~Template} instance
- // with content from {module:ui/template~TemplateDefinition}. See {@link module:ui/template~Template#extend} to learn more.
- //
- // @param {module:ui/template~Template} def A template instance to be extended.
- // @param {module:ui/template~TemplateDefinition} def A definition which is to extend the template instance.
- function extendTemplate( template, def ) {
- if ( def.attributes ) {
- if ( !template.attributes ) {
- template.attributes = {};
- }
- extendObjectValueArray( template.attributes, def.attributes );
- }
- if ( def.eventListeners ) {
- if ( !template.eventListeners ) {
- template.eventListeners = {};
- }
- extendObjectValueArray( template.eventListeners, def.eventListeners );
- }
- if ( def.text ) {
- template.text.push( ...def.text );
- }
- if ( def.children && def.children.length ) {
- if ( template.children.length != def.children.length ) {
- /**
- * The number of children in extended definition does not match.
- *
- * @error ui-template-extend-children-mismatch
- */
- throw new CKEditorError( 'ui-template-extend-children-mismatch: The number of children in extended definition does not match.' );
- }
- let childIndex = 0;
- for ( let childDef of def.children ) {
- extendTemplate( template.children.get( childIndex++ ), childDef );
- }
- }
- }
- // Checks if value is "falsy".
- // Note: 0 (Number) is not "falsy" in this context.
- //
- // @private
- // @param {*} value Value to be checked.
- function isFalsy( value ) {
- return !value && value !== 0;
- }
- // Checks if the item is an instance of {@link module:ui/view~View}
- //
- // @private
- // @param {*} value Value to be checked.
- function isView( item ) {
- return item instanceof View;
- }
- // Checks if the item is an instance of {@link module:ui/viewcollection~ViewCollection}
- //
- // @private
- // @param {*} value Value to be checked.
- function isViewCollection( item ) {
- return item instanceof ViewCollection;
- }
- /**
- * A definition of {@link module:ui/template~Template}.
- * See: {@link module:ui/template~TemplateValueSchema}.
- *
- * new Template( {
- * tag: 'p',
- * children: [
- * {
- * tag: 'span',
- * attributes: { ... },
- * children: [ ... ],
- * ...
- * },
- * {
- * text: 'static–text'
- * },
- * 'also-static–text',
- * <{@link module:ui/view~View} instance>
- * ...
- * ],
- * attributes: {
- * class: {@link module:ui/template~TemplateValueSchema},
- * id: {@link module:ui/template~TemplateValueSchema},
- * style: {@link module:ui/template~TemplateValueSchema}
- * ...
- * },
- * on: {
- * 'click': {@link module:ui/template~TemplateListenerSchema}
- * 'keyup@.some-class': {@link module:ui/template~TemplateListenerSchema},
- * ...
- * }
- * } );
- *
- * // An entire view collection can be used as a child in the definition.
- * new Template( {
- * tag: 'p',
- * children: <{@link module:ui/viewcollection~ViewCollection} instance>
- * } );
- *
- * @typedef module:ui/template~TemplateDefinition
- * @type Object
- * @property {String} tag
- * @property {Array.<module:ui/template~TemplateDefinition>} [children]
- * @property {Object.<String,module:ui/template~TemplateValueSchema>} [attributes]
- * @property {String|module:ui/template~TemplateValueSchema|Array.<String|module:ui/template~TemplateValueSchema>} [text]
- * @property {Object.<String,module:ui/template~TemplateListenerSchema>} [on]
- */
- /**
- * Describes a value of HTMLElement attribute or `textContent`. See:
- * * {@link module:ui/template~TemplateDefinition},
- * * {@link module:ui/template~Template.bind},
- *
- * const bind = Template.bind( observableInstance, emitterInstance );
- *
- * new Template( {
- * tag: 'p',
- * attributes: {
- * // Plain String schema.
- * class: 'static-text'
- *
- * // Object schema, an `ObservableMixin` binding.
- * class: bind.to( 'foo' )
- *
- * // Array schema, combines the above.
- * class: [
- * 'static-text',
- * bind.to( 'bar', () => { ... } )
- * ],
- *
- * // Array schema, with custom namespace.
- * class: {
- * ns: 'http://ns.url',
- * value: [
- * bind.if( 'baz', 'value-when-true' )
- * 'static-text'
- * ]
- * },
- *
- * // Object literal schema, specific for styles.
- * style: {
- * color: 'red',
- * backgroundColor: bind.to( 'qux', () => { ... } )
- * }
- * }
- * } );
- *
- * @typedef module:ui/template~TemplateValueSchema
- * @type {Object|String|Array}
- */
- /**
- * Describes a listener attached to HTMLElement. See: {@link module:ui/template~TemplateDefinition}.
- *
- * new Template( {
- * tag: 'p',
- * on: {
- * // Plain String schema.
- * click: 'clicked'
- *
- * // Object schema, an `ObservableMixin` binding.
- * click: {@link module:ui/template~TemplateBinding}
- *
- * // Array schema, combines the above.
- * click: [
- * 'clicked',
- * {@link module:ui/template~TemplateBinding}
- * ],
- *
- * // Array schema, with custom callback.
- * // Note: It will work for "click" event on class=".foo" children only.
- * 'click@.foo': {
- * 'clicked',
- * {@link module:ui/template~TemplateBinding},
- * () => { ... }
- * }
- * }
- * } );
- *
- * @typedef module:ui/template~TemplateListenerSchema
- * @type {Object|String|Array}
- */
|