@ckeditor/ckeditor5-link 39.0.1 → 40.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -1
- package/LICENSE.md +1 -1
- package/README.md +3 -3
- package/build/link.js.map +1 -0
- package/build/translations/de.js +1 -1
- package/build/translations/hy.js +1 -0
- package/build/translations/pt-br.js +1 -1
- package/lang/translations/ar.po +1 -0
- package/lang/translations/ast.po +1 -0
- package/lang/translations/az.po +1 -0
- package/lang/translations/bg.po +1 -0
- package/lang/translations/bn.po +1 -0
- package/lang/translations/ca.po +1 -0
- package/lang/translations/cs.po +1 -0
- package/lang/translations/da.po +1 -0
- package/lang/translations/de-ch.po +1 -0
- package/lang/translations/de.po +2 -1
- package/lang/translations/el.po +1 -0
- package/lang/translations/en-au.po +1 -0
- package/lang/translations/en-gb.po +1 -0
- package/lang/translations/en.po +1 -0
- package/lang/translations/eo.po +1 -0
- package/lang/translations/es.po +1 -0
- package/lang/translations/et.po +1 -0
- package/lang/translations/eu.po +1 -0
- package/lang/translations/fa.po +1 -0
- package/lang/translations/fi.po +1 -0
- package/lang/translations/fr.po +1 -0
- package/lang/translations/gl.po +1 -0
- package/lang/translations/he.po +1 -0
- package/lang/translations/hi.po +1 -0
- package/lang/translations/hr.po +1 -0
- package/lang/translations/hu.po +1 -0
- package/lang/translations/hy.po +54 -0
- package/lang/translations/id.po +1 -0
- package/lang/translations/it.po +1 -0
- package/lang/translations/ja.po +1 -0
- package/lang/translations/km.po +1 -0
- package/lang/translations/kn.po +1 -0
- package/lang/translations/ko.po +1 -0
- package/lang/translations/ku.po +1 -0
- package/lang/translations/lt.po +1 -0
- package/lang/translations/lv.po +1 -0
- package/lang/translations/ms.po +1 -0
- package/lang/translations/nb.po +1 -0
- package/lang/translations/ne.po +1 -0
- package/lang/translations/nl.po +1 -0
- package/lang/translations/no.po +1 -0
- package/lang/translations/pl.po +1 -0
- package/lang/translations/pt-br.po +3 -2
- package/lang/translations/pt.po +1 -0
- package/lang/translations/ro.po +1 -0
- package/lang/translations/ru.po +1 -0
- package/lang/translations/sk.po +1 -0
- package/lang/translations/sq.po +1 -0
- package/lang/translations/sr-latn.po +1 -0
- package/lang/translations/sr.po +1 -0
- package/lang/translations/sv.po +1 -0
- package/lang/translations/th.po +1 -0
- package/lang/translations/tk.po +1 -0
- package/lang/translations/tr.po +1 -0
- package/lang/translations/tt.po +1 -0
- package/lang/translations/ug.po +1 -0
- package/lang/translations/uk.po +1 -0
- package/lang/translations/ur.po +1 -0
- package/lang/translations/uz.po +1 -0
- package/lang/translations/vi.po +1 -0
- package/lang/translations/zh-cn.po +1 -0
- package/lang/translations/zh.po +1 -0
- package/package.json +3 -7
- package/src/augmentation.d.ts +30 -30
- package/src/augmentation.js +5 -5
- package/src/autolink.d.ts +60 -60
- package/src/autolink.js +216 -216
- package/src/index.d.ts +18 -18
- package/src/index.js +17 -17
- package/src/link.d.ts +27 -27
- package/src/link.js +31 -31
- package/src/linkcommand.d.ts +132 -132
- package/src/linkcommand.js +285 -285
- package/src/linkconfig.d.ts +251 -251
- package/src/linkconfig.js +5 -5
- package/src/linkediting.d.ts +106 -106
- package/src/linkediting.js +547 -547
- package/src/linkimage.d.ts +27 -27
- package/src/linkimage.js +31 -31
- package/src/linkimageediting.d.ts +39 -39
- package/src/linkimageediting.js +245 -245
- package/src/linkimageui.d.ts +40 -40
- package/src/linkimageui.js +96 -96
- package/src/linkui.d.ts +165 -165
- package/src/linkui.js +581 -581
- package/src/ui/linkactionsview.d.ts +101 -101
- package/src/ui/linkactionsview.js +156 -156
- package/src/ui/linkformview.d.ts +141 -141
- package/src/ui/linkformview.js +232 -232
- package/src/unlinkcommand.d.ts +31 -31
- package/src/unlinkcommand.js +66 -66
- package/src/utils/automaticdecorators.d.ts +45 -45
- package/src/utils/automaticdecorators.js +140 -140
- package/src/utils/manualdecorator.d.ts +72 -72
- package/src/utils/manualdecorator.js +47 -47
- package/src/utils.d.ts +80 -80
- package/src/utils.js +128 -128
package/src/linkcommand.js
CHANGED
|
@@ -1,285 +1,285 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* @module link/linkcommand
|
|
7
|
-
*/
|
|
8
|
-
import { Command } from 'ckeditor5/src/core';
|
|
9
|
-
import { findAttributeRange } from 'ckeditor5/src/typing';
|
|
10
|
-
import { Collection, first, toMap } from 'ckeditor5/src/utils';
|
|
11
|
-
import AutomaticDecorators from './utils/automaticdecorators';
|
|
12
|
-
import { isLinkableElement } from './utils';
|
|
13
|
-
/**
|
|
14
|
-
* The link command. It is used by the {@link module:link/link~Link link feature}.
|
|
15
|
-
*/
|
|
16
|
-
export default class LinkCommand extends Command {
|
|
17
|
-
constructor() {
|
|
18
|
-
super(...arguments);
|
|
19
|
-
/**
|
|
20
|
-
* A collection of {@link module:link/utils/manualdecorator~ManualDecorator manual decorators}
|
|
21
|
-
* corresponding to the {@link module:link/linkconfig~LinkConfig#decorators decorator configuration}.
|
|
22
|
-
*
|
|
23
|
-
* You can consider it a model with states of manual decorators added to the currently selected link.
|
|
24
|
-
*/
|
|
25
|
-
this.manualDecorators = new Collection();
|
|
26
|
-
/**
|
|
27
|
-
* An instance of the helper that ties together all {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition}
|
|
28
|
-
* that are used by the {@glink features/link link} and the {@glink features/images/images-linking linking images} features.
|
|
29
|
-
*/
|
|
30
|
-
this.automaticDecorators = new AutomaticDecorators();
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Synchronizes the state of {@link #manualDecorators} with the currently present elements in the model.
|
|
34
|
-
*/
|
|
35
|
-
restoreManualDecoratorStates() {
|
|
36
|
-
for (const manualDecorator of this.manualDecorators) {
|
|
37
|
-
manualDecorator.value = this._getDecoratorStateFromModel(manualDecorator.id);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* @inheritDoc
|
|
42
|
-
*/
|
|
43
|
-
refresh() {
|
|
44
|
-
const model = this.editor.model;
|
|
45
|
-
const selection = model.document.selection;
|
|
46
|
-
const selectedElement = selection.getSelectedElement() || first(selection.getSelectedBlocks());
|
|
47
|
-
// A check for any integration that allows linking elements (e.g. `LinkImage`).
|
|
48
|
-
// Currently the selection reads attributes from text nodes only. See #7429 and #7465.
|
|
49
|
-
if (isLinkableElement(selectedElement, model.schema)) {
|
|
50
|
-
this.value = selectedElement.getAttribute('linkHref');
|
|
51
|
-
this.isEnabled = model.schema.checkAttribute(selectedElement, 'linkHref');
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
this.value = selection.getAttribute('linkHref');
|
|
55
|
-
this.isEnabled = model.schema.checkAttributeInSelection(selection, 'linkHref');
|
|
56
|
-
}
|
|
57
|
-
for (const manualDecorator of this.manualDecorators) {
|
|
58
|
-
manualDecorator.value = this._getDecoratorStateFromModel(manualDecorator.id);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Executes the command.
|
|
63
|
-
*
|
|
64
|
-
* When the selection is non-collapsed, the `linkHref` attribute will be applied to nodes inside the selection, but only to
|
|
65
|
-
* those nodes where the `linkHref` attribute is allowed (disallowed nodes will be omitted).
|
|
66
|
-
*
|
|
67
|
-
* When the selection is collapsed and is not inside the text with the `linkHref` attribute, a
|
|
68
|
-
* new {@link module:engine/model/text~Text text node} with the `linkHref` attribute will be inserted in place of the caret, but
|
|
69
|
-
* only if such element is allowed in this place. The `_data` of the inserted text will equal the `href` parameter.
|
|
70
|
-
* The selection will be updated to wrap the just inserted text node.
|
|
71
|
-
*
|
|
72
|
-
* When the selection is collapsed and inside the text with the `linkHref` attribute, the attribute value will be updated.
|
|
73
|
-
*
|
|
74
|
-
* # Decorators and model attribute management
|
|
75
|
-
*
|
|
76
|
-
* There is an optional argument to this command that applies or removes model
|
|
77
|
-
* {@glink framework/architecture/editing-engine#text-attributes text attributes} brought by
|
|
78
|
-
* {@link module:link/utils/manualdecorator~ManualDecorator manual link decorators}.
|
|
79
|
-
*
|
|
80
|
-
* Text attribute names in the model correspond to the entries in the {@link module:link/linkconfig~LinkConfig#decorators
|
|
81
|
-
* configuration}.
|
|
82
|
-
* For every decorator configured, a model text attribute exists with the "link" prefix. For example, a `'linkMyDecorator'` attribute
|
|
83
|
-
* corresponds to `'myDecorator'` in the configuration.
|
|
84
|
-
*
|
|
85
|
-
* To learn more about link decorators, check out the {@link module:link/linkconfig~LinkConfig#decorators `config.link.decorators`}
|
|
86
|
-
* documentation.
|
|
87
|
-
*
|
|
88
|
-
* Here is how to manage decorator attributes with the link command:
|
|
89
|
-
*
|
|
90
|
-
* ```ts
|
|
91
|
-
* const linkCommand = editor.commands.get( 'link' );
|
|
92
|
-
*
|
|
93
|
-
* // Adding a new decorator attribute.
|
|
94
|
-
* linkCommand.execute( 'http://example.com', {
|
|
95
|
-
* linkIsExternal: true
|
|
96
|
-
* } );
|
|
97
|
-
*
|
|
98
|
-
* // Removing a decorator attribute from the selection.
|
|
99
|
-
* linkCommand.execute( 'http://example.com', {
|
|
100
|
-
* linkIsExternal: false
|
|
101
|
-
* } );
|
|
102
|
-
*
|
|
103
|
-
* // Adding multiple decorator attributes at the same time.
|
|
104
|
-
* linkCommand.execute( 'http://example.com', {
|
|
105
|
-
* linkIsExternal: true,
|
|
106
|
-
* linkIsDownloadable: true,
|
|
107
|
-
* } );
|
|
108
|
-
*
|
|
109
|
-
* // Removing and adding decorator attributes at the same time.
|
|
110
|
-
* linkCommand.execute( 'http://example.com', {
|
|
111
|
-
* linkIsExternal: false,
|
|
112
|
-
* linkFoo: true,
|
|
113
|
-
* linkIsDownloadable: false,
|
|
114
|
-
* } );
|
|
115
|
-
* ```
|
|
116
|
-
*
|
|
117
|
-
* **Note**: If the decorator attribute name is not specified, its state remains untouched.
|
|
118
|
-
*
|
|
119
|
-
* **Note**: {@link module:link/unlinkcommand~UnlinkCommand#execute `UnlinkCommand#execute()`} removes all
|
|
120
|
-
* decorator attributes.
|
|
121
|
-
*
|
|
122
|
-
* @fires execute
|
|
123
|
-
* @param href Link destination.
|
|
124
|
-
* @param manualDecoratorIds The information about manual decorator attributes to be applied or removed upon execution.
|
|
125
|
-
*/
|
|
126
|
-
execute(href, manualDecoratorIds = {}) {
|
|
127
|
-
const model = this.editor.model;
|
|
128
|
-
const selection = model.document.selection;
|
|
129
|
-
// Stores information about manual decorators to turn them on/off when command is applied.
|
|
130
|
-
const truthyManualDecorators = [];
|
|
131
|
-
const falsyManualDecorators = [];
|
|
132
|
-
for (const name in manualDecoratorIds) {
|
|
133
|
-
if (manualDecoratorIds[name]) {
|
|
134
|
-
truthyManualDecorators.push(name);
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
falsyManualDecorators.push(name);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
model.change(writer => {
|
|
141
|
-
// If selection is collapsed then update selected link or insert new one at the place of caret.
|
|
142
|
-
if (selection.isCollapsed) {
|
|
143
|
-
const position = selection.getFirstPosition();
|
|
144
|
-
// When selection is inside text with `linkHref` attribute.
|
|
145
|
-
if (selection.hasAttribute('linkHref')) {
|
|
146
|
-
const linkText = extractTextFromSelection(selection);
|
|
147
|
-
// Then update `linkHref` value.
|
|
148
|
-
let linkRange = findAttributeRange(position, 'linkHref', selection.getAttribute('linkHref'), model);
|
|
149
|
-
if (selection.getAttribute('linkHref') === linkText) {
|
|
150
|
-
linkRange = this._updateLinkContent(model, writer, linkRange, href);
|
|
151
|
-
}
|
|
152
|
-
writer.setAttribute('linkHref', href, linkRange);
|
|
153
|
-
truthyManualDecorators.forEach(item => {
|
|
154
|
-
writer.setAttribute(item, true, linkRange);
|
|
155
|
-
});
|
|
156
|
-
falsyManualDecorators.forEach(item => {
|
|
157
|
-
writer.removeAttribute(item, linkRange);
|
|
158
|
-
});
|
|
159
|
-
// Put the selection at the end of the updated link.
|
|
160
|
-
writer.setSelection(writer.createPositionAfter(linkRange.end.nodeBefore));
|
|
161
|
-
}
|
|
162
|
-
// If not then insert text node with `linkHref` attribute in place of caret.
|
|
163
|
-
// However, since selection is collapsed, attribute value will be used as data for text node.
|
|
164
|
-
// So, if `href` is empty, do not create text node.
|
|
165
|
-
else if (href !== '') {
|
|
166
|
-
const attributes = toMap(selection.getAttributes());
|
|
167
|
-
attributes.set('linkHref', href);
|
|
168
|
-
truthyManualDecorators.forEach(item => {
|
|
169
|
-
attributes.set(item, true);
|
|
170
|
-
});
|
|
171
|
-
const { end: positionAfter } = model.insertContent(writer.createText(href, attributes), position);
|
|
172
|
-
// Put the selection at the end of the inserted link.
|
|
173
|
-
// Using end of range returned from insertContent in case nodes with the same attributes got merged.
|
|
174
|
-
writer.setSelection(positionAfter);
|
|
175
|
-
}
|
|
176
|
-
// Remove the `linkHref` attribute and all link decorators from the selection.
|
|
177
|
-
// It stops adding a new content into the link element.
|
|
178
|
-
['linkHref', ...truthyManualDecorators, ...falsyManualDecorators].forEach(item => {
|
|
179
|
-
writer.removeSelectionAttribute(item);
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
// If selection has non-collapsed ranges, we change attribute on nodes inside those ranges
|
|
184
|
-
// omitting nodes where the `linkHref` attribute is disallowed.
|
|
185
|
-
const ranges = model.schema.getValidRanges(selection.getRanges(), 'linkHref');
|
|
186
|
-
// But for the first, check whether the `linkHref` attribute is allowed on selected blocks (e.g. the "image" element).
|
|
187
|
-
const allowedRanges = [];
|
|
188
|
-
for (const element of selection.getSelectedBlocks()) {
|
|
189
|
-
if (model.schema.checkAttribute(element, 'linkHref')) {
|
|
190
|
-
allowedRanges.push(writer.createRangeOn(element));
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
// Ranges that accept the `linkHref` attribute. Since we will iterate over `allowedRanges`, let's clone it.
|
|
194
|
-
const rangesToUpdate = allowedRanges.slice();
|
|
195
|
-
// For all selection ranges we want to check whether given range is inside an element that accepts the `linkHref` attribute.
|
|
196
|
-
// If so, we don't want to propagate applying the attribute to its children.
|
|
197
|
-
for (const range of ranges) {
|
|
198
|
-
if (this._isRangeToUpdate(range, allowedRanges)) {
|
|
199
|
-
rangesToUpdate.push(range);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
for (const range of rangesToUpdate) {
|
|
203
|
-
let linkRange = range;
|
|
204
|
-
if (rangesToUpdate.length === 1) {
|
|
205
|
-
// Current text of the link in the document.
|
|
206
|
-
const linkText = extractTextFromSelection(selection);
|
|
207
|
-
if (selection.getAttribute('linkHref') === linkText) {
|
|
208
|
-
linkRange = this._updateLinkContent(model, writer, range, href);
|
|
209
|
-
writer.setSelection(writer.createSelection(linkRange));
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
writer.setAttribute('linkHref', href, linkRange);
|
|
213
|
-
truthyManualDecorators.forEach(item => {
|
|
214
|
-
writer.setAttribute(item, true, linkRange);
|
|
215
|
-
});
|
|
216
|
-
falsyManualDecorators.forEach(item => {
|
|
217
|
-
writer.removeAttribute(item, linkRange);
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Provides information whether a decorator with a given name is present in the currently processed selection.
|
|
225
|
-
*
|
|
226
|
-
* @param decoratorName The name of the manual decorator used in the model
|
|
227
|
-
* @returns The information whether a given decorator is currently present in the selection.
|
|
228
|
-
*/
|
|
229
|
-
_getDecoratorStateFromModel(decoratorName) {
|
|
230
|
-
const model = this.editor.model;
|
|
231
|
-
const selection = model.document.selection;
|
|
232
|
-
const selectedElement = selection.getSelectedElement();
|
|
233
|
-
// A check for the `LinkImage` plugin. If the selection contains an element, get values from the element.
|
|
234
|
-
// Currently the selection reads attributes from text nodes only. See #7429 and #7465.
|
|
235
|
-
if (isLinkableElement(selectedElement, model.schema)) {
|
|
236
|
-
return selectedElement.getAttribute(decoratorName);
|
|
237
|
-
}
|
|
238
|
-
return selection.getAttribute(decoratorName);
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Checks whether specified `range` is inside an element that accepts the `linkHref` attribute.
|
|
242
|
-
*
|
|
243
|
-
* @param range A range to check.
|
|
244
|
-
* @param allowedRanges An array of ranges created on elements where the attribute is accepted.
|
|
245
|
-
*/
|
|
246
|
-
_isRangeToUpdate(range, allowedRanges) {
|
|
247
|
-
for (const allowedRange of allowedRanges) {
|
|
248
|
-
// A range is inside an element that will have the `linkHref` attribute. Do not modify its nodes.
|
|
249
|
-
if (allowedRange.containsRange(range)) {
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return true;
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* Updates selected link with a new value as its content and as its href attribute.
|
|
257
|
-
*
|
|
258
|
-
* @param model Model is need to insert content.
|
|
259
|
-
* @param writer Writer is need to create text element in model.
|
|
260
|
-
* @param range A range where should be inserted content.
|
|
261
|
-
* @param href A link value which should be in the href attribute and in the content.
|
|
262
|
-
*/
|
|
263
|
-
_updateLinkContent(model, writer, range, href) {
|
|
264
|
-
const text = writer.createText(href, { linkHref: href });
|
|
265
|
-
return model.insertContent(text, range);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
// Returns a text of a link under the collapsed selection or a selection that contains the entire link.
|
|
269
|
-
function extractTextFromSelection(selection) {
|
|
270
|
-
if (selection.isCollapsed) {
|
|
271
|
-
const firstPosition = selection.getFirstPosition();
|
|
272
|
-
return firstPosition.textNode && firstPosition.textNode.data;
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
const rangeItems = Array.from(selection.getFirstRange().getItems());
|
|
276
|
-
if (rangeItems.length > 1) {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
const firstNode = rangeItems[0];
|
|
280
|
-
if (firstNode.is('$text') || firstNode.is('$textProxy')) {
|
|
281
|
-
return firstNode.data;
|
|
282
|
-
}
|
|
283
|
-
return null;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* @module link/linkcommand
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'ckeditor5/src/core';
|
|
9
|
+
import { findAttributeRange } from 'ckeditor5/src/typing';
|
|
10
|
+
import { Collection, first, toMap } from 'ckeditor5/src/utils';
|
|
11
|
+
import AutomaticDecorators from './utils/automaticdecorators';
|
|
12
|
+
import { isLinkableElement } from './utils';
|
|
13
|
+
/**
|
|
14
|
+
* The link command. It is used by the {@link module:link/link~Link link feature}.
|
|
15
|
+
*/
|
|
16
|
+
export default class LinkCommand extends Command {
|
|
17
|
+
constructor() {
|
|
18
|
+
super(...arguments);
|
|
19
|
+
/**
|
|
20
|
+
* A collection of {@link module:link/utils/manualdecorator~ManualDecorator manual decorators}
|
|
21
|
+
* corresponding to the {@link module:link/linkconfig~LinkConfig#decorators decorator configuration}.
|
|
22
|
+
*
|
|
23
|
+
* You can consider it a model with states of manual decorators added to the currently selected link.
|
|
24
|
+
*/
|
|
25
|
+
this.manualDecorators = new Collection();
|
|
26
|
+
/**
|
|
27
|
+
* An instance of the helper that ties together all {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition}
|
|
28
|
+
* that are used by the {@glink features/link link} and the {@glink features/images/images-linking linking images} features.
|
|
29
|
+
*/
|
|
30
|
+
this.automaticDecorators = new AutomaticDecorators();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Synchronizes the state of {@link #manualDecorators} with the currently present elements in the model.
|
|
34
|
+
*/
|
|
35
|
+
restoreManualDecoratorStates() {
|
|
36
|
+
for (const manualDecorator of this.manualDecorators) {
|
|
37
|
+
manualDecorator.value = this._getDecoratorStateFromModel(manualDecorator.id);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* @inheritDoc
|
|
42
|
+
*/
|
|
43
|
+
refresh() {
|
|
44
|
+
const model = this.editor.model;
|
|
45
|
+
const selection = model.document.selection;
|
|
46
|
+
const selectedElement = selection.getSelectedElement() || first(selection.getSelectedBlocks());
|
|
47
|
+
// A check for any integration that allows linking elements (e.g. `LinkImage`).
|
|
48
|
+
// Currently the selection reads attributes from text nodes only. See #7429 and #7465.
|
|
49
|
+
if (isLinkableElement(selectedElement, model.schema)) {
|
|
50
|
+
this.value = selectedElement.getAttribute('linkHref');
|
|
51
|
+
this.isEnabled = model.schema.checkAttribute(selectedElement, 'linkHref');
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
this.value = selection.getAttribute('linkHref');
|
|
55
|
+
this.isEnabled = model.schema.checkAttributeInSelection(selection, 'linkHref');
|
|
56
|
+
}
|
|
57
|
+
for (const manualDecorator of this.manualDecorators) {
|
|
58
|
+
manualDecorator.value = this._getDecoratorStateFromModel(manualDecorator.id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Executes the command.
|
|
63
|
+
*
|
|
64
|
+
* When the selection is non-collapsed, the `linkHref` attribute will be applied to nodes inside the selection, but only to
|
|
65
|
+
* those nodes where the `linkHref` attribute is allowed (disallowed nodes will be omitted).
|
|
66
|
+
*
|
|
67
|
+
* When the selection is collapsed and is not inside the text with the `linkHref` attribute, a
|
|
68
|
+
* new {@link module:engine/model/text~Text text node} with the `linkHref` attribute will be inserted in place of the caret, but
|
|
69
|
+
* only if such element is allowed in this place. The `_data` of the inserted text will equal the `href` parameter.
|
|
70
|
+
* The selection will be updated to wrap the just inserted text node.
|
|
71
|
+
*
|
|
72
|
+
* When the selection is collapsed and inside the text with the `linkHref` attribute, the attribute value will be updated.
|
|
73
|
+
*
|
|
74
|
+
* # Decorators and model attribute management
|
|
75
|
+
*
|
|
76
|
+
* There is an optional argument to this command that applies or removes model
|
|
77
|
+
* {@glink framework/architecture/editing-engine#text-attributes text attributes} brought by
|
|
78
|
+
* {@link module:link/utils/manualdecorator~ManualDecorator manual link decorators}.
|
|
79
|
+
*
|
|
80
|
+
* Text attribute names in the model correspond to the entries in the {@link module:link/linkconfig~LinkConfig#decorators
|
|
81
|
+
* configuration}.
|
|
82
|
+
* For every decorator configured, a model text attribute exists with the "link" prefix. For example, a `'linkMyDecorator'` attribute
|
|
83
|
+
* corresponds to `'myDecorator'` in the configuration.
|
|
84
|
+
*
|
|
85
|
+
* To learn more about link decorators, check out the {@link module:link/linkconfig~LinkConfig#decorators `config.link.decorators`}
|
|
86
|
+
* documentation.
|
|
87
|
+
*
|
|
88
|
+
* Here is how to manage decorator attributes with the link command:
|
|
89
|
+
*
|
|
90
|
+
* ```ts
|
|
91
|
+
* const linkCommand = editor.commands.get( 'link' );
|
|
92
|
+
*
|
|
93
|
+
* // Adding a new decorator attribute.
|
|
94
|
+
* linkCommand.execute( 'http://example.com', {
|
|
95
|
+
* linkIsExternal: true
|
|
96
|
+
* } );
|
|
97
|
+
*
|
|
98
|
+
* // Removing a decorator attribute from the selection.
|
|
99
|
+
* linkCommand.execute( 'http://example.com', {
|
|
100
|
+
* linkIsExternal: false
|
|
101
|
+
* } );
|
|
102
|
+
*
|
|
103
|
+
* // Adding multiple decorator attributes at the same time.
|
|
104
|
+
* linkCommand.execute( 'http://example.com', {
|
|
105
|
+
* linkIsExternal: true,
|
|
106
|
+
* linkIsDownloadable: true,
|
|
107
|
+
* } );
|
|
108
|
+
*
|
|
109
|
+
* // Removing and adding decorator attributes at the same time.
|
|
110
|
+
* linkCommand.execute( 'http://example.com', {
|
|
111
|
+
* linkIsExternal: false,
|
|
112
|
+
* linkFoo: true,
|
|
113
|
+
* linkIsDownloadable: false,
|
|
114
|
+
* } );
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* **Note**: If the decorator attribute name is not specified, its state remains untouched.
|
|
118
|
+
*
|
|
119
|
+
* **Note**: {@link module:link/unlinkcommand~UnlinkCommand#execute `UnlinkCommand#execute()`} removes all
|
|
120
|
+
* decorator attributes.
|
|
121
|
+
*
|
|
122
|
+
* @fires execute
|
|
123
|
+
* @param href Link destination.
|
|
124
|
+
* @param manualDecoratorIds The information about manual decorator attributes to be applied or removed upon execution.
|
|
125
|
+
*/
|
|
126
|
+
execute(href, manualDecoratorIds = {}) {
|
|
127
|
+
const model = this.editor.model;
|
|
128
|
+
const selection = model.document.selection;
|
|
129
|
+
// Stores information about manual decorators to turn them on/off when command is applied.
|
|
130
|
+
const truthyManualDecorators = [];
|
|
131
|
+
const falsyManualDecorators = [];
|
|
132
|
+
for (const name in manualDecoratorIds) {
|
|
133
|
+
if (manualDecoratorIds[name]) {
|
|
134
|
+
truthyManualDecorators.push(name);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
falsyManualDecorators.push(name);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
model.change(writer => {
|
|
141
|
+
// If selection is collapsed then update selected link or insert new one at the place of caret.
|
|
142
|
+
if (selection.isCollapsed) {
|
|
143
|
+
const position = selection.getFirstPosition();
|
|
144
|
+
// When selection is inside text with `linkHref` attribute.
|
|
145
|
+
if (selection.hasAttribute('linkHref')) {
|
|
146
|
+
const linkText = extractTextFromSelection(selection);
|
|
147
|
+
// Then update `linkHref` value.
|
|
148
|
+
let linkRange = findAttributeRange(position, 'linkHref', selection.getAttribute('linkHref'), model);
|
|
149
|
+
if (selection.getAttribute('linkHref') === linkText) {
|
|
150
|
+
linkRange = this._updateLinkContent(model, writer, linkRange, href);
|
|
151
|
+
}
|
|
152
|
+
writer.setAttribute('linkHref', href, linkRange);
|
|
153
|
+
truthyManualDecorators.forEach(item => {
|
|
154
|
+
writer.setAttribute(item, true, linkRange);
|
|
155
|
+
});
|
|
156
|
+
falsyManualDecorators.forEach(item => {
|
|
157
|
+
writer.removeAttribute(item, linkRange);
|
|
158
|
+
});
|
|
159
|
+
// Put the selection at the end of the updated link.
|
|
160
|
+
writer.setSelection(writer.createPositionAfter(linkRange.end.nodeBefore));
|
|
161
|
+
}
|
|
162
|
+
// If not then insert text node with `linkHref` attribute in place of caret.
|
|
163
|
+
// However, since selection is collapsed, attribute value will be used as data for text node.
|
|
164
|
+
// So, if `href` is empty, do not create text node.
|
|
165
|
+
else if (href !== '') {
|
|
166
|
+
const attributes = toMap(selection.getAttributes());
|
|
167
|
+
attributes.set('linkHref', href);
|
|
168
|
+
truthyManualDecorators.forEach(item => {
|
|
169
|
+
attributes.set(item, true);
|
|
170
|
+
});
|
|
171
|
+
const { end: positionAfter } = model.insertContent(writer.createText(href, attributes), position);
|
|
172
|
+
// Put the selection at the end of the inserted link.
|
|
173
|
+
// Using end of range returned from insertContent in case nodes with the same attributes got merged.
|
|
174
|
+
writer.setSelection(positionAfter);
|
|
175
|
+
}
|
|
176
|
+
// Remove the `linkHref` attribute and all link decorators from the selection.
|
|
177
|
+
// It stops adding a new content into the link element.
|
|
178
|
+
['linkHref', ...truthyManualDecorators, ...falsyManualDecorators].forEach(item => {
|
|
179
|
+
writer.removeSelectionAttribute(item);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// If selection has non-collapsed ranges, we change attribute on nodes inside those ranges
|
|
184
|
+
// omitting nodes where the `linkHref` attribute is disallowed.
|
|
185
|
+
const ranges = model.schema.getValidRanges(selection.getRanges(), 'linkHref');
|
|
186
|
+
// But for the first, check whether the `linkHref` attribute is allowed on selected blocks (e.g. the "image" element).
|
|
187
|
+
const allowedRanges = [];
|
|
188
|
+
for (const element of selection.getSelectedBlocks()) {
|
|
189
|
+
if (model.schema.checkAttribute(element, 'linkHref')) {
|
|
190
|
+
allowedRanges.push(writer.createRangeOn(element));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Ranges that accept the `linkHref` attribute. Since we will iterate over `allowedRanges`, let's clone it.
|
|
194
|
+
const rangesToUpdate = allowedRanges.slice();
|
|
195
|
+
// For all selection ranges we want to check whether given range is inside an element that accepts the `linkHref` attribute.
|
|
196
|
+
// If so, we don't want to propagate applying the attribute to its children.
|
|
197
|
+
for (const range of ranges) {
|
|
198
|
+
if (this._isRangeToUpdate(range, allowedRanges)) {
|
|
199
|
+
rangesToUpdate.push(range);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const range of rangesToUpdate) {
|
|
203
|
+
let linkRange = range;
|
|
204
|
+
if (rangesToUpdate.length === 1) {
|
|
205
|
+
// Current text of the link in the document.
|
|
206
|
+
const linkText = extractTextFromSelection(selection);
|
|
207
|
+
if (selection.getAttribute('linkHref') === linkText) {
|
|
208
|
+
linkRange = this._updateLinkContent(model, writer, range, href);
|
|
209
|
+
writer.setSelection(writer.createSelection(linkRange));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
writer.setAttribute('linkHref', href, linkRange);
|
|
213
|
+
truthyManualDecorators.forEach(item => {
|
|
214
|
+
writer.setAttribute(item, true, linkRange);
|
|
215
|
+
});
|
|
216
|
+
falsyManualDecorators.forEach(item => {
|
|
217
|
+
writer.removeAttribute(item, linkRange);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Provides information whether a decorator with a given name is present in the currently processed selection.
|
|
225
|
+
*
|
|
226
|
+
* @param decoratorName The name of the manual decorator used in the model
|
|
227
|
+
* @returns The information whether a given decorator is currently present in the selection.
|
|
228
|
+
*/
|
|
229
|
+
_getDecoratorStateFromModel(decoratorName) {
|
|
230
|
+
const model = this.editor.model;
|
|
231
|
+
const selection = model.document.selection;
|
|
232
|
+
const selectedElement = selection.getSelectedElement();
|
|
233
|
+
// A check for the `LinkImage` plugin. If the selection contains an element, get values from the element.
|
|
234
|
+
// Currently the selection reads attributes from text nodes only. See #7429 and #7465.
|
|
235
|
+
if (isLinkableElement(selectedElement, model.schema)) {
|
|
236
|
+
return selectedElement.getAttribute(decoratorName);
|
|
237
|
+
}
|
|
238
|
+
return selection.getAttribute(decoratorName);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Checks whether specified `range` is inside an element that accepts the `linkHref` attribute.
|
|
242
|
+
*
|
|
243
|
+
* @param range A range to check.
|
|
244
|
+
* @param allowedRanges An array of ranges created on elements where the attribute is accepted.
|
|
245
|
+
*/
|
|
246
|
+
_isRangeToUpdate(range, allowedRanges) {
|
|
247
|
+
for (const allowedRange of allowedRanges) {
|
|
248
|
+
// A range is inside an element that will have the `linkHref` attribute. Do not modify its nodes.
|
|
249
|
+
if (allowedRange.containsRange(range)) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Updates selected link with a new value as its content and as its href attribute.
|
|
257
|
+
*
|
|
258
|
+
* @param model Model is need to insert content.
|
|
259
|
+
* @param writer Writer is need to create text element in model.
|
|
260
|
+
* @param range A range where should be inserted content.
|
|
261
|
+
* @param href A link value which should be in the href attribute and in the content.
|
|
262
|
+
*/
|
|
263
|
+
_updateLinkContent(model, writer, range, href) {
|
|
264
|
+
const text = writer.createText(href, { linkHref: href });
|
|
265
|
+
return model.insertContent(text, range);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Returns a text of a link under the collapsed selection or a selection that contains the entire link.
|
|
269
|
+
function extractTextFromSelection(selection) {
|
|
270
|
+
if (selection.isCollapsed) {
|
|
271
|
+
const firstPosition = selection.getFirstPosition();
|
|
272
|
+
return firstPosition.textNode && firstPosition.textNode.data;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
const rangeItems = Array.from(selection.getFirstRange().getItems());
|
|
276
|
+
if (rangeItems.length > 1) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const firstNode = rangeItems[0];
|
|
280
|
+
if (firstNode.is('$text') || firstNode.is('$textProxy')) {
|
|
281
|
+
return firstNode.data;
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|