@ckeditor/ckeditor5-link 41.3.0-alpha.1 → 41.3.0-alpha.2
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/build/link.js +1 -1
- package/dist/editor-index.css +12 -0
- package/dist/index.css +40 -0
- package/dist/index.css.map +1 -1
- package/dist/translations/ar.d.ts +8 -0
- package/dist/translations/ar.js +5 -0
- package/dist/translations/ast.d.ts +8 -0
- package/dist/translations/ast.js +5 -0
- package/dist/translations/az.d.ts +8 -0
- package/dist/translations/az.js +5 -0
- package/dist/translations/bg.d.ts +8 -0
- package/dist/translations/bg.js +5 -0
- package/dist/translations/bn.d.ts +8 -0
- package/dist/translations/bn.js +5 -0
- package/dist/translations/ca.d.ts +8 -0
- package/dist/translations/ca.js +5 -0
- package/dist/translations/cs.d.ts +8 -0
- package/dist/translations/cs.js +5 -0
- package/dist/translations/da.d.ts +8 -0
- package/dist/translations/da.js +5 -0
- package/dist/translations/de-ch.d.ts +8 -0
- package/dist/translations/de-ch.js +5 -0
- package/dist/translations/de.d.ts +8 -0
- package/dist/translations/de.js +5 -0
- package/dist/translations/el.d.ts +8 -0
- package/dist/translations/el.js +5 -0
- package/dist/translations/en-au.d.ts +8 -0
- package/dist/translations/en-au.js +5 -0
- package/dist/translations/en-gb.d.ts +8 -0
- package/dist/translations/en-gb.js +5 -0
- package/dist/translations/en.d.ts +8 -0
- package/dist/translations/en.js +5 -0
- package/dist/translations/eo.d.ts +8 -0
- package/dist/translations/eo.js +5 -0
- package/dist/translations/es.d.ts +8 -0
- package/dist/translations/es.js +5 -0
- package/dist/translations/et.d.ts +8 -0
- package/dist/translations/et.js +5 -0
- package/dist/translations/eu.d.ts +8 -0
- package/dist/translations/eu.js +5 -0
- package/dist/translations/fa.d.ts +8 -0
- package/dist/translations/fa.js +5 -0
- package/dist/translations/fi.d.ts +8 -0
- package/dist/translations/fi.js +5 -0
- package/dist/translations/fr.d.ts +8 -0
- package/dist/translations/fr.js +5 -0
- package/dist/translations/gl.d.ts +8 -0
- package/dist/translations/gl.js +5 -0
- package/dist/translations/he.d.ts +8 -0
- package/dist/translations/he.js +5 -0
- package/dist/translations/hi.d.ts +8 -0
- package/dist/translations/hi.js +5 -0
- package/dist/translations/hr.d.ts +8 -0
- package/dist/translations/hr.js +5 -0
- package/dist/translations/hu.d.ts +8 -0
- package/dist/translations/hu.js +5 -0
- package/dist/translations/hy.d.ts +8 -0
- package/dist/translations/hy.js +5 -0
- package/dist/translations/id.d.ts +8 -0
- package/dist/translations/id.js +5 -0
- package/dist/translations/it.d.ts +8 -0
- package/dist/translations/it.js +5 -0
- package/dist/translations/ja.d.ts +8 -0
- package/dist/translations/ja.js +5 -0
- package/dist/translations/km.d.ts +8 -0
- package/dist/translations/km.js +5 -0
- package/dist/translations/kn.d.ts +8 -0
- package/dist/translations/kn.js +5 -0
- package/dist/translations/ko.d.ts +8 -0
- package/dist/translations/ko.js +5 -0
- package/dist/translations/ku.d.ts +8 -0
- package/dist/translations/ku.js +5 -0
- package/dist/translations/lt.d.ts +8 -0
- package/dist/translations/lt.js +5 -0
- package/dist/translations/lv.d.ts +8 -0
- package/dist/translations/lv.js +5 -0
- package/dist/translations/ms.d.ts +8 -0
- package/dist/translations/ms.js +5 -0
- package/dist/translations/nb.d.ts +8 -0
- package/dist/translations/nb.js +5 -0
- package/dist/translations/ne.d.ts +8 -0
- package/dist/translations/ne.js +5 -0
- package/dist/translations/nl.d.ts +8 -0
- package/dist/translations/nl.js +5 -0
- package/dist/translations/no.d.ts +8 -0
- package/dist/translations/no.js +5 -0
- package/dist/translations/pl.d.ts +8 -0
- package/dist/translations/pl.js +5 -0
- package/dist/translations/pt-br.d.ts +8 -0
- package/dist/translations/pt-br.js +5 -0
- package/dist/translations/pt.d.ts +8 -0
- package/dist/translations/pt.js +5 -0
- package/dist/translations/ro.d.ts +8 -0
- package/dist/translations/ro.js +5 -0
- package/dist/translations/ru.d.ts +8 -0
- package/dist/translations/ru.js +5 -0
- package/dist/translations/sk.d.ts +8 -0
- package/dist/translations/sk.js +5 -0
- package/dist/translations/sq.d.ts +8 -0
- package/dist/translations/sq.js +5 -0
- package/dist/translations/sr-latn.d.ts +8 -0
- package/dist/translations/sr-latn.js +5 -0
- package/dist/translations/sr.d.ts +8 -0
- package/dist/translations/sr.js +5 -0
- package/dist/translations/sv.d.ts +8 -0
- package/dist/translations/sv.js +5 -0
- package/dist/translations/th.d.ts +8 -0
- package/dist/translations/th.js +5 -0
- package/dist/translations/tk.d.ts +8 -0
- package/dist/translations/tk.js +5 -0
- package/dist/translations/tr.d.ts +8 -0
- package/dist/translations/tr.js +5 -0
- package/dist/translations/tt.d.ts +8 -0
- package/dist/translations/tt.js +5 -0
- package/dist/translations/ug.d.ts +8 -0
- package/dist/translations/ug.js +5 -0
- package/dist/translations/uk.d.ts +8 -0
- package/dist/translations/uk.js +5 -0
- package/dist/translations/ur.d.ts +8 -0
- package/dist/translations/ur.js +5 -0
- package/dist/translations/uz.d.ts +8 -0
- package/dist/translations/uz.js +5 -0
- package/dist/translations/vi.d.ts +8 -0
- package/dist/translations/vi.js +5 -0
- package/dist/translations/zh-cn.d.ts +8 -0
- package/dist/translations/zh-cn.js +5 -0
- package/dist/translations/zh.d.ts +8 -0
- package/dist/translations/zh.js +5 -0
- package/dist/types/augmentation.d.ts +4 -0
- package/dist/types/autolink.d.ts +4 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/link.d.ts +4 -0
- package/dist/types/linkcommand.d.ts +4 -0
- package/dist/types/linkconfig.d.ts +4 -0
- package/dist/types/linkediting.d.ts +4 -0
- package/dist/types/linkimage.d.ts +4 -0
- package/dist/types/linkimageediting.d.ts +4 -0
- package/dist/types/linkimageui.d.ts +4 -0
- package/dist/types/linkui.d.ts +4 -0
- package/dist/types/ui/linkactionsview.d.ts +4 -0
- package/dist/types/ui/linkformview.d.ts +4 -0
- package/dist/types/unlinkcommand.d.ts +4 -0
- package/dist/types/utils/automaticdecorators.d.ts +4 -0
- package/dist/types/utils/manualdecorator.d.ts +4 -0
- package/dist/types/utils.d.ts +4 -0
- package/package.json +3 -3
- package/dist/index.js +0 -2599
- package/dist/index.js.map +0 -1
package/dist/index.js
DELETED
|
@@ -1,2599 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2024, 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
|
-
import { Command, Plugin, icons } from '@ckeditor/ckeditor5-core/dist/index.js';
|
|
6
|
-
import { findAttributeRange, TwoStepCaretMovement, Input, inlineHighlight, Delete, TextWatcher, getLastTextLine } from '@ckeditor/ckeditor5-typing/dist/index.js';
|
|
7
|
-
import { ClipboardPipeline } from '@ckeditor/ckeditor5-clipboard/dist/index.js';
|
|
8
|
-
import { toMap, Collection, first, ObservableMixin, env, keyCodes, FocusTracker, KeystrokeHandler } from '@ckeditor/ckeditor5-utils/dist/index.js';
|
|
9
|
-
import { upperFirst } from 'lodash-es';
|
|
10
|
-
import { ClickObserver, Matcher } from '@ckeditor/ckeditor5-engine/dist/index.js';
|
|
11
|
-
import { View, ViewCollection, FocusCycler, submitHandler, LabeledFieldView, createLabeledInputText, ButtonView, SwitchButtonView, ContextualBalloon, CssTransitionDisablerMixin, clickOutsideHandler } from '@ckeditor/ckeditor5-ui/dist/index.js';
|
|
12
|
-
import { isWidget } from '@ckeditor/ckeditor5-widget/dist/index.js';
|
|
13
|
-
import '@ckeditor/ckeditor5-ui/dist/index.js';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
17
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
18
|
-
*/
|
|
19
|
-
/**
|
|
20
|
-
* @module link/utils/automaticdecorators
|
|
21
|
-
*/
|
|
22
|
-
/**
|
|
23
|
-
* Helper class that ties together all {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition} and provides
|
|
24
|
-
* the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement downcast dispatchers} for them.
|
|
25
|
-
*/
|
|
26
|
-
class AutomaticDecorators {
|
|
27
|
-
constructor() {
|
|
28
|
-
/**
|
|
29
|
-
* Stores the definition of {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition automatic decorators}.
|
|
30
|
-
* This data is used as a source for a downcast dispatcher to create a proper conversion to output data.
|
|
31
|
-
*/
|
|
32
|
-
this._definitions = new Set();
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Gives information about the number of decorators stored in the {@link module:link/utils/automaticdecorators~AutomaticDecorators}
|
|
36
|
-
* instance.
|
|
37
|
-
*/
|
|
38
|
-
get length() {
|
|
39
|
-
return this._definitions.size;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Adds automatic decorator objects or an array with them to be used during downcasting.
|
|
43
|
-
*
|
|
44
|
-
* @param item A configuration object of automatic rules for decorating links. It might also be an array of such objects.
|
|
45
|
-
*/
|
|
46
|
-
add(item) {
|
|
47
|
-
if (Array.isArray(item)) {
|
|
48
|
-
item.forEach(item => this._definitions.add(item));
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
this._definitions.add(item);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Provides the conversion helper used in the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add} method.
|
|
56
|
-
*
|
|
57
|
-
* @returns A dispatcher function used as conversion helper in {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add}.
|
|
58
|
-
*/
|
|
59
|
-
getDispatcher() {
|
|
60
|
-
return dispatcher => {
|
|
61
|
-
dispatcher.on('attribute:linkHref', (evt, data, conversionApi) => {
|
|
62
|
-
// There is only test as this behavior decorates links and
|
|
63
|
-
// it is run before dispatcher which actually consumes this node.
|
|
64
|
-
// This allows on writing own dispatcher with highest priority,
|
|
65
|
-
// which blocks both native converter and this additional decoration.
|
|
66
|
-
if (!conversionApi.consumable.test(data.item, 'attribute:linkHref')) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
// Automatic decorators for block links are handled e.g. in LinkImageEditing.
|
|
70
|
-
if (!(data.item.is('selection') || conversionApi.schema.isInline(data.item))) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const viewWriter = conversionApi.writer;
|
|
74
|
-
const viewSelection = viewWriter.document.selection;
|
|
75
|
-
for (const item of this._definitions) {
|
|
76
|
-
const viewElement = viewWriter.createAttributeElement('a', item.attributes, {
|
|
77
|
-
priority: 5
|
|
78
|
-
});
|
|
79
|
-
if (item.classes) {
|
|
80
|
-
viewWriter.addClass(item.classes, viewElement);
|
|
81
|
-
}
|
|
82
|
-
for (const key in item.styles) {
|
|
83
|
-
viewWriter.setStyle(key, item.styles[key], viewElement);
|
|
84
|
-
}
|
|
85
|
-
viewWriter.setCustomProperty('link', true, viewElement);
|
|
86
|
-
if (item.callback(data.attributeNewValue)) {
|
|
87
|
-
if (data.item.is('selection')) {
|
|
88
|
-
viewWriter.wrap(viewSelection.getFirstRange(), viewElement);
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
viewWriter.wrap(conversionApi.mapper.toViewRange(data.range), viewElement);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
viewWriter.unwrap(conversionApi.mapper.toViewRange(data.range), viewElement);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}, { priority: 'high' });
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Provides the conversion helper used in the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add} method
|
|
103
|
-
* when linking images.
|
|
104
|
-
*
|
|
105
|
-
* @returns A dispatcher function used as conversion helper in {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add}.
|
|
106
|
-
*/
|
|
107
|
-
getDispatcherForLinkedImage() {
|
|
108
|
-
return dispatcher => {
|
|
109
|
-
dispatcher.on('attribute:linkHref:imageBlock', (evt, data, { writer, mapper }) => {
|
|
110
|
-
const viewFigure = mapper.toViewElement(data.item);
|
|
111
|
-
const linkInImage = Array.from(viewFigure.getChildren())
|
|
112
|
-
.find((child) => child.is('element', 'a'));
|
|
113
|
-
for (const item of this._definitions) {
|
|
114
|
-
const attributes = toMap(item.attributes);
|
|
115
|
-
if (item.callback(data.attributeNewValue)) {
|
|
116
|
-
for (const [key, val] of attributes) {
|
|
117
|
-
// Left for backward compatibility. Since v30 decorator should
|
|
118
|
-
// accept `classes` and `styles` separately from `attributes`.
|
|
119
|
-
if (key === 'class') {
|
|
120
|
-
writer.addClass(val, linkInImage);
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
writer.setAttribute(key, val, linkInImage);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (item.classes) {
|
|
127
|
-
writer.addClass(item.classes, linkInImage);
|
|
128
|
-
}
|
|
129
|
-
for (const key in item.styles) {
|
|
130
|
-
writer.setStyle(key, item.styles[key], linkInImage);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
for (const [key, val] of attributes) {
|
|
135
|
-
if (key === 'class') {
|
|
136
|
-
writer.removeClass(val, linkInImage);
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
writer.removeAttribute(key, linkInImage);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (item.classes) {
|
|
143
|
-
writer.removeClass(item.classes, linkInImage);
|
|
144
|
-
}
|
|
145
|
-
for (const key in item.styles) {
|
|
146
|
-
writer.removeStyle(key, linkInImage);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
157
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
158
|
-
*/
|
|
159
|
-
const ATTRIBUTE_WHITESPACES = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g; // eslint-disable-line no-control-regex
|
|
160
|
-
const SAFE_URL_TEMPLATE = '^(?:(?:<protocols>):|[^a-z]|[a-z+.-]+(?:[^a-z+.:-]|$))';
|
|
161
|
-
// Simplified email test - should be run over previously found URL.
|
|
162
|
-
const EMAIL_REG_EXP = /^[\S]+@((?![-_])(?:[-\w\u00a1-\uffff]{0,63}[^-_]\.))+(?:[a-z\u00a1-\uffff]{2,})$/i;
|
|
163
|
-
// The regex checks for the protocol syntax ('xxxx://' or 'xxxx:')
|
|
164
|
-
// or non-word characters at the beginning of the link ('/', '#' etc.).
|
|
165
|
-
const PROTOCOL_REG_EXP = /^((\w+:(\/{2,})?)|(\W))/i;
|
|
166
|
-
const DEFAULT_LINK_PROTOCOLS = [
|
|
167
|
-
'https?',
|
|
168
|
-
'ftps?',
|
|
169
|
-
'mailto'
|
|
170
|
-
];
|
|
171
|
-
/**
|
|
172
|
-
* A keystroke used by the {@link module:link/linkui~LinkUI link UI feature}.
|
|
173
|
-
*/
|
|
174
|
-
const LINK_KEYSTROKE = 'Ctrl+K';
|
|
175
|
-
/**
|
|
176
|
-
* Returns `true` if a given view node is the link element.
|
|
177
|
-
*/
|
|
178
|
-
function isLinkElement(node) {
|
|
179
|
-
return node.is('attributeElement') && !!node.getCustomProperty('link');
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Creates a link {@link module:engine/view/attributeelement~AttributeElement} with the provided `href` attribute.
|
|
183
|
-
*/
|
|
184
|
-
function createLinkElement(href, { writer }) {
|
|
185
|
-
// Priority 5 - https://github.com/ckeditor/ckeditor5-link/issues/121.
|
|
186
|
-
const linkElement = writer.createAttributeElement('a', { href }, { priority: 5 });
|
|
187
|
-
writer.setCustomProperty('link', true, linkElement);
|
|
188
|
-
return linkElement;
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Returns a safe URL based on a given value.
|
|
192
|
-
*
|
|
193
|
-
* A URL is considered safe if it is safe for the user (does not contain any malicious code).
|
|
194
|
-
*
|
|
195
|
-
* If a URL is considered unsafe, a simple `"#"` is returned.
|
|
196
|
-
*
|
|
197
|
-
* @internal
|
|
198
|
-
*/
|
|
199
|
-
function ensureSafeUrl(url, allowedProtocols = DEFAULT_LINK_PROTOCOLS) {
|
|
200
|
-
const urlString = String(url);
|
|
201
|
-
const protocolsList = allowedProtocols.join('|');
|
|
202
|
-
const customSafeRegex = new RegExp(`${SAFE_URL_TEMPLATE.replace('<protocols>', protocolsList)}`, 'i');
|
|
203
|
-
return isSafeUrl(urlString, customSafeRegex) ? urlString : '#';
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Checks whether the given URL is safe for the user (does not contain any malicious code).
|
|
207
|
-
*/
|
|
208
|
-
function isSafeUrl(url, customRegexp) {
|
|
209
|
-
const normalizedUrl = url.replace(ATTRIBUTE_WHITESPACES, '');
|
|
210
|
-
return !!normalizedUrl.match(customRegexp);
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Returns the {@link module:link/linkconfig~LinkConfig#decorators `config.link.decorators`} configuration processed
|
|
214
|
-
* to respect the locale of the editor, i.e. to display the {@link module:link/linkconfig~LinkDecoratorManualDefinition label}
|
|
215
|
-
* in the correct language.
|
|
216
|
-
*
|
|
217
|
-
* **Note**: Only the few most commonly used labels are translated automatically. Other labels should be manually
|
|
218
|
-
* translated in the {@link module:link/linkconfig~LinkConfig#decorators `config.link.decorators`} configuration.
|
|
219
|
-
*
|
|
220
|
-
* @param t Shorthand for {@link module:utils/locale~Locale#t Locale#t}.
|
|
221
|
-
* @param decorators The decorator reference where the label values should be localized.
|
|
222
|
-
*/
|
|
223
|
-
function getLocalizedDecorators(t, decorators) {
|
|
224
|
-
const localizedDecoratorsLabels = {
|
|
225
|
-
'Open in a new tab': t('Open in a new tab'),
|
|
226
|
-
'Downloadable': t('Downloadable')
|
|
227
|
-
};
|
|
228
|
-
decorators.forEach(decorator => {
|
|
229
|
-
if ('label' in decorator && localizedDecoratorsLabels[decorator.label]) {
|
|
230
|
-
decorator.label = localizedDecoratorsLabels[decorator.label];
|
|
231
|
-
}
|
|
232
|
-
return decorator;
|
|
233
|
-
});
|
|
234
|
-
return decorators;
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Converts an object with defined decorators to a normalized array of decorators. The `id` key is added for each decorator and
|
|
238
|
-
* is used as the attribute's name in the model.
|
|
239
|
-
*/
|
|
240
|
-
function normalizeDecorators(decorators) {
|
|
241
|
-
const retArray = [];
|
|
242
|
-
if (decorators) {
|
|
243
|
-
for (const [key, value] of Object.entries(decorators)) {
|
|
244
|
-
const decorator = Object.assign({}, value, { id: `link${upperFirst(key)}` });
|
|
245
|
-
retArray.push(decorator);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return retArray;
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Returns `true` if the specified `element` can be linked (the element allows the `linkHref` attribute).
|
|
252
|
-
*/
|
|
253
|
-
function isLinkableElement(element, schema) {
|
|
254
|
-
if (!element) {
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
return schema.checkAttribute(element.name, 'linkHref');
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Returns `true` if the specified `value` is an email.
|
|
261
|
-
*/
|
|
262
|
-
function isEmail(value) {
|
|
263
|
-
return EMAIL_REG_EXP.test(value);
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Adds the protocol prefix to the specified `link` when:
|
|
267
|
-
*
|
|
268
|
-
* * it does not contain it already, and there is a {@link module:link/linkconfig~LinkConfig#defaultProtocol `defaultProtocol` }
|
|
269
|
-
* configuration value provided,
|
|
270
|
-
* * or the link is an email address.
|
|
271
|
-
*/
|
|
272
|
-
function addLinkProtocolIfApplicable(link, defaultProtocol) {
|
|
273
|
-
const protocol = isEmail(link) ? 'mailto:' : defaultProtocol;
|
|
274
|
-
const isProtocolNeeded = !!protocol && !linkHasProtocol(link);
|
|
275
|
-
return link && isProtocolNeeded ? protocol + link : link;
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Checks if protocol is already included in the link.
|
|
279
|
-
*/
|
|
280
|
-
function linkHasProtocol(link) {
|
|
281
|
-
return PROTOCOL_REG_EXP.test(link);
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Opens the link in a new browser tab.
|
|
285
|
-
*/
|
|
286
|
-
function openLink(link) {
|
|
287
|
-
window.open(link, '_blank', 'noopener');
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
292
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
293
|
-
*/
|
|
294
|
-
/**
|
|
295
|
-
* @module link/linkcommand
|
|
296
|
-
*/
|
|
297
|
-
/**
|
|
298
|
-
* The link command. It is used by the {@link module:link/link~Link link feature}.
|
|
299
|
-
*/
|
|
300
|
-
class LinkCommand extends Command {
|
|
301
|
-
constructor() {
|
|
302
|
-
super(...arguments);
|
|
303
|
-
/**
|
|
304
|
-
* A collection of {@link module:link/utils/manualdecorator~ManualDecorator manual decorators}
|
|
305
|
-
* corresponding to the {@link module:link/linkconfig~LinkConfig#decorators decorator configuration}.
|
|
306
|
-
*
|
|
307
|
-
* You can consider it a model with states of manual decorators added to the currently selected link.
|
|
308
|
-
*/
|
|
309
|
-
this.manualDecorators = new Collection();
|
|
310
|
-
/**
|
|
311
|
-
* An instance of the helper that ties together all {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition}
|
|
312
|
-
* that are used by the {@glink features/link link} and the {@glink features/images/images-linking linking images} features.
|
|
313
|
-
*/
|
|
314
|
-
this.automaticDecorators = new AutomaticDecorators();
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Synchronizes the state of {@link #manualDecorators} with the currently present elements in the model.
|
|
318
|
-
*/
|
|
319
|
-
restoreManualDecoratorStates() {
|
|
320
|
-
for (const manualDecorator of this.manualDecorators) {
|
|
321
|
-
manualDecorator.value = this._getDecoratorStateFromModel(manualDecorator.id);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* @inheritDoc
|
|
326
|
-
*/
|
|
327
|
-
refresh() {
|
|
328
|
-
const model = this.editor.model;
|
|
329
|
-
const selection = model.document.selection;
|
|
330
|
-
const selectedElement = selection.getSelectedElement() || first(selection.getSelectedBlocks());
|
|
331
|
-
// A check for any integration that allows linking elements (e.g. `LinkImage`).
|
|
332
|
-
// Currently the selection reads attributes from text nodes only. See #7429 and #7465.
|
|
333
|
-
if (isLinkableElement(selectedElement, model.schema)) {
|
|
334
|
-
this.value = selectedElement.getAttribute('linkHref');
|
|
335
|
-
this.isEnabled = model.schema.checkAttribute(selectedElement, 'linkHref');
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
this.value = selection.getAttribute('linkHref');
|
|
339
|
-
this.isEnabled = model.schema.checkAttributeInSelection(selection, 'linkHref');
|
|
340
|
-
}
|
|
341
|
-
for (const manualDecorator of this.manualDecorators) {
|
|
342
|
-
manualDecorator.value = this._getDecoratorStateFromModel(manualDecorator.id);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Executes the command.
|
|
347
|
-
*
|
|
348
|
-
* When the selection is non-collapsed, the `linkHref` attribute will be applied to nodes inside the selection, but only to
|
|
349
|
-
* those nodes where the `linkHref` attribute is allowed (disallowed nodes will be omitted).
|
|
350
|
-
*
|
|
351
|
-
* When the selection is collapsed and is not inside the text with the `linkHref` attribute, a
|
|
352
|
-
* new {@link module:engine/model/text~Text text node} with the `linkHref` attribute will be inserted in place of the caret, but
|
|
353
|
-
* only if such element is allowed in this place. The `_data` of the inserted text will equal the `href` parameter.
|
|
354
|
-
* The selection will be updated to wrap the just inserted text node.
|
|
355
|
-
*
|
|
356
|
-
* When the selection is collapsed and inside the text with the `linkHref` attribute, the attribute value will be updated.
|
|
357
|
-
*
|
|
358
|
-
* # Decorators and model attribute management
|
|
359
|
-
*
|
|
360
|
-
* There is an optional argument to this command that applies or removes model
|
|
361
|
-
* {@glink framework/architecture/editing-engine#text-attributes text attributes} brought by
|
|
362
|
-
* {@link module:link/utils/manualdecorator~ManualDecorator manual link decorators}.
|
|
363
|
-
*
|
|
364
|
-
* Text attribute names in the model correspond to the entries in the {@link module:link/linkconfig~LinkConfig#decorators
|
|
365
|
-
* configuration}.
|
|
366
|
-
* For every decorator configured, a model text attribute exists with the "link" prefix. For example, a `'linkMyDecorator'` attribute
|
|
367
|
-
* corresponds to `'myDecorator'` in the configuration.
|
|
368
|
-
*
|
|
369
|
-
* To learn more about link decorators, check out the {@link module:link/linkconfig~LinkConfig#decorators `config.link.decorators`}
|
|
370
|
-
* documentation.
|
|
371
|
-
*
|
|
372
|
-
* Here is how to manage decorator attributes with the link command:
|
|
373
|
-
*
|
|
374
|
-
* ```ts
|
|
375
|
-
* const linkCommand = editor.commands.get( 'link' );
|
|
376
|
-
*
|
|
377
|
-
* // Adding a new decorator attribute.
|
|
378
|
-
* linkCommand.execute( 'http://example.com', {
|
|
379
|
-
* linkIsExternal: true
|
|
380
|
-
* } );
|
|
381
|
-
*
|
|
382
|
-
* // Removing a decorator attribute from the selection.
|
|
383
|
-
* linkCommand.execute( 'http://example.com', {
|
|
384
|
-
* linkIsExternal: false
|
|
385
|
-
* } );
|
|
386
|
-
*
|
|
387
|
-
* // Adding multiple decorator attributes at the same time.
|
|
388
|
-
* linkCommand.execute( 'http://example.com', {
|
|
389
|
-
* linkIsExternal: true,
|
|
390
|
-
* linkIsDownloadable: true,
|
|
391
|
-
* } );
|
|
392
|
-
*
|
|
393
|
-
* // Removing and adding decorator attributes at the same time.
|
|
394
|
-
* linkCommand.execute( 'http://example.com', {
|
|
395
|
-
* linkIsExternal: false,
|
|
396
|
-
* linkFoo: true,
|
|
397
|
-
* linkIsDownloadable: false,
|
|
398
|
-
* } );
|
|
399
|
-
* ```
|
|
400
|
-
*
|
|
401
|
-
* **Note**: If the decorator attribute name is not specified, its state remains untouched.
|
|
402
|
-
*
|
|
403
|
-
* **Note**: {@link module:link/unlinkcommand~UnlinkCommand#execute `UnlinkCommand#execute()`} removes all
|
|
404
|
-
* decorator attributes.
|
|
405
|
-
*
|
|
406
|
-
* @fires execute
|
|
407
|
-
* @param href Link destination.
|
|
408
|
-
* @param manualDecoratorIds The information about manual decorator attributes to be applied or removed upon execution.
|
|
409
|
-
*/
|
|
410
|
-
execute(href, manualDecoratorIds = {}) {
|
|
411
|
-
const model = this.editor.model;
|
|
412
|
-
const selection = model.document.selection;
|
|
413
|
-
// Stores information about manual decorators to turn them on/off when command is applied.
|
|
414
|
-
const truthyManualDecorators = [];
|
|
415
|
-
const falsyManualDecorators = [];
|
|
416
|
-
for (const name in manualDecoratorIds) {
|
|
417
|
-
if (manualDecoratorIds[name]) {
|
|
418
|
-
truthyManualDecorators.push(name);
|
|
419
|
-
}
|
|
420
|
-
else {
|
|
421
|
-
falsyManualDecorators.push(name);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
model.change(writer => {
|
|
425
|
-
// If selection is collapsed then update selected link or insert new one at the place of caret.
|
|
426
|
-
if (selection.isCollapsed) {
|
|
427
|
-
const position = selection.getFirstPosition();
|
|
428
|
-
// When selection is inside text with `linkHref` attribute.
|
|
429
|
-
if (selection.hasAttribute('linkHref')) {
|
|
430
|
-
const linkText = extractTextFromSelection(selection);
|
|
431
|
-
// Then update `linkHref` value.
|
|
432
|
-
let linkRange = findAttributeRange(position, 'linkHref', selection.getAttribute('linkHref'), model);
|
|
433
|
-
if (selection.getAttribute('linkHref') === linkText) {
|
|
434
|
-
linkRange = this._updateLinkContent(model, writer, linkRange, href);
|
|
435
|
-
}
|
|
436
|
-
writer.setAttribute('linkHref', href, linkRange);
|
|
437
|
-
truthyManualDecorators.forEach(item => {
|
|
438
|
-
writer.setAttribute(item, true, linkRange);
|
|
439
|
-
});
|
|
440
|
-
falsyManualDecorators.forEach(item => {
|
|
441
|
-
writer.removeAttribute(item, linkRange);
|
|
442
|
-
});
|
|
443
|
-
// Put the selection at the end of the updated link.
|
|
444
|
-
writer.setSelection(writer.createPositionAfter(linkRange.end.nodeBefore));
|
|
445
|
-
}
|
|
446
|
-
// If not then insert text node with `linkHref` attribute in place of caret.
|
|
447
|
-
// However, since selection is collapsed, attribute value will be used as data for text node.
|
|
448
|
-
// So, if `href` is empty, do not create text node.
|
|
449
|
-
else if (href !== '') {
|
|
450
|
-
const attributes = toMap(selection.getAttributes());
|
|
451
|
-
attributes.set('linkHref', href);
|
|
452
|
-
truthyManualDecorators.forEach(item => {
|
|
453
|
-
attributes.set(item, true);
|
|
454
|
-
});
|
|
455
|
-
const { end: positionAfter } = model.insertContent(writer.createText(href, attributes), position);
|
|
456
|
-
// Put the selection at the end of the inserted link.
|
|
457
|
-
// Using end of range returned from insertContent in case nodes with the same attributes got merged.
|
|
458
|
-
writer.setSelection(positionAfter);
|
|
459
|
-
}
|
|
460
|
-
// Remove the `linkHref` attribute and all link decorators from the selection.
|
|
461
|
-
// It stops adding a new content into the link element.
|
|
462
|
-
['linkHref', ...truthyManualDecorators, ...falsyManualDecorators].forEach(item => {
|
|
463
|
-
writer.removeSelectionAttribute(item);
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
else {
|
|
467
|
-
// If selection has non-collapsed ranges, we change attribute on nodes inside those ranges
|
|
468
|
-
// omitting nodes where the `linkHref` attribute is disallowed.
|
|
469
|
-
const ranges = model.schema.getValidRanges(selection.getRanges(), 'linkHref');
|
|
470
|
-
// But for the first, check whether the `linkHref` attribute is allowed on selected blocks (e.g. the "image" element).
|
|
471
|
-
const allowedRanges = [];
|
|
472
|
-
for (const element of selection.getSelectedBlocks()) {
|
|
473
|
-
if (model.schema.checkAttribute(element, 'linkHref')) {
|
|
474
|
-
allowedRanges.push(writer.createRangeOn(element));
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
// Ranges that accept the `linkHref` attribute. Since we will iterate over `allowedRanges`, let's clone it.
|
|
478
|
-
const rangesToUpdate = allowedRanges.slice();
|
|
479
|
-
// For all selection ranges we want to check whether given range is inside an element that accepts the `linkHref` attribute.
|
|
480
|
-
// If so, we don't want to propagate applying the attribute to its children.
|
|
481
|
-
for (const range of ranges) {
|
|
482
|
-
if (this._isRangeToUpdate(range, allowedRanges)) {
|
|
483
|
-
rangesToUpdate.push(range);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
for (const range of rangesToUpdate) {
|
|
487
|
-
let linkRange = range;
|
|
488
|
-
if (rangesToUpdate.length === 1) {
|
|
489
|
-
// Current text of the link in the document.
|
|
490
|
-
const linkText = extractTextFromSelection(selection);
|
|
491
|
-
if (selection.getAttribute('linkHref') === linkText) {
|
|
492
|
-
linkRange = this._updateLinkContent(model, writer, range, href);
|
|
493
|
-
writer.setSelection(writer.createSelection(linkRange));
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
writer.setAttribute('linkHref', href, linkRange);
|
|
497
|
-
truthyManualDecorators.forEach(item => {
|
|
498
|
-
writer.setAttribute(item, true, linkRange);
|
|
499
|
-
});
|
|
500
|
-
falsyManualDecorators.forEach(item => {
|
|
501
|
-
writer.removeAttribute(item, linkRange);
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
/**
|
|
508
|
-
* Provides information whether a decorator with a given name is present in the currently processed selection.
|
|
509
|
-
*
|
|
510
|
-
* @param decoratorName The name of the manual decorator used in the model
|
|
511
|
-
* @returns The information whether a given decorator is currently present in the selection.
|
|
512
|
-
*/
|
|
513
|
-
_getDecoratorStateFromModel(decoratorName) {
|
|
514
|
-
const model = this.editor.model;
|
|
515
|
-
const selection = model.document.selection;
|
|
516
|
-
const selectedElement = selection.getSelectedElement();
|
|
517
|
-
// A check for the `LinkImage` plugin. If the selection contains an element, get values from the element.
|
|
518
|
-
// Currently the selection reads attributes from text nodes only. See #7429 and #7465.
|
|
519
|
-
if (isLinkableElement(selectedElement, model.schema)) {
|
|
520
|
-
return selectedElement.getAttribute(decoratorName);
|
|
521
|
-
}
|
|
522
|
-
return selection.getAttribute(decoratorName);
|
|
523
|
-
}
|
|
524
|
-
/**
|
|
525
|
-
* Checks whether specified `range` is inside an element that accepts the `linkHref` attribute.
|
|
526
|
-
*
|
|
527
|
-
* @param range A range to check.
|
|
528
|
-
* @param allowedRanges An array of ranges created on elements where the attribute is accepted.
|
|
529
|
-
*/
|
|
530
|
-
_isRangeToUpdate(range, allowedRanges) {
|
|
531
|
-
for (const allowedRange of allowedRanges) {
|
|
532
|
-
// A range is inside an element that will have the `linkHref` attribute. Do not modify its nodes.
|
|
533
|
-
if (allowedRange.containsRange(range)) {
|
|
534
|
-
return false;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
return true;
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Updates selected link with a new value as its content and as its href attribute.
|
|
541
|
-
*
|
|
542
|
-
* @param model Model is need to insert content.
|
|
543
|
-
* @param writer Writer is need to create text element in model.
|
|
544
|
-
* @param range A range where should be inserted content.
|
|
545
|
-
* @param href A link value which should be in the href attribute and in the content.
|
|
546
|
-
*/
|
|
547
|
-
_updateLinkContent(model, writer, range, href) {
|
|
548
|
-
const text = writer.createText(href, { linkHref: href });
|
|
549
|
-
return model.insertContent(text, range);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
// Returns a text of a link under the collapsed selection or a selection that contains the entire link.
|
|
553
|
-
function extractTextFromSelection(selection) {
|
|
554
|
-
if (selection.isCollapsed) {
|
|
555
|
-
const firstPosition = selection.getFirstPosition();
|
|
556
|
-
return firstPosition.textNode && firstPosition.textNode.data;
|
|
557
|
-
}
|
|
558
|
-
else {
|
|
559
|
-
const rangeItems = Array.from(selection.getFirstRange().getItems());
|
|
560
|
-
if (rangeItems.length > 1) {
|
|
561
|
-
return null;
|
|
562
|
-
}
|
|
563
|
-
const firstNode = rangeItems[0];
|
|
564
|
-
if (firstNode.is('$text') || firstNode.is('$textProxy')) {
|
|
565
|
-
return firstNode.data;
|
|
566
|
-
}
|
|
567
|
-
return null;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
573
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
574
|
-
*/
|
|
575
|
-
/**
|
|
576
|
-
* @module link/unlinkcommand
|
|
577
|
-
*/
|
|
578
|
-
/**
|
|
579
|
-
* The unlink command. It is used by the {@link module:link/link~Link link plugin}.
|
|
580
|
-
*/
|
|
581
|
-
class UnlinkCommand extends Command {
|
|
582
|
-
/**
|
|
583
|
-
* @inheritDoc
|
|
584
|
-
*/
|
|
585
|
-
refresh() {
|
|
586
|
-
const model = this.editor.model;
|
|
587
|
-
const selection = model.document.selection;
|
|
588
|
-
const selectedElement = selection.getSelectedElement();
|
|
589
|
-
// A check for any integration that allows linking elements (e.g. `LinkImage`).
|
|
590
|
-
// Currently the selection reads attributes from text nodes only. See #7429 and #7465.
|
|
591
|
-
if (isLinkableElement(selectedElement, model.schema)) {
|
|
592
|
-
this.isEnabled = model.schema.checkAttribute(selectedElement, 'linkHref');
|
|
593
|
-
}
|
|
594
|
-
else {
|
|
595
|
-
this.isEnabled = model.schema.checkAttributeInSelection(selection, 'linkHref');
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* Executes the command.
|
|
600
|
-
*
|
|
601
|
-
* When the selection is collapsed, it removes the `linkHref` attribute from each node with the same `linkHref` attribute value.
|
|
602
|
-
* When the selection is non-collapsed, it removes the `linkHref` attribute from each node in selected ranges.
|
|
603
|
-
*
|
|
604
|
-
* # Decorators
|
|
605
|
-
*
|
|
606
|
-
* If {@link module:link/linkconfig~LinkConfig#decorators `config.link.decorators`} is specified,
|
|
607
|
-
* all configured decorators are removed together with the `linkHref` attribute.
|
|
608
|
-
*
|
|
609
|
-
* @fires execute
|
|
610
|
-
*/
|
|
611
|
-
execute() {
|
|
612
|
-
const editor = this.editor;
|
|
613
|
-
const model = this.editor.model;
|
|
614
|
-
const selection = model.document.selection;
|
|
615
|
-
const linkCommand = editor.commands.get('link');
|
|
616
|
-
model.change(writer => {
|
|
617
|
-
// Get ranges to unlink.
|
|
618
|
-
const rangesToUnlink = selection.isCollapsed ?
|
|
619
|
-
[findAttributeRange(selection.getFirstPosition(), 'linkHref', selection.getAttribute('linkHref'), model)] :
|
|
620
|
-
model.schema.getValidRanges(selection.getRanges(), 'linkHref');
|
|
621
|
-
// Remove `linkHref` attribute from specified ranges.
|
|
622
|
-
for (const range of rangesToUnlink) {
|
|
623
|
-
writer.removeAttribute('linkHref', range);
|
|
624
|
-
// If there are registered custom attributes, then remove them during unlink.
|
|
625
|
-
if (linkCommand) {
|
|
626
|
-
for (const manualDecorator of linkCommand.manualDecorators) {
|
|
627
|
-
writer.removeAttribute(manualDecorator.id, range);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
/**
|
|
636
|
-
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
637
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
638
|
-
*/
|
|
639
|
-
/**
|
|
640
|
-
* @module link/utils/manualdecorator
|
|
641
|
-
*/
|
|
642
|
-
/**
|
|
643
|
-
* Helper class that stores manual decorators with observable {@link module:link/utils/manualdecorator~ManualDecorator#value}
|
|
644
|
-
* to support integration with the UI state. An instance of this class is a model with the state of individual manual decorators.
|
|
645
|
-
* These decorators are kept as collections in {@link module:link/linkcommand~LinkCommand#manualDecorators}.
|
|
646
|
-
*/
|
|
647
|
-
class ManualDecorator extends ObservableMixin() {
|
|
648
|
-
/**
|
|
649
|
-
* Creates a new instance of {@link module:link/utils/manualdecorator~ManualDecorator}.
|
|
650
|
-
*
|
|
651
|
-
* @param config.id The name of the attribute used in the model that represents a given manual decorator.
|
|
652
|
-
* For example: `'linkIsExternal'`.
|
|
653
|
-
* @param config.label The label used in the user interface to toggle the manual decorator.
|
|
654
|
-
* @param config.attributes A set of attributes added to output data when the decorator is active for a specific link.
|
|
655
|
-
* Attributes should keep the format of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}.
|
|
656
|
-
* @param [config.defaultValue] Controls whether the decorator is "on" by default.
|
|
657
|
-
*/
|
|
658
|
-
constructor({ id, label, attributes, classes, styles, defaultValue }) {
|
|
659
|
-
super();
|
|
660
|
-
this.id = id;
|
|
661
|
-
this.set('value', undefined);
|
|
662
|
-
this.defaultValue = defaultValue;
|
|
663
|
-
this.label = label;
|
|
664
|
-
this.attributes = attributes;
|
|
665
|
-
this.classes = classes;
|
|
666
|
-
this.styles = styles;
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Returns {@link module:engine/view/matcher~MatcherPattern} with decorator attributes.
|
|
670
|
-
*
|
|
671
|
-
* @internal
|
|
672
|
-
*/
|
|
673
|
-
_createPattern() {
|
|
674
|
-
return {
|
|
675
|
-
attributes: this.attributes,
|
|
676
|
-
classes: this.classes,
|
|
677
|
-
styles: this.styles
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
684
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
685
|
-
*/
|
|
686
|
-
/**
|
|
687
|
-
* @module link/linkediting
|
|
688
|
-
*/
|
|
689
|
-
const HIGHLIGHT_CLASS = 'ck-link_selected';
|
|
690
|
-
const DECORATOR_AUTOMATIC = 'automatic';
|
|
691
|
-
const DECORATOR_MANUAL = 'manual';
|
|
692
|
-
const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
|
|
693
|
-
/**
|
|
694
|
-
* The link engine feature.
|
|
695
|
-
*
|
|
696
|
-
* It introduces the `linkHref="url"` attribute in the model which renders to the view as a `<a href="url">` element
|
|
697
|
-
* as well as `'link'` and `'unlink'` commands.
|
|
698
|
-
*/
|
|
699
|
-
class LinkEditing extends Plugin {
|
|
700
|
-
/**
|
|
701
|
-
* @inheritDoc
|
|
702
|
-
*/
|
|
703
|
-
static get pluginName() {
|
|
704
|
-
return 'LinkEditing';
|
|
705
|
-
}
|
|
706
|
-
/**
|
|
707
|
-
* @inheritDoc
|
|
708
|
-
*/
|
|
709
|
-
static get requires() {
|
|
710
|
-
// Clipboard is required for handling cut and paste events while typing over the link.
|
|
711
|
-
return [TwoStepCaretMovement, Input, ClipboardPipeline];
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* @inheritDoc
|
|
715
|
-
*/
|
|
716
|
-
constructor(editor) {
|
|
717
|
-
super(editor);
|
|
718
|
-
editor.config.define('link', {
|
|
719
|
-
allowCreatingEmptyLinks: false,
|
|
720
|
-
addTargetToExternalLinks: false
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
/**
|
|
724
|
-
* @inheritDoc
|
|
725
|
-
*/
|
|
726
|
-
init() {
|
|
727
|
-
const editor = this.editor;
|
|
728
|
-
const allowedProtocols = this.editor.config.get('link.allowedProtocols');
|
|
729
|
-
// Allow link attribute on all inline nodes.
|
|
730
|
-
editor.model.schema.extend('$text', { allowAttributes: 'linkHref' });
|
|
731
|
-
editor.conversion.for('dataDowncast')
|
|
732
|
-
.attributeToElement({ model: 'linkHref', view: createLinkElement });
|
|
733
|
-
editor.conversion.for('editingDowncast')
|
|
734
|
-
.attributeToElement({ model: 'linkHref', view: (href, conversionApi) => {
|
|
735
|
-
return createLinkElement(ensureSafeUrl(href, allowedProtocols), conversionApi);
|
|
736
|
-
} });
|
|
737
|
-
editor.conversion.for('upcast')
|
|
738
|
-
.elementToAttribute({
|
|
739
|
-
view: {
|
|
740
|
-
name: 'a',
|
|
741
|
-
attributes: {
|
|
742
|
-
href: true
|
|
743
|
-
}
|
|
744
|
-
},
|
|
745
|
-
model: {
|
|
746
|
-
key: 'linkHref',
|
|
747
|
-
value: (viewElement) => viewElement.getAttribute('href')
|
|
748
|
-
}
|
|
749
|
-
});
|
|
750
|
-
// Create linking commands.
|
|
751
|
-
editor.commands.add('link', new LinkCommand(editor));
|
|
752
|
-
editor.commands.add('unlink', new UnlinkCommand(editor));
|
|
753
|
-
const linkDecorators = getLocalizedDecorators(editor.t, normalizeDecorators(editor.config.get('link.decorators')));
|
|
754
|
-
this._enableAutomaticDecorators(linkDecorators
|
|
755
|
-
.filter((item) => item.mode === DECORATOR_AUTOMATIC));
|
|
756
|
-
this._enableManualDecorators(linkDecorators
|
|
757
|
-
.filter((item) => item.mode === DECORATOR_MANUAL));
|
|
758
|
-
// Enable two-step caret movement for `linkHref` attribute.
|
|
759
|
-
const twoStepCaretMovementPlugin = editor.plugins.get(TwoStepCaretMovement);
|
|
760
|
-
twoStepCaretMovementPlugin.registerAttribute('linkHref');
|
|
761
|
-
// Setup highlight over selected link.
|
|
762
|
-
inlineHighlight(editor, 'linkHref', 'a', HIGHLIGHT_CLASS);
|
|
763
|
-
// Handle link following by CTRL+click or ALT+ENTER
|
|
764
|
-
this._enableLinkOpen();
|
|
765
|
-
// Clears the DocumentSelection decorator attributes if the selection is no longer in a link (for example while using 2-SCM).
|
|
766
|
-
this._enableSelectionAttributesFixer();
|
|
767
|
-
// Handle adding default protocol to pasted links.
|
|
768
|
-
this._enableClipboardIntegration();
|
|
769
|
-
}
|
|
770
|
-
/**
|
|
771
|
-
* Processes an array of configured {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition automatic decorators}
|
|
772
|
-
* and registers a {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast dispatcher}
|
|
773
|
-
* for each one of them. Downcast dispatchers are obtained using the
|
|
774
|
-
* {@link module:link/utils/automaticdecorators~AutomaticDecorators#getDispatcher} method.
|
|
775
|
-
*
|
|
776
|
-
* **Note**: This method also activates the automatic external link decorator if enabled with
|
|
777
|
-
* {@link module:link/linkconfig~LinkConfig#addTargetToExternalLinks `config.link.addTargetToExternalLinks`}.
|
|
778
|
-
*/
|
|
779
|
-
_enableAutomaticDecorators(automaticDecoratorDefinitions) {
|
|
780
|
-
const editor = this.editor;
|
|
781
|
-
// Store automatic decorators in the command instance as we do the same with manual decorators.
|
|
782
|
-
// Thanks to that, `LinkImageEditing` plugin can re-use the same definitions.
|
|
783
|
-
const command = editor.commands.get('link');
|
|
784
|
-
const automaticDecorators = command.automaticDecorators;
|
|
785
|
-
// Adds a default decorator for external links.
|
|
786
|
-
if (editor.config.get('link.addTargetToExternalLinks')) {
|
|
787
|
-
automaticDecorators.add({
|
|
788
|
-
id: 'linkIsExternal',
|
|
789
|
-
mode: DECORATOR_AUTOMATIC,
|
|
790
|
-
callback: url => !!url && EXTERNAL_LINKS_REGEXP.test(url),
|
|
791
|
-
attributes: {
|
|
792
|
-
target: '_blank',
|
|
793
|
-
rel: 'noopener noreferrer'
|
|
794
|
-
}
|
|
795
|
-
});
|
|
796
|
-
}
|
|
797
|
-
automaticDecorators.add(automaticDecoratorDefinitions);
|
|
798
|
-
if (automaticDecorators.length) {
|
|
799
|
-
editor.conversion.for('downcast').add(automaticDecorators.getDispatcher());
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
/**
|
|
803
|
-
* Processes an array of configured {@link module:link/linkconfig~LinkDecoratorManualDefinition manual decorators},
|
|
804
|
-
* transforms them into {@link module:link/utils/manualdecorator~ManualDecorator} instances and stores them in the
|
|
805
|
-
* {@link module:link/linkcommand~LinkCommand#manualDecorators} collection (a model for manual decorators state).
|
|
806
|
-
*
|
|
807
|
-
* Also registers an {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement attribute-to-element}
|
|
808
|
-
* converter for each manual decorator and extends the {@link module:engine/model/schema~Schema model's schema}
|
|
809
|
-
* with adequate model attributes.
|
|
810
|
-
*/
|
|
811
|
-
_enableManualDecorators(manualDecoratorDefinitions) {
|
|
812
|
-
if (!manualDecoratorDefinitions.length) {
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
const editor = this.editor;
|
|
816
|
-
const command = editor.commands.get('link');
|
|
817
|
-
const manualDecorators = command.manualDecorators;
|
|
818
|
-
manualDecoratorDefinitions.forEach(decoratorDefinition => {
|
|
819
|
-
editor.model.schema.extend('$text', { allowAttributes: decoratorDefinition.id });
|
|
820
|
-
// Keeps reference to manual decorator to decode its name to attributes during downcast.
|
|
821
|
-
const decorator = new ManualDecorator(decoratorDefinition);
|
|
822
|
-
manualDecorators.add(decorator);
|
|
823
|
-
editor.conversion.for('downcast').attributeToElement({
|
|
824
|
-
model: decorator.id,
|
|
825
|
-
view: (manualDecoratorValue, { writer, schema }, { item }) => {
|
|
826
|
-
// Manual decorators for block links are handled e.g. in LinkImageEditing.
|
|
827
|
-
if (!(item.is('selection') || schema.isInline(item))) {
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
if (manualDecoratorValue) {
|
|
831
|
-
const element = writer.createAttributeElement('a', decorator.attributes, { priority: 5 });
|
|
832
|
-
if (decorator.classes) {
|
|
833
|
-
writer.addClass(decorator.classes, element);
|
|
834
|
-
}
|
|
835
|
-
for (const key in decorator.styles) {
|
|
836
|
-
writer.setStyle(key, decorator.styles[key], element);
|
|
837
|
-
}
|
|
838
|
-
writer.setCustomProperty('link', true, element);
|
|
839
|
-
return element;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
});
|
|
843
|
-
editor.conversion.for('upcast').elementToAttribute({
|
|
844
|
-
view: {
|
|
845
|
-
name: 'a',
|
|
846
|
-
...decorator._createPattern()
|
|
847
|
-
},
|
|
848
|
-
model: {
|
|
849
|
-
key: decorator.id
|
|
850
|
-
}
|
|
851
|
-
});
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* Attaches handlers for {@link module:engine/view/document~Document#event:enter} and
|
|
856
|
-
* {@link module:engine/view/document~Document#event:click} to enable link following.
|
|
857
|
-
*/
|
|
858
|
-
_enableLinkOpen() {
|
|
859
|
-
const editor = this.editor;
|
|
860
|
-
const view = editor.editing.view;
|
|
861
|
-
const viewDocument = view.document;
|
|
862
|
-
this.listenTo(viewDocument, 'click', (evt, data) => {
|
|
863
|
-
const shouldOpen = env.isMac ? data.domEvent.metaKey : data.domEvent.ctrlKey;
|
|
864
|
-
if (!shouldOpen) {
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
|
-
let clickedElement = data.domTarget;
|
|
868
|
-
if (clickedElement.tagName.toLowerCase() != 'a') {
|
|
869
|
-
clickedElement = clickedElement.closest('a');
|
|
870
|
-
}
|
|
871
|
-
if (!clickedElement) {
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
const url = clickedElement.getAttribute('href');
|
|
875
|
-
if (!url) {
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
evt.stop();
|
|
879
|
-
data.preventDefault();
|
|
880
|
-
openLink(url);
|
|
881
|
-
}, { context: '$capture' });
|
|
882
|
-
// Open link on Alt+Enter.
|
|
883
|
-
this.listenTo(viewDocument, 'keydown', (evt, data) => {
|
|
884
|
-
const linkCommand = editor.commands.get('link');
|
|
885
|
-
const url = linkCommand.value;
|
|
886
|
-
const shouldOpen = !!url && data.keyCode === keyCodes.enter && data.altKey;
|
|
887
|
-
if (!shouldOpen) {
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
evt.stop();
|
|
891
|
-
openLink(url);
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
/**
|
|
895
|
-
* Watches the DocumentSelection attribute changes and removes link decorator attributes when the linkHref attribute is removed.
|
|
896
|
-
*
|
|
897
|
-
* This is to ensure that there is no left-over link decorator attributes on the document selection that is no longer in a link.
|
|
898
|
-
*/
|
|
899
|
-
_enableSelectionAttributesFixer() {
|
|
900
|
-
const editor = this.editor;
|
|
901
|
-
const model = editor.model;
|
|
902
|
-
const selection = model.document.selection;
|
|
903
|
-
this.listenTo(selection, 'change:attribute', (evt, { attributeKeys }) => {
|
|
904
|
-
if (!attributeKeys.includes('linkHref') || selection.hasAttribute('linkHref')) {
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
model.change(writer => {
|
|
908
|
-
removeLinkAttributesFromSelection(writer, getLinkAttributesAllowedOnText(model.schema));
|
|
909
|
-
});
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
/**
|
|
913
|
-
* Enables URL fixing on pasting.
|
|
914
|
-
*/
|
|
915
|
-
_enableClipboardIntegration() {
|
|
916
|
-
const editor = this.editor;
|
|
917
|
-
const model = editor.model;
|
|
918
|
-
const defaultProtocol = this.editor.config.get('link.defaultProtocol');
|
|
919
|
-
if (!defaultProtocol) {
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
this.listenTo(editor.plugins.get('ClipboardPipeline'), 'contentInsertion', (evt, data) => {
|
|
923
|
-
model.change(writer => {
|
|
924
|
-
const range = writer.createRangeIn(data.content);
|
|
925
|
-
for (const item of range.getItems()) {
|
|
926
|
-
if (item.hasAttribute('linkHref')) {
|
|
927
|
-
const newLink = addLinkProtocolIfApplicable(item.getAttribute('linkHref'), defaultProtocol);
|
|
928
|
-
writer.setAttribute('linkHref', newLink, item);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
});
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
/**
|
|
936
|
-
* Make the selection free of link-related model attributes.
|
|
937
|
-
* All link-related model attributes start with "link". That includes not only "linkHref"
|
|
938
|
-
* but also all decorator attributes (they have dynamic names), or even custom plugins.
|
|
939
|
-
*/
|
|
940
|
-
function removeLinkAttributesFromSelection(writer, linkAttributes) {
|
|
941
|
-
writer.removeSelectionAttribute('linkHref');
|
|
942
|
-
for (const attribute of linkAttributes) {
|
|
943
|
-
writer.removeSelectionAttribute(attribute);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
/**
|
|
947
|
-
* Returns an array containing names of the attributes allowed on `$text` that describes the link item.
|
|
948
|
-
*/
|
|
949
|
-
function getLinkAttributesAllowedOnText(schema) {
|
|
950
|
-
const textAttributes = schema.getDefinition('$text').allowAttributes;
|
|
951
|
-
return textAttributes.filter(attribute => attribute.startsWith('link'));
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
/**
|
|
955
|
-
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
956
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
957
|
-
*/
|
|
958
|
-
/**
|
|
959
|
-
* @module link/ui/linkformview
|
|
960
|
-
*/
|
|
961
|
-
/**
|
|
962
|
-
* The link form view controller class.
|
|
963
|
-
*
|
|
964
|
-
* See {@link module:link/ui/linkformview~LinkFormView}.
|
|
965
|
-
*/
|
|
966
|
-
class LinkFormView extends View {
|
|
967
|
-
/**
|
|
968
|
-
* Creates an instance of the {@link module:link/ui/linkformview~LinkFormView} class.
|
|
969
|
-
*
|
|
970
|
-
* Also see {@link #render}.
|
|
971
|
-
*
|
|
972
|
-
* @param locale The localization services instance.
|
|
973
|
-
* @param linkCommand Reference to {@link module:link/linkcommand~LinkCommand}.
|
|
974
|
-
*/
|
|
975
|
-
constructor(locale, linkCommand) {
|
|
976
|
-
super(locale);
|
|
977
|
-
/**
|
|
978
|
-
* Tracks information about DOM focus in the form.
|
|
979
|
-
*/
|
|
980
|
-
this.focusTracker = new FocusTracker();
|
|
981
|
-
/**
|
|
982
|
-
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
|
|
983
|
-
*/
|
|
984
|
-
this.keystrokes = new KeystrokeHandler();
|
|
985
|
-
/**
|
|
986
|
-
* A collection of views that can be focused in the form.
|
|
987
|
-
*/
|
|
988
|
-
this._focusables = new ViewCollection();
|
|
989
|
-
const t = locale.t;
|
|
990
|
-
this.urlInputView = this._createUrlInput();
|
|
991
|
-
this.saveButtonView = this._createButton(t('Save'), icons.check, 'ck-button-save');
|
|
992
|
-
this.saveButtonView.type = 'submit';
|
|
993
|
-
this.cancelButtonView = this._createButton(t('Cancel'), icons.cancel, 'ck-button-cancel', 'cancel');
|
|
994
|
-
this._manualDecoratorSwitches = this._createManualDecoratorSwitches(linkCommand);
|
|
995
|
-
this.children = this._createFormChildren(linkCommand.manualDecorators);
|
|
996
|
-
this._focusCycler = new FocusCycler({
|
|
997
|
-
focusables: this._focusables,
|
|
998
|
-
focusTracker: this.focusTracker,
|
|
999
|
-
keystrokeHandler: this.keystrokes,
|
|
1000
|
-
actions: {
|
|
1001
|
-
// Navigate form fields backwards using the Shift + Tab keystroke.
|
|
1002
|
-
focusPrevious: 'shift + tab',
|
|
1003
|
-
// Navigate form fields forwards using the Tab key.
|
|
1004
|
-
focusNext: 'tab'
|
|
1005
|
-
}
|
|
1006
|
-
});
|
|
1007
|
-
const classList = ['ck', 'ck-link-form', 'ck-responsive-form'];
|
|
1008
|
-
if (linkCommand.manualDecorators.length) {
|
|
1009
|
-
classList.push('ck-link-form_layout-vertical', 'ck-vertical-form');
|
|
1010
|
-
}
|
|
1011
|
-
this.setTemplate({
|
|
1012
|
-
tag: 'form',
|
|
1013
|
-
attributes: {
|
|
1014
|
-
class: classList,
|
|
1015
|
-
// https://github.com/ckeditor/ckeditor5-link/issues/90
|
|
1016
|
-
tabindex: '-1'
|
|
1017
|
-
},
|
|
1018
|
-
children: this.children
|
|
1019
|
-
});
|
|
1020
|
-
}
|
|
1021
|
-
/**
|
|
1022
|
-
* Obtains the state of the {@link module:ui/button/switchbuttonview~SwitchButtonView switch buttons} representing
|
|
1023
|
-
* {@link module:link/linkcommand~LinkCommand#manualDecorators manual link decorators}
|
|
1024
|
-
* in the {@link module:link/ui/linkformview~LinkFormView}.
|
|
1025
|
-
*
|
|
1026
|
-
* @returns Key-value pairs, where the key is the name of the decorator and the value is its state.
|
|
1027
|
-
*/
|
|
1028
|
-
getDecoratorSwitchesState() {
|
|
1029
|
-
return Array
|
|
1030
|
-
.from(this._manualDecoratorSwitches)
|
|
1031
|
-
.reduce((accumulator, switchButton) => {
|
|
1032
|
-
accumulator[switchButton.name] = switchButton.isOn;
|
|
1033
|
-
return accumulator;
|
|
1034
|
-
}, {});
|
|
1035
|
-
}
|
|
1036
|
-
/**
|
|
1037
|
-
* @inheritDoc
|
|
1038
|
-
*/
|
|
1039
|
-
render() {
|
|
1040
|
-
super.render();
|
|
1041
|
-
submitHandler({
|
|
1042
|
-
view: this
|
|
1043
|
-
});
|
|
1044
|
-
const childViews = [
|
|
1045
|
-
this.urlInputView,
|
|
1046
|
-
...this._manualDecoratorSwitches,
|
|
1047
|
-
this.saveButtonView,
|
|
1048
|
-
this.cancelButtonView
|
|
1049
|
-
];
|
|
1050
|
-
childViews.forEach(v => {
|
|
1051
|
-
// Register the view as focusable.
|
|
1052
|
-
this._focusables.add(v);
|
|
1053
|
-
// Register the view in the focus tracker.
|
|
1054
|
-
this.focusTracker.add(v.element);
|
|
1055
|
-
});
|
|
1056
|
-
// Start listening for the keystrokes coming from #element.
|
|
1057
|
-
this.keystrokes.listenTo(this.element);
|
|
1058
|
-
}
|
|
1059
|
-
/**
|
|
1060
|
-
* @inheritDoc
|
|
1061
|
-
*/
|
|
1062
|
-
destroy() {
|
|
1063
|
-
super.destroy();
|
|
1064
|
-
this.focusTracker.destroy();
|
|
1065
|
-
this.keystrokes.destroy();
|
|
1066
|
-
}
|
|
1067
|
-
/**
|
|
1068
|
-
* Focuses the fist {@link #_focusables} in the form.
|
|
1069
|
-
*/
|
|
1070
|
-
focus() {
|
|
1071
|
-
this._focusCycler.focusFirst();
|
|
1072
|
-
}
|
|
1073
|
-
/**
|
|
1074
|
-
* Creates a labeled input view.
|
|
1075
|
-
*
|
|
1076
|
-
* @returns Labeled field view instance.
|
|
1077
|
-
*/
|
|
1078
|
-
_createUrlInput() {
|
|
1079
|
-
const t = this.locale.t;
|
|
1080
|
-
const labeledInput = new LabeledFieldView(this.locale, createLabeledInputText);
|
|
1081
|
-
labeledInput.label = t('Link URL');
|
|
1082
|
-
return labeledInput;
|
|
1083
|
-
}
|
|
1084
|
-
/**
|
|
1085
|
-
* Creates a button view.
|
|
1086
|
-
*
|
|
1087
|
-
* @param label The button label.
|
|
1088
|
-
* @param icon The button icon.
|
|
1089
|
-
* @param className The additional button CSS class name.
|
|
1090
|
-
* @param eventName An event name that the `ButtonView#execute` event will be delegated to.
|
|
1091
|
-
* @returns The button view instance.
|
|
1092
|
-
*/
|
|
1093
|
-
_createButton(label, icon, className, eventName) {
|
|
1094
|
-
const button = new ButtonView(this.locale);
|
|
1095
|
-
button.set({
|
|
1096
|
-
label,
|
|
1097
|
-
icon,
|
|
1098
|
-
tooltip: true
|
|
1099
|
-
});
|
|
1100
|
-
button.extendTemplate({
|
|
1101
|
-
attributes: {
|
|
1102
|
-
class: className
|
|
1103
|
-
}
|
|
1104
|
-
});
|
|
1105
|
-
if (eventName) {
|
|
1106
|
-
button.delegate('execute').to(this, eventName);
|
|
1107
|
-
}
|
|
1108
|
-
return button;
|
|
1109
|
-
}
|
|
1110
|
-
/**
|
|
1111
|
-
* Populates {@link module:ui/viewcollection~ViewCollection} of {@link module:ui/button/switchbuttonview~SwitchButtonView}
|
|
1112
|
-
* made based on {@link module:link/linkcommand~LinkCommand#manualDecorators}.
|
|
1113
|
-
*
|
|
1114
|
-
* @param linkCommand A reference to the link command.
|
|
1115
|
-
* @returns ViewCollection of switch buttons.
|
|
1116
|
-
*/
|
|
1117
|
-
_createManualDecoratorSwitches(linkCommand) {
|
|
1118
|
-
const switches = this.createCollection();
|
|
1119
|
-
for (const manualDecorator of linkCommand.manualDecorators) {
|
|
1120
|
-
const switchButton = new SwitchButtonView(this.locale);
|
|
1121
|
-
switchButton.set({
|
|
1122
|
-
name: manualDecorator.id,
|
|
1123
|
-
label: manualDecorator.label,
|
|
1124
|
-
withText: true
|
|
1125
|
-
});
|
|
1126
|
-
switchButton.bind('isOn').toMany([manualDecorator, linkCommand], 'value', (decoratorValue, commandValue) => {
|
|
1127
|
-
return commandValue === undefined && decoratorValue === undefined ? !!manualDecorator.defaultValue : !!decoratorValue;
|
|
1128
|
-
});
|
|
1129
|
-
switchButton.on('execute', () => {
|
|
1130
|
-
manualDecorator.set('value', !switchButton.isOn);
|
|
1131
|
-
});
|
|
1132
|
-
switches.add(switchButton);
|
|
1133
|
-
}
|
|
1134
|
-
return switches;
|
|
1135
|
-
}
|
|
1136
|
-
/**
|
|
1137
|
-
* Populates the {@link #children} collection of the form.
|
|
1138
|
-
*
|
|
1139
|
-
* If {@link module:link/linkcommand~LinkCommand#manualDecorators manual decorators} are configured in the editor, it creates an
|
|
1140
|
-
* additional `View` wrapping all {@link #_manualDecoratorSwitches} switch buttons corresponding
|
|
1141
|
-
* to these decorators.
|
|
1142
|
-
*
|
|
1143
|
-
* @param manualDecorators A reference to
|
|
1144
|
-
* the collection of manual decorators stored in the link command.
|
|
1145
|
-
* @returns The children of link form view.
|
|
1146
|
-
*/
|
|
1147
|
-
_createFormChildren(manualDecorators) {
|
|
1148
|
-
const children = this.createCollection();
|
|
1149
|
-
children.add(this.urlInputView);
|
|
1150
|
-
if (manualDecorators.length) {
|
|
1151
|
-
const additionalButtonsView = new View();
|
|
1152
|
-
additionalButtonsView.setTemplate({
|
|
1153
|
-
tag: 'ul',
|
|
1154
|
-
children: this._manualDecoratorSwitches.map(switchButton => ({
|
|
1155
|
-
tag: 'li',
|
|
1156
|
-
children: [switchButton],
|
|
1157
|
-
attributes: {
|
|
1158
|
-
class: [
|
|
1159
|
-
'ck',
|
|
1160
|
-
'ck-list__item'
|
|
1161
|
-
]
|
|
1162
|
-
}
|
|
1163
|
-
})),
|
|
1164
|
-
attributes: {
|
|
1165
|
-
class: [
|
|
1166
|
-
'ck',
|
|
1167
|
-
'ck-reset',
|
|
1168
|
-
'ck-list'
|
|
1169
|
-
]
|
|
1170
|
-
}
|
|
1171
|
-
});
|
|
1172
|
-
children.add(additionalButtonsView);
|
|
1173
|
-
}
|
|
1174
|
-
children.add(this.saveButtonView);
|
|
1175
|
-
children.add(this.cancelButtonView);
|
|
1176
|
-
return children;
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
var unlinkIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m11.077 15 .991-1.416a.75.75 0 1 1 1.229.86l-1.148 1.64a.748.748 0 0 1-.217.206 5.251 5.251 0 0 1-8.503-5.955.741.741 0 0 1 .12-.274l1.147-1.639a.75.75 0 1 1 1.228.86L4.933 10.7l.006.003a3.75 3.75 0 0 0 6.132 4.294l.006.004zm5.494-5.335a.748.748 0 0 1-.12.274l-1.147 1.639a.75.75 0 1 1-1.228-.86l.86-1.23a3.75 3.75 0 0 0-6.144-4.301l-.86 1.229a.75.75 0 0 1-1.229-.86l1.148-1.64a.748.748 0 0 1 .217-.206 5.251 5.251 0 0 1 8.503 5.955zm-4.563-2.532a.75.75 0 0 1 .184 1.045l-3.155 4.505a.75.75 0 1 1-1.229-.86l3.155-4.506a.75.75 0 0 1 1.045-.184zm4.919 10.562-1.414 1.414a.75.75 0 1 1-1.06-1.06l1.414-1.415-1.415-1.414a.75.75 0 0 1 1.061-1.06l1.414 1.414 1.414-1.415a.75.75 0 0 1 1.061 1.061l-1.414 1.414 1.414 1.415a.75.75 0 0 1-1.06 1.06l-1.415-1.414z\"/></svg>";
|
|
1181
|
-
|
|
1182
|
-
/**
|
|
1183
|
-
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
1184
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
1185
|
-
*/
|
|
1186
|
-
/**
|
|
1187
|
-
* @module link/ui/linkactionsview
|
|
1188
|
-
*/
|
|
1189
|
-
/**
|
|
1190
|
-
* The link actions view class. This view displays the link preview, allows
|
|
1191
|
-
* unlinking or editing the link.
|
|
1192
|
-
*/
|
|
1193
|
-
class LinkActionsView extends View {
|
|
1194
|
-
/**
|
|
1195
|
-
* @inheritDoc
|
|
1196
|
-
*/
|
|
1197
|
-
constructor(locale, linkConfig = {}) {
|
|
1198
|
-
super(locale);
|
|
1199
|
-
/**
|
|
1200
|
-
* Tracks information about DOM focus in the actions.
|
|
1201
|
-
*/
|
|
1202
|
-
this.focusTracker = new FocusTracker();
|
|
1203
|
-
/**
|
|
1204
|
-
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
|
|
1205
|
-
*/
|
|
1206
|
-
this.keystrokes = new KeystrokeHandler();
|
|
1207
|
-
/**
|
|
1208
|
-
* A collection of views that can be focused in the view.
|
|
1209
|
-
*/
|
|
1210
|
-
this._focusables = new ViewCollection();
|
|
1211
|
-
const t = locale.t;
|
|
1212
|
-
this.previewButtonView = this._createPreviewButton();
|
|
1213
|
-
this.unlinkButtonView = this._createButton(t('Unlink'), unlinkIcon, 'unlink');
|
|
1214
|
-
this.editButtonView = this._createButton(t('Edit link'), icons.pencil, 'edit');
|
|
1215
|
-
this.set('href', undefined);
|
|
1216
|
-
this._linkConfig = linkConfig;
|
|
1217
|
-
this._focusCycler = new FocusCycler({
|
|
1218
|
-
focusables: this._focusables,
|
|
1219
|
-
focusTracker: this.focusTracker,
|
|
1220
|
-
keystrokeHandler: this.keystrokes,
|
|
1221
|
-
actions: {
|
|
1222
|
-
// Navigate fields backwards using the Shift + Tab keystroke.
|
|
1223
|
-
focusPrevious: 'shift + tab',
|
|
1224
|
-
// Navigate fields forwards using the Tab key.
|
|
1225
|
-
focusNext: 'tab'
|
|
1226
|
-
}
|
|
1227
|
-
});
|
|
1228
|
-
this.setTemplate({
|
|
1229
|
-
tag: 'div',
|
|
1230
|
-
attributes: {
|
|
1231
|
-
class: [
|
|
1232
|
-
'ck',
|
|
1233
|
-
'ck-link-actions',
|
|
1234
|
-
'ck-responsive-form'
|
|
1235
|
-
],
|
|
1236
|
-
// https://github.com/ckeditor/ckeditor5-link/issues/90
|
|
1237
|
-
tabindex: '-1'
|
|
1238
|
-
},
|
|
1239
|
-
children: [
|
|
1240
|
-
this.previewButtonView,
|
|
1241
|
-
this.editButtonView,
|
|
1242
|
-
this.unlinkButtonView
|
|
1243
|
-
]
|
|
1244
|
-
});
|
|
1245
|
-
}
|
|
1246
|
-
/**
|
|
1247
|
-
* @inheritDoc
|
|
1248
|
-
*/
|
|
1249
|
-
render() {
|
|
1250
|
-
super.render();
|
|
1251
|
-
const childViews = [
|
|
1252
|
-
this.previewButtonView,
|
|
1253
|
-
this.editButtonView,
|
|
1254
|
-
this.unlinkButtonView
|
|
1255
|
-
];
|
|
1256
|
-
childViews.forEach(v => {
|
|
1257
|
-
// Register the view as focusable.
|
|
1258
|
-
this._focusables.add(v);
|
|
1259
|
-
// Register the view in the focus tracker.
|
|
1260
|
-
this.focusTracker.add(v.element);
|
|
1261
|
-
});
|
|
1262
|
-
// Start listening for the keystrokes coming from #element.
|
|
1263
|
-
this.keystrokes.listenTo(this.element);
|
|
1264
|
-
}
|
|
1265
|
-
/**
|
|
1266
|
-
* @inheritDoc
|
|
1267
|
-
*/
|
|
1268
|
-
destroy() {
|
|
1269
|
-
super.destroy();
|
|
1270
|
-
this.focusTracker.destroy();
|
|
1271
|
-
this.keystrokes.destroy();
|
|
1272
|
-
}
|
|
1273
|
-
/**
|
|
1274
|
-
* Focuses the fist {@link #_focusables} in the actions.
|
|
1275
|
-
*/
|
|
1276
|
-
focus() {
|
|
1277
|
-
this._focusCycler.focusFirst();
|
|
1278
|
-
}
|
|
1279
|
-
/**
|
|
1280
|
-
* Creates a button view.
|
|
1281
|
-
*
|
|
1282
|
-
* @param label The button label.
|
|
1283
|
-
* @param icon The button icon.
|
|
1284
|
-
* @param eventName An event name that the `ButtonView#execute` event will be delegated to.
|
|
1285
|
-
* @returns The button view instance.
|
|
1286
|
-
*/
|
|
1287
|
-
_createButton(label, icon, eventName) {
|
|
1288
|
-
const button = new ButtonView(this.locale);
|
|
1289
|
-
button.set({
|
|
1290
|
-
label,
|
|
1291
|
-
icon,
|
|
1292
|
-
tooltip: true
|
|
1293
|
-
});
|
|
1294
|
-
button.delegate('execute').to(this, eventName);
|
|
1295
|
-
return button;
|
|
1296
|
-
}
|
|
1297
|
-
/**
|
|
1298
|
-
* Creates a link href preview button.
|
|
1299
|
-
*
|
|
1300
|
-
* @returns The button view instance.
|
|
1301
|
-
*/
|
|
1302
|
-
_createPreviewButton() {
|
|
1303
|
-
const button = new ButtonView(this.locale);
|
|
1304
|
-
const bind = this.bindTemplate;
|
|
1305
|
-
const t = this.t;
|
|
1306
|
-
button.set({
|
|
1307
|
-
withText: true,
|
|
1308
|
-
tooltip: t('Open link in new tab')
|
|
1309
|
-
});
|
|
1310
|
-
button.extendTemplate({
|
|
1311
|
-
attributes: {
|
|
1312
|
-
class: [
|
|
1313
|
-
'ck',
|
|
1314
|
-
'ck-link-actions__preview'
|
|
1315
|
-
],
|
|
1316
|
-
href: bind.to('href', href => href && ensureSafeUrl(href, this._linkConfig.allowedProtocols)),
|
|
1317
|
-
target: '_blank',
|
|
1318
|
-
rel: 'noopener noreferrer'
|
|
1319
|
-
}
|
|
1320
|
-
});
|
|
1321
|
-
button.bind('label').to(this, 'href', href => {
|
|
1322
|
-
return href || t('This link has no URL');
|
|
1323
|
-
});
|
|
1324
|
-
button.bind('isEnabled').to(this, 'href', href => !!href);
|
|
1325
|
-
button.template.tag = 'a';
|
|
1326
|
-
button.template.eventListeners = {};
|
|
1327
|
-
return button;
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
var linkIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m11.077 15 .991-1.416a.75.75 0 1 1 1.229.86l-1.148 1.64a.748.748 0 0 1-.217.206 5.251 5.251 0 0 1-8.503-5.955.741.741 0 0 1 .12-.274l1.147-1.639a.75.75 0 1 1 1.228.86L4.933 10.7l.006.003a3.75 3.75 0 0 0 6.132 4.294l.006.004zm5.494-5.335a.748.748 0 0 1-.12.274l-1.147 1.639a.75.75 0 1 1-1.228-.86l.86-1.23a3.75 3.75 0 0 0-6.144-4.301l-.86 1.229a.75.75 0 0 1-1.229-.86l1.148-1.64a.748.748 0 0 1 .217-.206 5.251 5.251 0 0 1 8.503 5.955zm-4.563-2.532a.75.75 0 0 1 .184 1.045l-3.155 4.505a.75.75 0 1 1-1.229-.86l3.155-4.506a.75.75 0 0 1 1.045-.184z\"/></svg>";
|
|
1332
|
-
|
|
1333
|
-
/**
|
|
1334
|
-
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
1335
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
1336
|
-
*/
|
|
1337
|
-
/**
|
|
1338
|
-
* @module link/linkui
|
|
1339
|
-
*/
|
|
1340
|
-
const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
1341
|
-
/**
|
|
1342
|
-
* The link UI plugin. It introduces the `'link'` and `'unlink'` buttons and support for the <kbd>Ctrl+K</kbd> keystroke.
|
|
1343
|
-
*
|
|
1344
|
-
* It uses the
|
|
1345
|
-
* {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
|
|
1346
|
-
*/
|
|
1347
|
-
class LinkUI extends Plugin {
|
|
1348
|
-
constructor() {
|
|
1349
|
-
super(...arguments);
|
|
1350
|
-
/**
|
|
1351
|
-
* The actions view displayed inside of the balloon.
|
|
1352
|
-
*/
|
|
1353
|
-
this.actionsView = null;
|
|
1354
|
-
/**
|
|
1355
|
-
* The form view displayed inside the balloon.
|
|
1356
|
-
*/
|
|
1357
|
-
this.formView = null;
|
|
1358
|
-
}
|
|
1359
|
-
/**
|
|
1360
|
-
* @inheritDoc
|
|
1361
|
-
*/
|
|
1362
|
-
static get requires() {
|
|
1363
|
-
return [ContextualBalloon];
|
|
1364
|
-
}
|
|
1365
|
-
/**
|
|
1366
|
-
* @inheritDoc
|
|
1367
|
-
*/
|
|
1368
|
-
static get pluginName() {
|
|
1369
|
-
return 'LinkUI';
|
|
1370
|
-
}
|
|
1371
|
-
/**
|
|
1372
|
-
* @inheritDoc
|
|
1373
|
-
*/
|
|
1374
|
-
init() {
|
|
1375
|
-
const editor = this.editor;
|
|
1376
|
-
const t = this.editor.t;
|
|
1377
|
-
editor.editing.view.addObserver(ClickObserver);
|
|
1378
|
-
this._balloon = editor.plugins.get(ContextualBalloon);
|
|
1379
|
-
// Create toolbar buttons.
|
|
1380
|
-
this._createToolbarLinkButton();
|
|
1381
|
-
this._enableBalloonActivators();
|
|
1382
|
-
// Renders a fake visual selection marker on an expanded selection.
|
|
1383
|
-
editor.conversion.for('editingDowncast').markerToHighlight({
|
|
1384
|
-
model: VISUAL_SELECTION_MARKER_NAME,
|
|
1385
|
-
view: {
|
|
1386
|
-
classes: ['ck-fake-link-selection']
|
|
1387
|
-
}
|
|
1388
|
-
});
|
|
1389
|
-
// Renders a fake visual selection marker on a collapsed selection.
|
|
1390
|
-
editor.conversion.for('editingDowncast').markerToElement({
|
|
1391
|
-
model: VISUAL_SELECTION_MARKER_NAME,
|
|
1392
|
-
view: {
|
|
1393
|
-
name: 'span',
|
|
1394
|
-
classes: ['ck-fake-link-selection', 'ck-fake-link-selection_collapsed']
|
|
1395
|
-
}
|
|
1396
|
-
});
|
|
1397
|
-
// Add the information about the keystrokes to the accessibility database.
|
|
1398
|
-
editor.accessibility.addKeystrokeInfos({
|
|
1399
|
-
keystrokes: [
|
|
1400
|
-
{
|
|
1401
|
-
label: t('Create link'),
|
|
1402
|
-
keystroke: LINK_KEYSTROKE
|
|
1403
|
-
},
|
|
1404
|
-
{
|
|
1405
|
-
label: t('Move out of a link'),
|
|
1406
|
-
keystroke: [
|
|
1407
|
-
['arrowleft', 'arrowleft'],
|
|
1408
|
-
['arrowright', 'arrowright']
|
|
1409
|
-
]
|
|
1410
|
-
}
|
|
1411
|
-
]
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
/**
|
|
1415
|
-
* @inheritDoc
|
|
1416
|
-
*/
|
|
1417
|
-
destroy() {
|
|
1418
|
-
super.destroy();
|
|
1419
|
-
// Destroy created UI components as they are not automatically destroyed (see ckeditor5#1341).
|
|
1420
|
-
if (this.formView) {
|
|
1421
|
-
this.formView.destroy();
|
|
1422
|
-
}
|
|
1423
|
-
if (this.actionsView) {
|
|
1424
|
-
this.actionsView.destroy();
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
/**
|
|
1428
|
-
* Creates views.
|
|
1429
|
-
*/
|
|
1430
|
-
_createViews() {
|
|
1431
|
-
this.actionsView = this._createActionsView();
|
|
1432
|
-
this.formView = this._createFormView();
|
|
1433
|
-
// Attach lifecycle actions to the the balloon.
|
|
1434
|
-
this._enableUserBalloonInteractions();
|
|
1435
|
-
}
|
|
1436
|
-
/**
|
|
1437
|
-
* Creates the {@link module:link/ui/linkactionsview~LinkActionsView} instance.
|
|
1438
|
-
*/
|
|
1439
|
-
_createActionsView() {
|
|
1440
|
-
const editor = this.editor;
|
|
1441
|
-
const actionsView = new LinkActionsView(editor.locale, editor.config.get('link'));
|
|
1442
|
-
const linkCommand = editor.commands.get('link');
|
|
1443
|
-
const unlinkCommand = editor.commands.get('unlink');
|
|
1444
|
-
actionsView.bind('href').to(linkCommand, 'value');
|
|
1445
|
-
actionsView.editButtonView.bind('isEnabled').to(linkCommand);
|
|
1446
|
-
actionsView.unlinkButtonView.bind('isEnabled').to(unlinkCommand);
|
|
1447
|
-
// Execute unlink command after clicking on the "Edit" button.
|
|
1448
|
-
this.listenTo(actionsView, 'edit', () => {
|
|
1449
|
-
this._addFormView();
|
|
1450
|
-
});
|
|
1451
|
-
// Execute unlink command after clicking on the "Unlink" button.
|
|
1452
|
-
this.listenTo(actionsView, 'unlink', () => {
|
|
1453
|
-
editor.execute('unlink');
|
|
1454
|
-
this._hideUI();
|
|
1455
|
-
});
|
|
1456
|
-
// Close the panel on esc key press when the **actions have focus**.
|
|
1457
|
-
actionsView.keystrokes.set('Esc', (data, cancel) => {
|
|
1458
|
-
this._hideUI();
|
|
1459
|
-
cancel();
|
|
1460
|
-
});
|
|
1461
|
-
// Open the form view on Ctrl+K when the **actions have focus**..
|
|
1462
|
-
actionsView.keystrokes.set(LINK_KEYSTROKE, (data, cancel) => {
|
|
1463
|
-
this._addFormView();
|
|
1464
|
-
cancel();
|
|
1465
|
-
});
|
|
1466
|
-
return actionsView;
|
|
1467
|
-
}
|
|
1468
|
-
/**
|
|
1469
|
-
* Creates the {@link module:link/ui/linkformview~LinkFormView} instance.
|
|
1470
|
-
*/
|
|
1471
|
-
_createFormView() {
|
|
1472
|
-
const editor = this.editor;
|
|
1473
|
-
const linkCommand = editor.commands.get('link');
|
|
1474
|
-
const defaultProtocol = editor.config.get('link.defaultProtocol');
|
|
1475
|
-
const allowCreatingEmptyLinks = editor.config.get('link.allowCreatingEmptyLinks');
|
|
1476
|
-
const formView = new (CssTransitionDisablerMixin(LinkFormView))(editor.locale, linkCommand);
|
|
1477
|
-
formView.urlInputView.fieldView.bind('value').to(linkCommand, 'value');
|
|
1478
|
-
// Form elements should be read-only when corresponding commands are disabled.
|
|
1479
|
-
formView.urlInputView.bind('isEnabled').to(linkCommand, 'isEnabled');
|
|
1480
|
-
// Disable the "save" button if the command is disabled or the input is empty despite being required.
|
|
1481
|
-
formView.saveButtonView.bind('isEnabled').to(linkCommand, 'isEnabled', formView.urlInputView, 'isEmpty', (isCommandEnabled, isInputEmpty) => isCommandEnabled && (allowCreatingEmptyLinks || !isInputEmpty));
|
|
1482
|
-
// Execute link command after clicking the "Save" button.
|
|
1483
|
-
this.listenTo(formView, 'submit', () => {
|
|
1484
|
-
const { value } = formView.urlInputView.fieldView.element;
|
|
1485
|
-
const parsedUrl = addLinkProtocolIfApplicable(value, defaultProtocol);
|
|
1486
|
-
editor.execute('link', parsedUrl, formView.getDecoratorSwitchesState());
|
|
1487
|
-
this._closeFormView();
|
|
1488
|
-
});
|
|
1489
|
-
// Hide the panel after clicking the "Cancel" button.
|
|
1490
|
-
this.listenTo(formView, 'cancel', () => {
|
|
1491
|
-
this._closeFormView();
|
|
1492
|
-
});
|
|
1493
|
-
// Close the panel on esc key press when the **form has focus**.
|
|
1494
|
-
formView.keystrokes.set('Esc', (data, cancel) => {
|
|
1495
|
-
this._closeFormView();
|
|
1496
|
-
cancel();
|
|
1497
|
-
});
|
|
1498
|
-
return formView;
|
|
1499
|
-
}
|
|
1500
|
-
/**
|
|
1501
|
-
* Creates a toolbar Link button. Clicking this button will show
|
|
1502
|
-
* a {@link #_balloon} attached to the selection.
|
|
1503
|
-
*/
|
|
1504
|
-
_createToolbarLinkButton() {
|
|
1505
|
-
const editor = this.editor;
|
|
1506
|
-
const linkCommand = editor.commands.get('link');
|
|
1507
|
-
const t = editor.t;
|
|
1508
|
-
editor.ui.componentFactory.add('link', locale => {
|
|
1509
|
-
const button = new ButtonView(locale);
|
|
1510
|
-
button.isEnabled = true;
|
|
1511
|
-
button.label = t('Link');
|
|
1512
|
-
button.icon = linkIcon;
|
|
1513
|
-
button.keystroke = LINK_KEYSTROKE;
|
|
1514
|
-
button.tooltip = true;
|
|
1515
|
-
button.isToggleable = true;
|
|
1516
|
-
// Bind button to the command.
|
|
1517
|
-
button.bind('isEnabled').to(linkCommand, 'isEnabled');
|
|
1518
|
-
button.bind('isOn').to(linkCommand, 'value', value => !!value);
|
|
1519
|
-
// Show the panel on button click.
|
|
1520
|
-
this.listenTo(button, 'execute', () => this._showUI(true));
|
|
1521
|
-
return button;
|
|
1522
|
-
});
|
|
1523
|
-
}
|
|
1524
|
-
/**
|
|
1525
|
-
* Attaches actions that control whether the balloon panel containing the
|
|
1526
|
-
* {@link #formView} should be displayed.
|
|
1527
|
-
*/
|
|
1528
|
-
_enableBalloonActivators() {
|
|
1529
|
-
const editor = this.editor;
|
|
1530
|
-
const viewDocument = editor.editing.view.document;
|
|
1531
|
-
// Handle click on view document and show panel when selection is placed inside the link element.
|
|
1532
|
-
// Keep panel open until selection will be inside the same link element.
|
|
1533
|
-
this.listenTo(viewDocument, 'click', () => {
|
|
1534
|
-
const parentLink = this._getSelectedLinkElement();
|
|
1535
|
-
if (parentLink) {
|
|
1536
|
-
// Then show panel but keep focus inside editor editable.
|
|
1537
|
-
this._showUI();
|
|
1538
|
-
}
|
|
1539
|
-
});
|
|
1540
|
-
// Handle the `Ctrl+K` keystroke and show the panel.
|
|
1541
|
-
editor.keystrokes.set(LINK_KEYSTROKE, (keyEvtData, cancel) => {
|
|
1542
|
-
// Prevent focusing the search bar in FF, Chrome and Edge. See https://github.com/ckeditor/ckeditor5/issues/4811.
|
|
1543
|
-
cancel();
|
|
1544
|
-
if (editor.commands.get('link').isEnabled) {
|
|
1545
|
-
this._showUI(true);
|
|
1546
|
-
}
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1549
|
-
/**
|
|
1550
|
-
* Attaches actions that control whether the balloon panel containing the
|
|
1551
|
-
* {@link #formView} is visible or not.
|
|
1552
|
-
*/
|
|
1553
|
-
_enableUserBalloonInteractions() {
|
|
1554
|
-
// Focus the form if the balloon is visible and the Tab key has been pressed.
|
|
1555
|
-
this.editor.keystrokes.set('Tab', (data, cancel) => {
|
|
1556
|
-
if (this._areActionsVisible && !this.actionsView.focusTracker.isFocused) {
|
|
1557
|
-
this.actionsView.focus();
|
|
1558
|
-
cancel();
|
|
1559
|
-
}
|
|
1560
|
-
}, {
|
|
1561
|
-
// Use the high priority because the link UI navigation is more important
|
|
1562
|
-
// than other feature's actions, e.g. list indentation.
|
|
1563
|
-
// https://github.com/ckeditor/ckeditor5-link/issues/146
|
|
1564
|
-
priority: 'high'
|
|
1565
|
-
});
|
|
1566
|
-
// Close the panel on the Esc key press when the editable has focus and the balloon is visible.
|
|
1567
|
-
this.editor.keystrokes.set('Esc', (data, cancel) => {
|
|
1568
|
-
if (this._isUIVisible) {
|
|
1569
|
-
this._hideUI();
|
|
1570
|
-
cancel();
|
|
1571
|
-
}
|
|
1572
|
-
});
|
|
1573
|
-
// Close on click outside of balloon panel element.
|
|
1574
|
-
clickOutsideHandler({
|
|
1575
|
-
emitter: this.formView,
|
|
1576
|
-
activator: () => this._isUIInPanel,
|
|
1577
|
-
contextElements: () => [this._balloon.view.element],
|
|
1578
|
-
callback: () => this._hideUI()
|
|
1579
|
-
});
|
|
1580
|
-
}
|
|
1581
|
-
/**
|
|
1582
|
-
* Adds the {@link #actionsView} to the {@link #_balloon}.
|
|
1583
|
-
*
|
|
1584
|
-
* @internal
|
|
1585
|
-
*/
|
|
1586
|
-
_addActionsView() {
|
|
1587
|
-
if (!this.actionsView) {
|
|
1588
|
-
this._createViews();
|
|
1589
|
-
}
|
|
1590
|
-
if (this._areActionsInPanel) {
|
|
1591
|
-
return;
|
|
1592
|
-
}
|
|
1593
|
-
this._balloon.add({
|
|
1594
|
-
view: this.actionsView,
|
|
1595
|
-
position: this._getBalloonPositionData()
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
/**
|
|
1599
|
-
* Adds the {@link #formView} to the {@link #_balloon}.
|
|
1600
|
-
*/
|
|
1601
|
-
_addFormView() {
|
|
1602
|
-
if (!this.formView) {
|
|
1603
|
-
this._createViews();
|
|
1604
|
-
}
|
|
1605
|
-
if (this._isFormInPanel) {
|
|
1606
|
-
return;
|
|
1607
|
-
}
|
|
1608
|
-
const editor = this.editor;
|
|
1609
|
-
const linkCommand = editor.commands.get('link');
|
|
1610
|
-
this.formView.disableCssTransitions();
|
|
1611
|
-
this._balloon.add({
|
|
1612
|
-
view: this.formView,
|
|
1613
|
-
position: this._getBalloonPositionData()
|
|
1614
|
-
});
|
|
1615
|
-
// Make sure that each time the panel shows up, the URL field remains in sync with the value of
|
|
1616
|
-
// the command. If the user typed in the input, then canceled the balloon (`urlInputView.fieldView#value` stays
|
|
1617
|
-
// unaltered) and re-opened it without changing the value of the link command (e.g. because they
|
|
1618
|
-
// clicked the same link), they would see the old value instead of the actual value of the command.
|
|
1619
|
-
// https://github.com/ckeditor/ckeditor5-link/issues/78
|
|
1620
|
-
// https://github.com/ckeditor/ckeditor5-link/issues/123
|
|
1621
|
-
this.formView.urlInputView.fieldView.value = linkCommand.value || '';
|
|
1622
|
-
// Select input when form view is currently visible.
|
|
1623
|
-
if (this._balloon.visibleView === this.formView) {
|
|
1624
|
-
this.formView.urlInputView.fieldView.select();
|
|
1625
|
-
}
|
|
1626
|
-
this.formView.enableCssTransitions();
|
|
1627
|
-
}
|
|
1628
|
-
/**
|
|
1629
|
-
* Closes the form view. Decides whether the balloon should be hidden completely or if the action view should be shown. This is
|
|
1630
|
-
* decided upon the link command value (which has a value if the document selection is in the link).
|
|
1631
|
-
*
|
|
1632
|
-
* Additionally, if any {@link module:link/linkconfig~LinkConfig#decorators} are defined in the editor configuration, the state of
|
|
1633
|
-
* switch buttons responsible for manual decorator handling is restored.
|
|
1634
|
-
*/
|
|
1635
|
-
_closeFormView() {
|
|
1636
|
-
const linkCommand = this.editor.commands.get('link');
|
|
1637
|
-
// Restore manual decorator states to represent the current model state. This case is important to reset the switch buttons
|
|
1638
|
-
// when the user cancels the editing form.
|
|
1639
|
-
linkCommand.restoreManualDecoratorStates();
|
|
1640
|
-
if (linkCommand.value !== undefined) {
|
|
1641
|
-
this._removeFormView();
|
|
1642
|
-
}
|
|
1643
|
-
else {
|
|
1644
|
-
this._hideUI();
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
/**
|
|
1648
|
-
* Removes the {@link #formView} from the {@link #_balloon}.
|
|
1649
|
-
*/
|
|
1650
|
-
_removeFormView() {
|
|
1651
|
-
if (this._isFormInPanel) {
|
|
1652
|
-
// Blur the input element before removing it from DOM to prevent issues in some browsers.
|
|
1653
|
-
// See https://github.com/ckeditor/ckeditor5/issues/1501.
|
|
1654
|
-
this.formView.saveButtonView.focus();
|
|
1655
|
-
// Reset the URL field to update the state of the submit button.
|
|
1656
|
-
this.formView.urlInputView.fieldView.reset();
|
|
1657
|
-
this._balloon.remove(this.formView);
|
|
1658
|
-
// Because the form has an input which has focus, the focus must be brought back
|
|
1659
|
-
// to the editor. Otherwise, it would be lost.
|
|
1660
|
-
this.editor.editing.view.focus();
|
|
1661
|
-
this._hideFakeVisualSelection();
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
/**
|
|
1665
|
-
* Shows the correct UI type. It is either {@link #formView} or {@link #actionsView}.
|
|
1666
|
-
*
|
|
1667
|
-
* @internal
|
|
1668
|
-
*/
|
|
1669
|
-
_showUI(forceVisible = false) {
|
|
1670
|
-
if (!this.formView) {
|
|
1671
|
-
this._createViews();
|
|
1672
|
-
}
|
|
1673
|
-
// When there's no link under the selection, go straight to the editing UI.
|
|
1674
|
-
if (!this._getSelectedLinkElement()) {
|
|
1675
|
-
// Show visual selection on a text without a link when the contextual balloon is displayed.
|
|
1676
|
-
// See https://github.com/ckeditor/ckeditor5/issues/4721.
|
|
1677
|
-
this._showFakeVisualSelection();
|
|
1678
|
-
this._addActionsView();
|
|
1679
|
-
// Be sure panel with link is visible.
|
|
1680
|
-
if (forceVisible) {
|
|
1681
|
-
this._balloon.showStack('main');
|
|
1682
|
-
}
|
|
1683
|
-
this._addFormView();
|
|
1684
|
-
}
|
|
1685
|
-
// If there's a link under the selection...
|
|
1686
|
-
else {
|
|
1687
|
-
// Go to the editing UI if actions are already visible.
|
|
1688
|
-
if (this._areActionsVisible) {
|
|
1689
|
-
this._addFormView();
|
|
1690
|
-
}
|
|
1691
|
-
// Otherwise display just the actions UI.
|
|
1692
|
-
else {
|
|
1693
|
-
this._addActionsView();
|
|
1694
|
-
}
|
|
1695
|
-
// Be sure panel with link is visible.
|
|
1696
|
-
if (forceVisible) {
|
|
1697
|
-
this._balloon.showStack('main');
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
// Begin responding to ui#update once the UI is added.
|
|
1701
|
-
this._startUpdatingUI();
|
|
1702
|
-
}
|
|
1703
|
-
/**
|
|
1704
|
-
* Removes the {@link #formView} from the {@link #_balloon}.
|
|
1705
|
-
*
|
|
1706
|
-
* See {@link #_addFormView}, {@link #_addActionsView}.
|
|
1707
|
-
*/
|
|
1708
|
-
_hideUI() {
|
|
1709
|
-
if (!this._isUIInPanel) {
|
|
1710
|
-
return;
|
|
1711
|
-
}
|
|
1712
|
-
const editor = this.editor;
|
|
1713
|
-
this.stopListening(editor.ui, 'update');
|
|
1714
|
-
this.stopListening(this._balloon, 'change:visibleView');
|
|
1715
|
-
// Make sure the focus always gets back to the editable _before_ removing the focused form view.
|
|
1716
|
-
// Doing otherwise causes issues in some browsers. See https://github.com/ckeditor/ckeditor5-link/issues/193.
|
|
1717
|
-
editor.editing.view.focus();
|
|
1718
|
-
// Remove form first because it's on top of the stack.
|
|
1719
|
-
this._removeFormView();
|
|
1720
|
-
// Then remove the actions view because it's beneath the form.
|
|
1721
|
-
this._balloon.remove(this.actionsView);
|
|
1722
|
-
this._hideFakeVisualSelection();
|
|
1723
|
-
}
|
|
1724
|
-
/**
|
|
1725
|
-
* Makes the UI react to the {@link module:ui/editorui/editorui~EditorUI#event:update} event to
|
|
1726
|
-
* reposition itself when the editor UI should be refreshed.
|
|
1727
|
-
*
|
|
1728
|
-
* See: {@link #_hideUI} to learn when the UI stops reacting to the `update` event.
|
|
1729
|
-
*/
|
|
1730
|
-
_startUpdatingUI() {
|
|
1731
|
-
const editor = this.editor;
|
|
1732
|
-
const viewDocument = editor.editing.view.document;
|
|
1733
|
-
let prevSelectedLink = this._getSelectedLinkElement();
|
|
1734
|
-
let prevSelectionParent = getSelectionParent();
|
|
1735
|
-
const update = () => {
|
|
1736
|
-
const selectedLink = this._getSelectedLinkElement();
|
|
1737
|
-
const selectionParent = getSelectionParent();
|
|
1738
|
-
// Hide the panel if:
|
|
1739
|
-
//
|
|
1740
|
-
// * the selection went out of the EXISTING link element. E.g. user moved the caret out
|
|
1741
|
-
// of the link,
|
|
1742
|
-
// * the selection went to a different parent when creating a NEW link. E.g. someone
|
|
1743
|
-
// else modified the document.
|
|
1744
|
-
// * the selection has expanded (e.g. displaying link actions then pressing SHIFT+Right arrow).
|
|
1745
|
-
//
|
|
1746
|
-
// Note: #_getSelectedLinkElement will return a link for a non-collapsed selection only
|
|
1747
|
-
// when fully selected.
|
|
1748
|
-
if ((prevSelectedLink && !selectedLink) ||
|
|
1749
|
-
(!prevSelectedLink && selectionParent !== prevSelectionParent)) {
|
|
1750
|
-
this._hideUI();
|
|
1751
|
-
}
|
|
1752
|
-
// Update the position of the panel when:
|
|
1753
|
-
// * link panel is in the visible stack
|
|
1754
|
-
// * the selection remains in the original link element,
|
|
1755
|
-
// * there was no link element in the first place, i.e. creating a new link
|
|
1756
|
-
else if (this._isUIVisible) {
|
|
1757
|
-
// If still in a link element, simply update the position of the balloon.
|
|
1758
|
-
// If there was no link (e.g. inserting one), the balloon must be moved
|
|
1759
|
-
// to the new position in the editing view (a new native DOM range).
|
|
1760
|
-
this._balloon.updatePosition(this._getBalloonPositionData());
|
|
1761
|
-
}
|
|
1762
|
-
prevSelectedLink = selectedLink;
|
|
1763
|
-
prevSelectionParent = selectionParent;
|
|
1764
|
-
};
|
|
1765
|
-
function getSelectionParent() {
|
|
1766
|
-
return viewDocument.selection.focus.getAncestors()
|
|
1767
|
-
.reverse()
|
|
1768
|
-
.find((node) => node.is('element'));
|
|
1769
|
-
}
|
|
1770
|
-
this.listenTo(editor.ui, 'update', update);
|
|
1771
|
-
this.listenTo(this._balloon, 'change:visibleView', update);
|
|
1772
|
-
}
|
|
1773
|
-
/**
|
|
1774
|
-
* Returns `true` when {@link #formView} is in the {@link #_balloon}.
|
|
1775
|
-
*/
|
|
1776
|
-
get _isFormInPanel() {
|
|
1777
|
-
return !!this.formView && this._balloon.hasView(this.formView);
|
|
1778
|
-
}
|
|
1779
|
-
/**
|
|
1780
|
-
* Returns `true` when {@link #actionsView} is in the {@link #_balloon}.
|
|
1781
|
-
*/
|
|
1782
|
-
get _areActionsInPanel() {
|
|
1783
|
-
return !!this.actionsView && this._balloon.hasView(this.actionsView);
|
|
1784
|
-
}
|
|
1785
|
-
/**
|
|
1786
|
-
* Returns `true` when {@link #actionsView} is in the {@link #_balloon} and it is
|
|
1787
|
-
* currently visible.
|
|
1788
|
-
*/
|
|
1789
|
-
get _areActionsVisible() {
|
|
1790
|
-
return !!this.actionsView && this._balloon.visibleView === this.actionsView;
|
|
1791
|
-
}
|
|
1792
|
-
/**
|
|
1793
|
-
* Returns `true` when {@link #actionsView} or {@link #formView} is in the {@link #_balloon}.
|
|
1794
|
-
*/
|
|
1795
|
-
get _isUIInPanel() {
|
|
1796
|
-
return this._isFormInPanel || this._areActionsInPanel;
|
|
1797
|
-
}
|
|
1798
|
-
/**
|
|
1799
|
-
* Returns `true` when {@link #actionsView} or {@link #formView} is in the {@link #_balloon} and it is
|
|
1800
|
-
* currently visible.
|
|
1801
|
-
*/
|
|
1802
|
-
get _isUIVisible() {
|
|
1803
|
-
const visibleView = this._balloon.visibleView;
|
|
1804
|
-
return !!this.formView && visibleView == this.formView || this._areActionsVisible;
|
|
1805
|
-
}
|
|
1806
|
-
/**
|
|
1807
|
-
* Returns positioning options for the {@link #_balloon}. They control the way the balloon is attached
|
|
1808
|
-
* to the target element or selection.
|
|
1809
|
-
*
|
|
1810
|
-
* If the selection is collapsed and inside a link element, the panel will be attached to the
|
|
1811
|
-
* entire link element. Otherwise, it will be attached to the selection.
|
|
1812
|
-
*/
|
|
1813
|
-
_getBalloonPositionData() {
|
|
1814
|
-
const view = this.editor.editing.view;
|
|
1815
|
-
const model = this.editor.model;
|
|
1816
|
-
const viewDocument = view.document;
|
|
1817
|
-
let target;
|
|
1818
|
-
if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
|
1819
|
-
// There are cases when we highlight selection using a marker (#7705, #4721).
|
|
1820
|
-
const markerViewElements = Array.from(this.editor.editing.mapper.markerNameToElements(VISUAL_SELECTION_MARKER_NAME));
|
|
1821
|
-
const newRange = view.createRange(view.createPositionBefore(markerViewElements[0]), view.createPositionAfter(markerViewElements[markerViewElements.length - 1]));
|
|
1822
|
-
target = view.domConverter.viewRangeToDom(newRange);
|
|
1823
|
-
}
|
|
1824
|
-
else {
|
|
1825
|
-
// Make sure the target is calculated on demand at the last moment because a cached DOM range
|
|
1826
|
-
// (which is very fragile) can desynchronize with the state of the editing view if there was
|
|
1827
|
-
// any rendering done in the meantime. This can happen, for instance, when an inline widget
|
|
1828
|
-
// gets unlinked.
|
|
1829
|
-
target = () => {
|
|
1830
|
-
const targetLink = this._getSelectedLinkElement();
|
|
1831
|
-
return targetLink ?
|
|
1832
|
-
// When selection is inside link element, then attach panel to this element.
|
|
1833
|
-
view.domConverter.mapViewToDom(targetLink) :
|
|
1834
|
-
// Otherwise attach panel to the selection.
|
|
1835
|
-
view.domConverter.viewRangeToDom(viewDocument.selection.getFirstRange());
|
|
1836
|
-
};
|
|
1837
|
-
}
|
|
1838
|
-
return { target };
|
|
1839
|
-
}
|
|
1840
|
-
/**
|
|
1841
|
-
* Returns the link {@link module:engine/view/attributeelement~AttributeElement} under
|
|
1842
|
-
* the {@link module:engine/view/document~Document editing view's} selection or `null`
|
|
1843
|
-
* if there is none.
|
|
1844
|
-
*
|
|
1845
|
-
* **Note**: For a non–collapsed selection, the link element is returned when **fully**
|
|
1846
|
-
* selected and the **only** element within the selection boundaries, or when
|
|
1847
|
-
* a linked widget is selected.
|
|
1848
|
-
*/
|
|
1849
|
-
_getSelectedLinkElement() {
|
|
1850
|
-
const view = this.editor.editing.view;
|
|
1851
|
-
const selection = view.document.selection;
|
|
1852
|
-
const selectedElement = selection.getSelectedElement();
|
|
1853
|
-
// The selection is collapsed or some widget is selected (especially inline widget).
|
|
1854
|
-
if (selection.isCollapsed || selectedElement && isWidget(selectedElement)) {
|
|
1855
|
-
return findLinkElementAncestor(selection.getFirstPosition());
|
|
1856
|
-
}
|
|
1857
|
-
else {
|
|
1858
|
-
// The range for fully selected link is usually anchored in adjacent text nodes.
|
|
1859
|
-
// Trim it to get closer to the actual link element.
|
|
1860
|
-
const range = selection.getFirstRange().getTrimmed();
|
|
1861
|
-
const startLink = findLinkElementAncestor(range.start);
|
|
1862
|
-
const endLink = findLinkElementAncestor(range.end);
|
|
1863
|
-
if (!startLink || startLink != endLink) {
|
|
1864
|
-
return null;
|
|
1865
|
-
}
|
|
1866
|
-
// Check if the link element is fully selected.
|
|
1867
|
-
if (view.createRangeIn(startLink).getTrimmed().isEqual(range)) {
|
|
1868
|
-
return startLink;
|
|
1869
|
-
}
|
|
1870
|
-
else {
|
|
1871
|
-
return null;
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
/**
|
|
1876
|
-
* Displays a fake visual selection when the contextual balloon is displayed.
|
|
1877
|
-
*
|
|
1878
|
-
* This adds a 'link-ui' marker into the document that is rendered as a highlight on selected text fragment.
|
|
1879
|
-
*/
|
|
1880
|
-
_showFakeVisualSelection() {
|
|
1881
|
-
const model = this.editor.model;
|
|
1882
|
-
model.change(writer => {
|
|
1883
|
-
const range = model.document.selection.getFirstRange();
|
|
1884
|
-
if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
|
1885
|
-
writer.updateMarker(VISUAL_SELECTION_MARKER_NAME, { range });
|
|
1886
|
-
}
|
|
1887
|
-
else {
|
|
1888
|
-
if (range.start.isAtEnd) {
|
|
1889
|
-
const startPosition = range.start.getLastMatchingPosition(({ item }) => !model.schema.isContent(item), { boundaries: range });
|
|
1890
|
-
writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
|
|
1891
|
-
usingOperation: false,
|
|
1892
|
-
affectsData: false,
|
|
1893
|
-
range: writer.createRange(startPosition, range.end)
|
|
1894
|
-
});
|
|
1895
|
-
}
|
|
1896
|
-
else {
|
|
1897
|
-
writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
|
|
1898
|
-
usingOperation: false,
|
|
1899
|
-
affectsData: false,
|
|
1900
|
-
range
|
|
1901
|
-
});
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
});
|
|
1905
|
-
}
|
|
1906
|
-
/**
|
|
1907
|
-
* Hides the fake visual selection created in {@link #_showFakeVisualSelection}.
|
|
1908
|
-
*/
|
|
1909
|
-
_hideFakeVisualSelection() {
|
|
1910
|
-
const model = this.editor.model;
|
|
1911
|
-
if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
|
1912
|
-
model.change(writer => {
|
|
1913
|
-
writer.removeMarker(VISUAL_SELECTION_MARKER_NAME);
|
|
1914
|
-
});
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
}
|
|
1918
|
-
/**
|
|
1919
|
-
* Returns a link element if there's one among the ancestors of the provided `Position`.
|
|
1920
|
-
*
|
|
1921
|
-
* @param View position to analyze.
|
|
1922
|
-
* @returns Link element at the position or null.
|
|
1923
|
-
*/
|
|
1924
|
-
function findLinkElementAncestor(position) {
|
|
1925
|
-
return position.getAncestors().find((ancestor) => isLinkElement(ancestor)) || null;
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
/**
|
|
1929
|
-
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
1930
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
1931
|
-
*/
|
|
1932
|
-
/**
|
|
1933
|
-
* @module link/autolink
|
|
1934
|
-
*/
|
|
1935
|
-
const MIN_LINK_LENGTH_WITH_SPACE_AT_END = 4; // Ie: "t.co " (length 5).
|
|
1936
|
-
// This was a tweak from https://gist.github.com/dperini/729294.
|
|
1937
|
-
const URL_REG_EXP = new RegExp(
|
|
1938
|
-
// Group 1: Line start or after a space.
|
|
1939
|
-
'(^|\\s)' +
|
|
1940
|
-
// Group 2: Detected URL (or e-mail).
|
|
1941
|
-
'(' +
|
|
1942
|
-
// Protocol identifier or short syntax "//"
|
|
1943
|
-
// a. Full form http://user@foo.bar.baz:8080/foo/bar.html#baz?foo=bar
|
|
1944
|
-
'(' +
|
|
1945
|
-
'(?:(?:(?:https?|ftp):)?\\/\\/)' +
|
|
1946
|
-
// BasicAuth using user:pass (optional)
|
|
1947
|
-
'(?:\\S+(?::\\S*)?@)?' +
|
|
1948
|
-
'(?:' +
|
|
1949
|
-
// IP address dotted notation octets
|
|
1950
|
-
// excludes loopback network 0.0.0.0
|
|
1951
|
-
// excludes reserved space >= 224.0.0.0
|
|
1952
|
-
// excludes network & broadcast addresses
|
|
1953
|
-
// (first & last IP address of each class)
|
|
1954
|
-
'(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' +
|
|
1955
|
-
'(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' +
|
|
1956
|
-
'(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' +
|
|
1957
|
-
'|' +
|
|
1958
|
-
'(' +
|
|
1959
|
-
// Do not allow `www.foo` - see https://github.com/ckeditor/ckeditor5/issues/8050.
|
|
1960
|
-
'((?!www\\.)|(www\\.))' +
|
|
1961
|
-
// Host & domain names.
|
|
1962
|
-
'(?![-_])(?:[-_a-z0-9\\u00a1-\\uffff]{1,63}\\.)+' +
|
|
1963
|
-
// TLD identifier name.
|
|
1964
|
-
'(?:[a-z\\u00a1-\\uffff]{2,63})' +
|
|
1965
|
-
')' +
|
|
1966
|
-
')' +
|
|
1967
|
-
// port number (optional)
|
|
1968
|
-
'(?::\\d{2,5})?' +
|
|
1969
|
-
// resource path (optional)
|
|
1970
|
-
'(?:[/?#]\\S*)?' +
|
|
1971
|
-
')' +
|
|
1972
|
-
'|' +
|
|
1973
|
-
// b. Short form (either www.example.com or example@example.com)
|
|
1974
|
-
'(' +
|
|
1975
|
-
'(www.|(\\S+@))' +
|
|
1976
|
-
// Host & domain names.
|
|
1977
|
-
'((?![-_])(?:[-_a-z0-9\\u00a1-\\uffff]{1,63}\\.))+' +
|
|
1978
|
-
// TLD identifier name.
|
|
1979
|
-
'(?:[a-z\\u00a1-\\uffff]{2,63})' +
|
|
1980
|
-
')' +
|
|
1981
|
-
')$', 'i');
|
|
1982
|
-
const URL_GROUP_IN_MATCH = 2;
|
|
1983
|
-
/**
|
|
1984
|
-
* The autolink plugin.
|
|
1985
|
-
*/
|
|
1986
|
-
class AutoLink extends Plugin {
|
|
1987
|
-
/**
|
|
1988
|
-
* @inheritDoc
|
|
1989
|
-
*/
|
|
1990
|
-
static get requires() {
|
|
1991
|
-
return [Delete, LinkEditing];
|
|
1992
|
-
}
|
|
1993
|
-
/**
|
|
1994
|
-
* @inheritDoc
|
|
1995
|
-
*/
|
|
1996
|
-
static get pluginName() {
|
|
1997
|
-
return 'AutoLink';
|
|
1998
|
-
}
|
|
1999
|
-
/**
|
|
2000
|
-
* @inheritDoc
|
|
2001
|
-
*/
|
|
2002
|
-
init() {
|
|
2003
|
-
const editor = this.editor;
|
|
2004
|
-
const selection = editor.model.document.selection;
|
|
2005
|
-
selection.on('change:range', () => {
|
|
2006
|
-
// Disable plugin when selection is inside a code block.
|
|
2007
|
-
this.isEnabled = !selection.anchor.parent.is('element', 'codeBlock');
|
|
2008
|
-
});
|
|
2009
|
-
this._enableTypingHandling();
|
|
2010
|
-
}
|
|
2011
|
-
/**
|
|
2012
|
-
* @inheritDoc
|
|
2013
|
-
*/
|
|
2014
|
-
afterInit() {
|
|
2015
|
-
this._enableEnterHandling();
|
|
2016
|
-
this._enableShiftEnterHandling();
|
|
2017
|
-
this._enablePasteLinking();
|
|
2018
|
-
}
|
|
2019
|
-
/**
|
|
2020
|
-
* For given position, returns a range that includes the whole link that contains the position.
|
|
2021
|
-
*
|
|
2022
|
-
* If position is not inside a link, returns `null`.
|
|
2023
|
-
*/
|
|
2024
|
-
_expandLinkRange(model, position) {
|
|
2025
|
-
if (position.textNode && position.textNode.hasAttribute('linkHref')) {
|
|
2026
|
-
return findAttributeRange(position, 'linkHref', position.textNode.getAttribute('linkHref'), model);
|
|
2027
|
-
}
|
|
2028
|
-
else {
|
|
2029
|
-
return null;
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
/**
|
|
2033
|
-
* Extends the document selection to includes all links that intersects with given `selectedRange`.
|
|
2034
|
-
*/
|
|
2035
|
-
_selectEntireLinks(writer, selectedRange) {
|
|
2036
|
-
const editor = this.editor;
|
|
2037
|
-
const model = editor.model;
|
|
2038
|
-
const selection = model.document.selection;
|
|
2039
|
-
const selStart = selection.getFirstPosition();
|
|
2040
|
-
const selEnd = selection.getLastPosition();
|
|
2041
|
-
let updatedSelection = selectedRange.getJoined(this._expandLinkRange(model, selStart) || selectedRange);
|
|
2042
|
-
if (updatedSelection) {
|
|
2043
|
-
updatedSelection = updatedSelection.getJoined(this._expandLinkRange(model, selEnd) || selectedRange);
|
|
2044
|
-
}
|
|
2045
|
-
if (updatedSelection && (updatedSelection.start.isBefore(selStart) || updatedSelection.end.isAfter(selEnd))) {
|
|
2046
|
-
// Only update the selection if it changed.
|
|
2047
|
-
writer.setSelection(updatedSelection);
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
/**
|
|
2051
|
-
* Enables autolinking on pasting a URL when some content is selected.
|
|
2052
|
-
*/
|
|
2053
|
-
_enablePasteLinking() {
|
|
2054
|
-
const editor = this.editor;
|
|
2055
|
-
const model = editor.model;
|
|
2056
|
-
const selection = model.document.selection;
|
|
2057
|
-
const clipboardPipeline = editor.plugins.get('ClipboardPipeline');
|
|
2058
|
-
const linkCommand = editor.commands.get('link');
|
|
2059
|
-
clipboardPipeline.on('inputTransformation', (evt, data) => {
|
|
2060
|
-
if (!this.isEnabled || !linkCommand.isEnabled || selection.isCollapsed || data.method !== 'paste') {
|
|
2061
|
-
// Abort if we are disabled or the selection is collapsed.
|
|
2062
|
-
return;
|
|
2063
|
-
}
|
|
2064
|
-
if (selection.rangeCount > 1) {
|
|
2065
|
-
// Abort if there are multiple selection ranges.
|
|
2066
|
-
return;
|
|
2067
|
-
}
|
|
2068
|
-
const selectedRange = selection.getFirstRange();
|
|
2069
|
-
const newLink = data.dataTransfer.getData('text/plain');
|
|
2070
|
-
if (!newLink) {
|
|
2071
|
-
// Abort if there is no plain text on the clipboard.
|
|
2072
|
-
return;
|
|
2073
|
-
}
|
|
2074
|
-
const matches = newLink.match(URL_REG_EXP);
|
|
2075
|
-
// If the text in the clipboard has a URL, and that URL is the whole clipboard.
|
|
2076
|
-
if (matches && matches[2] === newLink) {
|
|
2077
|
-
model.change(writer => {
|
|
2078
|
-
this._selectEntireLinks(writer, selectedRange);
|
|
2079
|
-
linkCommand.execute(newLink);
|
|
2080
|
-
});
|
|
2081
|
-
evt.stop();
|
|
2082
|
-
}
|
|
2083
|
-
}, { priority: 'high' });
|
|
2084
|
-
}
|
|
2085
|
-
/**
|
|
2086
|
-
* Enables autolinking on typing.
|
|
2087
|
-
*/
|
|
2088
|
-
_enableTypingHandling() {
|
|
2089
|
-
const editor = this.editor;
|
|
2090
|
-
const watcher = new TextWatcher(editor.model, text => {
|
|
2091
|
-
// 1. Detect <kbd>Space</kbd> after a text with a potential link.
|
|
2092
|
-
if (!isSingleSpaceAtTheEnd(text)) {
|
|
2093
|
-
return;
|
|
2094
|
-
}
|
|
2095
|
-
// 2. Check text before last typed <kbd>Space</kbd>.
|
|
2096
|
-
const url = getUrlAtTextEnd(text.substr(0, text.length - 1));
|
|
2097
|
-
if (url) {
|
|
2098
|
-
return { url };
|
|
2099
|
-
}
|
|
2100
|
-
});
|
|
2101
|
-
watcher.on('matched:data', (evt, data) => {
|
|
2102
|
-
const { batch, range, url } = data;
|
|
2103
|
-
if (!batch.isTyping) {
|
|
2104
|
-
return;
|
|
2105
|
-
}
|
|
2106
|
-
const linkEnd = range.end.getShiftedBy(-1); // Executed after a space character.
|
|
2107
|
-
const linkStart = linkEnd.getShiftedBy(-url.length);
|
|
2108
|
-
const linkRange = editor.model.createRange(linkStart, linkEnd);
|
|
2109
|
-
this._applyAutoLink(url, linkRange);
|
|
2110
|
-
});
|
|
2111
|
-
watcher.bind('isEnabled').to(this);
|
|
2112
|
-
}
|
|
2113
|
-
/**
|
|
2114
|
-
* Enables autolinking on the <kbd>Enter</kbd> key.
|
|
2115
|
-
*/
|
|
2116
|
-
_enableEnterHandling() {
|
|
2117
|
-
const editor = this.editor;
|
|
2118
|
-
const model = editor.model;
|
|
2119
|
-
const enterCommand = editor.commands.get('enter');
|
|
2120
|
-
if (!enterCommand) {
|
|
2121
|
-
return;
|
|
2122
|
-
}
|
|
2123
|
-
enterCommand.on('execute', () => {
|
|
2124
|
-
const position = model.document.selection.getFirstPosition();
|
|
2125
|
-
if (!position.parent.previousSibling) {
|
|
2126
|
-
return;
|
|
2127
|
-
}
|
|
2128
|
-
const rangeToCheck = model.createRangeIn(position.parent.previousSibling);
|
|
2129
|
-
this._checkAndApplyAutoLinkOnRange(rangeToCheck);
|
|
2130
|
-
});
|
|
2131
|
-
}
|
|
2132
|
-
/**
|
|
2133
|
-
* Enables autolinking on the <kbd>Shift</kbd>+<kbd>Enter</kbd> keyboard shortcut.
|
|
2134
|
-
*/
|
|
2135
|
-
_enableShiftEnterHandling() {
|
|
2136
|
-
const editor = this.editor;
|
|
2137
|
-
const model = editor.model;
|
|
2138
|
-
const shiftEnterCommand = editor.commands.get('shiftEnter');
|
|
2139
|
-
if (!shiftEnterCommand) {
|
|
2140
|
-
return;
|
|
2141
|
-
}
|
|
2142
|
-
shiftEnterCommand.on('execute', () => {
|
|
2143
|
-
const position = model.document.selection.getFirstPosition();
|
|
2144
|
-
const rangeToCheck = model.createRange(model.createPositionAt(position.parent, 0), position.getShiftedBy(-1));
|
|
2145
|
-
this._checkAndApplyAutoLinkOnRange(rangeToCheck);
|
|
2146
|
-
});
|
|
2147
|
-
}
|
|
2148
|
-
/**
|
|
2149
|
-
* Checks if the passed range contains a linkable text.
|
|
2150
|
-
*/
|
|
2151
|
-
_checkAndApplyAutoLinkOnRange(rangeToCheck) {
|
|
2152
|
-
const model = this.editor.model;
|
|
2153
|
-
const { text, range } = getLastTextLine(rangeToCheck, model);
|
|
2154
|
-
const url = getUrlAtTextEnd(text);
|
|
2155
|
-
if (url) {
|
|
2156
|
-
const linkRange = model.createRange(range.end.getShiftedBy(-url.length), range.end);
|
|
2157
|
-
this._applyAutoLink(url, linkRange);
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
/**
|
|
2161
|
-
* Applies a link on a given range if the link should be applied.
|
|
2162
|
-
*
|
|
2163
|
-
* @param url The URL to link.
|
|
2164
|
-
* @param range The text range to apply the link attribute to.
|
|
2165
|
-
*/
|
|
2166
|
-
_applyAutoLink(url, range) {
|
|
2167
|
-
const model = this.editor.model;
|
|
2168
|
-
const defaultProtocol = this.editor.config.get('link.defaultProtocol');
|
|
2169
|
-
const fullUrl = addLinkProtocolIfApplicable(url, defaultProtocol);
|
|
2170
|
-
if (!this.isEnabled || !isLinkAllowedOnRange(range, model) || !linkHasProtocol(fullUrl) || linkIsAlreadySet(range)) {
|
|
2171
|
-
return;
|
|
2172
|
-
}
|
|
2173
|
-
this._persistAutoLink(fullUrl, range);
|
|
2174
|
-
}
|
|
2175
|
-
/**
|
|
2176
|
-
* Enqueues autolink changes in the model.
|
|
2177
|
-
*
|
|
2178
|
-
* @param url The URL to link.
|
|
2179
|
-
* @param range The text range to apply the link attribute to.
|
|
2180
|
-
*/
|
|
2181
|
-
_persistAutoLink(url, range) {
|
|
2182
|
-
const model = this.editor.model;
|
|
2183
|
-
const deletePlugin = this.editor.plugins.get('Delete');
|
|
2184
|
-
// Enqueue change to make undo step.
|
|
2185
|
-
model.enqueueChange(writer => {
|
|
2186
|
-
writer.setAttribute('linkHref', url, range);
|
|
2187
|
-
model.enqueueChange(() => {
|
|
2188
|
-
deletePlugin.requestUndoOnBackspace();
|
|
2189
|
-
});
|
|
2190
|
-
});
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
// Check if text should be evaluated by the plugin in order to reduce number of RegExp checks on whole text.
|
|
2194
|
-
function isSingleSpaceAtTheEnd(text) {
|
|
2195
|
-
return text.length > MIN_LINK_LENGTH_WITH_SPACE_AT_END && text[text.length - 1] === ' ' && text[text.length - 2] !== ' ';
|
|
2196
|
-
}
|
|
2197
|
-
function getUrlAtTextEnd(text) {
|
|
2198
|
-
const match = URL_REG_EXP.exec(text);
|
|
2199
|
-
return match ? match[URL_GROUP_IN_MATCH] : null;
|
|
2200
|
-
}
|
|
2201
|
-
function isLinkAllowedOnRange(range, model) {
|
|
2202
|
-
return model.schema.checkAttributeInSelection(model.createSelection(range), 'linkHref');
|
|
2203
|
-
}
|
|
2204
|
-
function linkIsAlreadySet(range) {
|
|
2205
|
-
const item = range.start.nodeAfter;
|
|
2206
|
-
return !!item && item.hasAttribute('linkHref');
|
|
2207
|
-
}
|
|
2208
|
-
|
|
2209
|
-
/**
|
|
2210
|
-
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
2211
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
2212
|
-
*/
|
|
2213
|
-
/**
|
|
2214
|
-
* @module link/link
|
|
2215
|
-
*/
|
|
2216
|
-
/**
|
|
2217
|
-
* The link plugin.
|
|
2218
|
-
*
|
|
2219
|
-
* This is a "glue" plugin that loads the {@link module:link/linkediting~LinkEditing link editing feature}
|
|
2220
|
-
* and {@link module:link/linkui~LinkUI link UI feature}.
|
|
2221
|
-
*/
|
|
2222
|
-
class Link extends Plugin {
|
|
2223
|
-
/**
|
|
2224
|
-
* @inheritDoc
|
|
2225
|
-
*/
|
|
2226
|
-
static get requires() {
|
|
2227
|
-
return [LinkEditing, LinkUI, AutoLink];
|
|
2228
|
-
}
|
|
2229
|
-
/**
|
|
2230
|
-
* @inheritDoc
|
|
2231
|
-
*/
|
|
2232
|
-
static get pluginName() {
|
|
2233
|
-
return 'Link';
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
/**
|
|
2238
|
-
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
2239
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
2240
|
-
*/
|
|
2241
|
-
/**
|
|
2242
|
-
* @module link/linkimageediting
|
|
2243
|
-
*/
|
|
2244
|
-
/**
|
|
2245
|
-
* The link image engine feature.
|
|
2246
|
-
*
|
|
2247
|
-
* It accepts the `linkHref="url"` attribute in the model for the {@link module:image/image~Image `<imageBlock>`} element
|
|
2248
|
-
* which allows linking images.
|
|
2249
|
-
*/
|
|
2250
|
-
class LinkImageEditing extends Plugin {
|
|
2251
|
-
/**
|
|
2252
|
-
* @inheritDoc
|
|
2253
|
-
*/
|
|
2254
|
-
static get requires() {
|
|
2255
|
-
return ['ImageEditing', 'ImageUtils', LinkEditing];
|
|
2256
|
-
}
|
|
2257
|
-
/**
|
|
2258
|
-
* @inheritDoc
|
|
2259
|
-
*/
|
|
2260
|
-
static get pluginName() {
|
|
2261
|
-
return 'LinkImageEditing';
|
|
2262
|
-
}
|
|
2263
|
-
/**
|
|
2264
|
-
* @inheritDoc
|
|
2265
|
-
*/
|
|
2266
|
-
afterInit() {
|
|
2267
|
-
const editor = this.editor;
|
|
2268
|
-
const schema = editor.model.schema;
|
|
2269
|
-
if (editor.plugins.has('ImageBlockEditing')) {
|
|
2270
|
-
schema.extend('imageBlock', { allowAttributes: ['linkHref'] });
|
|
2271
|
-
}
|
|
2272
|
-
editor.conversion.for('upcast').add(upcastLink(editor));
|
|
2273
|
-
editor.conversion.for('downcast').add(downcastImageLink(editor));
|
|
2274
|
-
// Definitions for decorators are provided by the `link` command and the `LinkEditing` plugin.
|
|
2275
|
-
this._enableAutomaticDecorators();
|
|
2276
|
-
this._enableManualDecorators();
|
|
2277
|
-
}
|
|
2278
|
-
/**
|
|
2279
|
-
* Processes {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition automatic decorators} definitions and
|
|
2280
|
-
* attaches proper converters that will work when linking an image.`
|
|
2281
|
-
*/
|
|
2282
|
-
_enableAutomaticDecorators() {
|
|
2283
|
-
const editor = this.editor;
|
|
2284
|
-
const command = editor.commands.get('link');
|
|
2285
|
-
const automaticDecorators = command.automaticDecorators;
|
|
2286
|
-
if (automaticDecorators.length) {
|
|
2287
|
-
editor.conversion.for('downcast').add(automaticDecorators.getDispatcherForLinkedImage());
|
|
2288
|
-
}
|
|
2289
|
-
}
|
|
2290
|
-
/**
|
|
2291
|
-
* Processes transformed {@link module:link/utils/manualdecorator~ManualDecorator} instances and attaches proper converters
|
|
2292
|
-
* that will work when linking an image.
|
|
2293
|
-
*/
|
|
2294
|
-
_enableManualDecorators() {
|
|
2295
|
-
const editor = this.editor;
|
|
2296
|
-
const command = editor.commands.get('link');
|
|
2297
|
-
for (const decorator of command.manualDecorators) {
|
|
2298
|
-
if (editor.plugins.has('ImageBlockEditing')) {
|
|
2299
|
-
editor.model.schema.extend('imageBlock', { allowAttributes: decorator.id });
|
|
2300
|
-
}
|
|
2301
|
-
if (editor.plugins.has('ImageInlineEditing')) {
|
|
2302
|
-
editor.model.schema.extend('imageInline', { allowAttributes: decorator.id });
|
|
2303
|
-
}
|
|
2304
|
-
editor.conversion.for('downcast').add(downcastImageLinkManualDecorator(decorator));
|
|
2305
|
-
editor.conversion.for('upcast').add(upcastImageLinkManualDecorator(editor, decorator));
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
}
|
|
2309
|
-
/**
|
|
2310
|
-
* Returns a converter for linked block images that consumes the "href" attribute
|
|
2311
|
-
* if a link contains an image.
|
|
2312
|
-
*
|
|
2313
|
-
* @param editor The editor instance.
|
|
2314
|
-
*/
|
|
2315
|
-
function upcastLink(editor) {
|
|
2316
|
-
const isImageInlinePluginLoaded = editor.plugins.has('ImageInlineEditing');
|
|
2317
|
-
const imageUtils = editor.plugins.get('ImageUtils');
|
|
2318
|
-
return dispatcher => {
|
|
2319
|
-
dispatcher.on('element:a', (evt, data, conversionApi) => {
|
|
2320
|
-
const viewLink = data.viewItem;
|
|
2321
|
-
const imageInLink = imageUtils.findViewImgElement(viewLink);
|
|
2322
|
-
if (!imageInLink) {
|
|
2323
|
-
return;
|
|
2324
|
-
}
|
|
2325
|
-
const blockImageView = imageInLink.findAncestor(element => imageUtils.isBlockImageView(element));
|
|
2326
|
-
// There are four possible cases to consider here
|
|
2327
|
-
//
|
|
2328
|
-
// 1. A "root > ... > figure.image > a > img" structure.
|
|
2329
|
-
// 2. A "root > ... > figure.image > a > picture > img" structure.
|
|
2330
|
-
// 3. A "root > ... > block > a > img" structure.
|
|
2331
|
-
// 4. A "root > ... > block > a > picture > img" structure.
|
|
2332
|
-
//
|
|
2333
|
-
// but the last 2 cases should only be considered by this converter when the inline image plugin
|
|
2334
|
-
// is NOT loaded in the editor (because otherwise, that would be a plain, linked inline image).
|
|
2335
|
-
if (isImageInlinePluginLoaded && !blockImageView) {
|
|
2336
|
-
return;
|
|
2337
|
-
}
|
|
2338
|
-
// There's an image inside an <a> element - we consume it so it won't be picked up by the Link plugin.
|
|
2339
|
-
const consumableAttributes = { attributes: ['href'] };
|
|
2340
|
-
// Consume the `href` attribute so the default one will not convert it to $text attribute.
|
|
2341
|
-
if (!conversionApi.consumable.consume(viewLink, consumableAttributes)) {
|
|
2342
|
-
// Might be consumed by something else - i.e. other converter with priority=highest - a standard check.
|
|
2343
|
-
return;
|
|
2344
|
-
}
|
|
2345
|
-
const linkHref = viewLink.getAttribute('href');
|
|
2346
|
-
// Missing the 'href' attribute.
|
|
2347
|
-
if (!linkHref) {
|
|
2348
|
-
return;
|
|
2349
|
-
}
|
|
2350
|
-
// A full definition of the image feature.
|
|
2351
|
-
// figure > a > img: parent of the view link element is an image element (figure).
|
|
2352
|
-
let modelElement = data.modelCursor.parent;
|
|
2353
|
-
if (!modelElement.is('element', 'imageBlock')) {
|
|
2354
|
-
// a > img: parent of the view link is not the image (figure) element. We need to convert it manually.
|
|
2355
|
-
const conversionResult = conversionApi.convertItem(imageInLink, data.modelCursor);
|
|
2356
|
-
// Set image range as conversion result.
|
|
2357
|
-
data.modelRange = conversionResult.modelRange;
|
|
2358
|
-
// Continue conversion where image conversion ends.
|
|
2359
|
-
data.modelCursor = conversionResult.modelCursor;
|
|
2360
|
-
modelElement = data.modelCursor.nodeBefore;
|
|
2361
|
-
}
|
|
2362
|
-
if (modelElement && modelElement.is('element', 'imageBlock')) {
|
|
2363
|
-
// Set the linkHref attribute from link element on model image element.
|
|
2364
|
-
conversionApi.writer.setAttribute('linkHref', linkHref, modelElement);
|
|
2365
|
-
}
|
|
2366
|
-
}, { priority: 'high' });
|
|
2367
|
-
// Using the same priority that `upcastImageLinkManualDecorator()` converter guarantees
|
|
2368
|
-
// that manual decorators will decorate the proper element.
|
|
2369
|
-
};
|
|
2370
|
-
}
|
|
2371
|
-
/**
|
|
2372
|
-
* Creates a converter that adds `<a>` to linked block image view elements.
|
|
2373
|
-
*/
|
|
2374
|
-
function downcastImageLink(editor) {
|
|
2375
|
-
const imageUtils = editor.plugins.get('ImageUtils');
|
|
2376
|
-
return dispatcher => {
|
|
2377
|
-
dispatcher.on('attribute:linkHref:imageBlock', (evt, data, conversionApi) => {
|
|
2378
|
-
if (!conversionApi.consumable.consume(data.item, evt.name)) {
|
|
2379
|
-
return;
|
|
2380
|
-
}
|
|
2381
|
-
// The image will be already converted - so it will be present in the view.
|
|
2382
|
-
const viewFigure = conversionApi.mapper.toViewElement(data.item);
|
|
2383
|
-
const writer = conversionApi.writer;
|
|
2384
|
-
// But we need to check whether the link element exists.
|
|
2385
|
-
const linkInImage = Array.from(viewFigure.getChildren())
|
|
2386
|
-
.find((child) => child.is('element', 'a'));
|
|
2387
|
-
const viewImage = imageUtils.findViewImgElement(viewFigure);
|
|
2388
|
-
// <picture>...<img/></picture> or <img/>
|
|
2389
|
-
const viewImgOrPicture = viewImage.parent.is('element', 'picture') ? viewImage.parent : viewImage;
|
|
2390
|
-
// If so, update the attribute if it's defined or remove the entire link if the attribute is empty.
|
|
2391
|
-
if (linkInImage) {
|
|
2392
|
-
if (data.attributeNewValue) {
|
|
2393
|
-
writer.setAttribute('href', data.attributeNewValue, linkInImage);
|
|
2394
|
-
}
|
|
2395
|
-
else {
|
|
2396
|
-
writer.move(writer.createRangeOn(viewImgOrPicture), writer.createPositionAt(viewFigure, 0));
|
|
2397
|
-
writer.remove(linkInImage);
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2400
|
-
else {
|
|
2401
|
-
// But if it does not exist. Let's wrap already converted image by newly created link element.
|
|
2402
|
-
// 1. Create an empty link element.
|
|
2403
|
-
const linkElement = writer.createContainerElement('a', { href: data.attributeNewValue });
|
|
2404
|
-
// 2. Insert link inside the associated image.
|
|
2405
|
-
writer.insert(writer.createPositionAt(viewFigure, 0), linkElement);
|
|
2406
|
-
// 3. Move the image to the link.
|
|
2407
|
-
writer.move(writer.createRangeOn(viewImgOrPicture), writer.createPositionAt(linkElement, 0));
|
|
2408
|
-
}
|
|
2409
|
-
}, { priority: 'high' });
|
|
2410
|
-
};
|
|
2411
|
-
}
|
|
2412
|
-
/**
|
|
2413
|
-
* Returns a converter that decorates the `<a>` element when the image is the link label.
|
|
2414
|
-
*/
|
|
2415
|
-
function downcastImageLinkManualDecorator(decorator) {
|
|
2416
|
-
return dispatcher => {
|
|
2417
|
-
dispatcher.on(`attribute:${decorator.id}:imageBlock`, (evt, data, conversionApi) => {
|
|
2418
|
-
const viewFigure = conversionApi.mapper.toViewElement(data.item);
|
|
2419
|
-
const linkInImage = Array.from(viewFigure.getChildren())
|
|
2420
|
-
.find((child) => child.is('element', 'a'));
|
|
2421
|
-
// The <a> element was removed by the time this converter is executed.
|
|
2422
|
-
// It may happen when the base `linkHref` and decorator attributes are removed
|
|
2423
|
-
// at the same time (see #8401).
|
|
2424
|
-
if (!linkInImage) {
|
|
2425
|
-
return;
|
|
2426
|
-
}
|
|
2427
|
-
for (const [key, val] of toMap(decorator.attributes)) {
|
|
2428
|
-
conversionApi.writer.setAttribute(key, val, linkInImage);
|
|
2429
|
-
}
|
|
2430
|
-
if (decorator.classes) {
|
|
2431
|
-
conversionApi.writer.addClass(decorator.classes, linkInImage);
|
|
2432
|
-
}
|
|
2433
|
-
for (const key in decorator.styles) {
|
|
2434
|
-
conversionApi.writer.setStyle(key, decorator.styles[key], linkInImage);
|
|
2435
|
-
}
|
|
2436
|
-
});
|
|
2437
|
-
};
|
|
2438
|
-
}
|
|
2439
|
-
/**
|
|
2440
|
-
* Returns a converter that checks whether manual decorators should be applied to the link.
|
|
2441
|
-
*/
|
|
2442
|
-
function upcastImageLinkManualDecorator(editor, decorator) {
|
|
2443
|
-
const isImageInlinePluginLoaded = editor.plugins.has('ImageInlineEditing');
|
|
2444
|
-
const imageUtils = editor.plugins.get('ImageUtils');
|
|
2445
|
-
return dispatcher => {
|
|
2446
|
-
dispatcher.on('element:a', (evt, data, conversionApi) => {
|
|
2447
|
-
const viewLink = data.viewItem;
|
|
2448
|
-
const imageInLink = imageUtils.findViewImgElement(viewLink);
|
|
2449
|
-
// We need to check whether an image is inside a link because the converter handles
|
|
2450
|
-
// only manual decorators for linked images. See #7975.
|
|
2451
|
-
if (!imageInLink) {
|
|
2452
|
-
return;
|
|
2453
|
-
}
|
|
2454
|
-
const blockImageView = imageInLink.findAncestor(element => imageUtils.isBlockImageView(element));
|
|
2455
|
-
if (isImageInlinePluginLoaded && !blockImageView) {
|
|
2456
|
-
return;
|
|
2457
|
-
}
|
|
2458
|
-
const matcher = new Matcher(decorator._createPattern());
|
|
2459
|
-
const result = matcher.match(viewLink);
|
|
2460
|
-
// The link element does not have required attributes or/and proper values.
|
|
2461
|
-
if (!result) {
|
|
2462
|
-
return;
|
|
2463
|
-
}
|
|
2464
|
-
// Check whether we can consume those attributes.
|
|
2465
|
-
if (!conversionApi.consumable.consume(viewLink, result.match)) {
|
|
2466
|
-
return;
|
|
2467
|
-
}
|
|
2468
|
-
// At this stage we can assume that we have the `<imageBlock>` element.
|
|
2469
|
-
// `nodeBefore` comes after conversion: `<a><img></a>`.
|
|
2470
|
-
// `parent` comes with full image definition: `<figure><a><img></a></figure>.
|
|
2471
|
-
// See the body of the `upcastLink()` function.
|
|
2472
|
-
const modelElement = data.modelCursor.nodeBefore || data.modelCursor.parent;
|
|
2473
|
-
conversionApi.writer.setAttribute(decorator.id, true, modelElement);
|
|
2474
|
-
}, { priority: 'high' });
|
|
2475
|
-
// Using the same priority that `upcastLink()` converter guarantees that the linked image was properly converted.
|
|
2476
|
-
};
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
/**
|
|
2480
|
-
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
2481
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
2482
|
-
*/
|
|
2483
|
-
/**
|
|
2484
|
-
* @module link/linkimageui
|
|
2485
|
-
*/
|
|
2486
|
-
/**
|
|
2487
|
-
* The link image UI plugin.
|
|
2488
|
-
*
|
|
2489
|
-
* This plugin provides the `'linkImage'` button that can be displayed in the {@link module:image/imagetoolbar~ImageToolbar}.
|
|
2490
|
-
* It can be used to wrap images in links.
|
|
2491
|
-
*/
|
|
2492
|
-
class LinkImageUI extends Plugin {
|
|
2493
|
-
/**
|
|
2494
|
-
* @inheritDoc
|
|
2495
|
-
*/
|
|
2496
|
-
static get requires() {
|
|
2497
|
-
return [LinkEditing, LinkUI, 'ImageBlockEditing'];
|
|
2498
|
-
}
|
|
2499
|
-
/**
|
|
2500
|
-
* @inheritDoc
|
|
2501
|
-
*/
|
|
2502
|
-
static get pluginName() {
|
|
2503
|
-
return 'LinkImageUI';
|
|
2504
|
-
}
|
|
2505
|
-
/**
|
|
2506
|
-
* @inheritDoc
|
|
2507
|
-
*/
|
|
2508
|
-
init() {
|
|
2509
|
-
const editor = this.editor;
|
|
2510
|
-
const viewDocument = editor.editing.view.document;
|
|
2511
|
-
this.listenTo(viewDocument, 'click', (evt, data) => {
|
|
2512
|
-
if (this._isSelectedLinkedImage(editor.model.document.selection)) {
|
|
2513
|
-
// Prevent browser navigation when clicking a linked image.
|
|
2514
|
-
data.preventDefault();
|
|
2515
|
-
// Block the `LinkUI` plugin when an image was clicked.
|
|
2516
|
-
// In such a case, we'd like to display the image toolbar.
|
|
2517
|
-
evt.stop();
|
|
2518
|
-
}
|
|
2519
|
-
}, { priority: 'high' });
|
|
2520
|
-
this._createToolbarLinkImageButton();
|
|
2521
|
-
}
|
|
2522
|
-
/**
|
|
2523
|
-
* Creates a `LinkImageUI` button view.
|
|
2524
|
-
*
|
|
2525
|
-
* Clicking this button shows a {@link module:link/linkui~LinkUI#_balloon} attached to the selection.
|
|
2526
|
-
* When an image is already linked, the view shows {@link module:link/linkui~LinkUI#actionsView} or
|
|
2527
|
-
* {@link module:link/linkui~LinkUI#formView} if it is not.
|
|
2528
|
-
*/
|
|
2529
|
-
_createToolbarLinkImageButton() {
|
|
2530
|
-
const editor = this.editor;
|
|
2531
|
-
const t = editor.t;
|
|
2532
|
-
editor.ui.componentFactory.add('linkImage', locale => {
|
|
2533
|
-
const button = new ButtonView(locale);
|
|
2534
|
-
const plugin = editor.plugins.get('LinkUI');
|
|
2535
|
-
const linkCommand = editor.commands.get('link');
|
|
2536
|
-
button.set({
|
|
2537
|
-
isEnabled: true,
|
|
2538
|
-
label: t('Link image'),
|
|
2539
|
-
icon: linkIcon,
|
|
2540
|
-
keystroke: LINK_KEYSTROKE,
|
|
2541
|
-
tooltip: true,
|
|
2542
|
-
isToggleable: true
|
|
2543
|
-
});
|
|
2544
|
-
// Bind button to the command.
|
|
2545
|
-
button.bind('isEnabled').to(linkCommand, 'isEnabled');
|
|
2546
|
-
button.bind('isOn').to(linkCommand, 'value', value => !!value);
|
|
2547
|
-
// Show the actionsView or formView (both from LinkUI) on button click depending on whether the image is linked already.
|
|
2548
|
-
this.listenTo(button, 'execute', () => {
|
|
2549
|
-
if (this._isSelectedLinkedImage(editor.model.document.selection)) {
|
|
2550
|
-
plugin._addActionsView();
|
|
2551
|
-
}
|
|
2552
|
-
else {
|
|
2553
|
-
plugin._showUI(true);
|
|
2554
|
-
}
|
|
2555
|
-
});
|
|
2556
|
-
return button;
|
|
2557
|
-
});
|
|
2558
|
-
}
|
|
2559
|
-
/**
|
|
2560
|
-
* Returns true if a linked image (either block or inline) is the only selected element
|
|
2561
|
-
* in the model document.
|
|
2562
|
-
*/
|
|
2563
|
-
_isSelectedLinkedImage(selection) {
|
|
2564
|
-
const selectedModelElement = selection.getSelectedElement();
|
|
2565
|
-
const imageUtils = this.editor.plugins.get('ImageUtils');
|
|
2566
|
-
return imageUtils.isImage(selectedModelElement) && selectedModelElement.hasAttribute('linkHref');
|
|
2567
|
-
}
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
/**
|
|
2571
|
-
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
2572
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
2573
|
-
*/
|
|
2574
|
-
/**
|
|
2575
|
-
* @module link/linkimage
|
|
2576
|
-
*/
|
|
2577
|
-
/**
|
|
2578
|
-
* The `LinkImage` plugin.
|
|
2579
|
-
*
|
|
2580
|
-
* This is a "glue" plugin that loads the {@link module:link/linkimageediting~LinkImageEditing link image editing feature}
|
|
2581
|
-
* and {@link module:link/linkimageui~LinkImageUI link image UI feature}.
|
|
2582
|
-
*/
|
|
2583
|
-
class LinkImage extends Plugin {
|
|
2584
|
-
/**
|
|
2585
|
-
* @inheritDoc
|
|
2586
|
-
*/
|
|
2587
|
-
static get requires() {
|
|
2588
|
-
return [LinkImageEditing, LinkImageUI];
|
|
2589
|
-
}
|
|
2590
|
-
/**
|
|
2591
|
-
* @inheritDoc
|
|
2592
|
-
*/
|
|
2593
|
-
static get pluginName() {
|
|
2594
|
-
return 'LinkImage';
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2597
|
-
|
|
2598
|
-
export { AutoLink, Link, LinkCommand, LinkEditing, LinkImage, LinkImageEditing, LinkImageUI, LinkUI, UnlinkCommand };
|
|
2599
|
-
//# sourceMappingURL=index.js.map
|