@@ -13,134 +13,242 @@ import '../../theme/placeholder.css';
1313const documentPlaceholders = new WeakMap ( ) ;
1414
1515/**
16- * Attaches placeholder to provided element and updates it's visibility. To change placeholder simply call this method
17- * once again with new parameters .
16+ * A helper that enables a placeholder on the provided view element (also updates its visibility).
17+ * The placeholder is a CSS pseudo–element ( with a text content) attached to the element .
1818 *
19- * @param {module:engine/view/view~View } view View controller.
20- * @param {module:engine/view/element~Element } element Element to attach placeholder to.
21- * @param {String } placeholderText Placeholder text to use.
22- * @param {Function } [checkFunction] If provided it will be called before checking if placeholder should be displayed.
23- * If function returns `false` placeholder will not be showed.
19+ * To change the placeholder text, simply call this method again with new options.
20+ *
21+ * To disable the placeholder, use {@link module:engine/view/placeholder~disablePlaceholder `disablePlaceholder()`} helper.
22+ *
23+ * @param {Object } [options] Configuration options of the placeholder.
24+ * @param {module:engine/view/view~View } options.view Editing view instance.
25+ * @param {module:engine/view/element~Element } options.element Element that will gain a placeholder.
26+ * See `options.isDirectHost` to learn more.
27+ * @param {String } options.text Placeholder text.
28+ * @param {Boolean } [options.isDirectHost=true] If set `false`, the placeholder will not be enabled directly
29+ * in the passed `element` but in one of its children (selected automatically, i.e. a first empty child element).
30+ * Useful when attaching placeholders to elements that can host other elements (not just text), for instance,
31+ * editable root elements.
2432 */
25- export function attachPlaceholder ( view , element , placeholderText , checkFunction ) {
26- const document = view . document ;
33+ export function enablePlaceholder ( options ) {
34+ const { view, element, text, isDirectHost = true } = options ;
35+ const doc = view . document ;
2736
28- // Single listener per document.
29- if ( ! documentPlaceholders . has ( document ) ) {
30- documentPlaceholders . set ( document , new Map ( ) ) ;
37+ // Use a single a single post fixer per— document to update all placeholders .
38+ if ( ! documentPlaceholders . has ( doc ) ) {
39+ documentPlaceholders . set ( doc , new Map ( ) ) ;
3140
32- // Create view post-fixer that will add placeholder where needed.
33- document . registerPostFixer ( writer => updateAllPlaceholders ( document , writer ) ) ;
41+ // If a post-fixer callback makes a change, it should return `true` so other post–fixers
42+ // can re–evaluate the document again.
43+ doc . registerPostFixer ( writer => updateDocumentPlaceholders ( doc , writer ) ) ;
3444 }
3545
36- // Store information about element with placeholder.
37- documentPlaceholders . get ( document ) . set ( element , {
38- placeholderText ,
39- checkFunction
46+ // Store information about the element placeholder under its document .
47+ documentPlaceholders . get ( doc ) . set ( element , {
48+ text ,
49+ isDirectHost
4050 } ) ;
4151
42- view . change ( writer => updateAllPlaceholders ( document , writer ) ) ;
52+ // Update the placeholders right away.
53+ view . change ( writer => updateDocumentPlaceholders ( doc , writer ) ) ;
4354}
4455
4556/**
46- * Removes placeholder functionality from given element.
57+ * Disables the placeholder functionality from a given element.
58+ *
59+ * See {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} to learn more.
4760 *
4861 * @param {module:engine/view/view~View } view
4962 * @param {module:engine/view/element~Element } element
5063 */
51- export function detachPlaceholder ( view , element ) {
64+ export function disablePlaceholder ( view , element ) {
5265 const doc = element . document ;
5366
5467 view . change ( writer => {
55- if ( documentPlaceholders . has ( doc ) ) {
56- documentPlaceholders . get ( doc ) . delete ( element ) ;
68+ if ( ! documentPlaceholders . has ( doc ) ) {
69+ return ;
5770 }
5871
59- writer . removeClass ( 'ck-placeholder' , element ) ;
60- writer . removeAttribute ( 'data-placeholder' , element ) ;
72+ const placeholders = documentPlaceholders . get ( doc ) ;
73+ const config = placeholders . get ( element ) ;
74+
75+ writer . removeAttribute ( 'data-placeholder' , config . hostElement ) ;
76+ hidePlaceholder ( writer , config . hostElement ) ;
77+
78+ placeholders . delete ( element ) ;
6179 } ) ;
6280}
6381
64- // Updates all placeholders of given document.
82+ /**
83+ * Shows a placeholder in the provided element by changing related attributes and CSS classes.
84+ *
85+ * **Note**: This helper will not update the placeholder visibility nor manage the
86+ * it in any way in the future. What it does is a one–time state change of an element. Use
87+ * {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} and
88+ * {@link module:engine/view/placeholder~disablePlaceholder `disablePlaceholder()`} for full
89+ * placeholder functionality.
90+ *
91+ * **Note**: This helper will blindly show the placeholder directly in the root editable element if
92+ * one is passed, which could result in a visual clash if the editable element has some children
93+ * (for instance, an empty paragraph). Use {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`}
94+ * in that case or make sure the correct element is passed to the helper.
95+ *
96+ * @param {module:engine/view/downcastwriter~DowncastWriter } writer
97+ * @param {module:engine/view/element~Element } element
98+ * @returns {Boolean } `true`, if any changes were made to the `element`.
99+ */
100+ export function showPlaceholder ( writer , element ) {
101+ if ( ! element . hasClass ( 'ck-placeholder' ) ) {
102+ writer . addClass ( 'ck-placeholder' , element ) ;
103+
104+ return true ;
105+ }
106+
107+ return false ;
108+ }
109+
110+ /**
111+ * Hides a placeholder in the element by changing related attributes and CSS classes.
112+ *
113+ * **Note**: This helper will not update the placeholder visibility nor manage the
114+ * it in any way in the future. What it does is a one–time state change of an element. Use
115+ * {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} and
116+ * {@link module:engine/view/placeholder~disablePlaceholder `disablePlaceholder()`} for full
117+ * placeholder functionality.
118+ *
119+ * @param {module:engine/view/downcastwriter~DowncastWriter } writer
120+ * @param {module:engine/view/element~Element } element
121+ * @returns {Boolean } `true`, if any changes were made to the `element`.
122+ */
123+ export function hidePlaceholder ( writer , element ) {
124+ if ( element . hasClass ( 'ck-placeholder' ) ) {
125+ writer . removeClass ( 'ck-placeholder' , element ) ;
126+
127+ return true ;
128+ }
129+
130+ return false ;
131+ }
132+
133+ /**
134+ * Checks if a placeholder should be displayed in the element.
135+ *
136+ * **Note**: This helper will blindly check the possibility of showing a placeholder directly in the
137+ * root editable element if one is passed, which may not be the expected result. If an element can
138+ * host other elements (not just text), most likely one of its children should be checked instead
139+ * because it will be the final host for the placeholder. Use
140+ * {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} in that case or make
141+ * sure the correct element is passed to the helper.
142+ *
143+ * @param {module:engine/view/downcastwriter~DowncastWriter } writer
144+ * @param {module:engine/view/element~Element } element
145+ * @param {String } text
146+ * @returns {Boolean }
147+ */
148+ export function needsPlaceholder ( element ) {
149+ const doc = element . document ;
150+
151+ // The element was removed from document.
152+ if ( ! doc ) {
153+ return false ;
154+ }
155+
156+ // The element is empty only as long as it contains nothing but uiElements.
157+ const isEmptyish = ! Array . from ( element . getChildren ( ) )
158+ . some ( element => ! element . is ( 'uiElement' ) ) ;
159+
160+ // If the element is empty and the document is blurred.
161+ if ( ! doc . isFocused && isEmptyish ) {
162+ return true ;
163+ }
164+
165+ const viewSelection = doc . selection ;
166+ const selectionAnchor = viewSelection . anchor ;
167+
168+ // If document is focused and the element is empty but the selection is not anchored inside it.
169+ if ( isEmptyish && selectionAnchor && selectionAnchor . parent !== element ) {
170+ return true ;
171+ }
172+
173+ return false ;
174+ }
175+
176+ // Updates all placeholders associated with a document in a post–fixer callback.
65177//
66178// @private
67- // @param {module:engine/view /document~Document } view
179+ // @param { module:engine/model /document~Document } doc
68180// @param {module:engine/view/downcastwriter~DowncastWriter } writer
69- function updateAllPlaceholders ( document , writer ) {
70- const placeholders = documentPlaceholders . get ( document ) ;
71- let changed = false ;
72-
73- for ( const [ element , info ] of placeholders ) {
74- if ( updateSinglePlaceholder ( writer , element , info ) ) {
75- changed = true ;
181+ // @returns {Boolean } True if any changes were made to the view document.
182+ function updateDocumentPlaceholders ( doc , writer ) {
183+ const placeholders = documentPlaceholders . get ( doc ) ;
184+ let wasViewModified = false ;
185+
186+ for ( const [ element , config ] of placeholders ) {
187+ if ( updatePlaceholder ( writer , element , config ) ) {
188+ wasViewModified = true ;
76189 }
77190 }
78191
79- return changed ;
192+ return wasViewModified ;
80193}
81194
82- // Updates placeholder class of given element .
195+ // Updates a single placeholder in a post–fixer callback .
83196//
84197// @private
85198// @param {module:engine/view/downcastwriter~DowncastWriter } writer
86199// @param {module:engine/view/element~Element } element
87- // @param {Object } info
88- function updateSinglePlaceholder ( writer , element , info ) {
89- const document = element . document ;
90- const text = info . placeholderText ;
91- let changed = false ;
92-
93- // Element was removed from document.
94- if ( ! document ) {
200+ // @param {Object } config Configuration of the placeholder
201+ // @param {String } config.text
202+ // @param {Boolean } config.isDirectHost
203+ // @returns {Boolean } True if any changes were made to the view document.
204+ function updatePlaceholder ( writer , element , config ) {
205+ const { text, isDirectHost } = config ;
206+ const hostElement = isDirectHost ? element : getChildPlaceholderHostSubstitute ( element ) ;
207+ let wasViewModified = false ;
208+
209+ // When not a direct host, it could happen that there is no child element
210+ // capable of displaying a placeholder.
211+ if ( ! hostElement ) {
95212 return false ;
96213 }
97214
98- // Update data attribute if needed.
99- if ( element . getAttribute ( 'data-placeholder' ) !== text ) {
100- writer . setAttribute ( 'data-placeholder' , text , element ) ;
101- changed = true ;
102- }
103-
104- const viewSelection = document . selection ;
105- const anchor = viewSelection . anchor ;
106- const checkFunction = info . checkFunction ;
107-
108- // If checkFunction is provided and returns false - remove placeholder.
109- if ( checkFunction && ! checkFunction ( ) ) {
110- if ( element . hasClass ( 'ck-placeholder' ) ) {
111- writer . removeClass ( 'ck-placeholder' , element ) ;
112- changed = true ;
113- }
215+ // Cache the host element. It will be necessary for disablePlaceholder() to know
216+ // which element should have class and attribute removed because, depending on
217+ // the config.isDirectHost value, it could be the element or one of its descendants.
218+ config . hostElement = hostElement ;
114219
115- return changed ;
220+ // This may be necessary when updating the placeholder text to something else.
221+ if ( hostElement . getAttribute ( 'data-placeholder' ) !== text ) {
222+ writer . setAttribute ( 'data-placeholder' , text , hostElement ) ;
223+ wasViewModified = true ;
116224 }
117225
118- // Element is empty for placeholder purposes when it has no children or only ui elements.
119- // This check is taken from `view.ContainerElement#getFillerOffset`.
120- const isEmptyish = ! Array . from ( element . getChildren ( ) ) . some ( element => ! element . is ( 'uiElement' ) ) ;
121-
122- // If element is empty and editor is blurred.
123- if ( ! document . isFocused && isEmptyish ) {
124- if ( ! element . hasClass ( 'ck-placeholder' ) ) {
125- writer . addClass ( 'ck-placeholder' , element ) ;
126- changed = true ;
226+ if ( needsPlaceholder ( hostElement ) ) {
227+ if ( showPlaceholder ( writer , hostElement ) ) {
228+ wasViewModified = true ;
127229 }
128-
129- return changed ;
230+ } else if ( hidePlaceholder ( writer , hostElement ) ) {
231+ wasViewModified = true ;
130232 }
131233
132- // It there are no child elements and selection is not placed inside element.
133- if ( isEmptyish && anchor && anchor . parent !== element ) {
134- if ( ! element . hasClass ( 'ck-placeholder' ) ) {
135- writer . addClass ( 'ck-placeholder' , element ) ;
136- changed = true ;
137- }
138- } else {
139- if ( element . hasClass ( 'ck-placeholder' ) ) {
140- writer . removeClass ( 'ck-placeholder' , element ) ;
141- changed = true ;
234+ return wasViewModified ;
235+ }
236+
237+ // Gets a child element capable of displaying a placeholder if a parent element can host more
238+ // than just text (for instance, when it is a root editable element). The child element
239+ // can then be used in other placeholder helpers as a substitute of its parent.
240+ //
241+ // @private
242+ // @param {module:engine/view/element~Element } parent
243+ // @returns {module:engine/view/element~Element|null }
244+ function getChildPlaceholderHostSubstitute ( parent ) {
245+ if ( parent . childCount === 1 ) {
246+ const firstChild = parent . getChild ( 0 ) ;
247+
248+ if ( firstChild . is ( 'element' ) && ! firstChild . is ( 'uiElement' ) ) {
249+ return firstChild ;
142250 }
143251 }
144252
145- return changed ;
253+ return null ;
146254}
0 commit comments