Skip to content

Commit 9c1e2e5

Browse files
authored
feat(ui): placeholders updated (#2758)
* data-empty mark * Update CHANGELOG.md * lint * tests added * Update DataEmpty.cy.ts * lint * fix tests * Update Placeholders.cy.ts * upd paragraph * rm redundant test * lint fix * disable test for firefox
1 parent fb3089c commit 9c1e2e5

File tree

20 files changed

+329
-42
lines changed

20 files changed

+329
-42
lines changed

docs/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
`Improvement`*Types*`BlockToolConstructorOptions` type improved, `block` and `config` are not optional anymore
3030
- `Improvement` - The Plus button and Block Tunes toggler are now better aligned with large line-height blocks, such as Headings
3131
- `Improvement` — Creating links on Android devices: now the mobile keyboard will have an "Enter" key for accepting the inserted link.
32+
- `Improvement` — Placeholders will stay visible on inputs focus.
33+
- `New` — Editor.js now supports contenteditable placeholders out of the box. Just add `data-placeholder` or `data-placeholder-active` attribute to make it work. The first one will work like native placeholder while the second one will show placeholder only when block is current.
34+
- `Improvement` — Now Paragraph placeholder will be shown for the current paragraph, not the only first one.
3235

3336
### 2.29.1
3437

index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@
203203
*/
204204
// defaultBlock: 'paragraph',
205205

206+
placeholder: 'Write something or press / to select a tool',
207+
autofocus: true,
208+
206209
/**
207210
* Initial Editor data
208211
*/

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"@editorjs/code": "^2.7.0",
4646
"@editorjs/delimiter": "^1.2.0",
4747
"@editorjs/header": "^2.7.0",
48-
"@editorjs/paragraph": "^2.11.4",
48+
"@editorjs/paragraph": "^2.11.6",
4949
"@editorjs/simple-image": "^1.4.1",
5050
"@types/node": "^18.15.11",
5151
"chai-subset": "^1.6.0",

src/components/block/index.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from '../../../types';
1111

1212
import { SavedData } from '../../../types/data-formats';
13-
import $ from '../dom';
13+
import $, { toggleEmptyMark } from '../dom';
1414
import * as _ from '../utils';
1515
import ApiModules from '../modules/api';
1616
import BlockAPI from './api';
@@ -183,11 +183,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {
183183
*/
184184
private unavailableTunesData: { [name: string]: BlockTuneData } = {};
185185

186-
/**
187-
* Editor`s API module
188-
*/
189-
private readonly api: ApiModules;
190-
191186
/**
192187
* Focused input index
193188
*
@@ -223,7 +218,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {
223218
id = _.generateBlockId(),
224219
data,
225220
tool,
226-
api,
227221
readOnly,
228222
tunesData,
229223
}: BlockConstructorOptions, eventBus?: EventsDispatcher<EditorEventMap>) {
@@ -232,7 +226,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {
232226
this.id = id;
233227
this.settings = tool.settings;
234228
this.config = tool.settings.config || {};
235-
this.api = api;
236229
this.editorEventBus = eventBus || null;
237230
this.blockAPI = new BlockAPI(this);
238231

@@ -262,6 +255,12 @@ export default class Block extends EventsDispatcher<BlockEvents> {
262255
* so we need to track focus events to update current input and clear cache.
263256
*/
264257
this.addInputEvents();
258+
259+
/**
260+
* We mark inputs with [data-empty] attribute
261+
* It can be useful for developers, for example for correct placeholder behavior
262+
*/
263+
this.toggleInputsEmptyMark();
265264
});
266265
}
267266

@@ -938,6 +937,11 @@ export default class Block extends EventsDispatcher<BlockEvents> {
938937
*/
939938
this.updateCurrentInput();
940939

940+
/**
941+
* We mark inputs with 'data-empty' attribute, so new inputs should be marked as well
942+
*/
943+
this.toggleInputsEmptyMark();
944+
941945
this.call(BlockToolAPI.UPDATED);
942946

943947
/**
@@ -1000,4 +1004,11 @@ export default class Block extends EventsDispatcher<BlockEvents> {
10001004
private dropInputsCache(): void {
10011005
this.cachedInputs = [];
10021006
}
1007+
1008+
/**
1009+
* Mark inputs with 'data-empty' attribute with the empty state
1010+
*/
1011+
private toggleInputsEmptyMark(): void {
1012+
this.inputs.forEach(toggleEmptyMark);
1013+
}
10031014
}

src/components/dom.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,3 +662,13 @@ export function calculateBaseline(element: Element): number {
662662

663663
return baselineY;
664664
}
665+
666+
/**
667+
* Toggles the [data-empty] attribute on element depending on its emptiness
668+
* Used to mark empty inputs with a special attribute for placeholders feature
669+
*
670+
* @param element - The element to toggle the [data-empty] attribute on
671+
*/
672+
export function toggleEmptyMark(element: HTMLElement): void {
673+
element.dataset.empty = Dom.isEmpty(element) ? 'true' : 'false';
674+
}

src/components/modules/blockManager.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ export default class BlockManager extends Module {
544544
* If first Block was removed, insert new Initial Block and set focus on it`s first input
545545
*/
546546
if (!this.blocks.length) {
547-
this.currentBlockIndex = -1;
547+
this.unsetCurrentBlock();
548548

549549
if (addLastBlock) {
550550
this.insert();
@@ -591,7 +591,7 @@ export default class BlockManager extends Module {
591591
this._blocks.remove(index);
592592
}
593593

594-
this.currentBlockIndex = -1;
594+
this.unsetCurrentBlock();
595595
this.insert();
596596
this.currentBlock.firstInput.focus();
597597
}
@@ -873,7 +873,7 @@ export default class BlockManager extends Module {
873873
* Sets current Block Index -1 which means unknown
874874
* and clear highlights
875875
*/
876-
public dropPointer(): void {
876+
public unsetCurrentBlock(): void {
877877
this.currentBlockIndex = -1;
878878
}
879879

@@ -895,7 +895,7 @@ export default class BlockManager extends Module {
895895

896896
await queue.completed;
897897

898-
this.dropPointer();
898+
this.unsetCurrentBlock();
899899

900900
if (needToAddDefaultBlock) {
901901
this.insert();

src/components/modules/ui.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* @type {UI}
66
*/
77
import Module from '../__module';
8-
import $ from '../dom';
8+
import $, { toggleEmptyMark } from '../dom';
99
import * as _ from '../utils';
1010

1111
import Selection from '../selection';
@@ -380,6 +380,12 @@ export default class UI extends Module<UINodes> {
380380
* Start watching 'block-hovered' events that is used by Toolbar for moving
381381
*/
382382
this.watchBlockHoveredEvents();
383+
384+
/**
385+
* We have custom logic for providing placeholders for contenteditable elements.
386+
* To make it work, we need to have data-empty mark on empty inputs.
387+
*/
388+
this.enableInputsEmptyMark();
383389
}
384390

385391

@@ -498,7 +504,7 @@ export default class UI extends Module<UINodes> {
498504
/**
499505
* Remove all highlights and remove caret
500506
*/
501-
this.Editor.BlockManager.dropPointer();
507+
this.Editor.BlockManager.unsetCurrentBlock();
502508

503509
/**
504510
* Close Toolbar
@@ -645,12 +651,12 @@ export default class UI extends Module<UINodes> {
645651

646652
if (!clickedInsideOfEditor) {
647653
/**
648-
* Clear highlights and pointer on BlockManager
654+
* Clear pointer on BlockManager
649655
*
650656
* Current page might contain several instances
651657
* Click between instances MUST clear focus, pointers and close toolbars
652658
*/
653-
this.Editor.BlockManager.dropPointer();
659+
this.Editor.BlockManager.unsetCurrentBlock();
654660
this.Editor.Toolbar.close();
655661
}
656662

@@ -874,4 +880,28 @@ export default class UI extends Module<UINodes> {
874880

875881
this.Editor.InlineToolbar.tryToShow(true);
876882
}
883+
884+
/**
885+
* Editor.js provides and ability to show placeholders for empty contenteditable elements
886+
*
887+
* This method watches for input and focus events and toggles 'data-empty' attribute
888+
* to workaroud the case, when inputs contains only <br>s and has no visible content
889+
* Then, CSS could rely on this attribute to show placeholders
890+
*/
891+
private enableInputsEmptyMark(): void {
892+
/**
893+
* Toggle data-empty attribute on input depending on its emptiness
894+
*
895+
* @param event - input or focus event
896+
*/
897+
function handleInputOrFocusChange(event: Event): void {
898+
const input = event.target as HTMLElement;
899+
900+
toggleEmptyMark(input);
901+
}
902+
903+
this.readOnlyMutableListeners.on(this.nodes.wrapper, 'input', handleInputOrFocusChange);
904+
this.readOnlyMutableListeners.on(this.nodes.wrapper, 'focusin', handleInputOrFocusChange);
905+
this.readOnlyMutableListeners.on(this.nodes.wrapper, 'focusout', handleInputOrFocusChange);
906+
}
877907
}

src/components/utils/mutations.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
export function isMutationBelongsToElement(mutationRecord: MutationRecord, element: Element): boolean {
88
const { type, target, addedNodes, removedNodes } = mutationRecord;
99

10+
/**
11+
* Skip own technical mutations, for example, data-empty attribute changes
12+
*/
13+
if (mutationRecord.type === 'attributes' && mutationRecord.attributeName === 'data-empty') {
14+
return false;
15+
}
16+
1017
/**
1118
* Covers all types of mutations happened to the element or it's descendants with the only one exception - removing/adding the element itself;
1219
*/

src/styles/main.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@
1111
@import './input.css';
1212
@import './popover.css';
1313
@import './popover-inline.css';
14+
@import './placeholders.css';
1415

src/styles/placeholders.css

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
2+
/**
3+
* We support two types of placeholders for contenteditable:
4+
*
5+
* 1. Regular-like placeholders. Will be visible when element is empty.
6+
-- Best choice for rare-used blocks like Headings.
7+
* 2. Current-block placeholders. Will be visible when element is empty and the block is focused.
8+
-- Best choice for common-used blocks like Paragraphs.
9+
*/
10+
:root {
11+
--placeholder {
12+
pointer-events: none;
13+
color: var(--grayText);
14+
cursor: text;
15+
}
16+
}
17+
18+
.codex-editor {
19+
/**
20+
* Use [data-placeholder="..."] to always show a placeholder on empty contenteditable.
21+
*/
22+
[data-placeholder]:empty,
23+
[data-placeholder][data-empty="true"] {
24+
&::before {
25+
@apply --placeholder;
26+
27+
content: attr(data-placeholder);
28+
}
29+
}
30+
31+
/**
32+
* Use [data-placeholder-active="..."] to show a placeholder on empty contenteditable in current block.
33+
*/
34+
[data-placeholder-active]:empty,
35+
[data-placeholder-active][data-empty="true"] {
36+
/* Paragraph tool shows the placeholder for the first block, event it is not focused, so we need to prepare styles for it */
37+
&::before {
38+
@apply --placeholder;
39+
}
40+
41+
&:focus::before {
42+
content: attr(data-placeholder-active);
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)