diff --git a/docs/_snippets/examples/bootstrap-ui-inner.js b/docs/_snippets/examples/bootstrap-ui-inner.js index c2018982..a74ee6d7 100644 --- a/docs/_snippets/examples/bootstrap-ui-inner.js +++ b/docs/_snippets/examples/bootstrap-ui-inner.js @@ -59,8 +59,8 @@ export default class BootstrapEditor extends StandardEditor { resolve( editor.initPlugins() - // Initialize the editable view in DOM first. - .then( () => editable.init() ) + // Render the editable view in DOM first. + .then( () => editable.render() ) // Replace the editor#element with editor.editable#element. .then( () => editor._elementReplacer.replace( element, editable.element ) ) // Handle the UI of the editor. diff --git a/docs/framework/guides/external-ui.md b/docs/framework/guides/external-ui.md index 2693b111..a546a026 100644 --- a/docs/framework/guides/external-ui.md +++ b/docs/framework/guides/external-ui.md @@ -83,8 +83,8 @@ export default class BootstrapEditor extends StandardEditor { resolve( editor.initPlugins() - // Initialize the editable view in DOM first. - .then( () => editable.init() ) + // Render the editable view in DOM first. + .then( () => editable.render() ) // Replace the editor#element with editor.editable#element. .then( () => editor._elementReplacer.replace( element, editable.element ) ) // Handle the UI of the editor. diff --git a/src/bindings/preventdefault.js b/src/bindings/preventdefault.js index a02a47e7..d5480189 100644 --- a/src/bindings/preventdefault.js +++ b/src/bindings/preventdefault.js @@ -17,7 +17,7 @@ * * // ... * - * this.template = new Template( { + * this.setTemplate( { * tag: 'div', * * on: { diff --git a/src/button/buttonview.js b/src/button/buttonview.js index ee1d8554..3ea95b01 100644 --- a/src/button/buttonview.js +++ b/src/button/buttonview.js @@ -8,7 +8,6 @@ */ import View from '../view'; -import Template from '../template'; import IconView from '../icon/iconview'; import TooltipView from '../tooltip/tooltipview'; @@ -26,7 +25,7 @@ import { getEnvKeystrokeText } from '@ckeditor/ckeditor5-utils/src/keyboard'; * withText: true * } ); * - * view.init(); + * view.render(); * * document.body.append( view.element ); * @@ -39,6 +38,8 @@ export default class ButtonView extends View { constructor( locale ) { super( locale ); + const bind = this.bindTemplate; + /** * The label of the button view visible to the user when {@link #withText} is `true`. * It can also be used to create a {@link #tooltip}. @@ -151,6 +152,30 @@ export default class ButtonView extends View { */ this.set( 'tabindex', -1 ); + /** + * Collection of the child views inside of the button {@link #element}. + * + * @readonly + * @member {module:ui/viewcollection~ViewCollection} + */ + this.children = this.createCollection(); + + /** + * Tooltip of the button view. It is configurable using the {@link #tooltip tooltip attribute}. + * + * @readonly + * @member {module:ui/tooltip/tooltipview~TooltipView} #tooltipView + */ + this.tooltipView = this._createTooltipView(); + + /** + * Label of the button view. It is configurable using the {@link #label label attribute}. + * + * @readonly + * @member {module:ui/view~View} #labelView + */ + this.labelView = this._createLabelView(); + /** * Tooltip of the button bound to the template. * @@ -167,14 +192,6 @@ export default class ButtonView extends View { this._getTooltipString.bind( this ) ); - /** - * Tooltip of the button view. It is configurable using the {@link #tooltip tooltip attribute}. - * - * @readonly - * @member {module:ui/tooltip/tooltipview~TooltipView} #tooltipView - */ - this.tooltipView = this._createTooltipView(); - /** * (Optional) The icon view of the button. Only present when the {@link #icon icon attribute} is defined. * @@ -182,9 +199,7 @@ export default class ButtonView extends View { * @member {module:ui/icon/iconview~IconView} #iconView */ - const bind = this.bindTemplate; - - this.template = new Template( { + this.setTemplate( { tag: 'button', attributes: { @@ -199,22 +214,7 @@ export default class ButtonView extends View { tabindex: bind.to( 'tabindex' ) }, - children: [ - { - tag: 'span', - - attributes: { - class: [ 'ck-button__label' ] - }, - - children: [ - { - text: bind.to( 'label' ) - } - ] - }, - this.tooltipView - ], + children: this.children, on: { mousedown: bind.to( evt => { @@ -246,18 +246,19 @@ export default class ButtonView extends View { /** * @inheritDoc */ - init() { + render() { + super.render(); + if ( this.icon ) { const iconView = this.iconView = new IconView(); iconView.bind( 'content' ).to( this, 'icon' ); - this.element.insertBefore( iconView.element, this.element.firstChild ); - // Make sure the icon will be destroyed along with the button. - this.addChildren( iconView ); + this.children.add( iconView ); } - super.init(); + this.children.add( this.tooltipView ); + this.children.add( this.labelView ); } /** @@ -283,6 +284,32 @@ export default class ButtonView extends View { return tooltipView; } + /** + * Creates a label view instance and binds it with button attributes. + * + * @private + * @returns {module:ui/view~View} + */ + _createLabelView() { + const labelView = new View(); + + labelView.setTemplate( { + tag: 'span', + + attributes: { + class: [ 'ck-button__label' ] + }, + + children: [ + { + text: this.bindTemplate.to( 'label' ) + } + ] + } ); + + return labelView; + } + /** * Gets the text for the {@link #tooltipView} from the combination of * {@link #tooltip}, {@link #label} and {@link #keystroke} attributes. diff --git a/src/dropdown/createdropdown.js b/src/dropdown/createdropdown.js index ac977324..b37ba245 100644 --- a/src/dropdown/createdropdown.js +++ b/src/dropdown/createdropdown.js @@ -24,7 +24,7 @@ import DropdownPanelView from './dropdownpanelview'; * * const dropdown = createDropdown( model ); * - * dropdown.init(); + * dropdown.render(); * * // Will render a dropdown labeled "A dropdown" with an empty panel. * document.body.appendChild( dropdown.element ); diff --git a/src/dropdown/dropdownpanelview.js b/src/dropdown/dropdownpanelview.js index 79e0fc58..4e23a20d 100644 --- a/src/dropdown/dropdownpanelview.js +++ b/src/dropdown/dropdownpanelview.js @@ -8,7 +8,6 @@ */ import View from '../view'; -import Template from '../template'; /** * The dropdown panel view class. @@ -46,7 +45,7 @@ export default class DropdownPanelView extends View { */ this.children = this.createCollection(); - this.template = new Template( { + this.setTemplate( { tag: 'div', attributes: { diff --git a/src/dropdown/dropdownview.js b/src/dropdown/dropdownview.js index 1af3e882..88de7f08 100644 --- a/src/dropdown/dropdownview.js +++ b/src/dropdown/dropdownview.js @@ -8,7 +8,6 @@ */ import View from '../view'; -import Template from '../template'; import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker'; import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler'; @@ -25,7 +24,7 @@ import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler'; * withText: true * } ); * - * dropdown.init(); + * dropdown.render(); * * // Will render a dropdown with a panel containing a "Content of the panel" text. * document.body.appendChild( dropdown.element ); @@ -46,7 +45,7 @@ export default class DropdownView extends View { // Extend button's template before it's registered as a child of the dropdown because // by doing so, its #element is rendered and any post–render template extension will // not be reflected in DOM. - Template.extend( buttonView.template, { + buttonView.extendTemplate( { attributes: { class: [ 'ck-dropdown__button' @@ -106,7 +105,7 @@ export default class DropdownView extends View { */ this.keystrokes = new KeystrokeHandler(); - this.template = new Template( { + this.setTemplate( { tag: 'div', attributes: { @@ -120,20 +119,22 @@ export default class DropdownView extends View { panelView ] } ); + } + + /** + * @inheritDoc + */ + render() { + super.render(); // Toggle the the dropdown when it's button has been clicked. - this.listenTo( buttonView, 'execute', () => { + this.listenTo( this.buttonView, 'execute', () => { this.isOpen = !this.isOpen; } ); // Toggle the visibility of the panel when the dropdown becomes open. - panelView.bind( 'isVisible' ).to( this, 'isOpen' ); - } + this.panelView.bind( 'isVisible' ).to( this, 'isOpen' ); - /** - * @inheritDoc - */ - init() { // Listen for keystrokes coming from within #element. this.keystrokes.listenTo( this.element ); @@ -167,8 +168,6 @@ export default class DropdownView extends View { // Close the dropdown using the arrow left/escape key. this.keystrokes.set( 'arrowleft', closeDropdown ); this.keystrokes.set( 'esc', closeDropdown ); - - super.init(); } /** diff --git a/src/dropdown/list/createlistdropdown.js b/src/dropdown/list/createlistdropdown.js index 5e649b4f..8a825955 100644 --- a/src/dropdown/list/createlistdropdown.js +++ b/src/dropdown/list/createlistdropdown.js @@ -31,10 +31,9 @@ import createDropdown from '../createdropdown'; * * const dropdown = createListDropdown( model, locale ); * - * dropdown.init(); - * * // Will render a dropdown labeled "A dropdown" with a list in the panel * // containing two items. + * dropdown.render() * document.body.appendChild( dropdown.element ); * * The model instance remains in control of the dropdown after it has been created. E.g. changes to the @@ -54,7 +53,6 @@ import createDropdown from '../createdropdown'; */ export default function createListDropdown( model, locale ) { const dropdownView = createDropdown( model, locale ); - const listView = dropdownView.listView = new ListView( locale ); listView.items.bindTo( model.items ).using( itemModel => { diff --git a/src/editableui/editableuiview.js b/src/editableui/editableuiview.js index b6ca7978..1bf1e0c7 100644 --- a/src/editableui/editableuiview.js +++ b/src/editableui/editableuiview.js @@ -8,7 +8,6 @@ */ import View from '../view'; -import Template from '../template'; /** * The editable UI view class. @@ -32,7 +31,7 @@ export default class EditableUIView extends View { this.element = this.editableElement = editableElement; } - this.template = new Template( { + this.setTemplate( { tag: 'div', attributes: { class: [ @@ -77,17 +76,17 @@ export default class EditableUIView extends View { } /** - * Initializes the view by either applying the {@link #template} to the existing + * Renders the view by either applying the {@link #template} to the existing * {@link #editableElement} or assigning {@link #element} as {@link #editableElement}. */ - init() { + render() { + super.render(); + if ( this.externalElement ) { - this.template.apply( this.externalElement ); + this.template.apply( this.element = this.externalElement ); } else { this.editableElement = this.element; } - - super.init(); } /** diff --git a/src/editableui/inline/inlineeditableuiview.js b/src/editableui/inline/inlineeditableuiview.js index 750585e2..bb4a579f 100644 --- a/src/editableui/inline/inlineeditableuiview.js +++ b/src/editableui/inline/inlineeditableuiview.js @@ -8,7 +8,6 @@ */ import EditableUIView from '../../editableui/editableuiview'; -import Template from '../../template'; /** * The inline editable UI class implementing an inline {@link module:ui/editableui/editableuiview~EditableUIView}. @@ -42,7 +41,7 @@ export default class InlineEditableUIView extends EditableUIView { return t( 'Rich Text Editor, %0', [ value ] ); }; - Template.extend( this.template, { + this.extendTemplate( { attributes: { role: 'textbox', 'aria-label': bind.to( 'name', getLabel ), diff --git a/src/editorui/boxed/boxededitoruiview.js b/src/editorui/boxed/boxededitoruiview.js index f91d9dba..40c79352 100644 --- a/src/editorui/boxed/boxededitoruiview.js +++ b/src/editorui/boxed/boxededitoruiview.js @@ -9,7 +9,6 @@ import EditorUIView from '../../editorui/editoruiview'; import uid from '@ckeditor/ckeditor5-utils/src/uid'; -import Template from '../../template'; /** * The boxed editor UI view class. This class represents an editor interface @@ -47,7 +46,7 @@ export default class BoxedEditorUIView extends EditorUIView { */ this.main = this.createCollection(); - this.template = new Template( { + this.setTemplate( { tag: 'div', attributes: { diff --git a/src/editorui/editoruiview.js b/src/editorui/editoruiview.js index cdbe618f..6c644e0d 100644 --- a/src/editorui/editoruiview.js +++ b/src/editorui/editoruiview.js @@ -46,9 +46,10 @@ export default class EditorUIView extends View { /** * @inheritDoc */ - init() { + render() { + super.render(); + this._renderBodyCollection(); - super.init(); } /** diff --git a/src/icon/iconview.js b/src/icon/iconview.js index e4d18796..cec1c56b 100644 --- a/src/icon/iconview.js +++ b/src/icon/iconview.js @@ -10,7 +10,6 @@ */ import View from '../view'; -import Template from '../template'; /** * The icon view class. @@ -32,7 +31,7 @@ export default class IconView extends View { * @observable * @member {String} #content */ - this.set( 'content' ); + this.set( 'content', '' ); /** * This attribute specifies the boundaries to which the @@ -44,7 +43,7 @@ export default class IconView extends View { */ this.set( 'viewBox', '0 0 20 20' ); - this.template = new Template( { + this.setTemplate( { tag: 'svg', ns: 'http://www.w3.org/2000/svg', attributes: { @@ -52,17 +51,37 @@ export default class IconView extends View { viewBox: bind.to( 'viewBox' ) } } ); + } + + /** + * @inheritDoc + */ + render() { + super.render(); + + this._updateXMLContent(); // This is a hack for lack of innerHTML binding. // See: https://github.com/ckeditor/ckeditor5-ui/issues/99. - this.on( 'change:content', ( evt, name, value ) => { + this.on( 'change:content', () => this._updateXMLContent() ); + } + + /** + * Updates the {@link #element} with the value of {@link #content}. + * + * @private + */ + _updateXMLContent() { + if ( this.content ) { const svg = new DOMParser() - .parseFromString( value.trim(), 'image/svg+xml' ) + .parseFromString( this.content.trim(), 'image/svg+xml' ) .firstChild; + this.element.innerHTML = ''; + while ( svg.childNodes.length > 0 ) { this.element.appendChild( svg.childNodes[ 0 ] ); } - } ); + } } } diff --git a/src/iframe/iframeview.js b/src/iframe/iframeview.js index c25d021a..e5b24315 100644 --- a/src/iframe/iframeview.js +++ b/src/iframe/iframeview.js @@ -8,7 +8,6 @@ */ import View from '../view'; -import Template from '../template'; /** * The iframe view class. @@ -26,7 +25,7 @@ export default class IframeView extends View { const bind = this.bindTemplate; - this.template = new Template( { + this.setTemplate( { tag: 'iframe', attributes: { class: [ 'ck-reset_all' ], @@ -38,45 +37,20 @@ export default class IframeView extends View { load: bind.to( 'loaded' ) } } ); - - /** - * A promise returned by {@link #init} since iframe loading may be asynchronous. - * - * **Note**: Listening to `load` in {@link #init} makes no sense because at this point - * the element is already in the DOM and the `load` event might already be fired. - * - * See {@link #_iframeDeferred}. - * - * @private - * @member {Promise} - */ - this._iframePromise = new Promise( ( resolve, reject ) => { - /** - * A deferred object used to resolve the iframe promise associated with - * asynchronous loading of `contentDocument`. See {@link #_iframePromise}. - * - * @private - * @member {Object} - */ - this._iframeDeferred = { resolve, reject }; - } ); - - this.on( 'loaded', () => { - this._iframeDeferred.resolve(); - } ); } /** - * Initializes iframe {@link #element} and returns a `Promise` for asynchronous - * child `contentDocument` loading process. See {@link #_iframePromise}. + * Renders the iframe's {@link #element} and returns a `Promise` for asynchronous + * child `contentDocument` loading process. * * @returns {Promise} A promise which resolves once the iframe `contentDocument` has * been {@link #event:loaded}. */ - init() { - super.init(); - - return this._iframePromise; + render() { + return new Promise( resolve => { + this.on( 'loaded', resolve ); + super.render(); + } ); } } diff --git a/src/inputtext/inputtextview.js b/src/inputtext/inputtextview.js index 8107ad48..af1d6deb 100644 --- a/src/inputtext/inputtextview.js +++ b/src/inputtext/inputtextview.js @@ -8,7 +8,6 @@ */ import View from '../view'; -import Template from '../template'; /** * The text input view class. @@ -56,7 +55,7 @@ export default class InputTextView extends View { const bind = this.bindTemplate; - this.template = new Template( { + this.setTemplate( { tag: 'input', attributes: { type: 'text', @@ -69,6 +68,13 @@ export default class InputTextView extends View { readonly: bind.to( 'isReadOnly' ) } } ); + } + + /** + * @inheritDoc + */ + render() { + super.render(); // Note: `value` cannot be an HTML attribute, because it doesn't change HTMLInputElement value once changed. this.on( 'change:value', ( evt, propertyName, value ) => { diff --git a/src/label/labelview.js b/src/label/labelview.js index 3eb73974..18853100 100644 --- a/src/label/labelview.js +++ b/src/label/labelview.js @@ -8,7 +8,6 @@ */ import View from '../view'; -import Template from '../template'; /** * The label view class. @@ -40,7 +39,7 @@ export default class LabelView extends View { const bind = this.bindTemplate; - this.template = new Template( { + this.setTemplate( { tag: 'label', attributes: { class: [ diff --git a/src/labeledinput/labeledinputview.js b/src/labeledinput/labeledinputview.js index 382e72cc..26254da1 100644 --- a/src/labeledinput/labeledinputview.js +++ b/src/labeledinput/labeledinputview.js @@ -8,7 +8,6 @@ */ import View from '../view'; -import Template from '../template'; import uid from '@ckeditor/ckeditor5-utils/src/uid'; import LabelView from '../label/labelview'; @@ -70,7 +69,7 @@ export default class LabeledInputView extends View { const bind = this.bindTemplate; - this.template = new Template( { + this.setTemplate( { tag: 'div', attributes: { class: [ diff --git a/src/list/listitemview.js b/src/list/listitemview.js index b0434163..adf2c819 100644 --- a/src/list/listitemview.js +++ b/src/list/listitemview.js @@ -8,7 +8,6 @@ */ import View from '../view'; -import Template from '../template'; import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler'; /** @@ -42,7 +41,7 @@ export default class ListItemView extends View { const bind = this.bindTemplate; - this.template = new Template( { + this.setTemplate( { tag: 'li', attributes: { @@ -104,7 +103,9 @@ export default class ListItemView extends View { /** * @inheritDoc */ - init() { + render() { + super.render(); + const onKeystrokePress = ( data, cancel ) => { this.fire( 'execute' ); cancel(); diff --git a/src/list/listview.js b/src/list/listview.js index eb544922..c2dbf2d8 100644 --- a/src/list/listview.js +++ b/src/list/listview.js @@ -8,7 +8,6 @@ */ import View from '../view'; -import Template from '../template'; import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker'; import FocusCycler from '../focuscycler'; import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler'; @@ -69,7 +68,7 @@ export default class ListView extends View { } } ); - this.template = new Template( { + this.setTemplate( { tag: 'ul', attributes: { @@ -81,6 +80,18 @@ export default class ListView extends View { children: this.items } ); + } + + /** + * @inheritDoc + */ + render() { + super.render(); + + // Items added before rendering should be known to the #focusTracker. + for ( const item of this.items ) { + this.focusTracker.add( item.element ); + } this.items.on( 'add', ( evt, item ) => { this.focusTracker.add( item.element ); @@ -89,16 +100,9 @@ export default class ListView extends View { this.items.on( 'remove', ( evt, item ) => { this.focusTracker.remove( item.element ); } ); - } - /** - * @inheritDoc - */ - init() { // Start listening for the keystrokes coming from #element. this.keystrokes.listenTo( this.element ); - - super.init(); } /** diff --git a/src/panel/balloon/balloonpanelview.js b/src/panel/balloon/balloonpanelview.js index 807b3135..6ac8bc07 100644 --- a/src/panel/balloon/balloonpanelview.js +++ b/src/panel/balloon/balloonpanelview.js @@ -8,7 +8,6 @@ */ import View from '../../view'; -import Template from '../../template'; import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position'; import isRange from '@ckeditor/ckeditor5-utils/src/dom/isrange'; import isElement from '@ckeditor/ckeditor5-utils/src/lib/lodash/isElement'; @@ -40,7 +39,7 @@ const defaultLimiterElement = global.document.body; * const childView = new ChildView(); * const positions = BalloonPanelView.defaultPositions; * - * panel.init(); + * panel.render(); * * // Add a child view to the panel's content collection. * panel.content.add( childView ); @@ -144,7 +143,7 @@ export default class BalloonPanelView extends View { */ this.content = this.createCollection(); - this.template = new Template( { + this.setTemplate( { tag: 'div', attributes: { class: [ @@ -194,7 +193,7 @@ export default class BalloonPanelView extends View { * const panel = new BalloonPanelView( locale ); * const positions = BalloonPanelView.defaultPositions; * - * panel.init(); + * panel.render(); * * // Attach the panel to an element with the "target" id DOM. * panel.attachTo( { @@ -249,7 +248,7 @@ export default class BalloonPanelView extends View { * const panel = new BalloonPanelView( locale ); * const positions = BalloonPanelView.defaultPositions; * - * panel.init(); + * panel.render(); * * // Pin the panel to an element with the "target" id DOM. * panel.pin( { diff --git a/src/panel/balloon/contextualballoon.js b/src/panel/balloon/contextualballoon.js index f3963084..2eb43e89 100644 --- a/src/panel/balloon/contextualballoon.js +++ b/src/panel/balloon/contextualballoon.js @@ -79,11 +79,11 @@ export default class ContextualBalloon extends Plugin { */ this._stack = new Map(); - // Editor should be focused when contextual balloon is focused. - this.editor.ui.focusTracker.add( this.view.element ); - // Add balloon panel view to editor `body` collection and wait until view will be ready. this.editor.ui.view.body.add( this.view ); + + // Editor should be focused when contextual balloon is focused. + this.editor.ui.focusTracker.add( this.view.element ); } /** diff --git a/src/panel/sticky/stickypanelview.js b/src/panel/sticky/stickypanelview.js index 528478eb..84864f14 100644 --- a/src/panel/sticky/stickypanelview.js +++ b/src/panel/sticky/stickypanelview.js @@ -141,19 +141,48 @@ export default class StickyPanelView extends View { * @member {Object} #_limiterRect */ - this.template = new Template( { + /** + * A dummy element which visually fills the space as long as the + * actual panel is sticky. It prevents flickering of the UI. + * + * @protected + * @property {HTMLElement} + */ + this._contentPanelPlaceholder = new Template( { + tag: 'div', + attributes: { + class: [ + 'ck-sticky-panel__placeholder' + ], + style: { + display: bind.to( 'isSticky', isSticky => isSticky ? 'block' : 'none' ), + height: bind.to( 'isSticky', isSticky => { + return isSticky ? toPx( this._panelRect.height ) : null; + } ) + } + } + } ).render(); + + /** + * The panel which accepts children into {@link #content} collection. + * Also an element which is positioned when {@link #isSticky}. + * + * @protected + * @property {HTMLElement} + */ + this._contentPanel = new Template( { tag: 'div', attributes: { class: [ - 'ck-sticky-panel', + 'ck-sticky-panel__content', // Toggle class of the panel when "sticky" state changes in the view. - bind.if( 'isSticky', 'ck-sticky-panel_sticky' ), - bind.if( '_isStickyToTheLimiter', 'ck-sticky-panel_sticky_bottom-limit' ), + bind.if( 'isSticky', 'ck-sticky-panel__content_sticky' ), + bind.if( '_isStickyToTheLimiter', 'ck-sticky-panel__content_sticky_bottom-limit' ), ], style: { width: bind.to( 'isSticky', isSticky => { - return isSticky ? toPx( this._elementPlaceholder.getBoundingClientRect().width ) : null; + return isSticky ? toPx( this._contentPanelPlaceholder.getBoundingClientRect().width ) : null; } ), top: bind.to( '_hasViewportTopOffset', _hasViewportTopOffset => { @@ -169,38 +198,27 @@ export default class StickyPanelView extends View { }, children: this.content - } ); + } ).render(); - /** - * A dummy element which visually fills the space as long as the - * actual panel is sticky. It prevents flickering of the UI. - * - * @private - * @property {HTMLElement} - */ - this._elementPlaceholder = new Template( { + this.setTemplate( { tag: 'div', attributes: { class: [ - 'ck-sticky-panel__placeholder' - ], - style: { - display: bind.to( 'isSticky', isSticky => isSticky ? 'block' : 'none' ), - height: bind.to( 'isSticky', isSticky => { - return isSticky ? toPx( this._panelRect.height ) : null; - } ) - } - } - } ).render(); + 'ck-sticky-panel' + ] + }, + children: [ + this._contentPanelPlaceholder, + this._contentPanel + ] + } ); } /** * @inheritDoc */ - init() { - super.init(); - - this.element.parentNode.insertBefore( this._elementPlaceholder, this.element ); + render() { + super.render(); // Check if the panel should go into the sticky state immediately. this._checkIfShouldBeSticky(); @@ -216,14 +234,6 @@ export default class StickyPanelView extends View { } ); } - /** - * Destroys the panel and removes the {@link #_elementPlaceholder}. - */ - destroy() { - super.destroy(); - this._elementPlaceholder.remove(); - } - /** * Analyzes the environment to decide whether the panel should * be sticky or not. @@ -231,18 +241,24 @@ export default class StickyPanelView extends View { * @protected */ _checkIfShouldBeSticky() { - const limiterRect = this._limiterRect = this.limiterElement.getBoundingClientRect(); - const panelRect = this._panelRect = this.element.getBoundingClientRect(); - - // The panel must be active to become sticky. - this.isSticky = this.isActive && - // The limiter's top edge must be beyond the upper edge of the visible viewport (+the viewportTopOffset). - limiterRect.top < this.viewportTopOffset && - // The model#limiterElement's height mustn't be smaller than the panel's height and model#limiterBottomOffset. - // There's no point in entering the sticky mode if the model#limiterElement is very, very small, because - // it would immediately set model#_isStickyToTheLimiter true and, given model#limiterBottomOffset, the panel - // would be positioned before the model#limiterElement. - this._panelRect.height + this.limiterBottomOffset < limiterRect.height; + const panelRect = this._panelRect = this._contentPanel.getBoundingClientRect(); + let limiterRect; + + if ( !this.limiterElement ) { + this.isSticky = false; + } else { + limiterRect = this._limiterRect = this.limiterElement.getBoundingClientRect(); + + // The panel must be active to become sticky. + this.isSticky = this.isActive && + // The limiter's top edge must be beyond the upper edge of the visible viewport (+the viewportTopOffset). + limiterRect.top < this.viewportTopOffset && + // The model#limiterElement's height mustn't be smaller than the panel's height and model#limiterBottomOffset. + // There's no point in entering the sticky mode if the model#limiterElement is very, very small, because + // it would immediately set model#_isStickyToTheLimiter true and, given model#limiterBottomOffset, the panel + // would be positioned before the model#limiterElement. + this._panelRect.height + this.limiterBottomOffset < limiterRect.height; + } // Stick the panel to the top edge of the viewport simulating CSS position:sticky. // TODO: Possibly replaced by CSS in the future http://caniuse.com/#feat=css-sticky diff --git a/src/template.js b/src/template.js index 2d3bfdcf..461297e4 100644 --- a/src/template.js +++ b/src/template.js @@ -16,6 +16,7 @@ import View from './view'; import ViewCollection from './viewcollection'; import cloneDeepWith from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeepWith'; import isObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isObject'; +import isDomNode from '@ckeditor/ckeditor5-utils/src/dom/isdomnode'; import log from '@ckeditor/ckeditor5-utils/src/log'; const xhtmlNs = 'http://www.w3.org/1999/xhtml'; @@ -98,12 +99,13 @@ export default class Template { */ /** - * Children of the template, sub–templates. Each one is an independent - * instance of {@link ~Template}. + * Children of the template. They can be either: + * * an independent instances of {@link ~Template} (sub–templates), + * * native DOM Nodes. * * **Note**: This property only makes sense when {@link #tag} is defined. * - * @member {module:utils/collection~Collection.} #children + * @member {Array.} #children */ /** @@ -209,6 +211,50 @@ export default class Template { this._revertTemplateFromNode( node, this._revertData ); } + /** + * Returns an iterator which traverses the template in search of {@link module:ui/view~View} + * instances and returns them one by one. + * + * const viewFoo = new View(); + * const viewBar = new View(); + * const viewBaz = new View(); + * const template = new Template( { + * tag: 'div', + * children: [ + * viewFoo, + * { + * tag: 'div', + * children: [ + * viewBar + * ] + * }, + * viewBaz + * ] + * } ); + * + * // Logs: viewFoo, viewBar, viewBaz + * for ( const view of template.getViews() ) { + * console.log( view ); + * } + * + * @returns {Iterator.} + */ + * getViews() { + function* search( def ) { + if ( def.children ) { + for ( const child of def.children ) { + if ( isView( child ) ) { + yield child; + } else { + yield* search( child ); + } + } + } + } + + yield* search( this ); + } + /** * An entry point to the interface which binds DOM nodes to * {@link module:utils/observablemixin~Observable observables}. @@ -305,7 +351,7 @@ export default class Template { * } ); * * // Child extension. - * Template.extend( template.children.get( 0 ), { + * Template.extend( template.children[ 0 ], { * attributes: { * class: 'd' * } @@ -644,14 +690,21 @@ export default class Template { if ( !isApplying ) { child.setParent( node ); + // Note: ViewCollection renders its children. for ( const view of child ) { container.appendChild( view.element ); } } } else if ( isView( child ) ) { if ( !isApplying ) { + if ( !child.isRendered ) { + child.render(); + } + container.appendChild( child.element ); } + } else if ( isDomNode( child ) ) { + container.appendChild( child ); } else { if ( isApplying ) { const revertData = data.revertData; @@ -1117,17 +1170,17 @@ function normalize( def ) { normalizeAttributes( def.attributes ); } - const children = new ViewCollection(); + const children = []; if ( def.children ) { if ( isViewCollection( def.children ) ) { - children.add( def.children ); + children.push( def.children ); } else { for ( const child of def.children ) { - if ( isTemplate( child ) || isView( child ) ) { - children.add( child ); + if ( isTemplate( child ) || isView( child ) || isDomNode( child ) ) { + children.push( child ); } else { - children.add( new Template( child ) ); + children.push( new Template( child ) ); } } } @@ -1336,7 +1389,7 @@ function extendTemplate( template, def ) { let childIndex = 0; for ( const childDef of def.children ) { - extendTemplate( template.children.get( childIndex++ ), childDef ); + extendTemplate( template.children[ childIndex++ ], childDef ); } } } @@ -1436,11 +1489,12 @@ function shouldExtend( attrName ) { * } * } ); * - * A {@link module:ui/view~View} or another {@link module:ui/template~Template} can also become a child - * of a template. In case of a view {@link module:ui/view~View#element} is used: + * A {@link module:ui/view~View}, another {@link module:ui/template~Template} or a native DOM node + * can also become a child of a template. When a view is passed, its {@link module:ui/view~View#element} is used: * * const view = new SomeView(); * const childTemplate = new Template( { ... } ); + * const childNode = document.createElement( 'b' ); * * new Template( { * tag: 'p', @@ -1450,7 +1504,10 @@ function shouldExtend( attrName ) { * view, * * // The output of childTemplate.render() will be added here. - * childTemplate + * childTemplate, + * + * // Native DOM nodes are included directly in the rendered output. + * childNode * ] * } ); * diff --git a/src/toolbar/contextual/contextualtoolbar.js b/src/toolbar/contextual/contextualtoolbar.js index 54d3c99a..27a0040f 100644 --- a/src/toolbar/contextual/contextualtoolbar.js +++ b/src/toolbar/contextual/contextualtoolbar.js @@ -7,7 +7,6 @@ * @module ui/toolbar/contextual/contextualtoolbar */ -import Template from '../../template'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import ContextualBalloon from '../../panel/balloon/contextualballoon'; import ToolbarView from '../toolbarview'; @@ -51,7 +50,7 @@ export default class ContextualToolbar extends Plugin { */ this.toolbarView = new ToolbarView( editor.locale ); - Template.extend( this.toolbarView.template, { + this.toolbarView.extendTemplate( { attributes: { class: [ 'ck-editor-toolbar', @@ -60,6 +59,8 @@ export default class ContextualToolbar extends Plugin { } } ); + this.toolbarView.render(); + /** * The contextual balloon plugin instance. * diff --git a/src/toolbar/toolbarseparatorview.js b/src/toolbar/toolbarseparatorview.js index f0244387..a37f8751 100644 --- a/src/toolbar/toolbarseparatorview.js +++ b/src/toolbar/toolbarseparatorview.js @@ -8,7 +8,6 @@ */ import View from '../view'; -import Template from '../template'; /** * The toolbar separator view class. @@ -22,7 +21,7 @@ export default class ToolbarSeparatorView extends View { constructor( locale ) { super( locale ); - this.template = new Template( { + this.setTemplate( { tag: 'span', attributes: { class: [ diff --git a/src/toolbar/toolbarview.js b/src/toolbar/toolbarview.js index 7bc35eb2..18533bf8 100644 --- a/src/toolbar/toolbarview.js +++ b/src/toolbar/toolbarview.js @@ -8,7 +8,6 @@ */ import View from '../view'; -import Template from '../template'; import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker'; import FocusCycler from '../focuscycler'; import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler'; @@ -72,7 +71,7 @@ export default class ToolbarView extends View { } } ); - this.template = new Template( { + this.setTemplate( { tag: 'div', attributes: { class: [ @@ -87,6 +86,18 @@ export default class ToolbarView extends View { mousedown: preventDefault( this ) } } ); + } + + /** + * @inheritDoc + */ + render() { + super.render(); + + // Items added before rendering should be known to the #focusTracker. + for ( const item of this.items ) { + this.focusTracker.add( item.element ); + } this.items.on( 'add', ( evt, item ) => { this.focusTracker.add( item.element ); @@ -95,16 +106,9 @@ export default class ToolbarView extends View { this.items.on( 'remove', ( evt, item ) => { this.focusTracker.remove( item.element ); } ); - } - /** - * @inheritDoc - */ - init() { // Start listening for the keystrokes coming from #element. this.keystrokes.listenTo( this.element ); - - super.init(); } /** diff --git a/src/tooltip/tooltipview.js b/src/tooltip/tooltipview.js index ad1881c2..ef524198 100644 --- a/src/tooltip/tooltipview.js +++ b/src/tooltip/tooltipview.js @@ -8,7 +8,6 @@ */ import View from '../view'; -import Template from '../template'; /** * The tooltip view class. @@ -53,7 +52,7 @@ export default class TooltipView extends View { const bind = this.bindTemplate; - this.template = new Template( { + this.setTemplate( { tag: 'span', attributes: { class: [ diff --git a/src/view.js b/src/view.js index 1df7c663..8f6c17de 100644 --- a/src/view.js +++ b/src/view.js @@ -21,9 +21,9 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable'; * {@link module:ui/view~View#template}. Views are building blocks of the user interface and handle * interaction * - * Views {@link module:ui/view~View#addChildren aggregate} children in + * Views {@link module:ui/view~View#registerChildren aggregate} children in * {@link module:ui/view~View#createCollection collections} and manage the life cycle of DOM - * listeners e.g. by handling initialization and destruction. + * listeners e.g. by handling rendering and destruction. * * See the {@link module:ui/template~TemplateDefinition} syntax to learn more about shaping view * elements, attributes and listeners. @@ -37,7 +37,7 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable'; * // Views define their interface (state) using observable attributes. * this.set( 'elementClass', 'bar' ); * - * this.template = new Template( { + * this.setTemplate( { * tag: 'p', * * // The element of the view can be defined with its children. @@ -66,8 +66,7 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable'; * * const view = new SampleView( locale ); * - * // Each view must be first initialized. - * view.init(); + * view.render(); * * // Append

Helloworld

to the * document.body.appendChild( view.element ); @@ -86,11 +85,52 @@ export default class View { /** * Creates an instance of the {@link module:ui/view~View} class. * - * Also see {@link #init}. + * Also see {@link #render}. * * @param {module:utils/locale~Locale} [locale] The localization services instance. */ constructor( locale ) { + /** + * An HTML element of the view. `null` until {@link #render rendered} + * from the {@link #template}. + * + * class SampleView extends View { + * constructor() { + * super(); + * + * // A template instance the #element will be created from. + * this.setTemplate( { + * tag: 'p' + * + * // ... + * } ); + * } + * } + * + * const view = new SampleView(); + * + * // Renders the #template + * view.render(); + * + * // Append the HTML element of the view to . + * document.body.appendChild( view.element ); + * + * **Note**: The element of the view can also be assigned directly: + * + * view.element = document.querySelector( '#my-container' ); + * + * @member {HTMLElement} + */ + this.element = null; + + /** + * Set `true` when the view has already been {@link module:ui/view~View#render rendered}. + * + * @readonly + * @member {Boolean} #isRendered + */ + this.isRendered = false; + /** * A set of tools to localize the user interface. * @@ -112,17 +152,6 @@ export default class View { */ this.t = locale && locale.t; - /** - * A flag set `true` after {@link #init initialization}. Because the process can be - * asynchronous, this {@link module:utils/observablemixin~Observable observable} flag allows - * deferring certain actions until it is finished. - * - * @readonly - * @observable - * @member {Boolean} #ready - */ - this.set( 'ready', false ); - /** * Collections registered with {@link #createCollection}. * @@ -147,18 +176,11 @@ export default class View { /** * Template of this view. It provides the {@link #element} representing - * the view in DOM. + * the view in DOM, which is {@link #render rendered}. * * @member {module:ui/template~Template} #template */ - /** - * An internal, cached element of this view. See {@link #element}. - * - * @private - * @member {HTMLElement} #_element - */ - /** * Cached {@link @link module:ui/template~BindChain bind chain} object created by the * {@link #template}. See {@link #bindTemplate}. @@ -168,54 +190,6 @@ export default class View { */ } - /** - * An HTML element of this view. The element is rendered on first reference - * by the {@link #template}: - * - * class SampleView extends View { - * constructor() { - * super(); - * - * // A template instance the #element will be created from. - * this.template = new Template( { - * tag: 'p' - * - * // ... - * } ); - * } - * } - * - * const view = new SampleView(); - * view.init(); - * - * // Renders the #template and appends the output to . - * document.body.appendChild( view.element ); - * - * The element of the view can also be assigned directly: - * - * view.element = document.querySelector( '#my-container' ); - * - * @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}, a binding * {@link module:ui/template~BindChain interface} pre–configured for the view instance. @@ -237,7 +211,7 @@ export default class View { * isEnabled: true * } ); * - * this.template = new Template( { + * this.setTemplate( { * tag: 'p', * * attributes: { @@ -277,7 +251,7 @@ export default class View { * * this.items = this.createCollection(); * - * this.template = new Template( { + * this.setTemplate( { * tag: 'p', * * // `items` collection will render here. @@ -289,7 +263,7 @@ export default class View { * const view = new SampleView( locale ); * const child = new ChildView( locale ); * - * view.init(); + * view.render(); * * // It will append

to the . * document.body.appendChild( view.element ); @@ -310,10 +284,10 @@ export default class View { /** * Registers a new child view under the view instance. Once registered, a child - * view is managed by its parent, including {@link #init initization} + * view is managed by its parent, including {@link #render rendering} * and {@link #destroy destruction}. * - * To revert this, use {@link #removeChildren}. + * To revert this, use {@link #deregisterChildren}. * * class SampleView extends View { * constructor( locale ) { @@ -322,23 +296,23 @@ export default class View { * this.childA = new SomeChildView( locale ); * this.childB = new SomeChildView( locale ); * - * this.template = new Template( { tag: 'p' } ); + * this.setTemplate( { tag: 'p' } ); * * // Register the children. - * this.addChildren( [ this.childA, this.childB ] ); + * this.registerChildren( [ this.childA, this.childB ] ); * } * - * init() { + * render() { + * super.render(); + * * this.element.appendChild( this.childA.element ); * this.element.appendChild( this.childB.element ); - * - * return super.init(); * } * } * * const view = new SampleView( locale ); * - * view.init(); + * view.render(); * * // Will append

. * document.body.appendChild( view.element ); @@ -353,11 +327,11 @@ export default class View { * this.childA = new SomeChildView( locale ); * this.childB = new SomeChildView( locale ); * - * this.template = new Template( { + * this.setTemplate( { * tag: 'p', * * // These children will be added automatically. There's no - * // need to call {@link #addChildren} for any of them. + * // need to call {@link #registerChildren} for any of them. * children: [ this.childA, this.childB ] * } ); * } @@ -367,68 +341,111 @@ export default class View { * * @param {module:ui/view~View|Iterable.} children Children views to be registered. */ - addChildren( children ) { + registerChildren( children ) { if ( !isIterable( children ) ) { children = [ children ]; } - children.map( c => this._unboundChildren.add( c ) ); + for ( const child of children ) { + this._unboundChildren.add( child ); + } } /** - * The opposite of {@link #addChildren}. Removes a child view from this view instance. + * The opposite of {@link #registerChildren}. Removes a child view from this view instance. * Once removed, the child is no longer managed by its parent, e.g. it can safely * become a child of another parent view. * - * @see #addChildren + * @see #registerChildren * @param {module:ui/view~View|Iterable.} children Child views to be removed. */ - removeChildren( children ) { + deregisterChildren( children ) { if ( !isIterable( children ) ) { children = [ children ]; } - children.map( c => this._unboundChildren.remove( c ) ); + for ( const child of children ) { + this._unboundChildren.remove( child ); + } + } + + /** + * Sets the {@link #template} of the view with with given definition. + * + * A shorthand for: + * + * view.setTemplate( definition ); + * + * @param {module:ui/template~TemplateDefinition} definition Definition of view's template. + */ + setTemplate( definition ) { + this.template = new Template( definition ); + } + + /** + * {@link module:ui/template~Template.extend Extends} the {@link #template} of the view with + * with given definition. + * + * A shorthand for: + * + * Template.extend( view.template, definition ); + * + * **Note**: Is requires the {@link #template} to be already set. See {@link #setTemplate}. + * + * @param {module:ui/template~TemplateDefinition} definition Definition which + * extends the {@link #template}. + */ + extendTemplate( definition ) { + Template.extend( this.template, definition ); } /** - * Initializes the view and its children added by {@link #addChildren} and residing in collections - * created by the {@link #createCollection} method. + * Recursively renders the view. + * + * Once the view is rendered: + * * the {@link #element} becomes an HTML element out of {@link #template}, + * * the {@link #isRendered} flag is set `true`. + * + * **Note**: The children of the view: + * * defined directly in the {@link #template} + * * residing in collections created by the {@link #createCollection} method, + * * and added by {@link #registerChildren} + * are also rendered in the process. * - * In general, `init()` is the right place to keep the code which refers to the {@link #element} - * and should be executed at the very beginning of the view's life cycle. It is possible to - * {@link module:ui/template~Template.extend} the {@link #template} before the first reference of - * the {@link #element}. To allow an early customization of the view (e.g. by its parent), - * such references should be done in `init()`. + * In general, `render()` method is the right place to keep the code which refers to the + * {@link #element} and should be executed at the very beginning of the view's life cycle. + * + * It is possible to {@link module:ui/template~Template.extend} the {@link #template} before + * the view is rendered. To allow an early customization of the view (e.g. by its parent), + * such references should be done in `render()`. * * class SampleView extends View { * constructor() { - * this.template = new Template( { + * this.setTemplate( { * // ... * } ); * }, * - * init() { - * super.init(); + * render() { + * // View#element becomes available. + * super.render(); * - * function scroll() { + * // The "scroll" listener depends on #element. + * this.listenTo( window, 'scroll', () => { * // A reference to #element would render the #template and make it non-extendable. * if ( window.scrollY > 0 ) { * this.element.scrollLeft = 100; * } else { * this.element.scrollLeft = 0; * } - * } - * - * this.listenTo( window, 'scroll', () => { - * scroll(); * } ); * } * } * * const view = new SampleView(); * - * Template.extend( view.template, { + * // Let's customize the view before it gets rendered. + * view.extendTemplate( { * attributes: { * class: [ * 'additional-class' @@ -436,30 +453,32 @@ export default class View { * } * } ); * - * // Late initialization allows customization of the view. - * view.init(); - * - * Once initialized, the view becomes {@link #ready}. + * // Late rendering allows customization of the view. + * view.render(); */ - init() { - if ( this.ready ) { + render() { + if ( this.isRendered ) { /** - * This View has already been initialized. + * This View has already been rendered. * - * @error ui-view-init-reinit + * @error ui-view-render-rendered */ - throw new CKEditorError( 'ui-view-init-reinit: This View has already been initialized.' ); + throw new CKEditorError( 'ui-view-render-already-rendered: This View has already been rendered.' ); } - // Initialize collections in #_viewCollections. - this._viewCollections.map( c => c.init() ); + // Render #element of the view. + if ( this.template ) { + this.element = this.template.render(); - // Spread the word that this view is ready! - this.ready = true; + // Auto–register view children from #template. + this.registerChildren( this.template.getViews() ); + } + + this.isRendered = true; } /** - * Recursively destroys the view instance and child views added by {@link #addChildren} and + * Recursively destroys the view instance and child views added by {@link #registerChildren} and * residing in collections created by the {@link #createCollection}. * * Destruction disables all event listeners: @@ -471,28 +490,6 @@ export default class View { 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 ); diff --git a/src/viewcollection.js b/src/viewcollection.js index 1ed7f6a2..9e3e5931 100644 --- a/src/viewcollection.js +++ b/src/viewcollection.js @@ -23,16 +23,13 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix'; * const viewA = new ChildView( locale ); * const viewB = new ChildView( locale ); * - * View collection manages view {@link module:ui/view~View#element elements}: + * View collection renders and manages view {@link module:ui/view~View#element elements}: * - * // parentView.element.children == [ viewA.element, vievB.element ] * collection.add( viewA ); * collection.add( viewB ); * - * It handles initialization of the children: - * - * // viewA.ready == viewB.ready == true; - * collection.init(); + * console.log( parentView.element.firsChild ); // -> viewA.element + * console.log( parentView.element.lastChild ); // -> viewB.element * * It {@link module:ui/viewcollection~ViewCollection#delegate propagates} DOM events too: * @@ -67,6 +64,10 @@ export default class ViewCollection extends Collection { // Handle {@link module:ui/view~View#element} in DOM when a new view is added to the collection. this.on( 'add', ( evt, view, index ) => { + if ( !view.isRendered ) { + view.render(); + } + if ( view.element && this._parentElement ) { this._parentElement.insertBefore( view.element, this._parentElement.children[ index ] ); } @@ -87,16 +88,6 @@ export default class ViewCollection extends Collection { */ this.locale = locale; - /** - * Set `true` when all views in the collection are {@link module:ui/view~View#ready}. - * See the view {@link module:ui/view~View#init init} method. - * - * @readonly - * @observable - * @member {Boolean} #ready - */ - this.set( 'ready', false ); - /** * A parent element within which child views are rendered and managed in DOM. * @@ -106,66 +97,12 @@ export default class ViewCollection extends Collection { this._parentElement = null; } - /** - * Initializes all child views in the collection by calling view {@link module:ui/view~View#init} - * method. - * - * Once finished, sets {@link #ready} `true`. - */ - init() { - if ( this.ready ) { - /** - * This ViewCollection has already been initialized. - * - * @error ui-viewcollection-init-reinit - */ - throw new CKEditorError( 'ui-viewcollection-init-reinit: This ViewCollection has already been initialized.' ); - } - - this.map( v => v.init() ); - - this.ready = true; - } - /** * Destroys the view collection along with child views. * See the view {@link module:ui/view~View#destroy} method. */ destroy() { - this.map( v => v.destroy() ); - } - - /** - * Adds a new child view to the collection. If the collection is - * {@link module:ui/viewcollection~ViewCollection#ready}, the child view is also - * {@link module:ui/view~View#init initialized} when added. - * - * Additionally, if the {@link #setParent parent element} of the collection has been set, the - * {@link module:ui/view~View#element element} of the view is also added in DOM, - * reflecting the order of the collection. - * - * const collection = new ViewCollection(); - * const parentElement = document.querySelector( '#container' ); - * collection.setParent( parentElement ); - * - * const viewA = new View(); - * const viewB = new View(); - * - * // parentElement.children == [ viewA.element, vievB.element ] - * collection.add( viewA ); - * collection.add( viewB ); - * - * See the {@link #remove} method. - * - * @param {module:ui/view~View} view A child view. - * @param {Number} [index] Index at which the child will be added to the collection. - */ - add( view, index ) { - super.add( view, index ); - - if ( this.ready && !view.ready ) { - view.init(); - } + this.map( view => view.destroy() ); } /** diff --git a/tests/_utils-tests/utils.js b/tests/_utils-tests/utils.js index 74cf3fdc..373a5488 100644 --- a/tests/_utils-tests/utils.js +++ b/tests/_utils-tests/utils.js @@ -26,13 +26,13 @@ describe( 'utils', () => { expect( view.bar._parentElement ).to.equal( document.body.lastChild ); } ); - it( 'is ready', () => { + it( 'is rendered', () => { const view = testUtils.createTestUIView( { foo: el => el.firstChild, bar: el => el.lastChild, } ); - expect( view.ready ).to.be.true; + expect( view.isRendered ).to.be.true; } ); } ); } ); diff --git a/tests/_utils/utils.js b/tests/_utils/utils.js index 94b37447..7b4228b1 100644 --- a/tests/_utils/utils.js +++ b/tests/_utils/utils.js @@ -12,21 +12,24 @@ import View from '../../src/view'; */ const utils = { /** - * Returns UI controller for given region/DOM selector pairs, which {@link ui.Controller#view} + * Returns a view for a given region/DOM selector pairs, which {@link module:ui/view~View#element} * is `document.body`. It is useful for manual tests which engage various UI components and/or - * UI {@link ui.Controller} instances, where initialization and the process of insertion into + * UI {@link module:ui/view~View} instances, where initialization and the process of insertion into * DOM could be problematic i.e. because of the number of instances. * * Usage: * - * // Get the controller. - * const controller = testUtils.createTestUIView(); + * // Get the view. + * const view = testUtils.createTestUIView( { + * 'some-collection': '#collection' + * } ); * * // Then use it to organize and initialize children. - * controller.add( 'some-collection', childControllerInstance ); + * view.add( 'some-collection', childControllerInstance ); * * @param {Object} regions An object literal with `regionName: [DOM Selector|callback]` pairs. - * See {@link ui.View#register}. + * + * See {@link module:ui/view~View#createCollection}. */ createTestUIView( regions ) { const TestUIView = class extends View { @@ -50,7 +53,7 @@ const utils = { const view = new TestUIView(); - view.init(); + view.render(); return view; } diff --git a/tests/bindings/preventdefault.js b/tests/bindings/preventdefault.js index 22305bf6..7bef9d77 100644 --- a/tests/bindings/preventdefault.js +++ b/tests/bindings/preventdefault.js @@ -7,13 +7,12 @@ import preventDefault from '../../src/bindings/preventdefault'; import View from '../../src/view'; -import Template from '../../src/template'; describe( 'preventDefault', () => { it( 'prevents default of a native DOM event', () => { const view = new View(); - view.template = new Template( { + view.setTemplate( { tag: 'div', on: { @@ -25,7 +24,7 @@ describe( 'preventDefault', () => { const spy = sinon.spy( evt, 'preventDefault' ); // Render to enable bubbling. - view.element; + view.render(); view.element.dispatchEvent( evt ); sinon.assert.calledOnce( spy ); @@ -35,11 +34,11 @@ describe( 'preventDefault', () => { const view = new View(); const child = new View(); - child.template = new Template( { + child.setTemplate( { tag: 'a' } ); - view.template = new Template( { + view.setTemplate( { tag: 'div', on: { @@ -55,7 +54,7 @@ describe( 'preventDefault', () => { const spy = sinon.spy( evt, 'preventDefault' ); // Render to enable bubbling. - view.element; + view.render(); child.element.dispatchEvent( evt ); sinon.assert.notCalled( spy ); diff --git a/tests/button/buttonview.js b/tests/button/buttonview.js index 17ea6419..5bb7da34 100644 --- a/tests/button/buttonview.js +++ b/tests/button/buttonview.js @@ -9,6 +9,8 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import ButtonView from '../../src/button/buttonview'; import IconView from '../../src/icon/iconview'; import TooltipView from '../../src/tooltip/tooltipview'; +import View from '../../src/view'; +import ViewCollection from '../../src/viewcollection'; testUtils.createSinonSandbox(); @@ -18,7 +20,22 @@ describe( 'ButtonView', () => { beforeEach( () => { locale = { t() {} }; - return ( view = new ButtonView( locale ) ).init(); + view = new ButtonView( locale ); + view.render(); + } ); + + describe( 'constructor()', () => { + it( 'creates view#children collection', () => { + expect( view.children ).to.be.instanceOf( ViewCollection ); + } ); + + it( 'creates #tooltipView', () => { + expect( view.tooltipView ).to.be.instanceOf( TooltipView ); + } ); + + it( 'creates #labelView', () => { + expect( view.labelView ).to.be.instanceOf( View ); + } ); } ); describe( '