| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- /**
- * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- import Selection from './selection.js';
- import Renderer from './renderer.js';
- import DomConverter from './domconverter.js';
- import RootEditableElement from './rooteditableelement.js';
- import { injectQuirksHandling } from './filler.js';
- import log from '../../utils/log.js';
- import MutationObserver from './observer/mutationobserver.js';
- import SelectionObserver from './observer/selectionobserver.js';
- import FocusObserver from './observer/focusobserver.js';
- import KeyObserver from './observer/keyobserver.js';
- import FakeSelectionObserver from './observer/fakeselectionobserver.js';
- import mix from '../../utils/mix.js';
- import ObservableMixin from '../../utils/observablemixin.js';
- /**
- * Document class creates an abstract layer over the content editable area.
- * It combines the actual tree of view elements, tree of DOM elements,
- * {@link engine.view.DomConverter DOM Converter}, {@link engine.view.Renderer renderer} and all
- * {@link engine.view.Observer observers}.
- *
- * If you want to only transform the tree of view elements to the DOM elements you can use the
- * {@link engine.view.DomConverter DomConverter}.
- *
- * Note that the following observers are added by the class constructor and are always available:
- *
- * * {@link view.observer.MutationObserver},
- * * {@link view.observer.SelectionObserver},
- * * {@link view.observer.FocusObserver},
- * * {@link view.observer.KeyObserver},
- * * {@link view.observer.FakeSelectionObserver}.
- *
- * @memberOf engine.view
- * @mixes utils.EmitterMixin
- */
- export default class Document {
- /**
- * Creates a Document instance.
- */
- constructor() {
- /**
- * Roots of the DOM tree. Map on the `HTMLElement`s with roots names as keys.
- *
- * @readonly
- * @member {Map} engine.view.Document#domRoots
- */
- this.domRoots = new Map();
- /**
- * Selection done on this document.
- *
- * @readonly
- * @member {engine.view.Selection} engine.view.Document#selection
- */
- this.selection = new Selection();
- /**
- * Instance of the {@link engine.view.DomConverter domConverter} use by
- * {@link engine.view.Document#renderer renderer} and {@link engine.view.observer.Observer observers}.
- *
- * @readonly
- * @member {engine.view.DomConverter} engine.view.Document#domConverter
- */
- this.domConverter = new DomConverter();
- /**
- * Roots of the view tree. Map of the {engine.view.Element view elements} with roots names as keys.
- *
- * @readonly
- * @member {Map} engine.view.Document#roots
- */
- this.roots = new Map();
- /**
- * True if document is focused.
- *
- * This property is updated by the {@link engine.view.obsever.FocusObserver}.
- * If the {@link engine.view.obsever.FocusObserver} is disabled this property will not change.
- *
- * @readonly
- * @observable
- * @member {Boolean} engine.view.Document#isFocused
- */
- this.set( 'isFocused', false );
- /**
- * Instance of the {@link engine.view.Document#renderer renderer}.
- *
- * @readonly
- * @member {engine.view.Renderer} engine.view.Document#renderer
- */
- this.renderer = new Renderer( this.domConverter, this.selection );
- this.renderer.bind( 'isFocused' ).to( this, 'isFocused' );
- /**
- * Map of registered {@link engine.view.Observer observers}.
- *
- * @private
- * @member {Map.<Function, engine.view.Observer>} engine.view.Document#_observers
- */
- this._observers = new Map();
- // Add default observers.
- this.addObserver( MutationObserver );
- this.addObserver( SelectionObserver );
- this.addObserver( FocusObserver );
- this.addObserver( KeyObserver );
- this.addObserver( FakeSelectionObserver );
- injectQuirksHandling( this );
- // Listens `render` event on default priority.
- // This way we can attach other listeners before or after rendering execution.
- this.on( 'render', () => {
- this.disableObservers();
- this.renderer.render();
- this.enableObservers();
- } );
- }
- /**
- * Creates observer of the given type if not yet created, {@link engine.view.Observer#enable enables} it
- * and {@link engine.view.observer.Observer#observe attaches} to all existing and future
- * {@link engine.view.Document#domRoots DOM roots}.
- *
- * Note: Observers are recognized by their constructor (classes). A single observer will be instantiated and used only
- * when registered for the first time. This means that features and other components can register a single observer
- * multiple times without caring whether it has been already added or not.
- *
- * @param {Function} Observer The constructor of an observer to add.
- * Should create an instance inheriting from {@link engine.view.observer.Observer}.
- * @returns {engine.view.observer.Observer} Added observer instance.
- */
- addObserver( Observer ) {
- let observer = this._observers.get( Observer );
- if ( observer ) {
- return observer;
- }
- observer = new Observer( this );
- this._observers.set( Observer, observer );
- for ( let [ name, domElement ] of this.domRoots ) {
- observer.observe( domElement, name );
- }
- observer.enable();
- return observer;
- }
- /**
- * Returns observer of the given type or `undefined` if such observer has not been added yet.
- *
- * @param {Function} Observer The constructor of an observer to get.
- * @returns {engine.view.observer.Observer|undefined} Observer instance or undefined.
- */
- getObserver( Observer ) {
- return this._observers.get( Observer );
- }
- /**
- * Creates a {@link engine.view.Document#roots view root element}.
- *
- * If the DOM element is passed as a first parameter it will be automatically
- * {@link engine.view.Document#attachDomRoot attached}:
- *
- * document.createRoot( document.querySelector( 'div#editor' ) ); // Will call document.attachDomRoot.
- *
- * However, if the string is passed, then only the view element will be created and the DOM element have to be
- * attached separately:
- *
- * document.createRoot( 'body' );
- * document.attachDomRoot( document.querySelector( 'body#editor' ) );
- *
- * In both cases, {@link engine.view.RootEditableElement#name element name} is always transformed to lower case.
- *
- * @param {Element|String} domRoot DOM root element or the tag name of view root element if the DOM element will be
- * attached later.
- * @param {String} [name='main'] Name of the root.
- * @returns {engine.view.RootEditableElement} The created view root element.
- */
- createRoot( domRoot, name = 'main' ) {
- const rootTag = typeof domRoot == 'string' ? domRoot : domRoot.tagName;
- const viewRoot = new RootEditableElement( this, rootTag.toLowerCase(), name );
- this.roots.set( name, viewRoot );
- // Mark changed nodes in the renderer.
- viewRoot.on( 'change:children', ( evt, node ) => this.renderer.markToSync( 'children', node ) );
- viewRoot.on( 'change:attributes', ( evt, node ) => this.renderer.markToSync( 'attributes', node ) );
- viewRoot.on( 'change:text', ( evt, node ) => this.renderer.markToSync( 'text', node ) );
- if ( this.domConverter.isElement( domRoot ) ) {
- this.attachDomRoot( domRoot, name );
- }
- return viewRoot;
- }
- /**
- * Attaches DOM root element to the view element and enable all observers on that element. This method also
- * {@link engine.view.Renderer#markToSync mark element} to be synchronized with the view what means that all child
- * nodes will be removed and replaced with content of the view root.
- *
- * Note that {@link engine.view.Document#createRoot} will call this method automatically if the DOM element is
- * passed to it.
- *
- * @param {Element|String} domRoot DOM root element.
- * @param {String} [name='main'] Name of the root.
- */
- attachDomRoot( domRoot, name = 'main' ) {
- const viewRoot = this.getRoot( name );
- this.domRoots.set( name, domRoot );
- this.domConverter.bindElements( domRoot, viewRoot );
- this.renderer.markToSync( 'children', viewRoot );
- this.renderer.domDocuments.add( domRoot.ownerDocument );
- for ( let observer of this._observers.values() ) {
- observer.observe( domRoot, name );
- }
- }
- /**
- * Gets a {@link engine.view.Document#roots view root element} with the specified name. If the name is not
- * specific "main" root is returned.
- *
- * @param {String} [name='main'] Name of the root.
- * @returns {engine.view.RootEditableElement} The view root element with the specified name.
- */
- getRoot( name = 'main' ) {
- return this.roots.get( name );
- }
- /**
- * Gets DOM root element.
- *
- * @param {String} [name='main'] Name of the root.
- * @returns {Element} DOM root element instance.
- */
- getDomRoot( name = 'main' ) {
- return this.domRoots.get( name );
- }
- /**
- * Renders all changes. In order to avoid triggering the observers (e.g. mutations) all observers are disabled
- * before rendering and re-enabled after that.
- *
- * @fires engine.view.Document#render
- */
- render() {
- this.fire( 'render' );
- }
- /**
- * Focuses document. It will focus {@link engine.view.EditableElement EditableElement} that is currently having
- * selection inside.
- */
- focus() {
- if ( !this.isFocused ) {
- const editable = this.selection.editableElement;
- if ( editable ) {
- this.domConverter.focus( editable );
- this.render();
- } else {
- /**
- * Before focusing view document, selection should be placed inside one of the view's editables.
- * Normally its selection will be converted from model document (which have default selection), but
- * when using view document on its own, we need to manually place selection before focusing it.
- *
- * @error view-focus-no-selection
- */
- log.warn( 'view-focus-no-selection: There is no selection in any editable to focus.' );
- }
- }
- }
- /**
- * Disables all added observers.
- */
- disableObservers() {
- for ( let observer of this._observers.values() ) {
- observer.disable();
- }
- }
- /**
- * Enables all added observers.
- */
- enableObservers() {
- for ( let observer of this._observers.values() ) {
- observer.enable();
- }
- }
- }
- mix( Document, ObservableMixin );
- /**
- * Enum representing type of the change.
- *
- * Possible values:
- *
- * * `children` - for child list changes,
- * * `attributes` - for element attributes changes,
- * * `text` - for text nodes changes.
- *
- * @typedef {String} engine.view.ChangeType
- */
- /**
- * Fired when {@link engine.view.Document#render render} method is called. Actual rendering is executed as a listener to
- * this event with default priority. This way other listeners can be used to run code before or after rendering.
- *
- * @event engine.view.Document.render
- */
|