-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
/
attributecommand.js
138 lines (124 loc) · 4.88 KB
/
attributecommand.js
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
/**
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module basic-styles/attributecommand
*/
import { Command } from 'ckeditor5/src/core';
/**
* An extension of the base {@link module:core/command~Command} class, which provides utilities for a command
* that toggles a single attribute on a text or an element.
*
* `AttributeCommand` uses {@link module:engine/model/document~Document#selection}
* to decide which nodes (if any) should be changed, and applies or removes the attribute from them.
*
* The command checks the {@link module:engine/model/model~Model#schema} to decide if it can be enabled
* for the current selection and to which nodes the attribute can be applied.
*
* @extends module:core/command~Command
*/
export default class AttributeCommand extends Command {
/**
* @param {module:core/editor/editor~Editor} editor
* @param {String} attributeKey Attribute that will be set by the command.
*/
constructor( editor, attributeKey ) {
super( editor );
/**
* The attribute that will be set by the command.
*
* @readonly
* @member {String}
*/
this.attributeKey = attributeKey;
/**
* Flag indicating whether the command is active. The command is active when the
* {@link module:engine/model/selection~Selection#hasAttribute selection has the attribute} which means that:
*
* * If the selection is not empty – That the attribute is set on the first node in the selection that allows this attribute.
* * If the selection is empty – That the selection has the attribute itself (which means that newly typed
* text will have this attribute, too).
*
* @observable
* @readonly
* @member {Boolean} #value
*/
}
/**
* Updates the command's {@link #value} and {@link #isEnabled} based on the current selection.
*/
refresh() {
const model = this.editor.model;
const doc = model.document;
this.value = this._getValueFromFirstAllowedNode();
this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, this.attributeKey );
}
/**
* Executes the command — applies the attribute to the selection or removes it from the selection.
*
* If the command is active (`value == true`), it will remove attributes. Otherwise, it will set attributes.
*
* The execution result differs, depending on the {@link module:engine/model/document~Document#selection}:
*
* * If the selection is on a range, the command applies the attribute to all nodes in that range
* (if they are allowed to have this attribute by the {@link module:engine/model/schema~Schema schema}).
* * If the selection is collapsed in a non-empty node, the command applies the attribute to the
* {@link module:engine/model/document~Document#selection} itself (note that typed characters copy attributes from the selection).
* * If the selection is collapsed in an empty node, the command applies the attribute to the parent node of the selection (note
* that the selection inherits all attributes from a node if it is in an empty node).
*
* @fires execute
* @param {Object} [options] Command options.
* @param {Boolean} [options.forceValue] If set, it will force the command behavior. If `true`, the command will apply the attribute,
* otherwise the command will remove the attribute.
* If not set, the command will look for its current value to decide what it should do.
*/
execute( options = {} ) {
const model = this.editor.model;
const doc = model.document;
const selection = doc.selection;
const value = ( options.forceValue === undefined ) ? !this.value : options.forceValue;
model.change( writer => {
if ( selection.isCollapsed ) {
if ( value ) {
writer.setSelectionAttribute( this.attributeKey, true );
} else {
writer.removeSelectionAttribute( this.attributeKey );
}
} else {
const ranges = model.schema.getValidRanges( selection.getRanges(), this.attributeKey );
for ( const range of ranges ) {
if ( value ) {
writer.setAttribute( this.attributeKey, value, range );
} else {
writer.removeAttribute( this.attributeKey, range );
}
}
}
} );
}
/**
* Checks the attribute value of the first node in the selection that allows the attribute.
* For the collapsed selection returns the selection attribute.
*
* @private
* @returns {Boolean} The attribute value.
*/
_getValueFromFirstAllowedNode() {
const model = this.editor.model;
const schema = model.schema;
const selection = model.document.selection;
if ( selection.isCollapsed ) {
return selection.hasAttribute( this.attributeKey );
}
for ( const range of selection.getRanges() ) {
for ( const item of range.getItems() ) {
if ( schema.checkAttribute( item, this.attributeKey ) ) {
return item.hasAttribute( this.attributeKey );
}
}
}
return false;
}
}