@ckeditor/ckeditor5-link 47.6.1-alpha.1 → 48.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -1
- package/ckeditor5-metadata.json +6 -6
- package/{src → dist}/autolink.d.ts +2 -2
- package/dist/index-editor.css +181 -105
- package/dist/index.css +169 -151
- package/dist/index.css.map +1 -1
- package/{src → dist}/index.d.ts +1 -1
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/{src → dist}/link.d.ts +1 -1
- package/{src → dist}/linkcommand.d.ts +2 -2
- package/{src → dist}/linkconfig.d.ts +1 -1
- package/{src → dist}/linkediting.d.ts +3 -3
- package/{src → dist}/linkimage.d.ts +1 -1
- package/{src → dist}/linkimageediting.d.ts +3 -2
- package/{src → dist}/linkimageui.d.ts +3 -2
- package/{src → dist}/linkui.d.ts +2 -2
- package/{src → dist}/ui/linkbuttonview.d.ts +2 -2
- package/{src → dist}/ui/linkformview.d.ts +2 -4
- package/{src → dist}/ui/linkpreviewbuttonview.d.ts +2 -2
- package/{src → dist}/ui/linkpropertiesview.d.ts +2 -2
- package/{src → dist}/ui/linkprovideritemsview.d.ts +2 -2
- package/{src → dist}/unlinkcommand.d.ts +1 -1
- package/{src → dist}/utils/automaticdecorators.d.ts +2 -2
- package/{src → dist}/utils/manualdecorator.d.ts +4 -4
- package/{src → dist}/utils.d.ts +2 -2
- package/package.json +29 -52
- package/build/link.js +0 -5
- package/build/translations/af.js +0 -1
- package/build/translations/ar.js +0 -1
- package/build/translations/ast.js +0 -1
- package/build/translations/az.js +0 -1
- package/build/translations/be.js +0 -1
- package/build/translations/bg.js +0 -1
- package/build/translations/bn.js +0 -1
- package/build/translations/bs.js +0 -1
- package/build/translations/ca.js +0 -1
- package/build/translations/cs.js +0 -1
- package/build/translations/da.js +0 -1
- package/build/translations/de-ch.js +0 -1
- package/build/translations/de.js +0 -1
- package/build/translations/el.js +0 -1
- package/build/translations/en-au.js +0 -1
- package/build/translations/en-gb.js +0 -1
- package/build/translations/eo.js +0 -1
- package/build/translations/es-co.js +0 -1
- package/build/translations/es.js +0 -1
- package/build/translations/et.js +0 -1
- package/build/translations/eu.js +0 -1
- package/build/translations/fa.js +0 -1
- package/build/translations/fi.js +0 -1
- package/build/translations/fr.js +0 -1
- package/build/translations/gl.js +0 -1
- package/build/translations/gu.js +0 -1
- package/build/translations/he.js +0 -1
- package/build/translations/hi.js +0 -1
- package/build/translations/hr.js +0 -1
- package/build/translations/hu.js +0 -1
- package/build/translations/hy.js +0 -1
- package/build/translations/id.js +0 -1
- package/build/translations/it.js +0 -1
- package/build/translations/ja.js +0 -1
- package/build/translations/jv.js +0 -1
- package/build/translations/kk.js +0 -1
- package/build/translations/km.js +0 -1
- package/build/translations/kn.js +0 -1
- package/build/translations/ko.js +0 -1
- package/build/translations/ku.js +0 -1
- package/build/translations/lt.js +0 -1
- package/build/translations/lv.js +0 -1
- package/build/translations/ms.js +0 -1
- package/build/translations/nb.js +0 -1
- package/build/translations/ne.js +0 -1
- package/build/translations/nl.js +0 -1
- package/build/translations/no.js +0 -1
- package/build/translations/oc.js +0 -1
- package/build/translations/pl.js +0 -1
- package/build/translations/pt-br.js +0 -1
- package/build/translations/pt.js +0 -1
- package/build/translations/ro.js +0 -1
- package/build/translations/ru.js +0 -1
- package/build/translations/si.js +0 -1
- package/build/translations/sk.js +0 -1
- package/build/translations/sl.js +0 -1
- package/build/translations/sq.js +0 -1
- package/build/translations/sr-latn.js +0 -1
- package/build/translations/sr.js +0 -1
- package/build/translations/sv.js +0 -1
- package/build/translations/th.js +0 -1
- package/build/translations/ti.js +0 -1
- package/build/translations/tk.js +0 -1
- package/build/translations/tr.js +0 -1
- package/build/translations/tt.js +0 -1
- package/build/translations/ug.js +0 -1
- package/build/translations/uk.js +0 -1
- package/build/translations/ur.js +0 -1
- package/build/translations/uz.js +0 -1
- package/build/translations/vi.js +0 -1
- package/build/translations/zh-cn.js +0 -1
- package/build/translations/zh.js +0 -1
- package/lang/contexts.json +0 -16
- package/lang/translations/af.po +0 -68
- package/lang/translations/ar.po +0 -68
- package/lang/translations/ast.po +0 -68
- package/lang/translations/az.po +0 -68
- package/lang/translations/be.po +0 -68
- package/lang/translations/bg.po +0 -68
- package/lang/translations/bn.po +0 -70
- package/lang/translations/bs.po +0 -68
- package/lang/translations/ca.po +0 -68
- package/lang/translations/cs.po +0 -68
- package/lang/translations/da.po +0 -68
- package/lang/translations/de-ch.po +0 -68
- package/lang/translations/de.po +0 -68
- package/lang/translations/el.po +0 -68
- package/lang/translations/en-au.po +0 -68
- package/lang/translations/en-gb.po +0 -68
- package/lang/translations/en.po +0 -68
- package/lang/translations/eo.po +0 -68
- package/lang/translations/es-co.po +0 -68
- package/lang/translations/es.po +0 -68
- package/lang/translations/et.po +0 -68
- package/lang/translations/eu.po +0 -68
- package/lang/translations/fa.po +0 -68
- package/lang/translations/fi.po +0 -68
- package/lang/translations/fr.po +0 -68
- package/lang/translations/gl.po +0 -68
- package/lang/translations/gu.po +0 -68
- package/lang/translations/he.po +0 -68
- package/lang/translations/hi.po +0 -68
- package/lang/translations/hr.po +0 -68
- package/lang/translations/hu.po +0 -68
- package/lang/translations/hy.po +0 -68
- package/lang/translations/id.po +0 -68
- package/lang/translations/it.po +0 -68
- package/lang/translations/ja.po +0 -68
- package/lang/translations/jv.po +0 -68
- package/lang/translations/kk.po +0 -68
- package/lang/translations/km.po +0 -68
- package/lang/translations/kn.po +0 -68
- package/lang/translations/ko.po +0 -68
- package/lang/translations/ku.po +0 -68
- package/lang/translations/lt.po +0 -68
- package/lang/translations/lv.po +0 -68
- package/lang/translations/ms.po +0 -68
- package/lang/translations/nb.po +0 -68
- package/lang/translations/ne.po +0 -68
- package/lang/translations/nl.po +0 -68
- package/lang/translations/no.po +0 -68
- package/lang/translations/oc.po +0 -68
- package/lang/translations/pl.po +0 -68
- package/lang/translations/pt-br.po +0 -68
- package/lang/translations/pt.po +0 -68
- package/lang/translations/ro.po +0 -68
- package/lang/translations/ru.po +0 -68
- package/lang/translations/si.po +0 -68
- package/lang/translations/sk.po +0 -68
- package/lang/translations/sl.po +0 -68
- package/lang/translations/sq.po +0 -68
- package/lang/translations/sr-latn.po +0 -68
- package/lang/translations/sr.po +0 -68
- package/lang/translations/sv.po +0 -68
- package/lang/translations/th.po +0 -68
- package/lang/translations/ti.po +0 -68
- package/lang/translations/tk.po +0 -68
- package/lang/translations/tr.po +0 -68
- package/lang/translations/tt.po +0 -68
- package/lang/translations/ug.po +0 -68
- package/lang/translations/uk.po +0 -68
- package/lang/translations/ur.po +0 -68
- package/lang/translations/uz.po +0 -68
- package/lang/translations/vi.po +0 -68
- package/lang/translations/zh-cn.po +0 -68
- package/lang/translations/zh.po +0 -68
- package/src/augmentation.js +0 -5
- package/src/autolink.js +0 -307
- package/src/index.js +0 -25
- package/src/link.js +0 -37
- package/src/linkcommand.js +0 -431
- package/src/linkconfig.js +0 -5
- package/src/linkediting.js +0 -402
- package/src/linkimage.js +0 -37
- package/src/linkimageediting.js +0 -264
- package/src/linkimageui.js +0 -102
- package/src/linkui.js +0 -1072
- package/src/ui/linkbuttonview.js +0 -54
- package/src/ui/linkformview.js +0 -302
- package/src/ui/linkpreviewbuttonview.js +0 -43
- package/src/ui/linkpropertiesview.js +0 -170
- package/src/ui/linkprovideritemsview.js +0 -207
- package/src/unlinkcommand.js +0 -66
- package/src/utils/automaticdecorators.js +0 -181
- package/src/utils/conflictingdecorators.js +0 -80
- package/src/utils/manualdecorator.js +0 -69
- package/src/utils.js +0 -157
- package/theme/link.css +0 -10
- package/theme/linkform.css +0 -24
- package/theme/linkimage.css +0 -16
- package/theme/linkproperties.css +0 -4
- package/theme/linkprovideritems.css +0 -18
- package/theme/linktoolbar.css +0 -12
- /package/{src → dist}/augmentation.d.ts +0 -0
- /package/{src → dist}/utils/conflictingdecorators.d.ts +0 -0
package/src/linkui.js
DELETED
|
@@ -1,1072 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* @module link/linkui
|
|
7
|
-
*/
|
|
8
|
-
import { Plugin } from 'ckeditor5/src/core.js';
|
|
9
|
-
import { IconLink, IconPencil, IconUnlink, IconSettings } from 'ckeditor5/src/icons.js';
|
|
10
|
-
import { ClickObserver } from 'ckeditor5/src/engine.js';
|
|
11
|
-
import { ButtonView, SwitchButtonView, ContextualBalloon, clickOutsideHandler, CssTransitionDisablerMixin, MenuBarMenuListItemButtonView, ToolbarView } from 'ckeditor5/src/ui.js';
|
|
12
|
-
import { Collection } from 'ckeditor5/src/utils.js';
|
|
13
|
-
import { isWidget } from 'ckeditor5/src/widget.js';
|
|
14
|
-
import { LinkEditing } from './linkediting.js';
|
|
15
|
-
import { LinkPreviewButtonView } from './ui/linkpreviewbuttonview.js';
|
|
16
|
-
import { LinkFormView } from './ui/linkformview.js';
|
|
17
|
-
import { LinkProviderItemsView } from './ui/linkprovideritemsview.js';
|
|
18
|
-
import { LinkPropertiesView } from './ui/linkpropertiesview.js';
|
|
19
|
-
import { LinkButtonView } from './ui/linkbuttonview.js';
|
|
20
|
-
import { addLinkProtocolIfApplicable, ensureSafeUrl, isLinkElement, extractTextFromLinkRange, LINK_KEYSTROKE } from './utils.js';
|
|
21
|
-
import '../theme/linktoolbar.css';
|
|
22
|
-
const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
23
|
-
/**
|
|
24
|
-
* The link UI plugin. It introduces the `'link'` and `'unlink'` buttons and support for the <kbd>Ctrl+K</kbd> keystroke.
|
|
25
|
-
*
|
|
26
|
-
* It uses the
|
|
27
|
-
* {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
|
|
28
|
-
*/
|
|
29
|
-
export class LinkUI extends Plugin {
|
|
30
|
-
/**
|
|
31
|
-
* The toolbar view displayed inside of the balloon.
|
|
32
|
-
*/
|
|
33
|
-
toolbarView = null;
|
|
34
|
-
/**
|
|
35
|
-
* The form view displayed inside the balloon.
|
|
36
|
-
*/
|
|
37
|
-
formView = null;
|
|
38
|
-
/**
|
|
39
|
-
* The view displaying links list.
|
|
40
|
-
*/
|
|
41
|
-
linkProviderItemsView = null;
|
|
42
|
-
/**
|
|
43
|
-
* The form view displaying properties link settings.
|
|
44
|
-
*/
|
|
45
|
-
propertiesView = null;
|
|
46
|
-
/**
|
|
47
|
-
* The contextual balloon plugin instance.
|
|
48
|
-
*/
|
|
49
|
-
_balloon;
|
|
50
|
-
/**
|
|
51
|
-
* The collection of the link providers.
|
|
52
|
-
*/
|
|
53
|
-
_linksProviders = new Collection();
|
|
54
|
-
/**
|
|
55
|
-
* @inheritDoc
|
|
56
|
-
*/
|
|
57
|
-
static get requires() {
|
|
58
|
-
return [ContextualBalloon, LinkEditing];
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* @inheritDoc
|
|
62
|
-
*/
|
|
63
|
-
static get pluginName() {
|
|
64
|
-
return 'LinkUI';
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* @inheritDoc
|
|
68
|
-
*/
|
|
69
|
-
static get isOfficialPlugin() {
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* @inheritDoc
|
|
74
|
-
*/
|
|
75
|
-
init() {
|
|
76
|
-
const editor = this.editor;
|
|
77
|
-
const t = this.editor.t;
|
|
78
|
-
this.set('selectedLinkableText', undefined);
|
|
79
|
-
editor.editing.view.addObserver(ClickObserver);
|
|
80
|
-
this._balloon = editor.plugins.get(ContextualBalloon);
|
|
81
|
-
// Create toolbar buttons.
|
|
82
|
-
this._registerComponents();
|
|
83
|
-
this._registerEditingOpeners();
|
|
84
|
-
this._enableBalloonActivators();
|
|
85
|
-
// Renders a fake visual selection marker on an expanded selection.
|
|
86
|
-
editor.conversion.for('editingDowncast').markerToHighlight({
|
|
87
|
-
model: VISUAL_SELECTION_MARKER_NAME,
|
|
88
|
-
view: {
|
|
89
|
-
classes: ['ck-fake-link-selection']
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
// Renders a fake visual selection marker on a collapsed selection.
|
|
93
|
-
editor.conversion.for('editingDowncast').markerToElement({
|
|
94
|
-
model: VISUAL_SELECTION_MARKER_NAME,
|
|
95
|
-
view: (data, { writer }) => {
|
|
96
|
-
if (!data.markerRange.isCollapsed) {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
const markerElement = writer.createUIElement('span');
|
|
100
|
-
writer.addClass(['ck-fake-link-selection', 'ck-fake-link-selection_collapsed'], markerElement);
|
|
101
|
-
return markerElement;
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
// Add the information about the keystrokes to the accessibility database.
|
|
105
|
-
editor.accessibility.addKeystrokeInfos({
|
|
106
|
-
keystrokes: [
|
|
107
|
-
{
|
|
108
|
-
label: t('Create link'),
|
|
109
|
-
keystroke: LINK_KEYSTROKE
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
label: t('Move out of a link'),
|
|
113
|
-
keystroke: [
|
|
114
|
-
['arrowleft', 'arrowleft'],
|
|
115
|
-
['arrowright', 'arrowright']
|
|
116
|
-
]
|
|
117
|
-
}
|
|
118
|
-
]
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* @inheritDoc
|
|
123
|
-
*/
|
|
124
|
-
destroy() {
|
|
125
|
-
super.destroy();
|
|
126
|
-
// Destroy created UI components as they are not automatically destroyed (see ckeditor5#1341).
|
|
127
|
-
if (this.propertiesView) {
|
|
128
|
-
this.propertiesView.destroy();
|
|
129
|
-
}
|
|
130
|
-
if (this.formView) {
|
|
131
|
-
this.formView.destroy();
|
|
132
|
-
}
|
|
133
|
-
if (this.toolbarView) {
|
|
134
|
-
this.toolbarView.destroy();
|
|
135
|
-
}
|
|
136
|
-
if (this.linkProviderItemsView) {
|
|
137
|
-
this.linkProviderItemsView.destroy();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Registers list of buttons below the link form view that
|
|
142
|
-
* open a list of links provided by the clicked provider.
|
|
143
|
-
*/
|
|
144
|
-
registerLinksListProvider(provider) {
|
|
145
|
-
const insertIndex = this._linksProviders
|
|
146
|
-
.filter(existing => (existing.order || 0) <= (provider.order || 0))
|
|
147
|
-
.length;
|
|
148
|
-
this._linksProviders.add(provider, insertIndex);
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Creates views.
|
|
152
|
-
*/
|
|
153
|
-
_createViews() {
|
|
154
|
-
const linkCommand = this.editor.commands.get('link');
|
|
155
|
-
this.toolbarView = this._createToolbarView();
|
|
156
|
-
this.formView = this._createFormView();
|
|
157
|
-
if (linkCommand.manualDecorators.length) {
|
|
158
|
-
this.propertiesView = this._createPropertiesView();
|
|
159
|
-
}
|
|
160
|
-
// Attach lifecycle actions to the the balloon.
|
|
161
|
-
this._enableUserBalloonInteractions();
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Creates the ToolbarView instance.
|
|
165
|
-
*/
|
|
166
|
-
_createToolbarView() {
|
|
167
|
-
const editor = this.editor;
|
|
168
|
-
const toolbarView = new ToolbarView(editor.locale);
|
|
169
|
-
const linkCommand = editor.commands.get('link');
|
|
170
|
-
toolbarView.class = 'ck-link-toolbar';
|
|
171
|
-
// Remove the linkProperties button if there are no manual decorators, as it would be useless.
|
|
172
|
-
let toolbarItems = editor.config.get('link.toolbar');
|
|
173
|
-
if (!linkCommand.manualDecorators.length) {
|
|
174
|
-
toolbarItems = toolbarItems.filter(item => item !== 'linkProperties');
|
|
175
|
-
}
|
|
176
|
-
toolbarView.fillFromConfig(toolbarItems, editor.ui.componentFactory);
|
|
177
|
-
// Close the panel on esc key press when the **link toolbar have focus**.
|
|
178
|
-
toolbarView.keystrokes.set('Esc', (data, cancel) => {
|
|
179
|
-
this._hideUI();
|
|
180
|
-
cancel();
|
|
181
|
-
});
|
|
182
|
-
// Open the form view on Ctrl+K when the **link toolbar have focus**..
|
|
183
|
-
toolbarView.keystrokes.set(LINK_KEYSTROKE, (data, cancel) => {
|
|
184
|
-
this._addFormView();
|
|
185
|
-
cancel();
|
|
186
|
-
});
|
|
187
|
-
// Register the toolbar, so it becomes available for Alt+F10 and Esc navigation.
|
|
188
|
-
// TODO this should be registered earlier to be able to open this toolbar without previously opening it by click or Ctrl+K
|
|
189
|
-
editor.ui.addToolbar(toolbarView, {
|
|
190
|
-
isContextual: true,
|
|
191
|
-
beforeFocus: () => {
|
|
192
|
-
if (this._getSelectedLinkElement() && !this._isToolbarVisible) {
|
|
193
|
-
this._showUI(true);
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
afterBlur: () => {
|
|
197
|
-
this._hideUI(false);
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
return toolbarView;
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Creates the {@link module:link/ui/linkformview~LinkFormView} instance.
|
|
204
|
-
*/
|
|
205
|
-
_createFormView() {
|
|
206
|
-
const editor = this.editor;
|
|
207
|
-
const t = editor.locale.t;
|
|
208
|
-
const linkCommand = editor.commands.get('link');
|
|
209
|
-
const defaultProtocol = editor.config.get('link.defaultProtocol');
|
|
210
|
-
const formView = new (CssTransitionDisablerMixin(LinkFormView))(editor.locale, getFormValidators(editor));
|
|
211
|
-
formView.displayedTextInputView.bind('isEnabled').to(this, 'selectedLinkableText', value => value !== undefined);
|
|
212
|
-
// Form elements should be read-only when corresponding commands are disabled.
|
|
213
|
-
formView.urlInputView.bind('isEnabled').to(linkCommand, 'isEnabled');
|
|
214
|
-
// Disable the "save" button if the command is disabled.
|
|
215
|
-
formView.saveButtonView.bind('isEnabled').to(linkCommand, 'isEnabled');
|
|
216
|
-
// Change the "Save" button label depending on the command state.
|
|
217
|
-
formView.saveButtonView.bind('label').to(linkCommand, 'value', value => value ? t('Update') : t('Insert'));
|
|
218
|
-
// Execute link command after clicking the "Save" button.
|
|
219
|
-
this.listenTo(formView, 'submit', () => {
|
|
220
|
-
if (formView.isValid()) {
|
|
221
|
-
const url = formView.urlInputView.fieldView.element.value;
|
|
222
|
-
const parsedUrl = addLinkProtocolIfApplicable(url, defaultProtocol);
|
|
223
|
-
const displayedText = formView.displayedTextInputView.fieldView.element.value;
|
|
224
|
-
editor.execute('link', parsedUrl, this._getDecoratorSwitchesState(), displayedText !== this.selectedLinkableText ? displayedText : undefined);
|
|
225
|
-
this._closeFormView();
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
// Update balloon position when form error changes.
|
|
229
|
-
this.listenTo(formView.urlInputView, 'change:errorText', () => {
|
|
230
|
-
editor.ui.update();
|
|
231
|
-
});
|
|
232
|
-
// Hide the panel after clicking the "Cancel" button.
|
|
233
|
-
this.listenTo(formView, 'cancel', () => {
|
|
234
|
-
this._closeFormView();
|
|
235
|
-
});
|
|
236
|
-
// Close the panel on esc key press when the **form has focus**.
|
|
237
|
-
formView.keystrokes.set('Esc', (data, cancel) => {
|
|
238
|
-
this._closeFormView();
|
|
239
|
-
cancel();
|
|
240
|
-
});
|
|
241
|
-
// Watch adding new link providers and add them to the buttons list.
|
|
242
|
-
formView.providersListChildren.bindTo(this._linksProviders).using(provider => this._createLinksListProviderButton(provider));
|
|
243
|
-
return formView;
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Creates a sorted array of buttons with link names.
|
|
247
|
-
*/
|
|
248
|
-
_createLinkProviderListView(provider) {
|
|
249
|
-
return provider.getListItems().map(({ href, label, icon }) => {
|
|
250
|
-
const buttonView = new ButtonView();
|
|
251
|
-
buttonView.set({
|
|
252
|
-
label,
|
|
253
|
-
icon,
|
|
254
|
-
tooltip: false,
|
|
255
|
-
withText: true
|
|
256
|
-
});
|
|
257
|
-
buttonView.on('execute', () => {
|
|
258
|
-
this.formView.resetFormStatus();
|
|
259
|
-
this.formView.urlInputView.fieldView.value = href;
|
|
260
|
-
// Set focus to the editing view to prevent from losing it while current view is removed.
|
|
261
|
-
this.editor.editing.view.focus();
|
|
262
|
-
this._removeLinksProviderView();
|
|
263
|
-
// Set the focus to the URL input field.
|
|
264
|
-
this.formView.focus();
|
|
265
|
-
});
|
|
266
|
-
return buttonView;
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Creates a view for links provider.
|
|
271
|
-
*/
|
|
272
|
-
_createLinkProviderItemsView(provider) {
|
|
273
|
-
const editor = this.editor;
|
|
274
|
-
const t = editor.locale.t;
|
|
275
|
-
const view = new LinkProviderItemsView(editor.locale);
|
|
276
|
-
const { emptyListPlaceholder, label } = provider;
|
|
277
|
-
view.emptyListPlaceholder = emptyListPlaceholder || t('No links available');
|
|
278
|
-
view.title = label;
|
|
279
|
-
// Hide the panel after clicking the "Cancel" button.
|
|
280
|
-
this.listenTo(view, 'cancel', () => {
|
|
281
|
-
// Set focus to the editing view to prevent from losing it while current view is removed.
|
|
282
|
-
editor.editing.view.focus();
|
|
283
|
-
this._removeLinksProviderView();
|
|
284
|
-
// Set the focus to the URL input field.
|
|
285
|
-
this.formView.focus();
|
|
286
|
-
});
|
|
287
|
-
return view;
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Creates the {@link module:link/ui/linkpropertiesview~LinkPropertiesView} instance.
|
|
291
|
-
*/
|
|
292
|
-
_createPropertiesView() {
|
|
293
|
-
const editor = this.editor;
|
|
294
|
-
const linkCommand = this.editor.commands.get('link');
|
|
295
|
-
const view = new (CssTransitionDisablerMixin(LinkPropertiesView))(editor.locale);
|
|
296
|
-
// Hide the panel after clicking the back button.
|
|
297
|
-
this.listenTo(view, 'back', () => {
|
|
298
|
-
// Move focus back to the editing view to prevent from losing it while current view is removed.
|
|
299
|
-
editor.editing.view.focus();
|
|
300
|
-
this._removePropertiesView();
|
|
301
|
-
});
|
|
302
|
-
view.listChildren.bindTo(linkCommand.manualDecorators).using(manualDecorator => {
|
|
303
|
-
const button = new SwitchButtonView(editor.locale);
|
|
304
|
-
button.set({
|
|
305
|
-
label: manualDecorator.label,
|
|
306
|
-
withText: true
|
|
307
|
-
});
|
|
308
|
-
button.bind('isOn').toMany([manualDecorator, linkCommand], 'value', (decoratorValue, commandValue) => {
|
|
309
|
-
return commandValue === undefined && decoratorValue === undefined ?
|
|
310
|
-
!!manualDecorator.defaultValue :
|
|
311
|
-
!!decoratorValue;
|
|
312
|
-
});
|
|
313
|
-
button.on('execute', () => {
|
|
314
|
-
editor.execute('link', linkCommand.value, {
|
|
315
|
-
...this._getDecoratorSwitchesState(),
|
|
316
|
-
[manualDecorator.id]: !button.isOn
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
return button;
|
|
320
|
-
});
|
|
321
|
-
return view;
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Obtains the state of the manual decorators.
|
|
325
|
-
*/
|
|
326
|
-
_getDecoratorSwitchesState() {
|
|
327
|
-
const linkCommand = this.editor.commands.get('link');
|
|
328
|
-
return Array
|
|
329
|
-
.from(linkCommand.manualDecorators)
|
|
330
|
-
.reduce((accumulator, manualDecorator) => {
|
|
331
|
-
const value = linkCommand.value === undefined && manualDecorator.value === undefined ?
|
|
332
|
-
manualDecorator.defaultValue :
|
|
333
|
-
manualDecorator.value;
|
|
334
|
-
return {
|
|
335
|
-
...accumulator,
|
|
336
|
-
[manualDecorator.id]: !!value
|
|
337
|
-
};
|
|
338
|
-
}, {});
|
|
339
|
-
}
|
|
340
|
-
/**
|
|
341
|
-
* Registers listeners used in editing plugin, used to open links.
|
|
342
|
-
*/
|
|
343
|
-
_registerEditingOpeners() {
|
|
344
|
-
const linkEditing = this.editor.plugins.get(LinkEditing);
|
|
345
|
-
linkEditing._registerLinkOpener(href => {
|
|
346
|
-
const match = this._getLinkProviderLinkByHref(href);
|
|
347
|
-
if (!match) {
|
|
348
|
-
return false;
|
|
349
|
-
}
|
|
350
|
-
const { item, provider } = match;
|
|
351
|
-
if (provider.navigate) {
|
|
352
|
-
return provider.navigate(item);
|
|
353
|
-
}
|
|
354
|
-
return false;
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Registers components in the ComponentFactory.
|
|
359
|
-
*/
|
|
360
|
-
_registerComponents() {
|
|
361
|
-
const editor = this.editor;
|
|
362
|
-
editor.ui.componentFactory.add('link', () => {
|
|
363
|
-
const button = this._createButton(ButtonView);
|
|
364
|
-
button.set({
|
|
365
|
-
tooltip: true
|
|
366
|
-
});
|
|
367
|
-
return button;
|
|
368
|
-
});
|
|
369
|
-
editor.ui.componentFactory.add('menuBar:link', () => {
|
|
370
|
-
const button = this._createButton(MenuBarMenuListItemButtonView);
|
|
371
|
-
button.set({
|
|
372
|
-
role: 'menuitemcheckbox'
|
|
373
|
-
});
|
|
374
|
-
return button;
|
|
375
|
-
});
|
|
376
|
-
editor.ui.componentFactory.add('linkPreview', locale => {
|
|
377
|
-
const button = new LinkPreviewButtonView(locale);
|
|
378
|
-
const allowedProtocols = editor.config.get('link.allowedProtocols');
|
|
379
|
-
const linkCommand = editor.commands.get('link');
|
|
380
|
-
const t = locale.t;
|
|
381
|
-
button.bind('isEnabled').to(linkCommand, 'value', href => !!href);
|
|
382
|
-
button.bind('href').to(linkCommand, 'value', href => {
|
|
383
|
-
return href && ensureSafeUrl(href, allowedProtocols);
|
|
384
|
-
});
|
|
385
|
-
const setHref = (href) => {
|
|
386
|
-
if (!href) {
|
|
387
|
-
button.label = undefined;
|
|
388
|
-
button.icon = undefined;
|
|
389
|
-
button.tooltip = t('Open link in new tab');
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
const selectedLinksProviderLink = this._getLinkProviderLinkByHref(href);
|
|
393
|
-
if (selectedLinksProviderLink) {
|
|
394
|
-
const { label, tooltip, icon } = selectedLinksProviderLink.item;
|
|
395
|
-
button.label = label;
|
|
396
|
-
button.tooltip = tooltip || false;
|
|
397
|
-
button.icon = icon;
|
|
398
|
-
}
|
|
399
|
-
else {
|
|
400
|
-
button.label = href;
|
|
401
|
-
button.icon = undefined;
|
|
402
|
-
button.tooltip = t('Open link in new tab');
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
|
-
setHref(linkCommand.value);
|
|
406
|
-
this.listenTo(linkCommand, 'change:value', (evt, name, href) => {
|
|
407
|
-
setHref(href);
|
|
408
|
-
});
|
|
409
|
-
this.listenTo(button, 'navigate', (evt, href, cancel) => {
|
|
410
|
-
const selectedLinksProviderLink = this._getLinkProviderLinkByHref(href);
|
|
411
|
-
if (!selectedLinksProviderLink) {
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
const { provider, item } = selectedLinksProviderLink;
|
|
415
|
-
const { navigate } = provider;
|
|
416
|
-
if (navigate && navigate(item)) {
|
|
417
|
-
evt.stop();
|
|
418
|
-
cancel();
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
return button;
|
|
422
|
-
});
|
|
423
|
-
editor.ui.componentFactory.add('unlink', locale => {
|
|
424
|
-
const unlinkCommand = editor.commands.get('unlink');
|
|
425
|
-
const button = new ButtonView(locale);
|
|
426
|
-
const t = locale.t;
|
|
427
|
-
button.set({
|
|
428
|
-
label: t('Unlink'),
|
|
429
|
-
icon: IconUnlink,
|
|
430
|
-
tooltip: true
|
|
431
|
-
});
|
|
432
|
-
button.bind('isEnabled').to(unlinkCommand);
|
|
433
|
-
this.listenTo(button, 'execute', () => {
|
|
434
|
-
editor.execute('unlink');
|
|
435
|
-
this._hideUI();
|
|
436
|
-
});
|
|
437
|
-
return button;
|
|
438
|
-
});
|
|
439
|
-
editor.ui.componentFactory.add('editLink', locale => {
|
|
440
|
-
const linkCommand = editor.commands.get('link');
|
|
441
|
-
const button = new ButtonView(locale);
|
|
442
|
-
const t = locale.t;
|
|
443
|
-
button.set({
|
|
444
|
-
label: t('Edit link'),
|
|
445
|
-
icon: IconPencil,
|
|
446
|
-
tooltip: true
|
|
447
|
-
});
|
|
448
|
-
button.bind('isEnabled').to(linkCommand);
|
|
449
|
-
this.listenTo(button, 'execute', () => {
|
|
450
|
-
this._addFormView();
|
|
451
|
-
});
|
|
452
|
-
return button;
|
|
453
|
-
});
|
|
454
|
-
editor.ui.componentFactory.add('linkProperties', locale => {
|
|
455
|
-
const linkCommand = editor.commands.get('link');
|
|
456
|
-
const button = new ButtonView(locale);
|
|
457
|
-
const t = locale.t;
|
|
458
|
-
button.set({
|
|
459
|
-
label: t('Link properties'),
|
|
460
|
-
icon: IconSettings,
|
|
461
|
-
tooltip: true
|
|
462
|
-
});
|
|
463
|
-
button.bind('isEnabled').to(linkCommand, 'isEnabled', linkCommand, 'value', linkCommand, 'manualDecorators', (isEnabled, href, manualDecorators) => isEnabled && !!href && manualDecorators.length > 0);
|
|
464
|
-
this.listenTo(button, 'execute', () => {
|
|
465
|
-
this._addPropertiesView();
|
|
466
|
-
});
|
|
467
|
-
return button;
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Creates a links button view.
|
|
472
|
-
*/
|
|
473
|
-
_createLinksListProviderButton(linkProvider) {
|
|
474
|
-
const locale = this.editor.locale;
|
|
475
|
-
const linksButton = new LinkButtonView(locale);
|
|
476
|
-
linksButton.set({
|
|
477
|
-
label: linkProvider.label
|
|
478
|
-
});
|
|
479
|
-
this.listenTo(linksButton, 'execute', () => {
|
|
480
|
-
this._showLinksProviderView(linkProvider);
|
|
481
|
-
});
|
|
482
|
-
return linksButton;
|
|
483
|
-
}
|
|
484
|
-
/**
|
|
485
|
-
* Creates a button for link command to use either in toolbar or in menu bar.
|
|
486
|
-
*/
|
|
487
|
-
_createButton(ButtonClass) {
|
|
488
|
-
const editor = this.editor;
|
|
489
|
-
const locale = editor.locale;
|
|
490
|
-
const command = editor.commands.get('link');
|
|
491
|
-
const view = new ButtonClass(editor.locale);
|
|
492
|
-
const t = locale.t;
|
|
493
|
-
view.set({
|
|
494
|
-
label: t('Link'),
|
|
495
|
-
icon: IconLink,
|
|
496
|
-
keystroke: LINK_KEYSTROKE,
|
|
497
|
-
isToggleable: true
|
|
498
|
-
});
|
|
499
|
-
view.bind('isEnabled').to(command, 'isEnabled');
|
|
500
|
-
view.bind('isOn').to(command, 'value', value => !!value);
|
|
501
|
-
// Show the panel on button click.
|
|
502
|
-
this.listenTo(view, 'execute', () => {
|
|
503
|
-
editor.editing.view.scrollToTheSelection();
|
|
504
|
-
this._showUI(true);
|
|
505
|
-
// Open the form view on-top of the toolbar view if it's already visible.
|
|
506
|
-
// It should be visible every time the link is selected.
|
|
507
|
-
if (this._getSelectedLinkElement()) {
|
|
508
|
-
this._addFormView();
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
return view;
|
|
512
|
-
}
|
|
513
|
-
/**
|
|
514
|
-
* Attaches actions that control whether the balloon panel containing the
|
|
515
|
-
* {@link #formView} should be displayed.
|
|
516
|
-
*/
|
|
517
|
-
_enableBalloonActivators() {
|
|
518
|
-
const editor = this.editor;
|
|
519
|
-
const viewDocument = editor.editing.view.document;
|
|
520
|
-
// Handle click on view document and show panel when selection is placed inside the link element.
|
|
521
|
-
// Keep panel open until selection will be inside the same link element.
|
|
522
|
-
this.listenTo(viewDocument, 'click', () => {
|
|
523
|
-
const parentLink = this._getSelectedLinkElement();
|
|
524
|
-
if (parentLink) {
|
|
525
|
-
// Then show panel but keep focus inside editor editable.
|
|
526
|
-
this._showUI();
|
|
527
|
-
}
|
|
528
|
-
});
|
|
529
|
-
// Handle the `Ctrl+K` keystroke and show the panel.
|
|
530
|
-
editor.keystrokes.set(LINK_KEYSTROKE, (keyEvtData, cancel) => {
|
|
531
|
-
// Prevent focusing the search bar in FF, Chrome and Edge. See https://github.com/ckeditor/ckeditor5/issues/4811.
|
|
532
|
-
cancel();
|
|
533
|
-
if (editor.commands.get('link').isEnabled) {
|
|
534
|
-
editor.editing.view.scrollToTheSelection();
|
|
535
|
-
this._showUI(true);
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Attaches actions that control whether the balloon panel containing the
|
|
541
|
-
* {@link #formView} is visible or not.
|
|
542
|
-
*/
|
|
543
|
-
_enableUserBalloonInteractions() {
|
|
544
|
-
// Focus the form if the balloon is visible and the Tab key has been pressed.
|
|
545
|
-
this.editor.keystrokes.set('Tab', (data, cancel) => {
|
|
546
|
-
if (this._isToolbarVisible && !this.toolbarView.focusTracker.isFocused) {
|
|
547
|
-
this.toolbarView.focus();
|
|
548
|
-
cancel();
|
|
549
|
-
}
|
|
550
|
-
}, {
|
|
551
|
-
// Use the high priority because the link UI navigation is more important
|
|
552
|
-
// than other feature's actions, e.g. list indentation.
|
|
553
|
-
// https://github.com/ckeditor/ckeditor5-link/issues/146
|
|
554
|
-
priority: 'high'
|
|
555
|
-
});
|
|
556
|
-
// Close the panel on the Esc key press when the editable has focus and the balloon is visible.
|
|
557
|
-
this.editor.keystrokes.set('Esc', (data, cancel) => {
|
|
558
|
-
if (this._isUIVisible) {
|
|
559
|
-
this._hideUI();
|
|
560
|
-
cancel();
|
|
561
|
-
}
|
|
562
|
-
});
|
|
563
|
-
// Close on click outside of balloon panel element.
|
|
564
|
-
clickOutsideHandler({
|
|
565
|
-
emitter: this.formView,
|
|
566
|
-
activator: () => this._isUIInPanel,
|
|
567
|
-
contextElements: () => [this._balloon.view.element],
|
|
568
|
-
callback: () => {
|
|
569
|
-
// Focusing on the editable during a click outside the balloon panel might
|
|
570
|
-
// cause the selection to move to the beginning of the editable, so we avoid
|
|
571
|
-
// focusing on it during this action.
|
|
572
|
-
// See: https://github.com/ckeditor/ckeditor5/issues/18253
|
|
573
|
-
this._hideUI(false);
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* Adds the {@link #toolbarView} to the {@link #_balloon}.
|
|
579
|
-
*
|
|
580
|
-
* @internal
|
|
581
|
-
*/
|
|
582
|
-
_addToolbarView() {
|
|
583
|
-
if (!this.toolbarView) {
|
|
584
|
-
this._createViews();
|
|
585
|
-
}
|
|
586
|
-
if (this._isToolbarInPanel) {
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
this._balloon.add({
|
|
590
|
-
view: this.toolbarView,
|
|
591
|
-
position: this._getBalloonPositionData(),
|
|
592
|
-
balloonClassName: 'ck-toolbar-container'
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
/**
|
|
596
|
-
* Adds the {@link #formView} to the {@link #_balloon}.
|
|
597
|
-
*/
|
|
598
|
-
_addFormView() {
|
|
599
|
-
if (!this.formView) {
|
|
600
|
-
this._createViews();
|
|
601
|
-
}
|
|
602
|
-
if (this._isFormInPanel) {
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
const linkCommand = this.editor.commands.get('link');
|
|
606
|
-
this.formView.disableCssTransitions();
|
|
607
|
-
this.formView.resetFormStatus();
|
|
608
|
-
this.formView.backButtonView.isVisible = linkCommand.isEnabled && !!linkCommand.value;
|
|
609
|
-
this._balloon.add({
|
|
610
|
-
view: this.formView,
|
|
611
|
-
position: this._getBalloonPositionData()
|
|
612
|
-
});
|
|
613
|
-
// Make sure that each time the panel shows up, the fields remains in sync with the value of
|
|
614
|
-
// the command. If the user typed in the input, then canceled the balloon (`urlInputView.fieldView#value` stays
|
|
615
|
-
// unaltered) and re-opened it without changing the value of the link command (e.g. because they
|
|
616
|
-
// clicked the same link), they would see the old value instead of the actual value of the command.
|
|
617
|
-
// https://github.com/ckeditor/ckeditor5-link/issues/78
|
|
618
|
-
// https://github.com/ckeditor/ckeditor5-link/issues/123
|
|
619
|
-
this.selectedLinkableText = this._getSelectedLinkableText();
|
|
620
|
-
this.formView.displayedTextInputView.fieldView.value = this.selectedLinkableText || '';
|
|
621
|
-
this.formView.urlInputView.fieldView.value = linkCommand.value || '';
|
|
622
|
-
// Select input when form view is currently visible.
|
|
623
|
-
if (this._balloon.visibleView === this.formView) {
|
|
624
|
-
this.formView.urlInputView.fieldView.select();
|
|
625
|
-
}
|
|
626
|
-
this.formView.enableCssTransitions();
|
|
627
|
-
}
|
|
628
|
-
/**
|
|
629
|
-
* Adds the {@link #propertiesView} to the {@link #_balloon}.
|
|
630
|
-
*/
|
|
631
|
-
_addPropertiesView() {
|
|
632
|
-
if (!this.propertiesView) {
|
|
633
|
-
this._createViews();
|
|
634
|
-
}
|
|
635
|
-
if (this._arePropertiesInPanel) {
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
this.propertiesView.disableCssTransitions();
|
|
639
|
-
this._balloon.add({
|
|
640
|
-
view: this.propertiesView,
|
|
641
|
-
position: this._getBalloonPositionData()
|
|
642
|
-
});
|
|
643
|
-
this.propertiesView.enableCssTransitions();
|
|
644
|
-
this.propertiesView.focus();
|
|
645
|
-
}
|
|
646
|
-
/**
|
|
647
|
-
* Shows the view with links provided by the given provider.
|
|
648
|
-
*/
|
|
649
|
-
_showLinksProviderView(provider) {
|
|
650
|
-
if (this.linkProviderItemsView) {
|
|
651
|
-
this._removeLinksProviderView();
|
|
652
|
-
}
|
|
653
|
-
this.linkProviderItemsView = this._createLinkProviderItemsView(provider);
|
|
654
|
-
this._addLinkProviderItemsView(provider);
|
|
655
|
-
}
|
|
656
|
-
/**
|
|
657
|
-
* Adds the {@link #linkProviderItemsView} to the {@link #_balloon}.
|
|
658
|
-
*/
|
|
659
|
-
_addLinkProviderItemsView(provider) {
|
|
660
|
-
// Clear the collection of links.
|
|
661
|
-
this.linkProviderItemsView.listChildren.clear();
|
|
662
|
-
// Add links to the collection.
|
|
663
|
-
this.linkProviderItemsView.listChildren.addMany(this._createLinkProviderListView(provider));
|
|
664
|
-
this._balloon.add({
|
|
665
|
-
view: this.linkProviderItemsView,
|
|
666
|
-
position: this._getBalloonPositionData()
|
|
667
|
-
});
|
|
668
|
-
this.linkProviderItemsView.focus();
|
|
669
|
-
}
|
|
670
|
-
/**
|
|
671
|
-
* Closes the form view. Decides whether the balloon should be hidden completely or if the action view should be shown. This is
|
|
672
|
-
* decided upon the link command value (which has a value if the document selection is in the link).
|
|
673
|
-
*/
|
|
674
|
-
_closeFormView() {
|
|
675
|
-
const linkCommand = this.editor.commands.get('link');
|
|
676
|
-
this.selectedLinkableText = undefined;
|
|
677
|
-
if (linkCommand.value !== undefined) {
|
|
678
|
-
this._removeFormView();
|
|
679
|
-
}
|
|
680
|
-
else {
|
|
681
|
-
this._hideUI();
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
/**
|
|
685
|
-
* Removes the {@link #propertiesView} from the {@link #_balloon}.
|
|
686
|
-
*/
|
|
687
|
-
_removePropertiesView() {
|
|
688
|
-
if (this._arePropertiesInPanel) {
|
|
689
|
-
this._balloon.remove(this.propertiesView);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
/**
|
|
693
|
-
* Removes the {@link #linkProviderItemsView} from the {@link #_balloon}.
|
|
694
|
-
*/
|
|
695
|
-
_removeLinksProviderView() {
|
|
696
|
-
if (this._isLinksListInPanel) {
|
|
697
|
-
this._balloon.remove(this.linkProviderItemsView);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
/**
|
|
701
|
-
* Removes the {@link #formView} from the {@link #_balloon}.
|
|
702
|
-
*/
|
|
703
|
-
_removeFormView(updateFocus = true) {
|
|
704
|
-
if (this._isFormInPanel) {
|
|
705
|
-
// Blur the input element before removing it from DOM to prevent issues in some browsers.
|
|
706
|
-
// See https://github.com/ckeditor/ckeditor5/issues/1501.
|
|
707
|
-
this.formView.saveButtonView.focus();
|
|
708
|
-
// Reset fields to update the state of the submit button.
|
|
709
|
-
this.formView.displayedTextInputView.fieldView.reset();
|
|
710
|
-
this.formView.urlInputView.fieldView.reset();
|
|
711
|
-
this._balloon.remove(this.formView);
|
|
712
|
-
// Because the form has an input which has focus, the focus must be brought back
|
|
713
|
-
// to the editor. Otherwise, it would be lost.
|
|
714
|
-
if (updateFocus) {
|
|
715
|
-
this.editor.editing.view.focus();
|
|
716
|
-
}
|
|
717
|
-
this._hideFakeVisualSelection();
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
|
-
* Shows the correct UI type. It is either {@link #formView} or {@link #toolbarView}.
|
|
722
|
-
*
|
|
723
|
-
* @internal
|
|
724
|
-
*/
|
|
725
|
-
_showUI(forceVisible = false) {
|
|
726
|
-
if (!this.formView) {
|
|
727
|
-
this._createViews();
|
|
728
|
-
}
|
|
729
|
-
// When there's no link under the selection, go straight to the editing UI.
|
|
730
|
-
if (!this._getSelectedLinkElement()) {
|
|
731
|
-
// Show visual selection on a text without a link when the contextual balloon is displayed.
|
|
732
|
-
// See https://github.com/ckeditor/ckeditor5/issues/4721.
|
|
733
|
-
this._showFakeVisualSelection();
|
|
734
|
-
this._addToolbarView();
|
|
735
|
-
// Be sure panel with link is visible.
|
|
736
|
-
if (forceVisible) {
|
|
737
|
-
this._balloon.showStack('main');
|
|
738
|
-
}
|
|
739
|
-
this._addFormView();
|
|
740
|
-
}
|
|
741
|
-
// If there's a link under the selection...
|
|
742
|
-
else {
|
|
743
|
-
// Go to the editing UI if toolbar is already visible.
|
|
744
|
-
if (this._isToolbarVisible) {
|
|
745
|
-
this._addFormView();
|
|
746
|
-
}
|
|
747
|
-
// Otherwise display just the toolbar.
|
|
748
|
-
else {
|
|
749
|
-
this._addToolbarView();
|
|
750
|
-
}
|
|
751
|
-
// Be sure panel with link is visible.
|
|
752
|
-
if (forceVisible) {
|
|
753
|
-
this._balloon.showStack('main');
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
// Begin responding to ui#update once the UI is added.
|
|
757
|
-
this._startUpdatingUI();
|
|
758
|
-
}
|
|
759
|
-
/**
|
|
760
|
-
* Removes the {@link #formView} from the {@link #_balloon}.
|
|
761
|
-
*
|
|
762
|
-
* See {@link #_addFormView}, {@link #_addToolbarView}.
|
|
763
|
-
*/
|
|
764
|
-
_hideUI(updateFocus = true) {
|
|
765
|
-
const editor = this.editor;
|
|
766
|
-
if (!this._isUIInPanel) {
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
this.stopListening(editor.ui, 'update');
|
|
770
|
-
this.stopListening(this._balloon, 'change:visibleView');
|
|
771
|
-
// Make sure the focus always gets back to the editable _before_ removing the focused form view.
|
|
772
|
-
// Doing otherwise causes issues in some browsers. See https://github.com/ckeditor/ckeditor5-link/issues/193.
|
|
773
|
-
if (updateFocus) {
|
|
774
|
-
editor.editing.view.focus();
|
|
775
|
-
}
|
|
776
|
-
// If the links view is visible, remove it because it can be on top of the stack.
|
|
777
|
-
this._removeLinksProviderView();
|
|
778
|
-
// If the properties form view is visible, remove it because it can be on top of the stack.
|
|
779
|
-
this._removePropertiesView();
|
|
780
|
-
// Then remove the form view because it's beneath the properties form.
|
|
781
|
-
this._removeFormView(updateFocus);
|
|
782
|
-
// Finally, remove the link toolbar view because it's last in the stack.
|
|
783
|
-
if (this._isToolbarInPanel) {
|
|
784
|
-
this._balloon.remove(this.toolbarView);
|
|
785
|
-
}
|
|
786
|
-
this._hideFakeVisualSelection();
|
|
787
|
-
}
|
|
788
|
-
/**
|
|
789
|
-
* Makes the UI react to the {@link module:ui/editorui/editorui~EditorUI#event:update} event to
|
|
790
|
-
* reposition itself when the editor UI should be refreshed.
|
|
791
|
-
*
|
|
792
|
-
* See: {@link #_hideUI} to learn when the UI stops reacting to the `update` event.
|
|
793
|
-
*/
|
|
794
|
-
_startUpdatingUI() {
|
|
795
|
-
const editor = this.editor;
|
|
796
|
-
const viewDocument = editor.editing.view.document;
|
|
797
|
-
let prevSelectedLink = this._getSelectedLinkElement();
|
|
798
|
-
let prevSelectionParent = getSelectionParent();
|
|
799
|
-
const update = () => {
|
|
800
|
-
const selectedLink = this._getSelectedLinkElement();
|
|
801
|
-
const selectionParent = getSelectionParent();
|
|
802
|
-
// Hide the panel if:
|
|
803
|
-
//
|
|
804
|
-
// * the selection went out of the EXISTING link element. E.g. user moved the caret out
|
|
805
|
-
// of the link,
|
|
806
|
-
// * the selection went to a different parent when creating a NEW link. E.g. someone
|
|
807
|
-
// else modified the document.
|
|
808
|
-
// * the selection has expanded (e.g. displaying link toolbar then pressing SHIFT+Right arrow).
|
|
809
|
-
//
|
|
810
|
-
// Note: #_getSelectedLinkElement will return a link for a non-collapsed selection only
|
|
811
|
-
// when fully selected.
|
|
812
|
-
if ((prevSelectedLink && !selectedLink) ||
|
|
813
|
-
(!prevSelectedLink && selectionParent !== prevSelectionParent)) {
|
|
814
|
-
this._hideUI();
|
|
815
|
-
}
|
|
816
|
-
// Update the position of the panel when:
|
|
817
|
-
// * link panel is in the visible stack
|
|
818
|
-
// * the selection remains in the original link element,
|
|
819
|
-
// * there was no link element in the first place, i.e. creating a new link
|
|
820
|
-
else if (this._isUIVisible) {
|
|
821
|
-
// If still in a link element, simply update the position of the balloon.
|
|
822
|
-
// If there was no link (e.g. inserting one), the balloon must be moved
|
|
823
|
-
// to the new position in the editing view (a new native DOM range).
|
|
824
|
-
this._balloon.updatePosition(this._getBalloonPositionData());
|
|
825
|
-
}
|
|
826
|
-
prevSelectedLink = selectedLink;
|
|
827
|
-
prevSelectionParent = selectionParent;
|
|
828
|
-
};
|
|
829
|
-
function getSelectionParent() {
|
|
830
|
-
return viewDocument.selection.focus.getAncestors()
|
|
831
|
-
.reverse()
|
|
832
|
-
.find((node) => node.is('element'));
|
|
833
|
-
}
|
|
834
|
-
this.listenTo(editor.ui, 'update', update);
|
|
835
|
-
this.listenTo(this._balloon, 'change:visibleView', update);
|
|
836
|
-
}
|
|
837
|
-
/**
|
|
838
|
-
* Returns `true` when {@link #propertiesView} is in the {@link #_balloon}.
|
|
839
|
-
*/
|
|
840
|
-
get _arePropertiesInPanel() {
|
|
841
|
-
return !!this.propertiesView && this._balloon.hasView(this.propertiesView);
|
|
842
|
-
}
|
|
843
|
-
/**
|
|
844
|
-
* Returns `true` when {@link #linkProviderItemsView} is in the {@link #_balloon}.
|
|
845
|
-
*/
|
|
846
|
-
get _isLinksListInPanel() {
|
|
847
|
-
return !!this.linkProviderItemsView && this._balloon.hasView(this.linkProviderItemsView);
|
|
848
|
-
}
|
|
849
|
-
/**
|
|
850
|
-
* Returns `true` when {@link #formView} is in the {@link #_balloon}.
|
|
851
|
-
*/
|
|
852
|
-
get _isFormInPanel() {
|
|
853
|
-
return !!this.formView && this._balloon.hasView(this.formView);
|
|
854
|
-
}
|
|
855
|
-
/**
|
|
856
|
-
* Returns `true` when {@link #toolbarView} is in the {@link #_balloon}.
|
|
857
|
-
*/
|
|
858
|
-
get _isToolbarInPanel() {
|
|
859
|
-
return !!this.toolbarView && this._balloon.hasView(this.toolbarView);
|
|
860
|
-
}
|
|
861
|
-
/**
|
|
862
|
-
* Returns `true` when {@link #propertiesView} is in the {@link #_balloon} and it is
|
|
863
|
-
* currently visible.
|
|
864
|
-
*/
|
|
865
|
-
get _isPropertiesVisible() {
|
|
866
|
-
return !!this.propertiesView && this._balloon.visibleView === this.propertiesView;
|
|
867
|
-
}
|
|
868
|
-
/**
|
|
869
|
-
* Returns `true` when {@link #formView} is in the {@link #_balloon} and it is
|
|
870
|
-
* currently visible.
|
|
871
|
-
*/
|
|
872
|
-
get _isFormVisible() {
|
|
873
|
-
return !!this.formView && this._balloon.visibleView == this.formView;
|
|
874
|
-
}
|
|
875
|
-
/**
|
|
876
|
-
* Returns `true` when {@link #toolbarView} is in the {@link #_balloon} and it is
|
|
877
|
-
* currently visible.
|
|
878
|
-
*/
|
|
879
|
-
get _isToolbarVisible() {
|
|
880
|
-
return !!this.toolbarView && this._balloon.visibleView === this.toolbarView;
|
|
881
|
-
}
|
|
882
|
-
/**
|
|
883
|
-
* Returns `true` when {@link #propertiesView}, {@link #toolbarView}, {@link #linkProviderItemsView}
|
|
884
|
-
* or {@link #formView} is in the {@link #_balloon}.
|
|
885
|
-
*/
|
|
886
|
-
get _isUIInPanel() {
|
|
887
|
-
return this._arePropertiesInPanel || this._isLinksListInPanel || this._isFormInPanel || this._isToolbarInPanel;
|
|
888
|
-
}
|
|
889
|
-
/**
|
|
890
|
-
* Returns `true` when {@link #propertiesView}, {@link #linkProviderItemsView}, {@link #toolbarView}
|
|
891
|
-
* or {@link #formView} is in the {@link #_balloon} and it is currently visible.
|
|
892
|
-
*/
|
|
893
|
-
get _isUIVisible() {
|
|
894
|
-
return this._isPropertiesVisible || this._isLinksListInPanel || this._isFormVisible || this._isToolbarVisible;
|
|
895
|
-
}
|
|
896
|
-
/**
|
|
897
|
-
* Returns positioning options for the {@link #_balloon}. They control the way the balloon is attached
|
|
898
|
-
* to the target element or selection.
|
|
899
|
-
*
|
|
900
|
-
* If the selection is collapsed and inside a link element, the panel will be attached to the
|
|
901
|
-
* entire link element. Otherwise, it will be attached to the selection.
|
|
902
|
-
*/
|
|
903
|
-
_getBalloonPositionData() {
|
|
904
|
-
const view = this.editor.editing.view;
|
|
905
|
-
const viewDocument = view.document;
|
|
906
|
-
const model = this.editor.model;
|
|
907
|
-
if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
|
908
|
-
// There are cases when we highlight selection using a marker (#7705, #4721).
|
|
909
|
-
const markerViewElements = this.editor.editing.mapper.markerNameToElements(VISUAL_SELECTION_MARKER_NAME);
|
|
910
|
-
// Marker could be removed by link text override and end up in the graveyard.
|
|
911
|
-
if (markerViewElements) {
|
|
912
|
-
const markerViewElementsArray = Array.from(markerViewElements);
|
|
913
|
-
const newRange = view.createRange(view.createPositionBefore(markerViewElementsArray[0]), view.createPositionAfter(markerViewElementsArray[markerViewElementsArray.length - 1]));
|
|
914
|
-
return {
|
|
915
|
-
target: view.domConverter.viewRangeToDom(newRange)
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
// Make sure the target is calculated on demand at the last moment because a cached DOM range
|
|
920
|
-
// (which is very fragile) can desynchronize with the state of the editing view if there was
|
|
921
|
-
// any rendering done in the meantime. This can happen, for instance, when an inline widget
|
|
922
|
-
// gets unlinked.
|
|
923
|
-
return {
|
|
924
|
-
target: () => {
|
|
925
|
-
const targetLink = this._getSelectedLinkElement();
|
|
926
|
-
return targetLink ?
|
|
927
|
-
// When selection is inside link element, then attach panel to this element.
|
|
928
|
-
view.domConverter.mapViewToDom(targetLink) :
|
|
929
|
-
// Otherwise attach panel to the selection.
|
|
930
|
-
view.domConverter.viewRangeToDom(viewDocument.selection.getFirstRange());
|
|
931
|
-
}
|
|
932
|
-
};
|
|
933
|
-
}
|
|
934
|
-
/**
|
|
935
|
-
* Returns the link {@link module:engine/view/attributeelement~ViewAttributeElement} under
|
|
936
|
-
* the {@link module:engine/view/document~ViewDocument editing view's} selection or `null`
|
|
937
|
-
* if there is none.
|
|
938
|
-
*
|
|
939
|
-
* **Note**: For a non–collapsed selection, the link element is returned when **fully**
|
|
940
|
-
* selected and the **only** element within the selection boundaries, or when
|
|
941
|
-
* a linked widget is selected.
|
|
942
|
-
*/
|
|
943
|
-
_getSelectedLinkElement() {
|
|
944
|
-
const view = this.editor.editing.view;
|
|
945
|
-
const selection = view.document.selection;
|
|
946
|
-
const selectedElement = selection.getSelectedElement();
|
|
947
|
-
// The selection is collapsed or some widget is selected (especially inline widget).
|
|
948
|
-
if (selection.isCollapsed || selectedElement && isWidget(selectedElement)) {
|
|
949
|
-
return findLinkElementAncestor(selection.getFirstPosition());
|
|
950
|
-
}
|
|
951
|
-
else {
|
|
952
|
-
// The range for fully selected link is usually anchored in adjacent text nodes.
|
|
953
|
-
// Trim it to get closer to the actual link element.
|
|
954
|
-
const range = selection.getFirstRange().getTrimmed();
|
|
955
|
-
const startLink = findLinkElementAncestor(range.start);
|
|
956
|
-
const endLink = findLinkElementAncestor(range.end);
|
|
957
|
-
if (!startLink || startLink != endLink) {
|
|
958
|
-
return null;
|
|
959
|
-
}
|
|
960
|
-
// Check if the link element is fully selected.
|
|
961
|
-
if (view.createRangeIn(startLink).getTrimmed().isEqual(range)) {
|
|
962
|
-
return startLink;
|
|
963
|
-
}
|
|
964
|
-
else {
|
|
965
|
-
return null;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
/**
|
|
970
|
-
* Returns selected link text content.
|
|
971
|
-
* If link is not selected it returns the selected text.
|
|
972
|
-
* If selection or link includes non text node (inline object or block) then returns undefined.
|
|
973
|
-
*/
|
|
974
|
-
_getSelectedLinkableText() {
|
|
975
|
-
const model = this.editor.model;
|
|
976
|
-
const editing = this.editor.editing;
|
|
977
|
-
const selectedLink = this._getSelectedLinkElement();
|
|
978
|
-
if (!selectedLink) {
|
|
979
|
-
return extractTextFromLinkRange(model.document.selection.getFirstRange());
|
|
980
|
-
}
|
|
981
|
-
const viewLinkRange = editing.view.createRangeOn(selectedLink);
|
|
982
|
-
const linkRange = editing.mapper.toModelRange(viewLinkRange);
|
|
983
|
-
return extractTextFromLinkRange(linkRange);
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Returns a provider by its URL.
|
|
987
|
-
*
|
|
988
|
-
* @param href URL of the link.
|
|
989
|
-
* @returns Link provider and item or `null` if not found.
|
|
990
|
-
*/
|
|
991
|
-
_getLinkProviderLinkByHref(href) {
|
|
992
|
-
if (!href) {
|
|
993
|
-
return null;
|
|
994
|
-
}
|
|
995
|
-
for (const provider of this._linksProviders) {
|
|
996
|
-
const item = provider.getItem ?
|
|
997
|
-
provider.getItem(href) :
|
|
998
|
-
provider.getListItems().find(item => item.href === href);
|
|
999
|
-
if (item) {
|
|
1000
|
-
return { provider, item };
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
return null;
|
|
1004
|
-
}
|
|
1005
|
-
/**
|
|
1006
|
-
* Displays a fake visual selection when the contextual balloon is displayed.
|
|
1007
|
-
*
|
|
1008
|
-
* This adds a 'link-ui' marker into the document that is rendered as a highlight on selected text fragment.
|
|
1009
|
-
*/
|
|
1010
|
-
_showFakeVisualSelection() {
|
|
1011
|
-
const model = this.editor.model;
|
|
1012
|
-
model.change(writer => {
|
|
1013
|
-
const range = model.document.selection.getFirstRange();
|
|
1014
|
-
if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
|
1015
|
-
writer.updateMarker(VISUAL_SELECTION_MARKER_NAME, { range });
|
|
1016
|
-
}
|
|
1017
|
-
else {
|
|
1018
|
-
if (range.start.isAtEnd) {
|
|
1019
|
-
const startPosition = range.start.getLastMatchingPosition(({ item }) => !model.schema.isContent(item), { boundaries: range });
|
|
1020
|
-
writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
|
|
1021
|
-
usingOperation: false,
|
|
1022
|
-
affectsData: false,
|
|
1023
|
-
range: writer.createRange(startPosition, range.end)
|
|
1024
|
-
});
|
|
1025
|
-
}
|
|
1026
|
-
else {
|
|
1027
|
-
writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
|
|
1028
|
-
usingOperation: false,
|
|
1029
|
-
affectsData: false,
|
|
1030
|
-
range
|
|
1031
|
-
});
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
});
|
|
1035
|
-
}
|
|
1036
|
-
/**
|
|
1037
|
-
* Hides the fake visual selection created in {@link #_showFakeVisualSelection}.
|
|
1038
|
-
*/
|
|
1039
|
-
_hideFakeVisualSelection() {
|
|
1040
|
-
const model = this.editor.model;
|
|
1041
|
-
if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
|
1042
|
-
model.change(writer => {
|
|
1043
|
-
writer.removeMarker(VISUAL_SELECTION_MARKER_NAME);
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
/**
|
|
1049
|
-
* Returns a link element if there's one among the ancestors of the provided `Position`.
|
|
1050
|
-
*
|
|
1051
|
-
* @param View position to analyze.
|
|
1052
|
-
* @returns Link element at the position or null.
|
|
1053
|
-
*/
|
|
1054
|
-
function findLinkElementAncestor(position) {
|
|
1055
|
-
return position.getAncestors().find((ancestor) => isLinkElement(ancestor)) || null;
|
|
1056
|
-
}
|
|
1057
|
-
/**
|
|
1058
|
-
* Returns link form validation callbacks.
|
|
1059
|
-
*
|
|
1060
|
-
* @param editor Editor instance.
|
|
1061
|
-
*/
|
|
1062
|
-
function getFormValidators(editor) {
|
|
1063
|
-
const t = editor.t;
|
|
1064
|
-
const allowCreatingEmptyLinks = editor.config.get('link.allowCreatingEmptyLinks');
|
|
1065
|
-
return [
|
|
1066
|
-
form => {
|
|
1067
|
-
if (!allowCreatingEmptyLinks && !form.url.length) {
|
|
1068
|
-
return t('Link URL must not be empty.');
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
];
|
|
1072
|
-
}
|