This repository has been archived by the owner on Jun 26, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #872 from ckeditor/t/857
Feature: Added placeholder utility that can be applied to view elements. Closes #857.
- Loading branch information
Showing
6 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
/** | ||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
/** | ||
* @module engine/view/placeholder | ||
*/ | ||
|
||
import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend'; | ||
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin'; | ||
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; | ||
import '../../theme/placeholder.scss'; | ||
|
||
const listener = {}; | ||
extend( listener, EmitterMixin ); | ||
|
||
// Each document stores information about its placeholder elements and check functions. | ||
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. | ||
* | ||
* @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. | ||
*/ | ||
export function attachPlaceholder( element, placeholderText, checkFunction ) { | ||
const document = element.document; | ||
|
||
if ( !document ) { | ||
/** | ||
* Provided element is not placed in any {@link module:engine/view/document~Document}. | ||
* | ||
* @error view-placeholder-element-is-detached | ||
*/ | ||
throw new CKEditorError( 'view-placeholder-element-is-detached: Provided element is not placed in document.' ); | ||
} | ||
|
||
// Detach placeholder if was used before. | ||
detachPlaceholder( element ); | ||
|
||
// Single listener per document. | ||
if ( !documentPlaceholders.has( document ) ) { | ||
documentPlaceholders.set( document, new Map() ); | ||
listener.listenTo( document, 'render', () => updateAllPlaceholders( document ), { priority: 'high' } ); | ||
} | ||
|
||
// Store text in element's data attribute. | ||
// This data attribute is used in CSS class to show the placeholder. | ||
element.setAttribute( 'data-placeholder', placeholderText ); | ||
|
||
// Store information about placeholder. | ||
documentPlaceholders.get( document ).set( element, checkFunction ); | ||
|
||
// Update right away too. | ||
updateSinglePlaceholder( element, checkFunction ); | ||
} | ||
|
||
/** | ||
* Removes placeholder functionality from given element. | ||
* | ||
* @param {module:engine/view/element~Element} element | ||
*/ | ||
export function detachPlaceholder( element ) { | ||
const document = element.document; | ||
|
||
element.removeClass( 'ck-placeholder' ); | ||
element.removeAttribute( 'data-placeholder' ); | ||
|
||
if ( documentPlaceholders.has( document ) ) { | ||
documentPlaceholders.get( document ).delete( element ); | ||
} | ||
} | ||
|
||
// Updates all placeholders of given document. | ||
// | ||
// @private | ||
// @param {module:engine/view/document~Document} document | ||
function updateAllPlaceholders( document ) { | ||
const placeholders = documentPlaceholders.get( document ); | ||
|
||
for ( let [ element, checkFunction ] of placeholders ) { | ||
updateSinglePlaceholder( element, checkFunction ); | ||
} | ||
} | ||
|
||
// Updates placeholder class of given element. | ||
// | ||
// @private | ||
// @param {module:engine/view/element~Element} element | ||
// @param {Function} checkFunction | ||
function updateSinglePlaceholder( element, checkFunction ) { | ||
const document = element.document; | ||
|
||
// Element was removed from document. | ||
if ( !document ) { | ||
return; | ||
} | ||
|
||
const viewSelection = document.selection; | ||
const anchor = viewSelection.anchor; | ||
|
||
// If checkFunction is provided and returns false - remove placeholder. | ||
if ( checkFunction && !checkFunction() ) { | ||
element.removeClass( 'ck-placeholder' ); | ||
|
||
return; | ||
} | ||
|
||
// If element is empty and editor is blurred. | ||
if ( !document.isFocused && !element.childCount ) { | ||
element.addClass( 'ck-placeholder' ); | ||
|
||
return; | ||
} | ||
|
||
// It there are no child elements and selection is not placed inside element. | ||
if ( !element.childCount && anchor && anchor.parent !== element ) { | ||
element.addClass( 'ck-placeholder' ); | ||
} else { | ||
element.removeClass( 'ck-placeholder' ); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<div id="editor"><h2></h2><p></p></div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/** | ||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
/* global console */ | ||
|
||
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic'; | ||
import Enter from '@ckeditor/ckeditor5-enter/src/enter'; | ||
import Typing from '@ckeditor/ckeditor5-typing/src/typing'; | ||
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; | ||
import Undo from '@ckeditor/ckeditor5-undo/src/undo'; | ||
import Heading from '@ckeditor/ckeditor5-heading/src/heading'; | ||
import global from '@ckeditor/ckeditor5-utils/src/dom/global'; | ||
import { attachPlaceholder } from '../../src/view/placeholder'; | ||
|
||
ClassicEditor.create( global.document.querySelector( '#editor' ), { | ||
plugins: [ Enter, Typing, Paragraph, Undo, Heading ], | ||
toolbar: [ 'headings', 'undo', 'redo' ] | ||
} ) | ||
.then( editor => { | ||
const viewDoc = editor.editing.view; | ||
const header = viewDoc.getRoot().getChild( 0 ); | ||
const paragraph = viewDoc.getRoot().getChild( 1 ); | ||
|
||
attachPlaceholder( header, 'Type some header text...' ); | ||
attachPlaceholder( paragraph, 'Type some paragraph text...' ); | ||
viewDoc.render(); | ||
} ) | ||
.catch( err => { | ||
console.error( err.stack ); | ||
} ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
### Placeholder creation | ||
|
||
* You should see two placeholders: | ||
* for heading: `Type some header text...`, | ||
* and for paragraph: `Type some paragraph text...`. | ||
* Clicking on header and paragraph should remove placeholder. | ||
* Clicking outside the editor should show both placeholders. | ||
* Type some text into paragraph, and click outside. Paragraph placeholder should be hidden. | ||
* Remove added text and click outside - paragraph placeholder should now be visible again. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
/** | ||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
import { attachPlaceholder, detachPlaceholder } from '../../src/view/placeholder'; | ||
import ViewContainerElement from '../../src/view/containerelement'; | ||
import ViewDocument from '../../src/view/document'; | ||
import ViewRange from '../../src/view/range'; | ||
import { setData } from '../../src/dev-utils/view'; | ||
|
||
describe( 'placeholder', () => { | ||
let viewDocument, viewRoot; | ||
|
||
beforeEach( () => { | ||
viewDocument = new ViewDocument(); | ||
viewRoot = viewDocument.createRoot( 'main' ); | ||
viewDocument.isFocused = true; | ||
} ); | ||
|
||
describe( 'createPlaceholder', () => { | ||
it( 'should throw if element is not inside document', () => { | ||
const element = new ViewContainerElement( 'div' ); | ||
|
||
expect( () => { | ||
attachPlaceholder( element, 'foo bar baz' ); | ||
} ).to.throw( 'view-placeholder-element-is-detached' ); | ||
} ); | ||
|
||
it( 'should attach proper CSS class and data attribute', () => { | ||
setData( viewDocument, '<div></div><div>{another div}</div>' ); | ||
const element = viewRoot.getChild( 0 ); | ||
|
||
attachPlaceholder( element, 'foo bar baz' ); | ||
|
||
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); | ||
expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; | ||
} ); | ||
|
||
it( 'if element has children set only data attribute', () => { | ||
setData( viewDocument, '<div>first div</div><div>{another div}</div>' ); | ||
const element = viewRoot.getChild( 0 ); | ||
|
||
attachPlaceholder( element, 'foo bar baz' ); | ||
|
||
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); | ||
expect( element.hasClass( 'ck-placeholder' ) ).to.be.false; | ||
} ); | ||
|
||
it( 'if element has selection inside set only data attribute', () => { | ||
setData( viewDocument, '<div>[]</div><div>another div</div>' ); | ||
const element = viewRoot.getChild( 0 ); | ||
|
||
attachPlaceholder( element, 'foo bar baz' ); | ||
|
||
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); | ||
expect( element.hasClass( 'ck-placeholder' ) ).to.be.false; | ||
} ); | ||
|
||
it( 'if element has selection inside but document is blurred should contain placeholder CSS class', () => { | ||
setData( viewDocument, '<div>[]</div><div>another div</div>' ); | ||
const element = viewRoot.getChild( 0 ); | ||
viewDocument.isFocused = false; | ||
|
||
attachPlaceholder( element, 'foo bar baz' ); | ||
|
||
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); | ||
expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; | ||
} ); | ||
|
||
it( 'use check function if one is provided', () => { | ||
setData( viewDocument, '<div></div><div>{another div}</div>' ); | ||
const element = viewRoot.getChild( 0 ); | ||
const spy = sinon.spy( () => false ); | ||
|
||
attachPlaceholder( element, 'foo bar baz', spy ); | ||
|
||
sinon.assert.calledOnce( spy ); | ||
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); | ||
expect( element.hasClass( 'ck-placeholder' ) ).to.be.false; | ||
} ); | ||
|
||
it( 'should remove CSS class if selection is moved inside', () => { | ||
setData( viewDocument, '<div></div><div>{another div}</div>' ); | ||
const element = viewRoot.getChild( 0 ); | ||
|
||
attachPlaceholder( element, 'foo bar baz' ); | ||
|
||
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); | ||
expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; | ||
|
||
viewDocument.selection.setRanges( [ ViewRange.createIn( element ) ] ); | ||
viewDocument.render(); | ||
|
||
expect( element.hasClass( 'ck-placeholder' ) ).to.be.false; | ||
} ); | ||
|
||
it( 'should change placeholder settings when called twice', () => { | ||
setData( viewDocument, '<div></div><div>{another div}</div>' ); | ||
const element = viewRoot.getChild( 0 ); | ||
|
||
attachPlaceholder( element, 'foo bar baz' ); | ||
attachPlaceholder( element, 'new text' ); | ||
|
||
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'new text' ); | ||
expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; | ||
} ); | ||
|
||
it( 'should not throw when element is no longer in document', () => { | ||
setData( viewDocument, '<div></div><div>{another div}</div>' ); | ||
const element = viewRoot.getChild( 0 ); | ||
|
||
attachPlaceholder( element, 'foo bar baz' ); | ||
setData( viewDocument, '<p>paragraph</p>' ); | ||
|
||
viewDocument.render(); | ||
} ); | ||
|
||
it( 'should allow to add placeholder to elements from different documents', () => { | ||
setData( viewDocument, '<div></div><div>{another div}</div>' ); | ||
const element = viewRoot.getChild( 0 ); | ||
const secondDocument = new ViewDocument(); | ||
secondDocument.isFocused = true; | ||
const secondRoot = secondDocument.createRoot( 'main' ); | ||
setData( secondDocument, '<div></div><div>{another div}</div>' ); | ||
const secondElement = secondRoot.getChild( 0 ); | ||
|
||
attachPlaceholder( element, 'first placeholder' ); | ||
attachPlaceholder( secondElement, 'second placeholder' ); | ||
|
||
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'first placeholder' ); | ||
expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; | ||
|
||
expect( secondElement.getAttribute( 'data-placeholder' ) ).to.equal( 'second placeholder' ); | ||
expect( secondElement.hasClass( 'ck-placeholder' ) ).to.be.true; | ||
|
||
// Move selection to the elements with placeholders. | ||
viewDocument.selection.setRanges( [ ViewRange.createIn( element ) ] ); | ||
secondDocument.selection.setRanges( [ ViewRange.createIn( secondElement ) ] ); | ||
|
||
// Render changes. | ||
viewDocument.render(); | ||
secondDocument.render(); | ||
|
||
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'first placeholder' ); | ||
expect( element.hasClass( 'ck-placeholder' ) ).to.be.false; | ||
|
||
expect( secondElement.getAttribute( 'data-placeholder' ) ).to.equal( 'second placeholder' ); | ||
expect( secondElement.hasClass( 'ck-placeholder' ) ).to.be.false; | ||
} ); | ||
} ); | ||
|
||
describe( 'detachPlaceholder', () => { | ||
it( 'should remove placeholder from element', () => { | ||
setData( viewDocument, '<div></div><div>{another div}</div>' ); | ||
const element = viewRoot.getChild( 0 ); | ||
|
||
attachPlaceholder( element, 'foo bar baz' ); | ||
|
||
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); | ||
expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; | ||
|
||
detachPlaceholder( element ); | ||
|
||
expect( element.hasAttribute( 'data-placeholder' ) ).to.be.false; | ||
expect( element.hasClass( 'ck-placeholder' ) ).to.be.false; | ||
} ); | ||
} ); | ||
} ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | ||
// For licensing, see LICENSE.md or http://ckeditor.com/license | ||
|
||
.ck-placeholder::before { | ||
content: attr( data-placeholder ); | ||
cursor: text; | ||
color: #c2c2c2; | ||
} |