diff --git a/docs/_snippets/features/update-placeholder.html b/docs/_snippets/features/update-placeholder.html new file mode 100644 index 00000000000..e6e19a63460 --- /dev/null +++ b/docs/_snippets/features/update-placeholder.html @@ -0,0 +1,4 @@ +
+

+
+ diff --git a/docs/_snippets/features/update-placeholder.js b/docs/_snippets/features/update-placeholder.js new file mode 100644 index 00000000000..9dac835d1f9 --- /dev/null +++ b/docs/_snippets/features/update-placeholder.js @@ -0,0 +1,36 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console, window, document, ClassicEditor */ + +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; + +ClassicEditor + .create( document.querySelector( '#snippet-update-placeholder' ), { + cloudServices: CS_CONFIG, + toolbar: [ + 'undo', 'redo', '|', 'heading', + '|', 'bold', 'italic', + '|', 'link', 'uploadImage', 'insertTable', 'mediaEmbed', + '|', 'bulletedList', 'numberedList', 'outdent', 'indent' + ], + ui: { + viewportOffset: { + top: window.getViewportTopOffsetConfig() + } + }, + placeholder: 'Type some content here!' + } ) + .then( editor => { + const button = document.getElementById( 'update-placeholder-button' ); + window.editor = editor; + + button.addEventListener( 'click', () => { + editor.editing.view.document.getRoot( 'main' ).placeholder = 'New placeholder'; + } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/docs/features/editor-placeholder.md b/docs/features/editor-placeholder.md index e5fd981e2d8..6b8f04fc08e 100644 --- a/docs/features/editor-placeholder.md +++ b/docs/features/editor-placeholder.md @@ -108,6 +108,16 @@ The editor placeholder text is displayed using a CSS pseudo–element (`::before **Note**: The `.ck-placeholder` class is also used to display placeholders in other places, for instance, {@link features/images-captions image captions}. Make sure your custom styles apply to the right subset of placeholders. +## Changing the placeholder + +The editor placeholder could be updated at runtime by changing the `placeholder` property in editing root. + +```js +editor.editing.view.document.getRoot( 'main' ).placeholder = 'new placeholder'; +``` + +{@snippet features/update-placeholder} + ## Contribute The source code of the feature is available on GitHub at [https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-core](https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-core). diff --git a/packages/ckeditor5-editor-balloon/src/ballooneditorui.ts b/packages/ckeditor5-editor-balloon/src/ballooneditorui.ts index 8ae764b8257..b83609aa51a 100644 --- a/packages/ckeditor5-editor-balloon/src/ballooneditorui.ts +++ b/packages/ckeditor5-editor-balloon/src/ballooneditorui.ts @@ -8,8 +8,7 @@ */ import { - type Editor, - type ElementApi + type Editor } from 'ckeditor5/src/core'; import { @@ -104,28 +103,27 @@ export default class BalloonEditorUI extends EditorUI { } /** - * Enable the placeholder text on the editing root, if any was configured. + * Enable the placeholder text on the editing root. */ private _initPlaceholder(): void { const editor = this.editor; const editingView = editor.editing.view; const editingRoot = editingView.document.getRoot()!; - const sourceElement = ( editor as Editor & ElementApi ).sourceElement; - const placeholder = editor.config.get( 'placeholder' ); if ( placeholder ) { const placeholderText = typeof placeholder === 'string' ? placeholder : placeholder[ editingRoot.rootName ]; if ( placeholderText ) { - enablePlaceholder( { - view: editingView, - element: editingRoot, - text: placeholderText, - isDirectHost: false, - keepOnFocus: true - } ); + editingRoot.placeholder = placeholderText; } } + + enablePlaceholder( { + view: editingView, + element: editingRoot, + isDirectHost: false, + keepOnFocus: true + } ); } } diff --git a/packages/ckeditor5-editor-classic/src/classiceditorui.ts b/packages/ckeditor5-editor-classic/src/classiceditorui.ts index aad897a4166..a7066a6bd2b 100644 --- a/packages/ckeditor5-editor-classic/src/classiceditorui.ts +++ b/packages/ckeditor5-editor-classic/src/classiceditorui.ts @@ -142,7 +142,7 @@ export default class ClassicEditorUI extends EditorUI { } /** - * Enable the placeholder text on the editing root, if any was configured. + * Enable the placeholder text on the editing root. */ private _initPlaceholder(): void { const editor = this.editor; @@ -162,14 +162,15 @@ export default class ClassicEditorUI extends EditorUI { } if ( placeholderText ) { - enablePlaceholder( { - view: editingView, - element: editingRoot, - text: placeholderText, - isDirectHost: false, - keepOnFocus: true - } ); + editingRoot.placeholder = placeholderText; } + + enablePlaceholder( { + view: editingView, + element: editingRoot, + isDirectHost: false, + keepOnFocus: true + } ); } /** diff --git a/packages/ckeditor5-editor-decoupled/src/decouplededitorui.ts b/packages/ckeditor5-editor-decoupled/src/decouplededitorui.ts index 0cd2e55327b..20fe5ae520c 100644 --- a/packages/ckeditor5-editor-decoupled/src/decouplededitorui.ts +++ b/packages/ckeditor5-editor-decoupled/src/decouplededitorui.ts @@ -8,8 +8,7 @@ */ import { - type Editor, - type ElementApi + type Editor } from 'ckeditor5/src/core'; import { @@ -112,28 +111,27 @@ export default class DecoupledEditorUI extends EditorUI { } /** - * Enable the placeholder text on the editing root, if any was configured. + * Enable the placeholder text on the editing root. */ private _initPlaceholder(): void { const editor = this.editor; const editingView = editor.editing.view; const editingRoot = editingView.document.getRoot()!; - const sourceElement = ( editor as Editor & ElementApi ).sourceElement; - const placeholder = editor.config.get( 'placeholder' ); if ( placeholder ) { const placeholderText = typeof placeholder === 'string' ? placeholder : placeholder[ editingRoot.rootName ]; if ( placeholderText ) { - enablePlaceholder( { - view: editingView, - element: editingRoot, - text: placeholderText, - isDirectHost: false, - keepOnFocus: true - } ); + editingRoot.placeholder = placeholderText; } } + + enablePlaceholder( { + view: editingView, + element: editingRoot, + isDirectHost: false, + keepOnFocus: true + } ); } } diff --git a/packages/ckeditor5-editor-inline/src/inlineeditorui.ts b/packages/ckeditor5-editor-inline/src/inlineeditorui.ts index c812882ac56..0ae5c1e2a1c 100644 --- a/packages/ckeditor5-editor-inline/src/inlineeditorui.ts +++ b/packages/ckeditor5-editor-inline/src/inlineeditorui.ts @@ -8,7 +8,6 @@ */ import { - type ElementApi, type Editor } from 'ckeditor5/src/core'; @@ -147,28 +146,27 @@ export default class InlineEditorUI extends EditorUI { } /** - * Enable the placeholder text on the editing root, if any was configured. + * Enable the placeholder text on the editing root. */ private _initPlaceholder(): void { const editor = this.editor; const editingView = editor.editing.view; const editingRoot = editingView.document.getRoot()!; - const sourceElement = ( editor as Editor & ElementApi ).sourceElement; - const placeholder = editor.config.get( 'placeholder' ); if ( placeholder ) { const placeholderText = typeof placeholder === 'string' ? placeholder : placeholder[ editingRoot.rootName ]; if ( placeholderText ) { - enablePlaceholder( { - view: editingView, - element: editingRoot, - text: placeholderText, - isDirectHost: false, - keepOnFocus: true - } ); + editingRoot.placeholder = placeholderText; } } + + enablePlaceholder( { + view: editingView, + element: editingRoot, + isDirectHost: false, + keepOnFocus: true + } ); } } diff --git a/packages/ckeditor5-editor-multi-root/src/multirooteditorui.ts b/packages/ckeditor5-editor-multi-root/src/multirooteditorui.ts index b9279832088..dd1475bd3f0 100644 --- a/packages/ckeditor5-editor-multi-root/src/multirooteditorui.ts +++ b/packages/ckeditor5-editor-multi-root/src/multirooteditorui.ts @@ -189,7 +189,7 @@ export default class MultiRootEditorUI extends EditorUI { } /** - * Enables the placeholder text on a given editable, if the placeholder was configured. + * Enables the placeholder text on a given editable. * * @param editable Editable on which the placeholder should be set. * @param placeholder Placeholder for the editable element. If not set, placeholder value from the @@ -204,17 +204,16 @@ export default class MultiRootEditorUI extends EditorUI { } } - if ( !placeholder ) { - return; - } - const editingView = this.editor.editing.view; const editingRoot = editingView.document.getRoot( editable.name! )!; + if ( placeholder ) { + editingRoot.placeholder = placeholder; + } + enablePlaceholder( { view: editingView, element: editingRoot, - text: placeholder, isDirectHost: false, keepOnFocus: true } ); diff --git a/packages/ckeditor5-engine/src/view/editableelement.ts b/packages/ckeditor5-engine/src/view/editableelement.ts index 19459835a33..eae3e9a967e 100644 --- a/packages/ckeditor5-engine/src/view/editableelement.ts +++ b/packages/ckeditor5-engine/src/view/editableelement.ts @@ -42,6 +42,17 @@ export default class EditableElement extends ObservableMixin( ContainerElement ) */ declare public isFocused: boolean; + /** + * Placeholder of editable element. + * + * ```ts + * editor.editing.view.document.getRoot( 'main' ).placeholder = 'New placeholder'; + * ``` + * + * @observable + */ + declare public placeholder?: string; + /** * Creates an editable element. * @@ -62,6 +73,7 @@ export default class EditableElement extends ObservableMixin( ContainerElement ) this.set( 'isReadOnly', false ); this.set( 'isFocused', false ); + this.set( 'placeholder', undefined ); this.bind( 'isReadOnly' ).to( document ); diff --git a/packages/ckeditor5-engine/src/view/placeholder.ts b/packages/ckeditor5-engine/src/view/placeholder.ts index b892d9f5e7c..0e374d08f48 100644 --- a/packages/ckeditor5-engine/src/view/placeholder.ts +++ b/packages/ckeditor5-engine/src/view/placeholder.ts @@ -11,6 +11,7 @@ import '../../theme/placeholder.css'; import type Document from './document'; import type DowncastWriter from './downcastwriter'; +import type EditableElement from './editableelement'; import type Element from './element'; import type View from './view'; @@ -23,24 +24,22 @@ const documentPlaceholders = new WeakMap { + setPlaceholder( text ); + } ); + } - // Update the placeholders right away. - view.change( writer => updateDocumentPlaceholders( doc, writer ) ); + if ( element.placeholder ) { + setPlaceholder( element.placeholder ); + } + + function setPlaceholder( text: string ) { + // Store information about the element placeholder under its document. + documentPlaceholders.get( doc )!.set( element, { + text, + isDirectHost, + keepOnFocus, + hostElement: isDirectHost ? element : null + } ); + + // Update the placeholders right away. + view.change( writer => updateDocumentPlaceholders( doc, writer ) ); + } } /** @@ -297,3 +308,14 @@ interface PlaceholderConfig { keepOnFocus: boolean; hostElement: Element | null; } + +/** + * Element that could have a placeholder. + */ +export interface PlaceholderableElement extends Element { + + /** + * The text of element's placeholder. + */ + placeholder?: string; +} diff --git a/packages/ckeditor5-engine/tests/view/placeholder.js b/packages/ckeditor5-engine/tests/view/placeholder.js index 77c3b7e0f94..afb25bb29c7 100644 --- a/packages/ckeditor5-engine/tests/view/placeholder.js +++ b/packages/ckeditor5-engine/tests/view/placeholder.js @@ -31,10 +31,10 @@ describe( 'placeholder', () => { setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); + element.placeholder = 'foo bar baz'; enablePlaceholder( { view, - element, - text: 'foo bar baz' + element } ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); @@ -45,10 +45,10 @@ describe( 'placeholder', () => { setData( view, '

' ); viewDocument.isFocused = false; + viewRoot.placeholder = 'foo bar baz'; enablePlaceholder( { view, element: viewRoot, - text: 'foo bar baz', isDirectHost: false } ); @@ -60,10 +60,10 @@ describe( 'placeholder', () => { setData( view, '
first div
{another div}
' ); const element = viewRoot.getChild( 0 ); + element.placeholder = 'foo bar baz'; enablePlaceholder( { view, - element, - text: 'foo bar baz' + element } ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); @@ -74,10 +74,10 @@ describe( 'placeholder', () => { setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); + element.placeholder = 'foo bar baz'; enablePlaceholder( { view, - element, - text: 'foo bar baz' + element } ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); @@ -88,10 +88,10 @@ describe( 'placeholder', () => { setData( view, '
[]
another div
' ); const element = viewRoot.getChild( 0 ); + element.placeholder = 'foo bar baz'; enablePlaceholder( { view, - element, - text: 'foo bar baz' + element } ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); @@ -103,10 +103,10 @@ describe( 'placeholder', () => { const element = viewRoot.getChild( 0 ); viewDocument.isFocused = false; + element.placeholder = 'foo bar baz'; enablePlaceholder( { view, - element, - text: 'foo bar baz' + element } ); view.forceRender(); @@ -119,10 +119,10 @@ describe( 'placeholder', () => { setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); + element.placeholder = 'foo bar baz'; enablePlaceholder( { view, - element, - text: 'foo bar baz' + element } ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); @@ -139,16 +139,16 @@ describe( 'placeholder', () => { setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); + element.placeholder = 'foo bar baz'; enablePlaceholder( { view, - element, - text: 'foo bar baz' + element } ); + element.placeholder = 'new text'; enablePlaceholder( { view, - element, - text: 'new text' + element } ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'new text' ); @@ -159,10 +159,10 @@ describe( 'placeholder', () => { setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); + element.placeholder = 'foo bar baz'; enablePlaceholder( { view, - element, - text: 'foo bar baz' + element } ); setData( view, '

paragraph

' ); @@ -180,16 +180,16 @@ describe( 'placeholder', () => { setData( secondView, '
{another div}
' ); const secondElement = secondRoot.getChild( 0 ); + element.placeholder = 'first placeholder'; enablePlaceholder( { view, - element, - text: 'first placeholder' + element } ); + secondElement.placeholder = 'second placeholder'; enablePlaceholder( { view: secondView, - element: secondElement, - text: 'second placeholder' + element: secondElement } ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'first placeholder' ); @@ -218,10 +218,10 @@ describe( 'placeholder', () => { setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); + element.placeholder = 'foo bar baz'; enablePlaceholder( { view, - element, - text: 'foo bar baz' + element } ); view.change( writer => { @@ -240,10 +240,10 @@ describe( 'placeholder', () => { setData( view, '

' ); viewDocument.isFocused = false; + viewRoot.placeholder = 'foo bar baz'; enablePlaceholder( { view, element: viewRoot, - text: 'foo bar baz', isDirectHost: false } ); @@ -256,10 +256,10 @@ describe( 'placeholder', () => { setData( view, '

foobar

' ); viewDocument.isFocused = false; + viewRoot.placeholder = 'foo bar baz'; enablePlaceholder( { view, element: viewRoot, - text: 'foo bar baz', isDirectHost: false } ); @@ -272,17 +272,17 @@ describe( 'placeholder', () => { setData( view, '

' ); viewDocument.isFocused = false; + viewRoot.placeholder = 'foo'; enablePlaceholder( { view, element: viewRoot, - text: 'foo', isDirectHost: false } ); + viewRoot.getChild( 0 ).placeholder = 'bar'; enablePlaceholder( { view, element: viewRoot.getChild( 0 ), - text: 'bar', isDirectHost: true } ); @@ -295,10 +295,10 @@ describe( 'placeholder', () => { setData( view, '

' ); viewDocument.isFocused = false; + viewRoot.placeholder = 'foo bar baz'; enablePlaceholder( { view, element: viewRoot, - text: 'foo bar baz', isDirectHost: false } ); @@ -312,10 +312,10 @@ describe( 'placeholder', () => { setData( view, '

foobar

' ); viewDocument.isFocused = false; + viewRoot.getChild( 0 ).placeholder = 'foo bar baz'; enablePlaceholder( { view, element: viewRoot.getChild( 0 ), - text: 'foo bar baz', isDirectHost: false } ); @@ -328,10 +328,10 @@ describe( 'placeholder', () => { setData( view, '' ); viewDocument.isFocused = false; + viewRoot.placeholder = 'foo bar baz'; enablePlaceholder( { view, element: viewRoot, - text: 'foo bar baz', isDirectHost: false } ); @@ -343,10 +343,10 @@ describe( 'placeholder', () => { setData( view, 'foo' ); viewDocument.isFocused = false; + viewRoot.placeholder = 'foo bar baz'; enablePlaceholder( { view, element: viewRoot, - text: 'foo bar baz', isDirectHost: false } ); @@ -358,10 +358,10 @@ describe( 'placeholder', () => { setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); + element.placeholder = 'foo bar baz'; enablePlaceholder( { view, element, - text: 'foo bar baz', keepOnFocus: true } ); @@ -384,10 +384,10 @@ describe( 'placeholder', () => { setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); + element.placeholder = 'foo bar baz'; enablePlaceholder( { view, - element, - text: 'foo bar baz' + element // Defaults: keepOnFocus = false } ); @@ -410,10 +410,10 @@ describe( 'placeholder', () => { setData( view, '
[]
' ); const element = viewRoot.getChild( 0 ); + element.placeholder = 'foo bar baz'; enablePlaceholder( { view, element, - text: 'foo bar baz', keepOnFocus: true } ); @@ -440,10 +440,10 @@ describe( 'placeholder', () => { setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); + element.placeholder = 'foo bar baz'; enablePlaceholder( { view, - element, - text: 'foo bar baz' + element } ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); @@ -469,10 +469,10 @@ describe( 'placeholder', () => { setData( view, '

' ); viewDocument.isFocused = false; + viewRoot.placeholder = 'foo bar baz'; enablePlaceholder( { view, element: viewRoot, - text: 'foo bar baz', isDirectHost: false } ); @@ -612,5 +612,32 @@ describe( 'placeholder', () => { expect( needsPlaceholder( element, true ) ).to.be.false; } ); + + it( 'should update placeholder when property in editing root is changed', () => { + setData( view, '
{another div}
' ); + + enablePlaceholder( { + view, + element: viewRoot, + text: 'foo bar baz' + } ); + viewRoot.placeholder = 'new placeholder'; + + expect( viewRoot.getAttribute( 'data-placeholder' ) ).to.equal( 'new placeholder' ); + } ); + + it( 'should update placeholder when property in editing root is changed (isDirectHost=false)', () => { + setData( view, '
{another div}
' ); + + enablePlaceholder( { + view, + element: viewRoot, + text: 'foo bar baz', + isDirectHost: false + } ); + viewRoot.placeholder = 'new placeholder'; + + expect( viewRoot.getChild( 0 ).getAttribute( 'data-placeholder' ) ).to.equal( 'new placeholder' ); + } ); } ); } ); diff --git a/packages/ckeditor5-heading/src/title.ts b/packages/ckeditor5-heading/src/title.ts index d2cc1f563db..aa509f2d187 100644 --- a/packages/ckeditor5-heading/src/title.ts +++ b/packages/ckeditor5-heading/src/title.ts @@ -25,7 +25,8 @@ import { type UpcastElementEvent, type View, type ViewElement, - type Writer + type Writer, + type PlaceholderableElement } from 'ckeditor5/src/engine'; // A list of element names that should be treated by the Title plugin as title-like. @@ -354,10 +355,13 @@ export default class Title extends Plugin { // Attach placeholder to the view title element. editor.editing.downcastDispatcher.on>( 'insert:title-content', ( evt, data, conversionApi ) => { + const element: PlaceholderableElement = conversionApi.mapper.toViewElement( data.item )!; + + element.placeholder = titlePlaceholder; + enablePlaceholder( { view, - element: conversionApi.mapper.toViewElement( data.item )!, - text: titlePlaceholder, + element, keepOnFocus: true } ); } ); diff --git a/packages/ckeditor5-image/src/imagecaption/imagecaptionediting.ts b/packages/ckeditor5-image/src/imagecaption/imagecaptionediting.ts index 24c4f77be5c..022b4be371d 100644 --- a/packages/ckeditor5-image/src/imagecaption/imagecaptionediting.ts +++ b/packages/ckeditor5-image/src/imagecaption/imagecaptionediting.ts @@ -8,7 +8,7 @@ */ import { type Editor, Plugin, type CommandExecuteEvent } from 'ckeditor5/src/core'; -import { Element, enablePlaceholder, type DocumentChangeEvent, type DiffItemAttribute } from 'ckeditor5/src/engine'; +import { Element, enablePlaceholder, type DocumentChangeEvent, type DiffItemAttribute, PlaceholderableElement } from 'ckeditor5/src/engine'; import { toWidgetEditable } from 'ckeditor5/src/widget'; import type { GetCallback } from 'ckeditor5/src/utils'; @@ -122,10 +122,10 @@ export default class ImageCaptionEditing extends Plugin { const figcaptionElement = writer.createEditableElement( 'figcaption' ); writer.setCustomProperty( 'imageCaption', true, figcaptionElement ); + figcaptionElement.placeholder = t( 'Enter image caption' ); enablePlaceholder( { view, element: figcaptionElement, - text: t( 'Enter image caption' ), keepOnFocus: true } ); diff --git a/packages/ckeditor5-media-embed/tests/integration.js b/packages/ckeditor5-media-embed/tests/integration.js index d6b9d4c1fb6..46ab59518ce 100644 --- a/packages/ckeditor5-media-embed/tests/integration.js +++ b/packages/ckeditor5-media-embed/tests/integration.js @@ -32,11 +32,12 @@ describe( 'MediaEmbed integration', () => { const editor = await ClassicTestEditor.create( element, { plugins: [ MediaEmbed, Paragraph ] } ); + const editingRoot = editor.editing.view.document.getRoot(); + editingRoot.placeholder = 'foo'; enablePlaceholder( { view: editor.editing.view, - element: editor.editing.view.document.getRoot(), - text: 'foo', + element: editingRoot, isDirectHost: false } ); diff --git a/packages/ckeditor5-table/src/tablecaption/tablecaptionediting.ts b/packages/ckeditor5-table/src/tablecaption/tablecaptionediting.ts index aebd514e034..b417d548aac 100644 --- a/packages/ckeditor5-table/src/tablecaption/tablecaptionediting.ts +++ b/packages/ckeditor5-table/src/tablecaption/tablecaptionediting.ts @@ -95,10 +95,11 @@ export default class TableCaptionEditing extends Plugin { const figcaptionElement = writer.createEditableElement( 'figcaption' ); writer.setCustomProperty( 'tableCaption', true, figcaptionElement ); + figcaptionElement.placeholder = t( 'Enter table caption' ); + enablePlaceholder( { view, element: figcaptionElement, - text: t( 'Enter table caption' ), keepOnFocus: true } );