/
inlinehighlight.ts
95 lines (86 loc) · 3.29 KB
/
inlinehighlight.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module typing/utils/inlinehighlight
*/
import findAttributeRange from './findattributerange.js';
import type { Editor } from '@ckeditor/ckeditor5-core';
import type { ViewElement } from '@ckeditor/ckeditor5-engine';
/**
* Adds a visual highlight style to an attribute element in which the selection is anchored.
* Together with two-step caret movement, they indicate that the user is typing inside the element.
*
* Highlight is turned on by adding the given class to the attribute element in the view:
*
* * The class is removed before the conversion has started, as callbacks added with the `'highest'` priority
* to {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} events.
* * The class is added in the view post fixer, after other changes in the model tree were converted to the view.
*
* This way, adding and removing the highlight does not interfere with conversion.
*
* Usage:
*
* ```ts
* import inlineHighlight from '@ckeditor/ckeditor5-typing/src/utils/inlinehighlight';
*
* // Make `ck-link_selected` class be applied on an `a` element
* // whenever the corresponding `linkHref` attribute element is selected.
* inlineHighlight( editor, 'linkHref', 'a', 'ck-link_selected' );
* ```
*
* @param editor The editor instance.
* @param attributeName The attribute name to check.
* @param tagName The tagName of a view item.
* @param className The class name to apply in the view.
*/
export default function inlineHighlight(
editor: Editor,
attributeName: string,
tagName: string,
className: string
): void {
const view = editor.editing.view;
const highlightedElements = new Set<ViewElement>();
// Adding the class.
view.document.registerPostFixer( writer => {
const selection = editor.model.document.selection;
let changed = false;
if ( selection.hasAttribute( attributeName ) ) {
const modelRange = findAttributeRange(
selection.getFirstPosition()!,
attributeName,
selection.getAttribute( attributeName ),
editor.model
);
const viewRange = editor.editing.mapper.toViewRange( modelRange );
// There might be multiple view elements in the `viewRange`, for example, when the `a` element is
// broken by a UIElement.
for ( const item of viewRange.getItems() ) {
if ( item.is( 'element', tagName ) && !item.hasClass( className ) ) {
writer.addClass( className, item );
highlightedElements.add( item );
changed = true;
}
}
}
return changed;
} );
// Removing the class.
editor.conversion.for( 'editingDowncast' ).add( dispatcher => {
// Make sure the highlight is removed on every possible event, before conversion is started.
dispatcher.on( 'insert', removeHighlight, { priority: 'highest' } );
dispatcher.on( 'remove', removeHighlight, { priority: 'highest' } );
dispatcher.on( 'attribute', removeHighlight, { priority: 'highest' } );
dispatcher.on( 'selection', removeHighlight, { priority: 'highest' } );
function removeHighlight() {
view.change( writer => {
for ( const item of highlightedElements.values() ) {
writer.removeClass( className, item );
highlightedElements.delete( item );
}
} );
}
} );
}