1 | /**
|
2 | * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
|
3 | * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
4 | */
|
5 | /**
|
6 | * @module link/linkui
|
7 | */
|
8 | import { Plugin } from 'ckeditor5/src/core';
|
9 | import { ClickObserver } from 'ckeditor5/src/engine';
|
10 | import { ButtonView, ContextualBalloon, clickOutsideHandler, CssTransitionDisablerMixin } from 'ckeditor5/src/ui';
|
11 | import { isWidget } from 'ckeditor5/src/widget';
|
12 | import LinkFormView from './ui/linkformview';
|
13 | import LinkActionsView from './ui/linkactionsview';
|
14 | import { addLinkProtocolIfApplicable, isLinkElement, LINK_KEYSTROKE } from './utils';
|
15 | import linkIcon from '../theme/icons/link.svg';
|
16 | const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
17 | /**
|
18 | * The link UI plugin. It introduces the `'link'` and `'unlink'` buttons and support for the <kbd>Ctrl+K</kbd> keystroke.
|
19 | *
|
20 | * It uses the
|
21 | * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
|
22 | */
|
23 | export default class LinkUI extends Plugin {
|
24 | constructor() {
|
25 | super(...arguments);
|
26 | /**
|
27 | * The actions view displayed inside of the balloon.
|
28 | */
|
29 | this.actionsView = null;
|
30 | /**
|
31 | * The form view displayed inside the balloon.
|
32 | */
|
33 | this.formView = null;
|
34 | }
|
35 | /**
|
36 | * @inheritDoc
|
37 | */
|
38 | static get requires() {
|
39 | return [ContextualBalloon];
|
40 | }
|
41 | /**
|
42 | * @inheritDoc
|
43 | */
|
44 | static get pluginName() {
|
45 | return 'LinkUI';
|
46 | }
|
47 | /**
|
48 | * @inheritDoc
|
49 | */
|
50 | init() {
|
51 | const editor = this.editor;
|
52 | editor.editing.view.addObserver(ClickObserver);
|
53 | this._balloon = editor.plugins.get(ContextualBalloon);
|
54 | // Create toolbar buttons.
|
55 | this._createToolbarLinkButton();
|
56 | this._enableBalloonActivators();
|
57 | // Renders a fake visual selection marker on an expanded selection.
|
58 | editor.conversion.for('editingDowncast').markerToHighlight({
|
59 | model: VISUAL_SELECTION_MARKER_NAME,
|
60 | view: {
|
61 | classes: ['ck-fake-link-selection']
|
62 | }
|
63 | });
|
64 | // Renders a fake visual selection marker on a collapsed selection.
|
65 | editor.conversion.for('editingDowncast').markerToElement({
|
66 | model: VISUAL_SELECTION_MARKER_NAME,
|
67 | view: {
|
68 | name: 'span',
|
69 | classes: ['ck-fake-link-selection', 'ck-fake-link-selection_collapsed']
|
70 | }
|
71 | });
|
72 | }
|
73 | /**
|
74 | * @inheritDoc
|
75 | */
|
76 | destroy() {
|
77 | super.destroy();
|
78 | // Destroy created UI components as they are not automatically destroyed (see ckeditor5#1341).
|
79 | if (this.formView) {
|
80 | this.formView.destroy();
|
81 | }
|
82 | if (this.actionsView) {
|
83 | this.actionsView.destroy();
|
84 | }
|
85 | }
|
86 | /**
|
87 | * Creates views.
|
88 | */
|
89 | _createViews() {
|
90 | this.actionsView = this._createActionsView();
|
91 | this.formView = this._createFormView();
|
92 | // Attach lifecycle actions to the the balloon.
|
93 | this._enableUserBalloonInteractions();
|
94 | }
|
95 | /**
|
96 | * Creates the {@link module:link/ui/linkactionsview~LinkActionsView} instance.
|
97 | */
|
98 | _createActionsView() {
|
99 | const editor = this.editor;
|
100 | const actionsView = new LinkActionsView(editor.locale);
|
101 | const linkCommand = editor.commands.get('link');
|
102 | const unlinkCommand = editor.commands.get('unlink');
|
103 | actionsView.bind('href').to(linkCommand, 'value');
|
104 | actionsView.editButtonView.bind('isEnabled').to(linkCommand);
|
105 | actionsView.unlinkButtonView.bind('isEnabled').to(unlinkCommand);
|
106 | // Execute unlink command after clicking on the "Edit" button.
|
107 | this.listenTo(actionsView, 'edit', () => {
|
108 | this._addFormView();
|
109 | });
|
110 | // Execute unlink command after clicking on the "Unlink" button.
|
111 | this.listenTo(actionsView, 'unlink', () => {
|
112 | editor.execute('unlink');
|
113 | this._hideUI();
|
114 | });
|
115 | // Close the panel on esc key press when the **actions have focus**.
|
116 | actionsView.keystrokes.set('Esc', (data, cancel) => {
|
117 | this._hideUI();
|
118 | cancel();
|
119 | });
|
120 | // Open the form view on Ctrl+K when the **actions have focus**..
|
121 | actionsView.keystrokes.set(LINK_KEYSTROKE, (data, cancel) => {
|
122 | this._addFormView();
|
123 | cancel();
|
124 | });
|
125 | return actionsView;
|
126 | }
|
127 | /**
|
128 | * Creates the {@link module:link/ui/linkformview~LinkFormView} instance.
|
129 | */
|
130 | _createFormView() {
|
131 | const editor = this.editor;
|
132 | const linkCommand = editor.commands.get('link');
|
133 | const defaultProtocol = editor.config.get('link.defaultProtocol');
|
134 | const formView = new (CssTransitionDisablerMixin(LinkFormView))(editor.locale, linkCommand);
|
135 | formView.urlInputView.fieldView.bind('value').to(linkCommand, 'value');
|
136 | // Form elements should be read-only when corresponding commands are disabled.
|
137 | formView.urlInputView.bind('isEnabled').to(linkCommand, 'isEnabled');
|
138 | formView.saveButtonView.bind('isEnabled').to(linkCommand);
|
139 | // Execute link command after clicking the "Save" button.
|
140 | this.listenTo(formView, 'submit', () => {
|
141 | const { value } = formView.urlInputView.fieldView.element;
|
142 | const parsedUrl = addLinkProtocolIfApplicable(value, defaultProtocol);
|
143 | editor.execute('link', parsedUrl, formView.getDecoratorSwitchesState());
|
144 | this._closeFormView();
|
145 | });
|
146 | // Hide the panel after clicking the "Cancel" button.
|
147 | this.listenTo(formView, 'cancel', () => {
|
148 | this._closeFormView();
|
149 | });
|
150 | // Close the panel on esc key press when the **form has focus**.
|
151 | formView.keystrokes.set('Esc', (data, cancel) => {
|
152 | this._closeFormView();
|
153 | cancel();
|
154 | });
|
155 | return formView;
|
156 | }
|
157 | /**
|
158 | * Creates a toolbar Link button. Clicking this button will show
|
159 | * a {@link #_balloon} attached to the selection.
|
160 | */
|
161 | _createToolbarLinkButton() {
|
162 | const editor = this.editor;
|
163 | const linkCommand = editor.commands.get('link');
|
164 | const t = editor.t;
|
165 | editor.ui.componentFactory.add('link', locale => {
|
166 | const button = new ButtonView(locale);
|
167 | button.isEnabled = true;
|
168 | button.label = t('Link');
|
169 | button.icon = linkIcon;
|
170 | button.keystroke = LINK_KEYSTROKE;
|
171 | button.tooltip = true;
|
172 | button.isToggleable = true;
|
173 | // Bind button to the command.
|
174 | button.bind('isEnabled').to(linkCommand, 'isEnabled');
|
175 | button.bind('isOn').to(linkCommand, 'value', value => !!value);
|
176 | // Show the panel on button click.
|
177 | this.listenTo(button, 'execute', () => this._showUI(true));
|
178 | return button;
|
179 | });
|
180 | }
|
181 | /**
|
182 | * Attaches actions that control whether the balloon panel containing the
|
183 | * {@link #formView} should be displayed.
|
184 | */
|
185 | _enableBalloonActivators() {
|
186 | const editor = this.editor;
|
187 | const viewDocument = editor.editing.view.document;
|
188 | // Handle click on view document and show panel when selection is placed inside the link element.
|
189 | // Keep panel open until selection will be inside the same link element.
|
190 | this.listenTo(viewDocument, 'click', () => {
|
191 | const parentLink = this._getSelectedLinkElement();
|
192 | if (parentLink) {
|
193 | // Then show panel but keep focus inside editor editable.
|
194 | this._showUI();
|
195 | }
|
196 | });
|
197 | // Handle the `Ctrl+K` keystroke and show the panel.
|
198 | editor.keystrokes.set(LINK_KEYSTROKE, (keyEvtData, cancel) => {
|
199 | // Prevent focusing the search bar in FF, Chrome and Edge. See https://github.com/ckeditor/ckeditor5/issues/4811.
|
200 | cancel();
|
201 | if (editor.commands.get('link').isEnabled) {
|
202 | this._showUI(true);
|
203 | }
|
204 | });
|
205 | }
|
206 | /**
|
207 | * Attaches actions that control whether the balloon panel containing the
|
208 | * {@link #formView} is visible or not.
|
209 | */
|
210 | _enableUserBalloonInteractions() {
|
211 | // Focus the form if the balloon is visible and the Tab key has been pressed.
|
212 | this.editor.keystrokes.set('Tab', (data, cancel) => {
|
213 | if (this._areActionsVisible && !this.actionsView.focusTracker.isFocused) {
|
214 | this.actionsView.focus();
|
215 | cancel();
|
216 | }
|
217 | }, {
|
218 | // Use the high priority because the link UI navigation is more important
|
219 | // than other feature's actions, e.g. list indentation.
|
220 | // https://github.com/ckeditor/ckeditor5-link/issues/146
|
221 | priority: 'high'
|
222 | });
|
223 | // Close the panel on the Esc key press when the editable has focus and the balloon is visible.
|
224 | this.editor.keystrokes.set('Esc', (data, cancel) => {
|
225 | if (this._isUIVisible) {
|
226 | this._hideUI();
|
227 | cancel();
|
228 | }
|
229 | });
|
230 | // Close on click outside of balloon panel element.
|
231 | clickOutsideHandler({
|
232 | emitter: this.formView,
|
233 | activator: () => this._isUIInPanel,
|
234 | contextElements: () => [this._balloon.view.element],
|
235 | callback: () => this._hideUI()
|
236 | });
|
237 | }
|
238 | /**
|
239 | * Adds the {@link #actionsView} to the {@link #_balloon}.
|
240 | *
|
241 | * @internal
|
242 | */
|
243 | _addActionsView() {
|
244 | if (!this.actionsView) {
|
245 | this._createViews();
|
246 | }
|
247 | if (this._areActionsInPanel) {
|
248 | return;
|
249 | }
|
250 | this._balloon.add({
|
251 | view: this.actionsView,
|
252 | position: this._getBalloonPositionData()
|
253 | });
|
254 | }
|
255 | /**
|
256 | * Adds the {@link #formView} to the {@link #_balloon}.
|
257 | */
|
258 | _addFormView() {
|
259 | if (!this.formView) {
|
260 | this._createViews();
|
261 | }
|
262 | if (this._isFormInPanel) {
|
263 | return;
|
264 | }
|
265 | const editor = this.editor;
|
266 | const linkCommand = editor.commands.get('link');
|
267 | this.formView.disableCssTransitions();
|
268 | this._balloon.add({
|
269 | view: this.formView,
|
270 | position: this._getBalloonPositionData()
|
271 | });
|
272 | // Select input when form view is currently visible.
|
273 | if (this._balloon.visibleView === this.formView) {
|
274 | this.formView.urlInputView.fieldView.select();
|
275 | }
|
276 | this.formView.enableCssTransitions();
|
277 | // Make sure that each time the panel shows up, the URL field remains in sync with the value of
|
278 | // the command. If the user typed in the input, then canceled the balloon (`urlInputView.fieldView#value` stays
|
279 | // unaltered) and re-opened it without changing the value of the link command (e.g. because they
|
280 | // clicked the same link), they would see the old value instead of the actual value of the command.
|
281 | // https://github.com/ckeditor/ckeditor5-link/issues/78
|
282 | // https://github.com/ckeditor/ckeditor5-link/issues/123
|
283 | this.formView.urlInputView.fieldView.element.value = linkCommand.value || '';
|
284 | }
|
285 | /**
|
286 | * Closes the form view. Decides whether the balloon should be hidden completely or if the action view should be shown. This is
|
287 | * decided upon the link command value (which has a value if the document selection is in the link).
|
288 | *
|
289 | * Additionally, if any {@link module:link/linkconfig~LinkConfig#decorators} are defined in the editor configuration, the state of
|
290 | * switch buttons responsible for manual decorator handling is restored.
|
291 | */
|
292 | _closeFormView() {
|
293 | const linkCommand = this.editor.commands.get('link');
|
294 | // Restore manual decorator states to represent the current model state. This case is important to reset the switch buttons
|
295 | // when the user cancels the editing form.
|
296 | linkCommand.restoreManualDecoratorStates();
|
297 | if (linkCommand.value !== undefined) {
|
298 | this._removeFormView();
|
299 | }
|
300 | else {
|
301 | this._hideUI();
|
302 | }
|
303 | }
|
304 | /**
|
305 | * Removes the {@link #formView} from the {@link #_balloon}.
|
306 | */
|
307 | _removeFormView() {
|
308 | if (this._isFormInPanel) {
|
309 | // Blur the input element before removing it from DOM to prevent issues in some browsers.
|
310 | // See https://github.com/ckeditor/ckeditor5/issues/1501.
|
311 | this.formView.saveButtonView.focus();
|
312 | this._balloon.remove(this.formView);
|
313 | // Because the form has an input which has focus, the focus must be brought back
|
314 | // to the editor. Otherwise, it would be lost.
|
315 | this.editor.editing.view.focus();
|
316 | this._hideFakeVisualSelection();
|
317 | }
|
318 | }
|
319 | /**
|
320 | * Shows the correct UI type. It is either {@link #formView} or {@link #actionsView}.
|
321 | *
|
322 | * @internal
|
323 | */
|
324 | _showUI(forceVisible = false) {
|
325 | if (!this.formView) {
|
326 | this._createViews();
|
327 | }
|
328 | // When there's no link under the selection, go straight to the editing UI.
|
329 | if (!this._getSelectedLinkElement()) {
|
330 | // Show visual selection on a text without a link when the contextual balloon is displayed.
|
331 | // See https://github.com/ckeditor/ckeditor5/issues/4721.
|
332 | this._showFakeVisualSelection();
|
333 | this._addActionsView();
|
334 | // Be sure panel with link is visible.
|
335 | if (forceVisible) {
|
336 | this._balloon.showStack('main');
|
337 | }
|
338 | this._addFormView();
|
339 | }
|
340 | // If there's a link under the selection...
|
341 | else {
|
342 | // Go to the editing UI if actions are already visible.
|
343 | if (this._areActionsVisible) {
|
344 | this._addFormView();
|
345 | }
|
346 | // Otherwise display just the actions UI.
|
347 | else {
|
348 | this._addActionsView();
|
349 | }
|
350 | // Be sure panel with link is visible.
|
351 | if (forceVisible) {
|
352 | this._balloon.showStack('main');
|
353 | }
|
354 | }
|
355 | // Begin responding to ui#update once the UI is added.
|
356 | this._startUpdatingUI();
|
357 | }
|
358 | /**
|
359 | * Removes the {@link #formView} from the {@link #_balloon}.
|
360 | *
|
361 | * See {@link #_addFormView}, {@link #_addActionsView}.
|
362 | */
|
363 | _hideUI() {
|
364 | if (!this._isUIInPanel) {
|
365 | return;
|
366 | }
|
367 | const editor = this.editor;
|
368 | this.stopListening(editor.ui, 'update');
|
369 | this.stopListening(this._balloon, 'change:visibleView');
|
370 | // Make sure the focus always gets back to the editable _before_ removing the focused form view.
|
371 | // Doing otherwise causes issues in some browsers. See https://github.com/ckeditor/ckeditor5-link/issues/193.
|
372 | editor.editing.view.focus();
|
373 | // Remove form first because it's on top of the stack.
|
374 | this._removeFormView();
|
375 | // Then remove the actions view because it's beneath the form.
|
376 | this._balloon.remove(this.actionsView);
|
377 | this._hideFakeVisualSelection();
|
378 | }
|
379 | /**
|
380 | * Makes the UI react to the {@link module:ui/editorui/editorui~EditorUI#event:update} event to
|
381 | * reposition itself when the editor UI should be refreshed.
|
382 | *
|
383 | * See: {@link #_hideUI} to learn when the UI stops reacting to the `update` event.
|
384 | */
|
385 | _startUpdatingUI() {
|
386 | const editor = this.editor;
|
387 | const viewDocument = editor.editing.view.document;
|
388 | let prevSelectedLink = this._getSelectedLinkElement();
|
389 | let prevSelectionParent = getSelectionParent();
|
390 | const update = () => {
|
391 | const selectedLink = this._getSelectedLinkElement();
|
392 | const selectionParent = getSelectionParent();
|
393 | // Hide the panel if:
|
394 | //
|
395 | // * the selection went out of the EXISTING link element. E.g. user moved the caret out
|
396 | // of the link,
|
397 | // * the selection went to a different parent when creating a NEW link. E.g. someone
|
398 | // else modified the document.
|
399 | // * the selection has expanded (e.g. displaying link actions then pressing SHIFT+Right arrow).
|
400 | //
|
401 | // Note: #_getSelectedLinkElement will return a link for a non-collapsed selection only
|
402 | // when fully selected.
|
403 | if ((prevSelectedLink && !selectedLink) ||
|
404 | (!prevSelectedLink && selectionParent !== prevSelectionParent)) {
|
405 | this._hideUI();
|
406 | }
|
407 | // Update the position of the panel when:
|
408 | // * link panel is in the visible stack
|
409 | // * the selection remains in the original link element,
|
410 | // * there was no link element in the first place, i.e. creating a new link
|
411 | else if (this._isUIVisible) {
|
412 | // If still in a link element, simply update the position of the balloon.
|
413 | // If there was no link (e.g. inserting one), the balloon must be moved
|
414 | // to the new position in the editing view (a new native DOM range).
|
415 | this._balloon.updatePosition(this._getBalloonPositionData());
|
416 | }
|
417 | prevSelectedLink = selectedLink;
|
418 | prevSelectionParent = selectionParent;
|
419 | };
|
420 | function getSelectionParent() {
|
421 | return viewDocument.selection.focus.getAncestors()
|
422 | .reverse()
|
423 | .find((node) => node.is('element'));
|
424 | }
|
425 | this.listenTo(editor.ui, 'update', update);
|
426 | this.listenTo(this._balloon, 'change:visibleView', update);
|
427 | }
|
428 | /**
|
429 | * Returns `true` when {@link #formView} is in the {@link #_balloon}.
|
430 | */
|
431 | get _isFormInPanel() {
|
432 | return !!this.formView && this._balloon.hasView(this.formView);
|
433 | }
|
434 | /**
|
435 | * Returns `true` when {@link #actionsView} is in the {@link #_balloon}.
|
436 | */
|
437 | get _areActionsInPanel() {
|
438 | return !!this.actionsView && this._balloon.hasView(this.actionsView);
|
439 | }
|
440 | /**
|
441 | * Returns `true` when {@link #actionsView} is in the {@link #_balloon} and it is
|
442 | * currently visible.
|
443 | */
|
444 | get _areActionsVisible() {
|
445 | return !!this.actionsView && this._balloon.visibleView === this.actionsView;
|
446 | }
|
447 | /**
|
448 | * Returns `true` when {@link #actionsView} or {@link #formView} is in the {@link #_balloon}.
|
449 | */
|
450 | get _isUIInPanel() {
|
451 | return this._isFormInPanel || this._areActionsInPanel;
|
452 | }
|
453 | /**
|
454 | * Returns `true` when {@link #actionsView} or {@link #formView} is in the {@link #_balloon} and it is
|
455 | * currently visible.
|
456 | */
|
457 | get _isUIVisible() {
|
458 | const visibleView = this._balloon.visibleView;
|
459 | return !!this.formView && visibleView == this.formView || this._areActionsVisible;
|
460 | }
|
461 | /**
|
462 | * Returns positioning options for the {@link #_balloon}. They control the way the balloon is attached
|
463 | * to the target element or selection.
|
464 | *
|
465 | * If the selection is collapsed and inside a link element, the panel will be attached to the
|
466 | * entire link element. Otherwise, it will be attached to the selection.
|
467 | */
|
468 | _getBalloonPositionData() {
|
469 | const view = this.editor.editing.view;
|
470 | const model = this.editor.model;
|
471 | const viewDocument = view.document;
|
472 | let target;
|
473 | if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
474 | // There are cases when we highlight selection using a marker (#7705, #4721).
|
475 | const markerViewElements = Array.from(this.editor.editing.mapper.markerNameToElements(VISUAL_SELECTION_MARKER_NAME));
|
476 | const newRange = view.createRange(view.createPositionBefore(markerViewElements[0]), view.createPositionAfter(markerViewElements[markerViewElements.length - 1]));
|
477 | target = view.domConverter.viewRangeToDom(newRange);
|
478 | }
|
479 | else {
|
480 | // Make sure the target is calculated on demand at the last moment because a cached DOM range
|
481 | // (which is very fragile) can desynchronize with the state of the editing view if there was
|
482 | // any rendering done in the meantime. This can happen, for instance, when an inline widget
|
483 | // gets unlinked.
|
484 | target = () => {
|
485 | const targetLink = this._getSelectedLinkElement();
|
486 | return targetLink ?
|
487 | // When selection is inside link element, then attach panel to this element.
|
488 | view.domConverter.mapViewToDom(targetLink) :
|
489 | // Otherwise attach panel to the selection.
|
490 | view.domConverter.viewRangeToDom(viewDocument.selection.getFirstRange());
|
491 | };
|
492 | }
|
493 | return { target };
|
494 | }
|
495 | /**
|
496 | * Returns the link {@link module:engine/view/attributeelement~AttributeElement} under
|
497 | * the {@link module:engine/view/document~Document editing view's} selection or `null`
|
498 | * if there is none.
|
499 | *
|
500 | * **Note**: For a non–collapsed selection, the link element is returned when **fully**
|
501 | * selected and the **only** element within the selection boundaries, or when
|
502 | * a linked widget is selected.
|
503 | */
|
504 | _getSelectedLinkElement() {
|
505 | const view = this.editor.editing.view;
|
506 | const selection = view.document.selection;
|
507 | const selectedElement = selection.getSelectedElement();
|
508 | // The selection is collapsed or some widget is selected (especially inline widget).
|
509 | if (selection.isCollapsed || selectedElement && isWidget(selectedElement)) {
|
510 | return findLinkElementAncestor(selection.getFirstPosition());
|
511 | }
|
512 | else {
|
513 | // The range for fully selected link is usually anchored in adjacent text nodes.
|
514 | // Trim it to get closer to the actual link element.
|
515 | const range = selection.getFirstRange().getTrimmed();
|
516 | const startLink = findLinkElementAncestor(range.start);
|
517 | const endLink = findLinkElementAncestor(range.end);
|
518 | if (!startLink || startLink != endLink) {
|
519 | return null;
|
520 | }
|
521 | // Check if the link element is fully selected.
|
522 | if (view.createRangeIn(startLink).getTrimmed().isEqual(range)) {
|
523 | return startLink;
|
524 | }
|
525 | else {
|
526 | return null;
|
527 | }
|
528 | }
|
529 | }
|
530 | /**
|
531 | * Displays a fake visual selection when the contextual balloon is displayed.
|
532 | *
|
533 | * This adds a 'link-ui' marker into the document that is rendered as a highlight on selected text fragment.
|
534 | */
|
535 | _showFakeVisualSelection() {
|
536 | const model = this.editor.model;
|
537 | model.change(writer => {
|
538 | const range = model.document.selection.getFirstRange();
|
539 | if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
540 | writer.updateMarker(VISUAL_SELECTION_MARKER_NAME, { range });
|
541 | }
|
542 | else {
|
543 | if (range.start.isAtEnd) {
|
544 | const startPosition = range.start.getLastMatchingPosition(({ item }) => !model.schema.isContent(item), { boundaries: range });
|
545 | writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
|
546 | usingOperation: false,
|
547 | affectsData: false,
|
548 | range: writer.createRange(startPosition, range.end)
|
549 | });
|
550 | }
|
551 | else {
|
552 | writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
|
553 | usingOperation: false,
|
554 | affectsData: false,
|
555 | range
|
556 | });
|
557 | }
|
558 | }
|
559 | });
|
560 | }
|
561 | /**
|
562 | * Hides the fake visual selection created in {@link #_showFakeVisualSelection}.
|
563 | */
|
564 | _hideFakeVisualSelection() {
|
565 | const model = this.editor.model;
|
566 | if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
567 | model.change(writer => {
|
568 | writer.removeMarker(VISUAL_SELECTION_MARKER_NAME);
|
569 | });
|
570 | }
|
571 | }
|
572 | }
|
573 | /**
|
574 | * Returns a link element if there's one among the ancestors of the provided `Position`.
|
575 | *
|
576 | * @param View position to analyze.
|
577 | * @returns Link element at the position or null.
|
578 | */
|
579 | function findLinkElementAncestor(position) {
|
580 | return position.getAncestors().find((ancestor) => isLinkElement(ancestor)) || null;
|
581 | }
|