placeholder.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module engine/view/placeholder
  7. */
  8. import '../../theme/placeholder.css';
  9. // Each document stores information about its placeholder elements and check functions.
  10. const documentPlaceholders = new WeakMap();
  11. /**
  12. * A helper that enables a placeholder on the provided view element (also updates its visibility).
  13. * The placeholder is a CSS pseudo–element (with a text content) attached to the element.
  14. *
  15. * To change the placeholder text, simply call this method again with new options.
  16. *
  17. * To disable the placeholder, use {@link module:engine/view/placeholder~disablePlaceholder `disablePlaceholder()`} helper.
  18. *
  19. * @param {Object} [options] Configuration options of the placeholder.
  20. * @param {module:engine/view/view~View} options.view Editing view instance.
  21. * @param {module:engine/view/element~Element} options.element Element that will gain a placeholder.
  22. * See `options.isDirectHost` to learn more.
  23. * @param {String} options.text Placeholder text.
  24. * @param {Boolean} [options.isDirectHost=true] If set `false`, the placeholder will not be enabled directly
  25. * in the passed `element` but in one of its children (selected automatically, i.e. a first empty child element).
  26. * Useful when attaching placeholders to elements that can host other elements (not just text), for instance,
  27. * editable root elements.
  28. */
  29. export function enablePlaceholder( options ) {
  30. const { view, element, text, isDirectHost = true } = options;
  31. const doc = view.document;
  32. // Use a single a single post fixer per—document to update all placeholders.
  33. if ( !documentPlaceholders.has( doc ) ) {
  34. documentPlaceholders.set( doc, new Map() );
  35. // If a post-fixer callback makes a change, it should return `true` so other post–fixers
  36. // can re–evaluate the document again.
  37. doc.registerPostFixer( writer => updateDocumentPlaceholders( doc, writer ) );
  38. }
  39. // Store information about the element placeholder under its document.
  40. documentPlaceholders.get( doc ).set( element, {
  41. text,
  42. isDirectHost
  43. } );
  44. // Update the placeholders right away.
  45. view.change( writer => updateDocumentPlaceholders( doc, writer ) );
  46. }
  47. /**
  48. * Disables the placeholder functionality from a given element.
  49. *
  50. * See {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} to learn more.
  51. *
  52. * @param {module:engine/view/view~View} view
  53. * @param {module:engine/view/element~Element} element
  54. */
  55. export function disablePlaceholder( view, element ) {
  56. const doc = element.document;
  57. view.change( writer => {
  58. if ( !documentPlaceholders.has( doc ) ) {
  59. return;
  60. }
  61. const placeholders = documentPlaceholders.get( doc );
  62. const config = placeholders.get( element );
  63. writer.removeAttribute( 'data-placeholder', config.hostElement );
  64. hidePlaceholder( writer, config.hostElement );
  65. placeholders.delete( element );
  66. } );
  67. }
  68. /**
  69. * Shows a placeholder in the provided element by changing related attributes and CSS classes.
  70. *
  71. * **Note**: This helper will not update the placeholder visibility nor manage the
  72. * it in any way in the future. What it does is a one–time state change of an element. Use
  73. * {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} and
  74. * {@link module:engine/view/placeholder~disablePlaceholder `disablePlaceholder()`} for full
  75. * placeholder functionality.
  76. *
  77. * **Note**: This helper will blindly show the placeholder directly in the root editable element if
  78. * one is passed, which could result in a visual clash if the editable element has some children
  79. * (for instance, an empty paragraph). Use {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`}
  80. * in that case or make sure the correct element is passed to the helper.
  81. *
  82. * @param {module:engine/view/downcastwriter~DowncastWriter} writer
  83. * @param {module:engine/view/element~Element} element
  84. * @returns {Boolean} `true`, if any changes were made to the `element`.
  85. */
  86. export function showPlaceholder( writer, element ) {
  87. if ( !element.hasClass( 'ck-placeholder' ) ) {
  88. writer.addClass( 'ck-placeholder', element );
  89. return true;
  90. }
  91. return false;
  92. }
  93. /**
  94. * Hides a placeholder in the element by changing related attributes and CSS classes.
  95. *
  96. * **Note**: This helper will not update the placeholder visibility nor manage the
  97. * it in any way in the future. What it does is a one–time state change of an element. Use
  98. * {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} and
  99. * {@link module:engine/view/placeholder~disablePlaceholder `disablePlaceholder()`} for full
  100. * placeholder functionality.
  101. *
  102. * @param {module:engine/view/downcastwriter~DowncastWriter} writer
  103. * @param {module:engine/view/element~Element} element
  104. * @returns {Boolean} `true`, if any changes were made to the `element`.
  105. */
  106. export function hidePlaceholder( writer, element ) {
  107. if ( element.hasClass( 'ck-placeholder' ) ) {
  108. writer.removeClass( 'ck-placeholder', element );
  109. return true;
  110. }
  111. return false;
  112. }
  113. /**
  114. * Checks if a placeholder should be displayed in the element.
  115. *
  116. * **Note**: This helper will blindly check the possibility of showing a placeholder directly in the
  117. * root editable element if one is passed, which may not be the expected result. If an element can
  118. * host other elements (not just text), most likely one of its children should be checked instead
  119. * because it will be the final host for the placeholder. Use
  120. * {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} in that case or make
  121. * sure the correct element is passed to the helper.
  122. *
  123. * @param {module:engine/view/element~Element} element
  124. * @returns {Boolean}
  125. */
  126. export function needsPlaceholder( element ) {
  127. const doc = element.document;
  128. // The element was removed from document.
  129. if ( !doc ) {
  130. return false;
  131. }
  132. // The element is empty only as long as it contains nothing but uiElements.
  133. const isEmptyish = !Array.from( element.getChildren() )
  134. .some( element => !element.is( 'uiElement' ) );
  135. // If the element is empty and the document is blurred.
  136. if ( !doc.isFocused && isEmptyish ) {
  137. return true;
  138. }
  139. const viewSelection = doc.selection;
  140. const selectionAnchor = viewSelection.anchor;
  141. // If document is focused and the element is empty but the selection is not anchored inside it.
  142. if ( isEmptyish && selectionAnchor && selectionAnchor.parent !== element ) {
  143. return true;
  144. }
  145. return false;
  146. }
  147. // Updates all placeholders associated with a document in a post–fixer callback.
  148. //
  149. // @private
  150. // @param { module:engine/view/document~Document} doc
  151. // @param {module:engine/view/downcastwriter~DowncastWriter} writer
  152. // @returns {Boolean} True if any changes were made to the view document.
  153. function updateDocumentPlaceholders( doc, writer ) {
  154. const placeholders = documentPlaceholders.get( doc );
  155. let wasViewModified = false;
  156. for ( const [ element, config ] of placeholders ) {
  157. if ( updatePlaceholder( writer, element, config ) ) {
  158. wasViewModified = true;
  159. }
  160. }
  161. return wasViewModified;
  162. }
  163. // Updates a single placeholder in a post–fixer callback.
  164. //
  165. // @private
  166. // @param {module:engine/view/downcastwriter~DowncastWriter} writer
  167. // @param {module:engine/view/element~Element} element
  168. // @param {Object} config Configuration of the placeholder
  169. // @param {String} config.text
  170. // @param {Boolean} config.isDirectHost
  171. // @returns {Boolean} True if any changes were made to the view document.
  172. function updatePlaceholder( writer, element, config ) {
  173. const { text, isDirectHost } = config;
  174. const hostElement = isDirectHost ? element : getChildPlaceholderHostSubstitute( element );
  175. let wasViewModified = false;
  176. // When not a direct host, it could happen that there is no child element
  177. // capable of displaying a placeholder.
  178. if ( !hostElement ) {
  179. return false;
  180. }
  181. // Cache the host element. It will be necessary for disablePlaceholder() to know
  182. // which element should have class and attribute removed because, depending on
  183. // the config.isDirectHost value, it could be the element or one of its descendants.
  184. config.hostElement = hostElement;
  185. // This may be necessary when updating the placeholder text to something else.
  186. if ( hostElement.getAttribute( 'data-placeholder' ) !== text ) {
  187. writer.setAttribute( 'data-placeholder', text, hostElement );
  188. wasViewModified = true;
  189. }
  190. if ( needsPlaceholder( hostElement ) ) {
  191. if ( showPlaceholder( writer, hostElement ) ) {
  192. wasViewModified = true;
  193. }
  194. } else if ( hidePlaceholder( writer, hostElement ) ) {
  195. wasViewModified = true;
  196. }
  197. return wasViewModified;
  198. }
  199. // Gets a child element capable of displaying a placeholder if a parent element can host more
  200. // than just text (for instance, when it is a root editable element). The child element
  201. // can then be used in other placeholder helpers as a substitute of its parent.
  202. //
  203. // @private
  204. // @param {module:engine/view/element~Element} parent
  205. // @returns {module:engine/view/element~Element|null}
  206. function getChildPlaceholderHostSubstitute( parent ) {
  207. if ( parent.childCount === 1 ) {
  208. const firstChild = parent.getChild( 0 );
  209. if ( firstChild.is( 'element' ) && !firstChild.is( 'uiElement' ) ) {
  210. return firstChild;
  211. }
  212. }
  213. return null;
  214. }