Bläddra i källkod

Code refactoring: re-organized the placeholder helper utilities.

Aleksander Nowodzinski 7 år sedan
förälder
incheckning
c6877484d8
1 ändrade filer med 158 tillägg och 127 borttagningar
  1. 158 127
      packages/ckeditor5-engine/src/view/placeholder.js

+ 158 - 127
packages/ckeditor5-engine/src/view/placeholder.js

@@ -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;
 }