Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge 0e49518 into c31bea6
Browse files Browse the repository at this point in the history
  • Loading branch information
oleq committed Feb 7, 2019
2 parents c31bea6 + 0e49518 commit 36f08ce
Show file tree
Hide file tree
Showing 6 changed files with 609 additions and 131 deletions.
274 changes: 191 additions & 83 deletions src/view/placeholder.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,134 +13,242 @@ import '../../theme/placeholder.css';
const documentPlaceholders = new WeakMap();

/**
* Attaches placeholder to provided element and updates it's visibility. To change placeholder simply call this method
* once 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} element Element to attach placeholder to.
* @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 attachPlaceholder( view, element, placeholderText, checkFunction ) {
const document = view.document;
export function enablePlaceholder( options ) {
const { view, element, text, isDirectHost = true } = options;
const doc = view.document;

// Single listener per document.
if ( !documentPlaceholders.has( document ) ) {
documentPlaceholders.set( document, new Map() );
// Use a single a single post fixer per—document to update all placeholders.
if ( !documentPlaceholders.has( doc ) ) {
documentPlaceholders.set( doc, new Map() );

// Create view post-fixer that will add placeholder where needed.
document.registerPostFixer( writer => updateAllPlaceholders( document, writer ) );
// 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 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
} );

view.change( writer => updateAllPlaceholders( document, writer ) );
// Update the placeholders right away.
view.change( writer => updateDocumentPlaceholders( doc, writer ) );
}

/**
* Removes placeholder functionality from 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 detachPlaceholder( view, element ) {
export function disablePlaceholder( view, element ) {
const doc = element.document;

view.change( writer => {
if ( documentPlaceholders.has( doc ) ) {
documentPlaceholders.get( doc ).delete( element );
if ( !documentPlaceholders.has( doc ) ) {
return;
}

writer.removeClass( 'ck-placeholder', element );
writer.removeAttribute( 'data-placeholder', element );
const placeholders = documentPlaceholders.get( doc );
const config = placeholders.get( element );

writer.removeAttribute( 'data-placeholder', config.hostElement );
hidePlaceholder( writer, config.hostElement );

placeholders.delete( element );
} );
}

// Updates all placeholders of given document.
/**
* 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/downcastwriter~DowncastWriter} writer
* @param {module:engine/view/element~Element} element
* @param {String} text
* @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} view
// @param { module:engine/model/document~Document} doc
// @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;
// @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 changed;
return wasViewModified;
}

// Updates placeholder class of given element.
// 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} info
function updateSinglePlaceholder( writer, element, info ) {
const document = element.document;
const text = info.placeholderText;
let changed = false;

// Element was removed from document.
if ( !document ) {
// @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;
}

// Update data attribute if needed.
if ( element.getAttribute( 'data-placeholder' ) !== text ) {
writer.setAttribute( 'data-placeholder', text, element );
changed = true;
}

const viewSelection = document.selection;
const anchor = viewSelection.anchor;
const checkFunction = info.checkFunction;

// If checkFunction is provided and returns false - remove placeholder.
if ( checkFunction && !checkFunction() ) {
if ( element.hasClass( 'ck-placeholder' ) ) {
writer.removeClass( 'ck-placeholder', element );
changed = true;
}
// 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;

return changed;
// 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;
}

// 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' ) );

// If element is empty and editor is blurred.
if ( !document.isFocused && isEmptyish ) {
if ( !element.hasClass( 'ck-placeholder' ) ) {
writer.addClass( 'ck-placeholder', element );
changed = true;
if ( needsPlaceholder( hostElement ) ) {
if ( showPlaceholder( writer, hostElement ) ) {
wasViewModified = true;
}

return changed;
} else if ( hidePlaceholder( writer, hostElement ) ) {
wasViewModified = 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;
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 changed;
return null;
}

0 comments on commit 36f08ce

Please sign in to comment.