|
|
@@ -13,49 +13,79 @@ import '../../theme/placeholder.css';
|
|
|
const documentPlaceholders = new WeakMap();
|
|
|
|
|
|
/**
|
|
|
- * Adds a placeholder to the provided view element and updates it's visibility. To change the placeholder,
|
|
|
- * simply call this method again with new parameters.
|
|
|
+ * 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.
|
|
|
*
|
|
|
- * @param {module:engine/view/view~View} view View controller.
|
|
|
- * @param {module:engine/view/element~Element|Function} element Element to add placeholder to or a function
|
|
|
- * that returns such an element when an {@link module:engine/view/rooteditableelement~RootEditableElement root editable}
|
|
|
- * instance is passed into it.
|
|
|
- * @param {String} placeholderText Placeholder text to use.
|
|
|
- * @param {Function} [checkFunction] If provided it will be called before checking if placeholder should be displayed.
|
|
|
- * If function returns `false` placeholder will not be showed.
|
|
|
+ * 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 addPlaceholder( view, element, placeholderText, checkFunction ) {
|
|
|
- const document = view.document;
|
|
|
-
|
|
|
- // Single listener per document.
|
|
|
- if ( !documentPlaceholders.has( document ) ) {
|
|
|
- documentPlaceholders.set( document, new Map() );
|
|
|
+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() );
|
|
|
+
|
|
|
+ doc.registerPostFixer( writer => {
|
|
|
+ const placeholders = documentPlaceholders.get( doc );
|
|
|
+
|
|
|
+ // If a post-fixer callback makes a change, it should return `true` so other post–fixers
|
|
|
+ // can re–evaluate the document again.
|
|
|
+ let wasViewModified = false;
|
|
|
+
|
|
|
+ for ( const [ element, { text, isDirectHost } ] of placeholders ) {
|
|
|
+ const hostElement = isDirectHost ? element : getChildPlaceholderHostSubstitute( element );
|
|
|
+
|
|
|
+ // When not a direct host, it could happen that there is no child element
|
|
|
+ // capable of displaying a placeholder.
|
|
|
+ if ( !hostElement ) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( needsPlaceholder( hostElement ) ) {
|
|
|
+ if ( showPlaceholder( writer, hostElement, text ) ) {
|
|
|
+ wasViewModified = true;
|
|
|
+ }
|
|
|
+ } else if ( hidePlaceholder( writer, hostElement ) ) {
|
|
|
+ wasViewModified = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // Create view post-fixer that will add placeholder where needed.
|
|
|
- document.registerPostFixer( writer => updateAllPlaceholders( document, writer ) );
|
|
|
+ return wasViewModified;
|
|
|
+ } );
|
|
|
}
|
|
|
|
|
|
- // Store information about element with placeholder.
|
|
|
- documentPlaceholders.get( document ).set( element, {
|
|
|
- placeholderText,
|
|
|
- checkFunction
|
|
|
+ // Store information about the element placeholder under its document.
|
|
|
+ documentPlaceholders.get( doc ).set( element, {
|
|
|
+ text,
|
|
|
+ isDirectHost
|
|
|
} );
|
|
|
|
|
|
- // Update view right away.
|
|
|
+ // Update the view right away.
|
|
|
view.render();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Removes the placeholder functionality from a given element.
|
|
|
+ * 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 removePlaceholder( view, element ) {
|
|
|
- if ( typeof element == 'function' ) {
|
|
|
- element = element();
|
|
|
- }
|
|
|
-
|
|
|
+export function disablePlaceholder( view, element ) {
|
|
|
const doc = element.document;
|
|
|
|
|
|
view.change( writer => {
|
|
|
@@ -63,127 +93,128 @@ export function removePlaceholder( view, element ) {
|
|
|
documentPlaceholders.get( doc ).delete( element );
|
|
|
}
|
|
|
|
|
|
- writer.removeClass( 'ck-placeholder', element );
|
|
|
- writer.removeAttribute( 'data-placeholder', element );
|
|
|
+ hidePlaceholder( writer, element );
|
|
|
} );
|
|
|
}
|
|
|
|
|
|
-// Updates all placeholders of given document.
|
|
|
-//
|
|
|
-// @private
|
|
|
-// @param {module:engine/view/document~Document} view
|
|
|
-// @param {module:engine/view/downcastwriter~DowncastWriter} writer
|
|
|
-function updateAllPlaceholders( document, writer ) {
|
|
|
- const placeholders = documentPlaceholders.get( document );
|
|
|
- let changed = false;
|
|
|
-
|
|
|
- for ( const [ element, info ] of placeholders ) {
|
|
|
- if ( updateSinglePlaceholder( writer, element, info ) ) {
|
|
|
- changed = true;
|
|
|
- }
|
|
|
- }
|
|
|
+/**
|
|
|
+ * 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
|
|
|
+ * @param {String} text
|
|
|
+ * @returns {Boolean} `true`, if any changes were made to the `element`.
|
|
|
+ */
|
|
|
+export function showPlaceholder( writer, element, text ) {
|
|
|
+ let wasElementModified = false;
|
|
|
|
|
|
- return changed;
|
|
|
-}
|
|
|
+ // This may be necessary when updating the placeholder text to something else.
|
|
|
+ if ( element.getAttribute( 'data-placeholder' ) !== text ) {
|
|
|
+ writer.setAttribute( 'data-placeholder', text, element );
|
|
|
+ wasElementModified = true;
|
|
|
+ }
|
|
|
|
|
|
-// Updates placeholder class of given element.
|
|
|
-//
|
|
|
-// @private
|
|
|
-// @param {module:engine/view/downcastwriter~DowncastWriter} writer
|
|
|
-// @param {module:engine/view/element~Element} element
|
|
|
-// @param {Object} info
|
|
|
-function updateSinglePlaceholder( writer, element, info ) {
|
|
|
- if ( typeof element == 'function' ) {
|
|
|
- element = element();
|
|
|
-
|
|
|
- if ( !element ) {
|
|
|
- return false;
|
|
|
- }
|
|
|
+ if ( !element.hasClass( 'ck-placeholder' ) ) {
|
|
|
+ writer.addClass( 'ck-placeholder', element );
|
|
|
+ wasElementModified = true;
|
|
|
}
|
|
|
|
|
|
- const document = element.document;
|
|
|
- const text = info.placeholderText;
|
|
|
- let changed = false;
|
|
|
+ return wasElementModified;
|
|
|
+}
|
|
|
|
|
|
- // Element was removed from document.
|
|
|
- if ( !document ) {
|
|
|
- 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 ) {
|
|
|
+ let wasElementModified = false;
|
|
|
|
|
|
- // Update data attribute if needed.
|
|
|
- if ( element.getAttribute( 'data-placeholder' ) !== text ) {
|
|
|
- writer.setAttribute( 'data-placeholder', text, element );
|
|
|
- changed = true;
|
|
|
+ if ( element.hasClass( 'ck-placeholder' ) ) {
|
|
|
+ writer.removeClass( 'ck-placeholder', element );
|
|
|
+ wasElementModified = true;
|
|
|
}
|
|
|
|
|
|
- const viewSelection = document.selection;
|
|
|
- const anchor = viewSelection.anchor;
|
|
|
- const checkFunction = info.checkFunction;
|
|
|
+ return wasElementModified;
|
|
|
+}
|
|
|
|
|
|
- // If checkFunction is provided and returns false - remove placeholder.
|
|
|
- if ( checkFunction && !checkFunction() ) {
|
|
|
- if ( element.hasClass( 'ck-placeholder' ) ) {
|
|
|
- writer.removeClass( 'ck-placeholder', element );
|
|
|
- changed = true;
|
|
|
- }
|
|
|
+/**
|
|
|
+ * 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/downcastwriter~DowncastWriter} writer
|
|
|
+ * @param {module:engine/view/element~Element} element
|
|
|
+ * @param {String} text
|
|
|
+ * @returns {Boolean}
|
|
|
+ */
|
|
|
+export function needsPlaceholder( element ) {
|
|
|
+ const doc = element.document;
|
|
|
|
|
|
- return changed;
|
|
|
+ // The element was removed from document.
|
|
|
+ if ( !doc ) {
|
|
|
+ return false;
|
|
|
}
|
|
|
|
|
|
- // Element is empty for placeholder purposes when it has no children or only ui elements.
|
|
|
- // This check is taken from `view.ContainerElement#getFillerOffset`.
|
|
|
- const isEmptyish = !Array.from( element.getChildren() ).some( element => !element.is( 'uiElement' ) );
|
|
|
+ // 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 element is empty and editor is blurred.
|
|
|
- if ( !document.isFocused && isEmptyish ) {
|
|
|
- if ( !element.hasClass( 'ck-placeholder' ) ) {
|
|
|
- writer.addClass( 'ck-placeholder', element );
|
|
|
- changed = true;
|
|
|
- }
|
|
|
-
|
|
|
- return changed;
|
|
|
+ // If the element is empty and the document is blurred.
|
|
|
+ if ( !doc.isFocused && isEmptyish ) {
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
- // It there are no child elements and selection is not placed inside element.
|
|
|
- if ( isEmptyish && anchor && anchor.parent !== element ) {
|
|
|
- if ( !element.hasClass( 'ck-placeholder' ) ) {
|
|
|
- writer.addClass( 'ck-placeholder', element );
|
|
|
- changed = true;
|
|
|
- }
|
|
|
- } else {
|
|
|
- if ( element.hasClass( 'ck-placeholder' ) ) {
|
|
|
- writer.removeClass( 'ck-placeholder', element );
|
|
|
- changed = 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 changed;
|
|
|
+ return false;
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * Returns a view element the placeholder can be added to inside a view editing root.
|
|
|
- *
|
|
|
- * Even if empty, the editing root usually hosts at least one empty element (paragraph, heading, etc.).
|
|
|
- * Because of that, the placeholder cannot be added directly to the root and doing so would mean both
|
|
|
- * a CSS pseudo–element (with a placeholder text) and an empty element are displayed next to each other.
|
|
|
- *
|
|
|
- * Instead, the placeholder must be added to that empty element and this helper returns it, if there
|
|
|
- * is one.
|
|
|
- *
|
|
|
- * @param {module:engine/view/rooteditableelement~RootEditableElement} root The root editable view that
|
|
|
- * is to have a placeholder.
|
|
|
- * @returns {module:engine/view/element~Element|null} An element the placeholder can be added to.
|
|
|
- */
|
|
|
-export function getRootPlaceholderElement( root ) {
|
|
|
- return () => {
|
|
|
- if ( root.childCount === 1 ) {
|
|
|
- const firstRootChild = root.getChild( 0 );
|
|
|
-
|
|
|
- if ( firstRootChild.is( 'element' ) ) {
|
|
|
- return firstRootChild;
|
|
|
- }
|
|
|
+// 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' ) ) {
|
|
|
+ return firstChild;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- return null;
|
|
|
- };
|
|
|
+ return null;
|
|
|
}
|