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

Commit 21dee6b

Browse files
author
Piotr Jasiun
authored
Merge pull request #1644 from ckeditor/t/ckeditor5/479
Feature: Moved the root element DOM attributes management from the UI to the engine. Made it possible to use `addPlaceholder()` (now `enablePlaceholder()`) on the root editable. Introduced the `View.detachDomRoot()` method. Implemented additional placeholder helpers (`showPlaceholder()`, `hidePlaceholder()`, `needsPlaceholder()`) (see ckeditor/ckeditor5#479). Closes #899. BREAKING CHANGE: The `attachPlaceholder()` has been renamed to `enablePlaceholder()`. BREAKING CHANGE: `enablePlaceholder()` accepts a configuration object instead of separate parameters. BREAKING CHANGE: The `detachPlaceholder()` has been renamed to `disablePlaceholder()`.
2 parents c31bea6 + 0e49518 commit 21dee6b

File tree

6 files changed

+609
-131
lines changed

6 files changed

+609
-131
lines changed

src/view/placeholder.js

Lines changed: 191 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -13,134 +13,242 @@ import '../../theme/placeholder.css';
1313
const 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

Comments
 (0)