/** * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ /** * @module ui/view */ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import ViewCollection from './viewcollection'; import Template from './template'; import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import Collection from '@ckeditor/ckeditor5-utils/src/collection'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable'; /** * Basic View class. * * class SampleView extends View { * constructor( locale ) { * super( locale ); * * this.template = new Template( { * tag: 'p', * children: [ * 'Hello', * { * tag: 'b', * children: [ * 'world!' * ] * } * ], * attributes: { * class: 'foo' * } * } ); * } * } * * const view = new SampleView( locale ); * * view.init(); * * // Will append

Helloworld

* document.body.appendChild( view.element ); * * @mixes module:utils/observablemixin~ObservableMixin */ export default class View { /** * Creates an instance of the {@link module:ui/view~View} class. * * @param {module:utils/locale~Locale} [locale] The {@link module:core/editor/editor~Editor editor's locale} instance. */ constructor( locale ) { /** * A set of tools to localize the user interface. See {@link module:core/editor/editor~Editor}. * * @readonly * @member {module:utils/locale~Locale} */ this.locale = locale; /** * Shorthand for {@link module:utils/locale~Locale#t}. * * Note: If locale instance hasn't been passed to the view this method may not be available. * * @see module:utils/locale~Locale#t * @method */ this.t = locale && locale.t; /** * Set `true` after {@link #init}, which can be asynchronous. * * @readonly * @observable * @member {Boolean} #ready */ this.set( 'ready', false ); /** * Collections registered with {@link #createCollection}. * * @protected * @member {Set.} */ this._viewCollections = new Collection(); /** * A collection of view instances, which have been added directly * into the {@link module:ui/template~Template#children}. * * @protected * @member {module:ui/viewcollection~ViewCollection} */ this._unboundChildren = this.createCollection(); // Pass parent locale to its children. this._viewCollections.on( 'add', ( evt, collection ) => { collection.locale = locale; } ); /** * Template of this view. * * @member {module:ui/template~Template} #template */ /** * Element of this view. * * @private * @member {HTMLElement} #_element */ /** * Cached {@link module:ui/template~Template} binder object specific for this instance. * See {@link #bindTemplate}. * * @private * @member {Object} #_bindTemplate */ } /** * Element of this view. The element is rendered on first reference * using {@link #template} definition. * * @type {HTMLElement} */ get element() { if ( this._element ) { return this._element; } // No template means no element (a virtual view). if ( !this.template ) { return null; } this._addTemplateChildren(); return ( this._element = this.template.render() ); } set element( el ) { this._element = el; } /** * Shorthand for {@link module:ui/template~Template.bind}, bound to {@link ~View} on the first access. * * Cached {@link module:ui/template~Template.bind} object is stored in {@link #_bindTemplate}. * * @method #bindTemplate */ get bindTemplate() { if ( this._bindTemplate ) { return this._bindTemplate; } return ( this._bindTemplate = Template.bind( this, this ) ); } /** * Creates a new collection of views, which can be used in this view instance, * e.g. as a member of {@link module:ui/template~TemplateDefinition TemplateDefinition} children. * * class SampleView extends View { * constructor( locale ) { * super( locale ); * * this.items = this.createCollection(); * * this.template = new Template( { * tag: 'p', * * // `items` collection will render here. * children: this.items * } ); * } * } * * const view = new SampleView( locale ); * const anotherView = new AnotherSampleView( locale ); * * view.init(); * * // Will append

* document.body.appendChild( view.element ); * * // `anotherView` becomes a child of the view, which is reflected in DOM * //

* view.items.add( anotherView ); * * @returns {module:ui/viewcollection~ViewCollection} A new collection of view instances. */ createCollection() { const collection = new ViewCollection(); this._viewCollections.add( collection ); return collection; } /** * Registers a new child view under this view instance. Once registered, a child * view is managed by its parent, including initialization ({@link #init}) * and destruction ({@link #destroy}). * * class SampleView extends View { * constructor( locale ) { * super( locale ); * * this.childA = new SomeChildView( locale ); * this.childB = new SomeChildView( locale ); * * this.template = new Template( { tag: 'p' } ); * * // Register children. * this.addChildren( [ this.childA, this.childB ] ); * } * * init() { * this.element.appendChild( this.childA.element ); * this.element.appendChild( this.childB.element ); * * return super.init(); * } * } * * const view = new SampleView( locale ); * * view.init(); * * // Will append

* document.body.appendChild( view.element ); * * **Note**: There's no need to add child views if they're used in the * {@link #template} explicitly: * * class SampleView extends View { * constructor( locale ) { * super( locale ); * * this.childA = new SomeChildView( locale ); * this.childB = new SomeChildView( locale ); * * this.template = new Template( { * tag: 'p', * * // These children will be added automatically. There's no * // need to call {@link #addChildren} for any of them. * children: [ this.childA, this.childB ] * } ); * } * * ... * } * * @param {module:ui/view~View|Iterable.} children Children views to be registered. */ addChildren( children ) { if ( !isIterable( children ) ) { children = [ children ]; } children.map( c => this._unboundChildren.add( c ) ); } /** * Initializes the view and child views located in {@link #_viewCollections}. */ init() { if ( this.ready ) { /** * This View has already been initialized. * * @error ui-view-init-reinit */ throw new CKEditorError( 'ui-view-init-reinit: This View has already been initialized.' ); } // Initialize collections in #_viewCollections. this._viewCollections.map( c => c.init() ); // Spread the word that this view is ready! this.ready = true; } /** * Destroys the view instance and child views located in {@link #_viewCollections}. */ destroy() { this.stopListening(); this._viewCollections.map( c => c.destroy() ); } /** * Recursively traverses {@link #template} in search of {@link module:ui/view~View} * instances and automatically registers them using {@link #addChildren} method. * * @protected */ _addTemplateChildren() { const search = def => { if ( def.children ) { for ( const defOrView of def.children ) { if ( defOrView instanceof View ) { this.addChildren( defOrView ); } else { search( defOrView ); } } } }; search( this.template ); } } mix( View, DomEmitterMixin ); mix( View, ObservableMixin );