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

Commit 79b42da

Browse files
author
Piotr Jasiun
authored
Merge pull request #872 from ckeditor/t/857
Feature: Added placeholder utility that can be applied to view elements. Closes #857.
2 parents 115a91b + 010fc06 commit 79b42da

File tree

6 files changed

+345
-0
lines changed

6 files changed

+345
-0
lines changed

src/view/placeholder.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
/**
7+
* @module engine/view/placeholder
8+
*/
9+
10+
import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
11+
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
12+
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
13+
import '../../theme/placeholder.scss';
14+
15+
const listener = {};
16+
extend( listener, EmitterMixin );
17+
18+
// Each document stores information about its placeholder elements and check functions.
19+
const documentPlaceholders = new WeakMap();
20+
21+
/**
22+
* Attaches placeholder to provided element and updates it's visibility. To change placeholder simply call this method
23+
* once again with new parameters.
24+
*
25+
* @param {module:engine/view/element~Element} element Element to attach placeholder to.
26+
* @param {String} placeholderText Placeholder text to use.
27+
* @param {Function} [checkFunction] If provided it will be called before checking if placeholder should be displayed.
28+
* If function returns `false` placeholder will not be showed.
29+
*/
30+
export function attachPlaceholder( element, placeholderText, checkFunction ) {
31+
const document = element.document;
32+
33+
if ( !document ) {
34+
/**
35+
* Provided element is not placed in any {@link module:engine/view/document~Document}.
36+
*
37+
* @error view-placeholder-element-is-detached
38+
*/
39+
throw new CKEditorError( 'view-placeholder-element-is-detached: Provided element is not placed in document.' );
40+
}
41+
42+
// Detach placeholder if was used before.
43+
detachPlaceholder( element );
44+
45+
// Single listener per document.
46+
if ( !documentPlaceholders.has( document ) ) {
47+
documentPlaceholders.set( document, new Map() );
48+
listener.listenTo( document, 'render', () => updateAllPlaceholders( document ), { priority: 'high' } );
49+
}
50+
51+
// Store text in element's data attribute.
52+
// This data attribute is used in CSS class to show the placeholder.
53+
element.setAttribute( 'data-placeholder', placeholderText );
54+
55+
// Store information about placeholder.
56+
documentPlaceholders.get( document ).set( element, checkFunction );
57+
58+
// Update right away too.
59+
updateSinglePlaceholder( element, checkFunction );
60+
}
61+
62+
/**
63+
* Removes placeholder functionality from given element.
64+
*
65+
* @param {module:engine/view/element~Element} element
66+
*/
67+
export function detachPlaceholder( element ) {
68+
const document = element.document;
69+
70+
element.removeClass( 'ck-placeholder' );
71+
element.removeAttribute( 'data-placeholder' );
72+
73+
if ( documentPlaceholders.has( document ) ) {
74+
documentPlaceholders.get( document ).delete( element );
75+
}
76+
}
77+
78+
// Updates all placeholders of given document.
79+
//
80+
// @private
81+
// @param {module:engine/view/document~Document} document
82+
function updateAllPlaceholders( document ) {
83+
const placeholders = documentPlaceholders.get( document );
84+
85+
for ( let [ element, checkFunction ] of placeholders ) {
86+
updateSinglePlaceholder( element, checkFunction );
87+
}
88+
}
89+
90+
// Updates placeholder class of given element.
91+
//
92+
// @private
93+
// @param {module:engine/view/element~Element} element
94+
// @param {Function} checkFunction
95+
function updateSinglePlaceholder( element, checkFunction ) {
96+
const document = element.document;
97+
98+
// Element was removed from document.
99+
if ( !document ) {
100+
return;
101+
}
102+
103+
const viewSelection = document.selection;
104+
const anchor = viewSelection.anchor;
105+
106+
// If checkFunction is provided and returns false - remove placeholder.
107+
if ( checkFunction && !checkFunction() ) {
108+
element.removeClass( 'ck-placeholder' );
109+
110+
return;
111+
}
112+
113+
// If element is empty and editor is blurred.
114+
if ( !document.isFocused && !element.childCount ) {
115+
element.addClass( 'ck-placeholder' );
116+
117+
return;
118+
}
119+
120+
// It there are no child elements and selection is not placed inside element.
121+
if ( !element.childCount && anchor && anchor.parent !== element ) {
122+
element.addClass( 'ck-placeholder' );
123+
} else {
124+
element.removeClass( 'ck-placeholder' );
125+
}
126+
}

tests/manual/placeholder.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div id="editor"><h2></h2><p></p></div>

tests/manual/placeholder.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
/* global console */
7+
8+
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
9+
import Enter from '@ckeditor/ckeditor5-enter/src/enter';
10+
import Typing from '@ckeditor/ckeditor5-typing/src/typing';
11+
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
12+
import Undo from '@ckeditor/ckeditor5-undo/src/undo';
13+
import Heading from '@ckeditor/ckeditor5-heading/src/heading';
14+
import global from '@ckeditor/ckeditor5-utils/src/dom/global';
15+
import { attachPlaceholder } from '../../src/view/placeholder';
16+
17+
ClassicEditor.create( global.document.querySelector( '#editor' ), {
18+
plugins: [ Enter, Typing, Paragraph, Undo, Heading ],
19+
toolbar: [ 'headings', 'undo', 'redo' ]
20+
} )
21+
.then( editor => {
22+
const viewDoc = editor.editing.view;
23+
const header = viewDoc.getRoot().getChild( 0 );
24+
const paragraph = viewDoc.getRoot().getChild( 1 );
25+
26+
attachPlaceholder( header, 'Type some header text...' );
27+
attachPlaceholder( paragraph, 'Type some paragraph text...' );
28+
viewDoc.render();
29+
} )
30+
.catch( err => {
31+
console.error( err.stack );
32+
} );

tests/manual/placeholder.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
### Placeholder creation
2+
3+
* You should see two placeholders:
4+
* for heading: `Type some header text...`,
5+
* and for paragraph: `Type some paragraph text...`.
6+
* Clicking on header and paragraph should remove placeholder.
7+
* Clicking outside the editor should show both placeholders.
8+
* Type some text into paragraph, and click outside. Paragraph placeholder should be hidden.
9+
* Remove added text and click outside - paragraph placeholder should now be visible again.

tests/view/placeholder.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
import { attachPlaceholder, detachPlaceholder } from '../../src/view/placeholder';
7+
import ViewContainerElement from '../../src/view/containerelement';
8+
import ViewDocument from '../../src/view/document';
9+
import ViewRange from '../../src/view/range';
10+
import { setData } from '../../src/dev-utils/view';
11+
12+
describe( 'placeholder', () => {
13+
let viewDocument, viewRoot;
14+
15+
beforeEach( () => {
16+
viewDocument = new ViewDocument();
17+
viewRoot = viewDocument.createRoot( 'main' );
18+
viewDocument.isFocused = true;
19+
} );
20+
21+
describe( 'createPlaceholder', () => {
22+
it( 'should throw if element is not inside document', () => {
23+
const element = new ViewContainerElement( 'div' );
24+
25+
expect( () => {
26+
attachPlaceholder( element, 'foo bar baz' );
27+
} ).to.throw( 'view-placeholder-element-is-detached' );
28+
} );
29+
30+
it( 'should attach proper CSS class and data attribute', () => {
31+
setData( viewDocument, '<div></div><div>{another div}</div>' );
32+
const element = viewRoot.getChild( 0 );
33+
34+
attachPlaceholder( element, 'foo bar baz' );
35+
36+
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
37+
expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
38+
} );
39+
40+
it( 'if element has children set only data attribute', () => {
41+
setData( viewDocument, '<div>first div</div><div>{another div}</div>' );
42+
const element = viewRoot.getChild( 0 );
43+
44+
attachPlaceholder( element, 'foo bar baz' );
45+
46+
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
47+
expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
48+
} );
49+
50+
it( 'if element has selection inside set only data attribute', () => {
51+
setData( viewDocument, '<div>[]</div><div>another div</div>' );
52+
const element = viewRoot.getChild( 0 );
53+
54+
attachPlaceholder( element, 'foo bar baz' );
55+
56+
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
57+
expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
58+
} );
59+
60+
it( 'if element has selection inside but document is blurred should contain placeholder CSS class', () => {
61+
setData( viewDocument, '<div>[]</div><div>another div</div>' );
62+
const element = viewRoot.getChild( 0 );
63+
viewDocument.isFocused = false;
64+
65+
attachPlaceholder( element, 'foo bar baz' );
66+
67+
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
68+
expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
69+
} );
70+
71+
it( 'use check function if one is provided', () => {
72+
setData( viewDocument, '<div></div><div>{another div}</div>' );
73+
const element = viewRoot.getChild( 0 );
74+
const spy = sinon.spy( () => false );
75+
76+
attachPlaceholder( element, 'foo bar baz', spy );
77+
78+
sinon.assert.calledOnce( spy );
79+
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
80+
expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
81+
} );
82+
83+
it( 'should remove CSS class if selection is moved inside', () => {
84+
setData( viewDocument, '<div></div><div>{another div}</div>' );
85+
const element = viewRoot.getChild( 0 );
86+
87+
attachPlaceholder( element, 'foo bar baz' );
88+
89+
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
90+
expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
91+
92+
viewDocument.selection.setRanges( [ ViewRange.createIn( element ) ] );
93+
viewDocument.render();
94+
95+
expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
96+
} );
97+
98+
it( 'should change placeholder settings when called twice', () => {
99+
setData( viewDocument, '<div></div><div>{another div}</div>' );
100+
const element = viewRoot.getChild( 0 );
101+
102+
attachPlaceholder( element, 'foo bar baz' );
103+
attachPlaceholder( element, 'new text' );
104+
105+
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'new text' );
106+
expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
107+
} );
108+
109+
it( 'should not throw when element is no longer in document', () => {
110+
setData( viewDocument, '<div></div><div>{another div}</div>' );
111+
const element = viewRoot.getChild( 0 );
112+
113+
attachPlaceholder( element, 'foo bar baz' );
114+
setData( viewDocument, '<p>paragraph</p>' );
115+
116+
viewDocument.render();
117+
} );
118+
119+
it( 'should allow to add placeholder to elements from different documents', () => {
120+
setData( viewDocument, '<div></div><div>{another div}</div>' );
121+
const element = viewRoot.getChild( 0 );
122+
const secondDocument = new ViewDocument();
123+
secondDocument.isFocused = true;
124+
const secondRoot = secondDocument.createRoot( 'main' );
125+
setData( secondDocument, '<div></div><div>{another div}</div>' );
126+
const secondElement = secondRoot.getChild( 0 );
127+
128+
attachPlaceholder( element, 'first placeholder' );
129+
attachPlaceholder( secondElement, 'second placeholder' );
130+
131+
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'first placeholder' );
132+
expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
133+
134+
expect( secondElement.getAttribute( 'data-placeholder' ) ).to.equal( 'second placeholder' );
135+
expect( secondElement.hasClass( 'ck-placeholder' ) ).to.be.true;
136+
137+
// Move selection to the elements with placeholders.
138+
viewDocument.selection.setRanges( [ ViewRange.createIn( element ) ] );
139+
secondDocument.selection.setRanges( [ ViewRange.createIn( secondElement ) ] );
140+
141+
// Render changes.
142+
viewDocument.render();
143+
secondDocument.render();
144+
145+
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'first placeholder' );
146+
expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
147+
148+
expect( secondElement.getAttribute( 'data-placeholder' ) ).to.equal( 'second placeholder' );
149+
expect( secondElement.hasClass( 'ck-placeholder' ) ).to.be.false;
150+
} );
151+
} );
152+
153+
describe( 'detachPlaceholder', () => {
154+
it( 'should remove placeholder from element', () => {
155+
setData( viewDocument, '<div></div><div>{another div}</div>' );
156+
const element = viewRoot.getChild( 0 );
157+
158+
attachPlaceholder( element, 'foo bar baz' );
159+
160+
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
161+
expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
162+
163+
detachPlaceholder( element );
164+
165+
expect( element.hasAttribute( 'data-placeholder' ) ).to.be.false;
166+
expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
167+
} );
168+
} );
169+
} );

theme/placeholder.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
2+
// For licensing, see LICENSE.md or http://ckeditor.com/license
3+
4+
.ck-placeholder::before {
5+
content: attr( data-placeholder );
6+
cursor: text;
7+
color: #c2c2c2;
8+
}

0 commit comments

Comments
 (0)