-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Expand file tree
/
Copy pathremoveformatcommand.ts
More file actions
176 lines (151 loc) · 5.18 KB
/
removeformatcommand.ts
File metadata and controls
176 lines (151 loc) · 5.18 KB
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module remove-format/removeformatcommand
*/
import type { ModelDocumentSelection, ModelItem, ModelRange, ModelWriter } from 'ckeditor5/src/engine.js';
import { Command } from 'ckeditor5/src/core.js';
import { first } from 'ckeditor5/src/utils.js';
/**
* The remove format command.
*
* It is used by the {@link module:remove-format/removeformat~RemoveFormat remove format feature}
* to clear the formatting in the selection.
*
* ```ts
* editor.execute( 'removeFormat' );
* ```
*/
export class RemoveFormatCommand extends Command {
declare public value: boolean;
/**
* List of all registered custom attribute handlers.
*/
private _customAttributesHandlers: Array<{
isFormatting: IsFormattingCallback;
removeFormatting: RemoveFormattingCallback;
}> = [];
/**
* @inheritDoc
*/
public override refresh(): void {
const model = this.editor.model;
this.isEnabled = !!first( this._getFormattingItems( model.document.selection ) );
}
/**
* @inheritDoc
*/
public override execute(): void {
const model = this.editor.model;
model.change( writer => {
for ( const item of this._getFormattingItems( model.document.selection ) ) {
if ( item.is( 'selection' ) ) {
for ( const attributeName of this._getFormattingAttributes( item ) ) {
writer.removeSelectionAttribute( attributeName );
}
} else {
// Workaround for items with multiple removable attributes. See
// https://github.com/ckeditor/ckeditor5-remove-format/pull/1#pullrequestreview-220515609
const itemRange = writer.createRangeOn( item );
for ( const attributeName of this._getFormattingAttributes( item ) ) {
this._removeFormatting( attributeName, item, itemRange, writer );
}
}
}
} );
}
/**
* Registers a custom attribute handler that will be used to determine if an attribute is formatting and how to remove it.
*
* @internal
*/
public registerCustomAttribute( isFormatting: IsFormattingCallback, removeFormatting: RemoveFormattingCallback ): void {
this._customAttributesHandlers.push( {
isFormatting,
removeFormatting
} );
}
/**
* Helper method that removes a formatting attribute from an item either using custom callbacks or writer remove attribute.
*/
private _removeFormatting( attributeName: string, item: ModelItem, itemRange: ModelRange, writer: ModelWriter ) {
let customHandled = false;
for ( const { isFormatting, removeFormatting } of this._customAttributesHandlers ) {
if ( isFormatting( attributeName, item ) ) {
removeFormatting( attributeName, itemRange, writer );
customHandled = true;
}
}
if ( !customHandled ) {
writer.removeAttribute( attributeName, itemRange );
}
}
/**
* Returns an iterable of items in a selection (including the selection itself) that have formatting model
* attributes to be removed by the feature.
*/
private* _getFormattingItems( selection: ModelDocumentSelection ) {
const model = this.editor.model;
const schema = model.schema;
const itemHasRemovableFormatting = ( item: ModelItem | ModelDocumentSelection ) => {
return !!first( this._getFormattingAttributes( item ) );
};
// Check formatting on selected items.
for ( const curRange of selection.getRanges() ) {
for ( const item of curRange.getItems() ) {
// Ignore last block if range ends at the beginning of it.
if ( schema.isBlock( item ) && curRange.end.isTouching( model.createPositionAt( item, 0 ) ) ) {
continue;
}
if ( itemHasRemovableFormatting( item ) ) {
yield item;
}
}
}
// Check formatting from selected blocks (to include partly selected blocks).
for ( const block of selection.getSelectedBlocks() ) {
if ( itemHasRemovableFormatting( block ) ) {
yield block;
}
}
// Finally the selection might be formatted as well, so make sure to check it.
if ( itemHasRemovableFormatting( selection ) ) {
yield selection;
}
}
/**
* Returns an iterable of formatting attributes of a given model item.
*
* **Note:** Formatting items have the `isFormatting` property set to `true`.
*
* @returns The names of formatting attributes found in a given item.
*/
private* _getFormattingAttributes( item: ModelItem | ModelDocumentSelection ) {
const schema = this.editor.model.schema;
for ( const [ attributeName ] of item.getAttributes() ) {
for ( const { isFormatting } of this._customAttributesHandlers ) {
if ( isFormatting( attributeName, item ) ) {
yield attributeName;
}
}
const attributeProperties = schema.getAttributeProperties( attributeName );
if ( attributeProperties && attributeProperties.isFormatting ) {
yield attributeName;
}
}
}
}
/**
* Callback that checks if an attribute is a formatting attribute.
*
* @internal
*/
export type IsFormattingCallback = ( attributeName: string, item: ModelItem | ModelDocumentSelection ) => boolean;
/**
* Callback that removes formatting from an item.
*
* @internal
*/
export type RemoveFormattingCallback = ( attributeName: string, range: ModelRange, writer: ModelWriter ) => void;