| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
- /**
- * @module engine/view/placeholder
- */
- import '../../theme/placeholder.css';
- // Each document stores information about its placeholder elements and check functions.
- const documentPlaceholders = new WeakMap();
- /**
- * A helper that enables a placeholder on the provided view element (also updates its visibility).
- * The placeholder is a CSS pseudo–element (with a text content) attached to the element.
- *
- * To change the placeholder text, simply call this method again with new options.
- *
- * To disable the placeholder, use {@link module:engine/view/placeholder~disablePlaceholder `disablePlaceholder()`} helper.
- *
- * @param {Object} [options] Configuration options of the placeholder.
- * @param {module:engine/view/view~View} options.view Editing view instance.
- * @param {module:engine/view/element~Element} options.element Element that will gain a placeholder.
- * See `options.isDirectHost` to learn more.
- * @param {String} options.text Placeholder text.
- * @param {Boolean} [options.isDirectHost=true] If set `false`, the placeholder will not be enabled directly
- * in the passed `element` but in one of its children (selected automatically, i.e. a first empty child element).
- * Useful when attaching placeholders to elements that can host other elements (not just text), for instance,
- * editable root elements.
- */
- export function enablePlaceholder( options ) {
- const { view, element, text, isDirectHost = true } = options;
- const doc = view.document;
- // Use a single a single post fixer per—document to update all placeholders.
- if ( !documentPlaceholders.has( doc ) ) {
- documentPlaceholders.set( doc, new Map() );
- // If a post-fixer callback makes a change, it should return `true` so other post–fixers
- // can re–evaluate the document again.
- doc.registerPostFixer( writer => updateDocumentPlaceholders( doc, writer ) );
- }
- // Store information about the element placeholder under its document.
- documentPlaceholders.get( doc ).set( element, {
- text,
- isDirectHost
- } );
- // Update the placeholders right away.
- view.change( writer => updateDocumentPlaceholders( doc, writer ) );
- }
- /**
- * Disables the placeholder functionality from a given element.
- *
- * See {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} to learn more.
- *
- * @param {module:engine/view/view~View} view
- * @param {module:engine/view/element~Element} element
- */
- export function disablePlaceholder( view, element ) {
- const doc = element.document;
- view.change( writer => {
- if ( !documentPlaceholders.has( doc ) ) {
- return;
- }
- const placeholders = documentPlaceholders.get( doc );
- const config = placeholders.get( element );
- writer.removeAttribute( 'data-placeholder', config.hostElement );
- hidePlaceholder( writer, config.hostElement );
- placeholders.delete( element );
- } );
- }
- /**
- * Shows a placeholder in the provided element by changing related attributes and CSS classes.
- *
- * **Note**: This helper will not update the placeholder visibility nor manage the
- * it in any way in the future. What it does is a one–time state change of an element. Use
- * {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} and
- * {@link module:engine/view/placeholder~disablePlaceholder `disablePlaceholder()`} for full
- * placeholder functionality.
- *
- * **Note**: This helper will blindly show the placeholder directly in the root editable element if
- * one is passed, which could result in a visual clash if the editable element has some children
- * (for instance, an empty paragraph). Use {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`}
- * in that case or make sure the correct element is passed to the helper.
- *
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer
- * @param {module:engine/view/element~Element} element
- * @returns {Boolean} `true`, if any changes were made to the `element`.
- */
- export function showPlaceholder( writer, element ) {
- if ( !element.hasClass( 'ck-placeholder' ) ) {
- writer.addClass( 'ck-placeholder', element );
- return true;
- }
- return false;
- }
- /**
- * Hides a placeholder in the element by changing related attributes and CSS classes.
- *
- * **Note**: This helper will not update the placeholder visibility nor manage the
- * it in any way in the future. What it does is a one–time state change of an element. Use
- * {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} and
- * {@link module:engine/view/placeholder~disablePlaceholder `disablePlaceholder()`} for full
- * placeholder functionality.
- *
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer
- * @param {module:engine/view/element~Element} element
- * @returns {Boolean} `true`, if any changes were made to the `element`.
- */
- export function hidePlaceholder( writer, element ) {
- if ( element.hasClass( 'ck-placeholder' ) ) {
- writer.removeClass( 'ck-placeholder', element );
- return true;
- }
- return false;
- }
- /**
- * Checks if a placeholder should be displayed in the element.
- *
- * **Note**: This helper will blindly check the possibility of showing a placeholder directly in the
- * root editable element if one is passed, which may not be the expected result. If an element can
- * host other elements (not just text), most likely one of its children should be checked instead
- * because it will be the final host for the placeholder. Use
- * {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} in that case or make
- * sure the correct element is passed to the helper.
- *
- * @param {module:engine/view/element~Element} element
- * @returns {Boolean}
- */
- export function needsPlaceholder( element ) {
- const doc = element.document;
- // The element was removed from document.
- if ( !doc ) {
- return false;
- }
- // The element is empty only as long as it contains nothing but uiElements.
- const isEmptyish = !Array.from( element.getChildren() )
- .some( element => !element.is( 'uiElement' ) );
- // If the element is empty and the document is blurred.
- if ( !doc.isFocused && isEmptyish ) {
- return true;
- }
- const viewSelection = doc.selection;
- const selectionAnchor = viewSelection.anchor;
- // If document is focused and the element is empty but the selection is not anchored inside it.
- if ( isEmptyish && selectionAnchor && selectionAnchor.parent !== element ) {
- return true;
- }
- return false;
- }
- // Updates all placeholders associated with a document in a post–fixer callback.
- //
- // @private
- // @param { module:engine/view/document~Document} doc
- // @param {module:engine/view/downcastwriter~DowncastWriter} writer
- // @returns {Boolean} True if any changes were made to the view document.
- function updateDocumentPlaceholders( doc, writer ) {
- const placeholders = documentPlaceholders.get( doc );
- let wasViewModified = false;
- for ( const [ element, config ] of placeholders ) {
- if ( updatePlaceholder( writer, element, config ) ) {
- wasViewModified = true;
- }
- }
- return wasViewModified;
- }
- // Updates a single placeholder in a post–fixer callback.
- //
- // @private
- // @param {module:engine/view/downcastwriter~DowncastWriter} writer
- // @param {module:engine/view/element~Element} element
- // @param {Object} config Configuration of the placeholder
- // @param {String} config.text
- // @param {Boolean} config.isDirectHost
- // @returns {Boolean} True if any changes were made to the view document.
- function updatePlaceholder( writer, element, config ) {
- const { text, isDirectHost } = config;
- const hostElement = isDirectHost ? element : getChildPlaceholderHostSubstitute( element );
- let wasViewModified = false;
- // When not a direct host, it could happen that there is no child element
- // capable of displaying a placeholder.
- if ( !hostElement ) {
- return false;
- }
- // Cache the host element. It will be necessary for disablePlaceholder() to know
- // which element should have class and attribute removed because, depending on
- // the config.isDirectHost value, it could be the element or one of its descendants.
- config.hostElement = hostElement;
- // This may be necessary when updating the placeholder text to something else.
- if ( hostElement.getAttribute( 'data-placeholder' ) !== text ) {
- writer.setAttribute( 'data-placeholder', text, hostElement );
- wasViewModified = true;
- }
- if ( needsPlaceholder( hostElement ) ) {
- if ( showPlaceholder( writer, hostElement ) ) {
- wasViewModified = true;
- }
- } else if ( hidePlaceholder( writer, hostElement ) ) {
- wasViewModified = true;
- }
- return wasViewModified;
- }
- // Gets a child element capable of displaying a placeholder if a parent element can host more
- // than just text (for instance, when it is a root editable element). The child element
- // can then be used in other placeholder helpers as a substitute of its parent.
- //
- // @private
- // @param {module:engine/view/element~Element} parent
- // @returns {module:engine/view/element~Element|null}
- function getChildPlaceholderHostSubstitute( parent ) {
- if ( parent.childCount === 1 ) {
- const firstChild = parent.getChild( 0 );
- if ( firstChild.is( 'element' ) && !firstChild.is( 'uiElement' ) ) {
- return firstChild;
- }
- }
- return null;
- }
|