@ckeditor/ckeditor5-link 44.3.0 → 45.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/build/link.js +2 -2
- package/build/translations/af.js +1 -1
- package/build/translations/ar.js +1 -1
- package/build/translations/ast.js +1 -1
- package/build/translations/az.js +1 -1
- package/build/translations/be.js +1 -0
- package/build/translations/bg.js +1 -1
- package/build/translations/bn.js +1 -1
- package/build/translations/bs.js +1 -1
- package/build/translations/ca.js +1 -1
- package/build/translations/cs.js +1 -1
- package/build/translations/da.js +1 -1
- package/build/translations/de-ch.js +1 -1
- package/build/translations/de.js +1 -1
- package/build/translations/el.js +1 -1
- package/build/translations/en-au.js +1 -1
- package/build/translations/en-gb.js +1 -1
- package/build/translations/eo.js +1 -1
- package/build/translations/es-co.js +1 -1
- package/build/translations/es.js +1 -1
- package/build/translations/et.js +1 -1
- package/build/translations/eu.js +1 -1
- package/build/translations/fa.js +1 -1
- package/build/translations/fi.js +1 -1
- package/build/translations/fr.js +1 -1
- package/build/translations/gl.js +1 -1
- package/build/translations/gu.js +1 -1
- package/build/translations/he.js +1 -1
- package/build/translations/hi.js +1 -1
- package/build/translations/hr.js +1 -1
- package/build/translations/hu.js +1 -1
- package/build/translations/hy.js +1 -1
- package/build/translations/id.js +1 -1
- package/build/translations/it.js +1 -1
- package/build/translations/ja.js +1 -1
- package/build/translations/jv.js +1 -1
- package/build/translations/kk.js +1 -1
- package/build/translations/km.js +1 -1
- package/build/translations/kn.js +1 -1
- package/build/translations/ko.js +1 -1
- package/build/translations/ku.js +1 -1
- package/build/translations/lt.js +1 -1
- package/build/translations/lv.js +1 -1
- package/build/translations/ms.js +1 -1
- package/build/translations/nb.js +1 -1
- package/build/translations/ne.js +1 -1
- package/build/translations/nl.js +1 -1
- package/build/translations/no.js +1 -1
- package/build/translations/oc.js +1 -1
- package/build/translations/pl.js +1 -1
- package/build/translations/pt-br.js +1 -1
- package/build/translations/pt.js +1 -1
- package/build/translations/ro.js +1 -1
- package/build/translations/ru.js +1 -1
- package/build/translations/si.js +1 -1
- package/build/translations/sk.js +1 -1
- package/build/translations/sl.js +1 -1
- package/build/translations/sq.js +1 -1
- package/build/translations/sr-latn.js +1 -1
- package/build/translations/sr.js +1 -1
- package/build/translations/sv.js +1 -1
- package/build/translations/th.js +1 -1
- package/build/translations/ti.js +1 -1
- package/build/translations/tk.js +1 -1
- package/build/translations/tr.js +1 -1
- package/build/translations/tt.js +1 -1
- package/build/translations/ug.js +1 -1
- package/build/translations/uk.js +1 -1
- package/build/translations/ur.js +1 -1
- package/build/translations/uz.js +1 -1
- package/build/translations/vi.js +1 -1
- package/build/translations/zh-cn.js +1 -1
- package/build/translations/zh.js +1 -1
- package/ckeditor5-metadata.json +2 -2
- package/dist/index-editor.css +87 -47
- package/dist/index.css +108 -58
- package/dist/index.css.map +1 -1
- package/dist/index.js +1161 -425
- package/dist/index.js.map +1 -1
- package/dist/translations/af.js +1 -1
- package/dist/translations/af.umd.js +1 -1
- package/dist/translations/ar.js +1 -1
- package/dist/translations/ar.umd.js +1 -1
- package/dist/translations/ast.js +1 -1
- package/dist/translations/ast.umd.js +1 -1
- package/dist/translations/az.js +1 -1
- package/dist/translations/az.umd.js +1 -1
- package/dist/translations/be.d.ts +8 -0
- package/dist/translations/be.js +5 -0
- package/dist/translations/be.umd.js +11 -0
- package/dist/translations/bg.js +1 -1
- package/dist/translations/bg.umd.js +1 -1
- package/dist/translations/bn.js +1 -1
- package/dist/translations/bn.umd.js +1 -1
- package/dist/translations/bs.js +1 -1
- package/dist/translations/bs.umd.js +1 -1
- package/dist/translations/ca.js +1 -1
- package/dist/translations/ca.umd.js +1 -1
- package/dist/translations/cs.js +1 -1
- package/dist/translations/cs.umd.js +1 -1
- package/dist/translations/da.js +1 -1
- package/dist/translations/da.umd.js +1 -1
- package/dist/translations/de-ch.js +1 -1
- package/dist/translations/de-ch.umd.js +1 -1
- package/dist/translations/de.js +1 -1
- package/dist/translations/de.umd.js +1 -1
- package/dist/translations/el.js +1 -1
- package/dist/translations/el.umd.js +1 -1
- package/dist/translations/en-au.js +1 -1
- package/dist/translations/en-au.umd.js +1 -1
- package/dist/translations/en-gb.js +1 -1
- package/dist/translations/en-gb.umd.js +1 -1
- package/dist/translations/en.js +1 -1
- package/dist/translations/en.umd.js +1 -1
- package/dist/translations/eo.js +1 -1
- package/dist/translations/eo.umd.js +1 -1
- package/dist/translations/es-co.js +1 -1
- package/dist/translations/es-co.umd.js +1 -1
- package/dist/translations/es.js +1 -1
- package/dist/translations/es.umd.js +1 -1
- package/dist/translations/et.js +1 -1
- package/dist/translations/et.umd.js +1 -1
- package/dist/translations/eu.js +1 -1
- package/dist/translations/eu.umd.js +1 -1
- package/dist/translations/fa.js +1 -1
- package/dist/translations/fa.umd.js +1 -1
- package/dist/translations/fi.js +1 -1
- package/dist/translations/fi.umd.js +1 -1
- package/dist/translations/fr.js +1 -1
- package/dist/translations/fr.umd.js +1 -1
- package/dist/translations/gl.js +1 -1
- package/dist/translations/gl.umd.js +1 -1
- package/dist/translations/gu.js +1 -1
- package/dist/translations/gu.umd.js +1 -1
- package/dist/translations/he.js +1 -1
- package/dist/translations/he.umd.js +1 -1
- package/dist/translations/hi.js +1 -1
- package/dist/translations/hi.umd.js +1 -1
- package/dist/translations/hr.js +1 -1
- package/dist/translations/hr.umd.js +1 -1
- package/dist/translations/hu.js +1 -1
- package/dist/translations/hu.umd.js +1 -1
- package/dist/translations/hy.js +1 -1
- package/dist/translations/hy.umd.js +1 -1
- package/dist/translations/id.js +1 -1
- package/dist/translations/id.umd.js +1 -1
- package/dist/translations/it.js +1 -1
- package/dist/translations/it.umd.js +1 -1
- package/dist/translations/ja.js +1 -1
- package/dist/translations/ja.umd.js +1 -1
- package/dist/translations/jv.js +1 -1
- package/dist/translations/jv.umd.js +1 -1
- package/dist/translations/kk.js +1 -1
- package/dist/translations/kk.umd.js +1 -1
- package/dist/translations/km.js +1 -1
- package/dist/translations/km.umd.js +1 -1
- package/dist/translations/kn.js +1 -1
- package/dist/translations/kn.umd.js +1 -1
- package/dist/translations/ko.js +1 -1
- package/dist/translations/ko.umd.js +1 -1
- package/dist/translations/ku.js +1 -1
- package/dist/translations/ku.umd.js +1 -1
- package/dist/translations/lt.js +1 -1
- package/dist/translations/lt.umd.js +1 -1
- package/dist/translations/lv.js +1 -1
- package/dist/translations/lv.umd.js +1 -1
- package/dist/translations/ms.js +1 -1
- package/dist/translations/ms.umd.js +1 -1
- package/dist/translations/nb.js +1 -1
- package/dist/translations/nb.umd.js +1 -1
- package/dist/translations/ne.js +1 -1
- package/dist/translations/ne.umd.js +1 -1
- package/dist/translations/nl.js +1 -1
- package/dist/translations/nl.umd.js +1 -1
- package/dist/translations/no.js +1 -1
- package/dist/translations/no.umd.js +1 -1
- package/dist/translations/oc.js +1 -1
- package/dist/translations/oc.umd.js +1 -1
- package/dist/translations/pl.js +1 -1
- package/dist/translations/pl.umd.js +1 -1
- package/dist/translations/pt-br.js +1 -1
- package/dist/translations/pt-br.umd.js +1 -1
- package/dist/translations/pt.js +1 -1
- package/dist/translations/pt.umd.js +1 -1
- package/dist/translations/ro.js +1 -1
- package/dist/translations/ro.umd.js +1 -1
- package/dist/translations/ru.js +1 -1
- package/dist/translations/ru.umd.js +1 -1
- package/dist/translations/si.js +1 -1
- package/dist/translations/si.umd.js +1 -1
- package/dist/translations/sk.js +1 -1
- package/dist/translations/sk.umd.js +1 -1
- package/dist/translations/sl.js +1 -1
- package/dist/translations/sl.umd.js +1 -1
- package/dist/translations/sq.js +1 -1
- package/dist/translations/sq.umd.js +1 -1
- package/dist/translations/sr-latn.js +1 -1
- package/dist/translations/sr-latn.umd.js +1 -1
- package/dist/translations/sr.js +1 -1
- package/dist/translations/sr.umd.js +1 -1
- package/dist/translations/sv.js +1 -1
- package/dist/translations/sv.umd.js +1 -1
- package/dist/translations/th.js +1 -1
- package/dist/translations/th.umd.js +1 -1
- package/dist/translations/ti.js +1 -1
- package/dist/translations/ti.umd.js +1 -1
- package/dist/translations/tk.js +1 -1
- package/dist/translations/tk.umd.js +1 -1
- package/dist/translations/tr.js +1 -1
- package/dist/translations/tr.umd.js +1 -1
- package/dist/translations/tt.js +1 -1
- package/dist/translations/tt.umd.js +1 -1
- package/dist/translations/ug.js +1 -1
- package/dist/translations/ug.umd.js +1 -1
- package/dist/translations/uk.js +1 -1
- package/dist/translations/uk.umd.js +1 -1
- package/dist/translations/ur.js +1 -1
- package/dist/translations/ur.umd.js +1 -1
- package/dist/translations/uz.js +1 -1
- package/dist/translations/uz.umd.js +1 -1
- package/dist/translations/vi.js +1 -1
- package/dist/translations/vi.umd.js +1 -1
- package/dist/translations/zh-cn.js +1 -1
- package/dist/translations/zh-cn.umd.js +1 -1
- package/dist/translations/zh.js +1 -1
- package/dist/translations/zh.umd.js +1 -1
- package/lang/contexts.json +4 -3
- package/lang/translations/af.po +10 -6
- package/lang/translations/ar.po +11 -7
- package/lang/translations/ast.po +10 -6
- package/lang/translations/az.po +10 -6
- package/lang/translations/be.po +68 -0
- package/lang/translations/bg.po +11 -7
- package/lang/translations/bn.po +11 -7
- package/lang/translations/bs.po +10 -6
- package/lang/translations/ca.po +11 -7
- package/lang/translations/cs.po +11 -7
- package/lang/translations/da.po +11 -7
- package/lang/translations/de-ch.po +10 -6
- package/lang/translations/de.po +11 -7
- package/lang/translations/el.po +11 -7
- package/lang/translations/en-au.po +11 -7
- package/lang/translations/en-gb.po +11 -7
- package/lang/translations/en.po +11 -7
- package/lang/translations/eo.po +10 -6
- package/lang/translations/es-co.po +10 -6
- package/lang/translations/es.po +11 -7
- package/lang/translations/et.po +11 -7
- package/lang/translations/eu.po +10 -6
- package/lang/translations/fa.po +10 -6
- package/lang/translations/fi.po +11 -7
- package/lang/translations/fr.po +11 -7
- package/lang/translations/gl.po +10 -6
- package/lang/translations/gu.po +10 -6
- package/lang/translations/he.po +11 -7
- package/lang/translations/hi.po +11 -7
- package/lang/translations/hr.po +10 -6
- package/lang/translations/hu.po +11 -7
- package/lang/translations/hy.po +10 -6
- package/lang/translations/id.po +11 -7
- package/lang/translations/it.po +11 -7
- package/lang/translations/ja.po +11 -7
- package/lang/translations/jv.po +10 -6
- package/lang/translations/kk.po +10 -6
- package/lang/translations/km.po +10 -6
- package/lang/translations/kn.po +10 -6
- package/lang/translations/ko.po +11 -7
- package/lang/translations/ku.po +10 -6
- package/lang/translations/lt.po +11 -7
- package/lang/translations/lv.po +11 -7
- package/lang/translations/ms.po +11 -7
- package/lang/translations/nb.po +10 -6
- package/lang/translations/ne.po +10 -6
- package/lang/translations/nl.po +11 -7
- package/lang/translations/no.po +11 -7
- package/lang/translations/oc.po +10 -6
- package/lang/translations/pl.po +11 -7
- package/lang/translations/pt-br.po +11 -7
- package/lang/translations/pt.po +11 -7
- package/lang/translations/ro.po +11 -7
- package/lang/translations/ru.po +11 -7
- package/lang/translations/si.po +10 -6
- package/lang/translations/sk.po +11 -7
- package/lang/translations/sl.po +10 -6
- package/lang/translations/sq.po +10 -6
- package/lang/translations/sr-latn.po +10 -6
- package/lang/translations/sr.po +11 -7
- package/lang/translations/sv.po +11 -7
- package/lang/translations/th.po +11 -7
- package/lang/translations/ti.po +10 -6
- package/lang/translations/tk.po +10 -6
- package/lang/translations/tr.po +11 -7
- package/lang/translations/tt.po +10 -6
- package/lang/translations/ug.po +10 -6
- package/lang/translations/uk.po +11 -7
- package/lang/translations/ur.po +10 -6
- package/lang/translations/uz.po +10 -6
- package/lang/translations/vi.po +11 -7
- package/lang/translations/zh-cn.po +11 -7
- package/lang/translations/zh.po +11 -7
- package/package.json +12 -12
- package/src/autolink.js +3 -0
- package/src/index.d.ts +1 -2
- package/src/index.js +0 -1
- package/src/linkcommand.d.ts +17 -10
- package/src/linkcommand.js +212 -82
- package/src/linkconfig.d.ts +28 -0
- package/src/linkediting.d.ts +18 -0
- package/src/linkediting.js +19 -9
- package/src/linkimageui.d.ts +1 -1
- package/src/linkimageui.js +4 -4
- package/src/linkui.d.ts +215 -24
- package/src/linkui.js +517 -109
- package/src/ui/linkbuttonview.d.ts +31 -0
- package/src/ui/linkbuttonview.js +54 -0
- package/src/ui/linkformview.d.ts +34 -49
- package/src/ui/linkformview.js +163 -134
- package/src/ui/linkpreviewbuttonview.d.ts +35 -0
- package/src/ui/linkpreviewbuttonview.js +43 -0
- package/src/ui/linkpropertiesview.d.ts +88 -0
- package/src/ui/linkpropertiesview.js +170 -0
- package/src/ui/linkprovideritemsview.d.ts +114 -0
- package/src/ui/linkprovideritemsview.js +207 -0
- package/src/utils/automaticdecorators.js +5 -7
- package/src/utils/manualdecorator.js +27 -0
- package/src/utils.d.ts +5 -5
- package/src/utils.js +12 -32
- package/theme/linkform.css +11 -33
- package/theme/linkproperties.css +4 -0
- package/theme/linkprovideritems.css +18 -0
- package/theme/linktoolbar.css +12 -0
- package/src/ui/linkactionsview.d.ts +0 -117
- package/src/ui/linkactionsview.js +0 -173
- package/theme/icons/link.svg +0 -1
- package/theme/icons/unlink.svg +0 -1
- package/theme/linkactions.css +0 -32
package/dist/index.js
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
3
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
4
|
*/
|
|
5
|
-
import { Command, Plugin
|
|
5
|
+
import { Command, Plugin } from '@ckeditor/ckeditor5-core/dist/index.js';
|
|
6
6
|
import { findAttributeRange, TwoStepCaretMovement, Input, inlineHighlight, Delete, TextWatcher, getLastTextLine } from '@ckeditor/ckeditor5-typing/dist/index.js';
|
|
7
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 {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
8
|
+
import { toMap, Collection, first, diff, ObservableMixin, env, keyCodes, FocusTracker, KeystrokeHandler } from '@ckeditor/ckeditor5-utils/dist/index.js';
|
|
9
|
+
import { LivePosition, ClickObserver, Matcher } from '@ckeditor/ckeditor5-engine/dist/index.js';
|
|
10
|
+
import { upperFirst } from 'es-toolkit/compat';
|
|
11
|
+
import { IconPreviousArrow, IconUnlink, IconPencil, IconSettings, IconLink } from '@ckeditor/ckeditor5-icons/dist/index.js';
|
|
12
|
+
import { ButtonView, View, ViewCollection, FocusCycler, submitHandler, FormHeaderView, ListView, ListItemView, LabeledFieldView, createLabeledInputText, FormRowView, IconView, ContextualBalloon, ToolbarView, CssTransitionDisablerMixin, SwitchButtonView, MenuBarMenuListItemButtonView, clickOutsideHandler } from '@ckeditor/ckeditor5-ui/dist/index.js';
|
|
12
13
|
import { isWidget } from '@ckeditor/ckeditor5-widget/dist/index.js';
|
|
14
|
+
import { IconPreviousArrow as IconPreviousArrow$1, IconNextArrow } from '@ckeditor/ckeditor5-icons/dist/index.js';
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Helper class that ties together all {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition} and provides
|
|
@@ -262,31 +264,18 @@ const DEFAULT_LINK_PROTOCOLS = [
|
|
|
262
264
|
window.open(link, '_blank', 'noopener');
|
|
263
265
|
}
|
|
264
266
|
/**
|
|
265
|
-
*
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
*/ function scrollToTarget(link) {
|
|
276
|
-
const bookmarkId = link.slice(1);
|
|
277
|
-
const modelBookmark = bookmarkEditing.getElementForBookmarkId(bookmarkId);
|
|
278
|
-
editor.model.change((writer)=>{
|
|
279
|
-
writer.setSelection(modelBookmark, 'on');
|
|
280
|
-
});
|
|
281
|
-
editor.editing.view.scrollToTheSelection({
|
|
282
|
-
alignToTop: true,
|
|
283
|
-
forceScroll: true
|
|
284
|
-
});
|
|
267
|
+
* Returns a text of a link range.
|
|
268
|
+
*
|
|
269
|
+
* If the returned value is `undefined`, the range contains elements other than text nodes.
|
|
270
|
+
*/ function extractTextFromLinkRange(range) {
|
|
271
|
+
let text = '';
|
|
272
|
+
for (const item of range.getItems()){
|
|
273
|
+
if (!item.is('$text') && !item.is('$textProxy')) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
text += item.data;
|
|
285
277
|
}
|
|
286
|
-
return
|
|
287
|
-
isScrollableToTarget,
|
|
288
|
-
scrollToTarget
|
|
289
|
-
};
|
|
278
|
+
return text;
|
|
290
279
|
}
|
|
291
280
|
|
|
292
281
|
/**
|
|
@@ -389,10 +378,26 @@ const DEFAULT_LINK_PROTOCOLS = [
|
|
|
389
378
|
* **Note**: {@link module:link/unlinkcommand~UnlinkCommand#execute `UnlinkCommand#execute()`} removes all
|
|
390
379
|
* decorator attributes.
|
|
391
380
|
*
|
|
381
|
+
* An optional parameter called `displayedText` is to add or update text of the link that represents the `href`. For example:
|
|
382
|
+
*
|
|
383
|
+
* ```ts
|
|
384
|
+
* const linkCommand = editor.commands.get( 'link' );
|
|
385
|
+
*
|
|
386
|
+
* // Adding a new link with `displayedText` attribute.
|
|
387
|
+
* linkCommand.execute( 'http://example.com', {}, 'Example' );
|
|
388
|
+
* ```
|
|
389
|
+
*
|
|
390
|
+
* The above code will create an anchor like this:
|
|
391
|
+
*
|
|
392
|
+
* ```html
|
|
393
|
+
* <a href="http://example.com">Example</a>
|
|
394
|
+
* ```
|
|
395
|
+
*
|
|
392
396
|
* @fires execute
|
|
393
397
|
* @param href Link destination.
|
|
394
398
|
* @param manualDecoratorIds The information about manual decorator attributes to be applied or removed upon execution.
|
|
395
|
-
|
|
399
|
+
* @param displayedText Text of the link.
|
|
400
|
+
*/ execute(href, manualDecoratorIds = {}, displayedText) {
|
|
396
401
|
const model = this.editor.model;
|
|
397
402
|
const selection = model.document.selection;
|
|
398
403
|
// Stores information about manual decorators to turn them on/off when command is applied.
|
|
@@ -406,50 +411,100 @@ const DEFAULT_LINK_PROTOCOLS = [
|
|
|
406
411
|
}
|
|
407
412
|
}
|
|
408
413
|
model.change((writer)=>{
|
|
414
|
+
const updateLinkAttributes = (itemOrRange)=>{
|
|
415
|
+
writer.setAttribute('linkHref', href, itemOrRange);
|
|
416
|
+
truthyManualDecorators.forEach((item)=>writer.setAttribute(item, true, itemOrRange));
|
|
417
|
+
falsyManualDecorators.forEach((item)=>writer.removeAttribute(item, itemOrRange));
|
|
418
|
+
};
|
|
419
|
+
const updateLinkTextIfNeeded = (range, linkHref)=>{
|
|
420
|
+
const linkText = extractTextFromLinkRange(range);
|
|
421
|
+
if (!linkText) {
|
|
422
|
+
return range;
|
|
423
|
+
}
|
|
424
|
+
// Make a copy not to override the command param value.
|
|
425
|
+
let newText = displayedText;
|
|
426
|
+
if (!newText) {
|
|
427
|
+
// Replace the link text with the new href if previously href was equal to text.
|
|
428
|
+
// For example: `<a href="http://ckeditor.com/">http://ckeditor.com/</a>`.
|
|
429
|
+
newText = linkHref && linkHref == linkText ? href : linkText;
|
|
430
|
+
}
|
|
431
|
+
// Only if needed.
|
|
432
|
+
if (newText != linkText) {
|
|
433
|
+
const changes = findChanges(linkText, newText);
|
|
434
|
+
let insertsLength = 0;
|
|
435
|
+
for (const { offset, actual, expected } of changes){
|
|
436
|
+
const updatedOffset = offset + insertsLength;
|
|
437
|
+
const subRange = writer.createRange(range.start.getShiftedBy(updatedOffset), range.start.getShiftedBy(updatedOffset + actual.length));
|
|
438
|
+
// Collect formatting attributes from replaced text.
|
|
439
|
+
const textNode = getLinkPartTextNode(subRange, range);
|
|
440
|
+
const attributes = textNode.getAttributes();
|
|
441
|
+
const formattingAttributes = Array.from(attributes).filter(([key])=>model.schema.getAttributeProperties(key).isFormatting);
|
|
442
|
+
// Create a new text node.
|
|
443
|
+
const newTextNode = writer.createText(expected, formattingAttributes);
|
|
444
|
+
// Set link attributes before inserting to document to avoid Differ attributes edge case.
|
|
445
|
+
updateLinkAttributes(newTextNode);
|
|
446
|
+
// Replace text with formatting.
|
|
447
|
+
model.insertContent(newTextNode, subRange);
|
|
448
|
+
// Sum of all previous inserts.
|
|
449
|
+
insertsLength += expected.length;
|
|
450
|
+
}
|
|
451
|
+
return writer.createRange(range.start, range.start.getShiftedBy(newText.length));
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
const collapseSelectionAtLinkEnd = (linkRange)=>{
|
|
455
|
+
const { plugins } = this.editor;
|
|
456
|
+
writer.setSelection(linkRange.end);
|
|
457
|
+
if (plugins.has('TwoStepCaretMovement')) {
|
|
458
|
+
// After replacing the text of the link, we need to move the caret to the end of the link,
|
|
459
|
+
// override it's gravity to forward to prevent keeping e.g. bold attribute on the caret
|
|
460
|
+
// which was previously inside the link.
|
|
461
|
+
//
|
|
462
|
+
// If the plugin is not available, the caret will be placed at the end of the link and the
|
|
463
|
+
// bold attribute will be kept even if command moved caret outside the link.
|
|
464
|
+
plugins.get('TwoStepCaretMovement')._handleForwardMovement();
|
|
465
|
+
} else {
|
|
466
|
+
// Remove the `linkHref` attribute and all link decorators from the selection.
|
|
467
|
+
// It stops adding a new content into the link element.
|
|
468
|
+
for (const key of [
|
|
469
|
+
'linkHref',
|
|
470
|
+
...truthyManualDecorators,
|
|
471
|
+
...falsyManualDecorators
|
|
472
|
+
]){
|
|
473
|
+
writer.removeSelectionAttribute(key);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
};
|
|
409
477
|
// If selection is collapsed then update selected link or insert new one at the place of caret.
|
|
410
478
|
if (selection.isCollapsed) {
|
|
411
479
|
const position = selection.getFirstPosition();
|
|
412
480
|
// When selection is inside text with `linkHref` attribute.
|
|
413
481
|
if (selection.hasAttribute('linkHref')) {
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
482
|
+
const linkHref = selection.getAttribute('linkHref');
|
|
483
|
+
const linkRange = findAttributeRange(position, 'linkHref', linkHref, model);
|
|
484
|
+
const newLinkRange = updateLinkTextIfNeeded(linkRange, linkHref);
|
|
485
|
+
updateLinkAttributes(newLinkRange || linkRange);
|
|
486
|
+
// Put the selection at the end of the updated link only when text was changed.
|
|
487
|
+
// When text was not altered we keep the original selection.
|
|
488
|
+
if (newLinkRange) {
|
|
489
|
+
collapseSelectionAtLinkEnd(newLinkRange);
|
|
419
490
|
}
|
|
420
|
-
writer.setAttribute('linkHref', href, linkRange);
|
|
421
|
-
truthyManualDecorators.forEach((item)=>{
|
|
422
|
-
writer.setAttribute(item, true, linkRange);
|
|
423
|
-
});
|
|
424
|
-
falsyManualDecorators.forEach((item)=>{
|
|
425
|
-
writer.removeAttribute(item, linkRange);
|
|
426
|
-
});
|
|
427
|
-
// Put the selection at the end of the updated link.
|
|
428
|
-
writer.setSelection(writer.createPositionAfter(linkRange.end.nodeBefore));
|
|
429
491
|
} else if (href !== '') {
|
|
430
492
|
const attributes = toMap(selection.getAttributes());
|
|
431
493
|
attributes.set('linkHref', href);
|
|
432
494
|
truthyManualDecorators.forEach((item)=>{
|
|
433
495
|
attributes.set(item, true);
|
|
434
496
|
});
|
|
435
|
-
const
|
|
497
|
+
const newLinkRange = model.insertContent(writer.createText(displayedText || href, attributes), position);
|
|
436
498
|
// Put the selection at the end of the inserted link.
|
|
437
499
|
// Using end of range returned from insertContent in case nodes with the same attributes got merged.
|
|
438
|
-
|
|
500
|
+
collapseSelectionAtLinkEnd(newLinkRange);
|
|
439
501
|
}
|
|
440
|
-
// Remove the `linkHref` attribute and all link decorators from the selection.
|
|
441
|
-
// It stops adding a new content into the link element.
|
|
442
|
-
[
|
|
443
|
-
'linkHref',
|
|
444
|
-
...truthyManualDecorators,
|
|
445
|
-
...falsyManualDecorators
|
|
446
|
-
].forEach((item)=>{
|
|
447
|
-
writer.removeSelectionAttribute(item);
|
|
448
|
-
});
|
|
449
502
|
} else {
|
|
503
|
+
// Non-collapsed selection.
|
|
450
504
|
// If selection has non-collapsed ranges, we change attribute on nodes inside those ranges
|
|
451
505
|
// omitting nodes where the `linkHref` attribute is disallowed.
|
|
452
|
-
const
|
|
506
|
+
const selectionRanges = Array.from(selection.getRanges());
|
|
507
|
+
const ranges = model.schema.getValidRanges(selectionRanges, 'linkHref');
|
|
453
508
|
// But for the first, check whether the `linkHref` attribute is allowed on selected blocks (e.g. the "image" element).
|
|
454
509
|
const allowedRanges = [];
|
|
455
510
|
for (const element of selection.getSelectedBlocks()){
|
|
@@ -466,24 +521,25 @@ const DEFAULT_LINK_PROTOCOLS = [
|
|
|
466
521
|
rangesToUpdate.push(range);
|
|
467
522
|
}
|
|
468
523
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
writer.setAttribute('linkHref', href, linkRange);
|
|
480
|
-
truthyManualDecorators.forEach((item)=>{
|
|
481
|
-
writer.setAttribute(item, true, linkRange);
|
|
482
|
-
});
|
|
483
|
-
falsyManualDecorators.forEach((item)=>{
|
|
484
|
-
writer.removeAttribute(item, linkRange);
|
|
485
|
-
});
|
|
524
|
+
// Store the selection ranges in a pseudo live range array (stickiness to the outside of the range).
|
|
525
|
+
const stickyPseudoRanges = selectionRanges.map((range)=>({
|
|
526
|
+
start: LivePosition.fromPosition(range.start, 'toPrevious'),
|
|
527
|
+
end: LivePosition.fromPosition(range.end, 'toNext')
|
|
528
|
+
}));
|
|
529
|
+
// Update or set links (including text update if needed).
|
|
530
|
+
for (let range of rangesToUpdate){
|
|
531
|
+
const linkHref = (range.start.textNode || range.start.nodeAfter).getAttribute('linkHref');
|
|
532
|
+
range = updateLinkTextIfNeeded(range, linkHref) || range;
|
|
533
|
+
updateLinkAttributes(range);
|
|
486
534
|
}
|
|
535
|
+
// The original selection got trimmed by replacing content so we need to restore it.
|
|
536
|
+
writer.setSelection(stickyPseudoRanges.map((pseudoRange)=>{
|
|
537
|
+
const start = pseudoRange.start.toPosition();
|
|
538
|
+
const end = pseudoRange.end.toPosition();
|
|
539
|
+
pseudoRange.start.detach();
|
|
540
|
+
pseudoRange.end.detach();
|
|
541
|
+
return model.createRange(start, end);
|
|
542
|
+
}));
|
|
487
543
|
}
|
|
488
544
|
});
|
|
489
545
|
}
|
|
@@ -517,35 +573,99 @@ const DEFAULT_LINK_PROTOCOLS = [
|
|
|
517
573
|
}
|
|
518
574
|
return true;
|
|
519
575
|
}
|
|
520
|
-
/**
|
|
521
|
-
* Updates selected link with a new value as its content and as its href attribute.
|
|
522
|
-
*
|
|
523
|
-
* @param model Model is need to insert content.
|
|
524
|
-
* @param writer Writer is need to create text element in model.
|
|
525
|
-
* @param range A range where should be inserted content.
|
|
526
|
-
* @param href A link value which should be in the href attribute and in the content.
|
|
527
|
-
*/ _updateLinkContent(model, writer, range, href) {
|
|
528
|
-
const text = writer.createText(href, {
|
|
529
|
-
linkHref: href
|
|
530
|
-
});
|
|
531
|
-
return model.insertContent(text, range);
|
|
532
|
-
}
|
|
533
576
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
577
|
+
/**
|
|
578
|
+
* Compares two strings and returns an array of changes needed to transform one into another.
|
|
579
|
+
* Uses the diff utility to find the differences and groups them into chunks containing information
|
|
580
|
+
* about the offset and actual/expected content.
|
|
581
|
+
*
|
|
582
|
+
* @param oldText The original text to compare.
|
|
583
|
+
* @param newText The new text to compare against.
|
|
584
|
+
* @returns Array of change objects containing offset and actual/expected content.
|
|
585
|
+
*
|
|
586
|
+
* @example
|
|
587
|
+
* findChanges( 'hello world', 'hi there' );
|
|
588
|
+
*
|
|
589
|
+
* Returns:
|
|
590
|
+
* [
|
|
591
|
+
* {
|
|
592
|
+
* "offset": 1,
|
|
593
|
+
* "actual": "ello",
|
|
594
|
+
* "expected": "i"
|
|
595
|
+
* },
|
|
596
|
+
* {
|
|
597
|
+
* "offset": 2,
|
|
598
|
+
* "actual": "wo",
|
|
599
|
+
* "expected": "the"
|
|
600
|
+
* },
|
|
601
|
+
* {
|
|
602
|
+
* "offset": 3,
|
|
603
|
+
* "actual": "ld",
|
|
604
|
+
* "expected": "e"
|
|
605
|
+
* }
|
|
606
|
+
* ]
|
|
607
|
+
*/ function findChanges(oldText, newText) {
|
|
608
|
+
// Get array of operations (insert/delete/equal) needed to transform oldText into newText.
|
|
609
|
+
// Example: diff('abc', 'abxc') returns ['equal', 'equal', 'insert', 'equal']
|
|
610
|
+
const changes = diff(oldText, newText);
|
|
611
|
+
// Track position in both strings based on operation type.
|
|
612
|
+
const counter = {
|
|
613
|
+
equal: 0,
|
|
614
|
+
insert: 0,
|
|
615
|
+
delete: 0
|
|
616
|
+
};
|
|
617
|
+
const result = [];
|
|
618
|
+
// Accumulate consecutive changes into slices before creating change objects.
|
|
619
|
+
let actualSlice = '';
|
|
620
|
+
let expectedSlice = '';
|
|
621
|
+
// Adding null as sentinel value to handle final accumulated changes.
|
|
622
|
+
for (const action of [
|
|
623
|
+
...changes,
|
|
624
|
+
null
|
|
625
|
+
]){
|
|
626
|
+
if (action == 'insert') {
|
|
627
|
+
// Example: for 'abc' -> 'abxc', at insert position, adds 'x' to expectedSlice.
|
|
628
|
+
expectedSlice += newText[counter.equal + counter.insert];
|
|
629
|
+
} else if (action == 'delete') {
|
|
630
|
+
// Example: for 'abc' -> 'ac', at delete position, adds 'b' to actualSlice.
|
|
631
|
+
actualSlice += oldText[counter.equal + counter.delete];
|
|
632
|
+
} else if (actualSlice.length || expectedSlice.length) {
|
|
633
|
+
// On 'equal' or end: bundle accumulated changes into a single change object.
|
|
634
|
+
// Example: { offset: 2, actual: "", expected: "x" }
|
|
635
|
+
result.push({
|
|
636
|
+
offset: counter.equal,
|
|
637
|
+
actual: actualSlice,
|
|
638
|
+
expected: expectedSlice
|
|
639
|
+
});
|
|
640
|
+
actualSlice = '';
|
|
641
|
+
expectedSlice = '';
|
|
543
642
|
}
|
|
544
|
-
|
|
545
|
-
if (
|
|
546
|
-
|
|
643
|
+
// Increment appropriate counter for the current operation.
|
|
644
|
+
if (action) {
|
|
645
|
+
counter[action]++;
|
|
547
646
|
}
|
|
548
|
-
|
|
647
|
+
}
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Returns text node withing the link range that should be updated.
|
|
652
|
+
*
|
|
653
|
+
* @param range Partial link range.
|
|
654
|
+
* @param linkRange Range of the entire link.
|
|
655
|
+
* @returns Text node.
|
|
656
|
+
*/ function getLinkPartTextNode(range, linkRange) {
|
|
657
|
+
if (!range.isCollapsed) {
|
|
658
|
+
return first(range.getItems());
|
|
659
|
+
}
|
|
660
|
+
const position = range.start;
|
|
661
|
+
if (position.textNode) {
|
|
662
|
+
return position.textNode;
|
|
663
|
+
}
|
|
664
|
+
// If the range is at the start of a link range then prefer node inside a link range.
|
|
665
|
+
if (!position.nodeBefore || position.isEqual(linkRange.start)) {
|
|
666
|
+
return position.nodeAfter;
|
|
667
|
+
} else {
|
|
668
|
+
return position.nodeBefore;
|
|
549
669
|
}
|
|
550
670
|
}
|
|
551
671
|
|
|
@@ -670,6 +790,9 @@ const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
|
|
|
670
790
|
* It introduces the `linkHref="url"` attribute in the model which renders to the view as a `<a href="url">` element
|
|
671
791
|
* as well as `'link'` and `'unlink'` commands.
|
|
672
792
|
*/ class LinkEditing extends Plugin {
|
|
793
|
+
/**
|
|
794
|
+
* A list of functions that handles opening links. If any of them returns `true`, the link is considered to be opened.
|
|
795
|
+
*/ _linkOpeners = [];
|
|
673
796
|
/**
|
|
674
797
|
* @inheritDoc
|
|
675
798
|
*/ static get pluginName() {
|
|
@@ -696,7 +819,14 @@ const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
|
|
|
696
819
|
super(editor);
|
|
697
820
|
editor.config.define('link', {
|
|
698
821
|
allowCreatingEmptyLinks: false,
|
|
699
|
-
addTargetToExternalLinks: false
|
|
822
|
+
addTargetToExternalLinks: false,
|
|
823
|
+
toolbar: [
|
|
824
|
+
'linkPreview',
|
|
825
|
+
'|',
|
|
826
|
+
'editLink',
|
|
827
|
+
'linkProperties',
|
|
828
|
+
'unlink'
|
|
829
|
+
]
|
|
700
830
|
});
|
|
701
831
|
}
|
|
702
832
|
/**
|
|
@@ -748,6 +878,14 @@ const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
|
|
|
748
878
|
// Handle adding default protocol to pasted links.
|
|
749
879
|
this._enableClipboardIntegration();
|
|
750
880
|
}
|
|
881
|
+
/**
|
|
882
|
+
* Registers a function that opens links in a new browser tab.
|
|
883
|
+
*
|
|
884
|
+
* @param linkOpener The function that opens a link in a new browser tab.
|
|
885
|
+
* @internal
|
|
886
|
+
*/ _registerLinkOpener(linkOpener) {
|
|
887
|
+
this._linkOpeners.push(linkOpener);
|
|
888
|
+
}
|
|
751
889
|
/**
|
|
752
890
|
* Processes an array of configured {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition automatic decorators}
|
|
753
891
|
* and registers a {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast dispatcher}
|
|
@@ -841,14 +979,11 @@ const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
|
|
|
841
979
|
const editor = this.editor;
|
|
842
980
|
const view = editor.editing.view;
|
|
843
981
|
const viewDocument = view.document;
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
if (bookmarkCallbacks.isScrollableToTarget(url)) {
|
|
847
|
-
bookmarkCallbacks.scrollToTarget(url);
|
|
848
|
-
} else {
|
|
982
|
+
const handleLinkOpening = (url)=>{
|
|
983
|
+
if (!this._linkOpeners.some((opener)=>opener(url))) {
|
|
849
984
|
openLink(url);
|
|
850
985
|
}
|
|
851
|
-
}
|
|
986
|
+
};
|
|
852
987
|
this.listenTo(viewDocument, 'click', (evt, data)=>{
|
|
853
988
|
const shouldOpen = env.isMac ? data.domEvent.metaKey : data.domEvent.ctrlKey;
|
|
854
989
|
if (!shouldOpen) {
|
|
@@ -940,9 +1075,43 @@ const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
|
|
|
940
1075
|
}
|
|
941
1076
|
|
|
942
1077
|
/**
|
|
943
|
-
* The link
|
|
1078
|
+
* The link button class. Rendered as an `<a>` tag with link opening in a new tab.
|
|
944
1079
|
*
|
|
945
|
-
*
|
|
1080
|
+
* Provides a custom `navigate` cancelable event.
|
|
1081
|
+
*/ class LinkPreviewButtonView extends ButtonView {
|
|
1082
|
+
/**
|
|
1083
|
+
* @inheritDoc
|
|
1084
|
+
*/ constructor(locale){
|
|
1085
|
+
super(locale);
|
|
1086
|
+
const bind = this.bindTemplate;
|
|
1087
|
+
this.set({
|
|
1088
|
+
href: undefined,
|
|
1089
|
+
withText: true
|
|
1090
|
+
});
|
|
1091
|
+
this.extendTemplate({
|
|
1092
|
+
attributes: {
|
|
1093
|
+
class: [
|
|
1094
|
+
'ck-link-toolbar__preview'
|
|
1095
|
+
],
|
|
1096
|
+
href: bind.to('href'),
|
|
1097
|
+
target: '_blank',
|
|
1098
|
+
rel: 'noopener noreferrer'
|
|
1099
|
+
},
|
|
1100
|
+
on: {
|
|
1101
|
+
click: bind.to((evt)=>{
|
|
1102
|
+
if (this.href) {
|
|
1103
|
+
const cancel = ()=>evt.preventDefault();
|
|
1104
|
+
this.fire('navigate', this.href, cancel);
|
|
1105
|
+
}
|
|
1106
|
+
})
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
this.template.tag = 'a';
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* The link form view.
|
|
946
1115
|
*/ class LinkFormView extends View {
|
|
947
1116
|
/**
|
|
948
1117
|
* Tracks information about DOM focus in the form.
|
|
@@ -951,22 +1120,23 @@ const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
|
|
|
951
1120
|
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
|
|
952
1121
|
*/ keystrokes = new KeystrokeHandler();
|
|
953
1122
|
/**
|
|
954
|
-
* The
|
|
955
|
-
*/
|
|
1123
|
+
* The Back button view displayed in the header.
|
|
1124
|
+
*/ backButtonView;
|
|
956
1125
|
/**
|
|
957
1126
|
* The Save button view.
|
|
958
1127
|
*/ saveButtonView;
|
|
959
1128
|
/**
|
|
960
|
-
* The
|
|
961
|
-
*/
|
|
1129
|
+
* The "Displayed text" input view.
|
|
1130
|
+
*/ displayedTextInputView;
|
|
962
1131
|
/**
|
|
963
|
-
*
|
|
964
|
-
|
|
965
|
-
* configured in the editor.
|
|
966
|
-
*/ _manualDecoratorSwitches;
|
|
1132
|
+
* The URL input view.
|
|
1133
|
+
*/ urlInputView;
|
|
967
1134
|
/**
|
|
968
|
-
* A collection of child views
|
|
1135
|
+
* A collection of child views.
|
|
969
1136
|
*/ children;
|
|
1137
|
+
/**
|
|
1138
|
+
* A collection of child views in the providers list.
|
|
1139
|
+
*/ providersListChildren;
|
|
970
1140
|
/**
|
|
971
1141
|
* An array of form validators used by {@link #isValid}.
|
|
972
1142
|
*/ _validators;
|
|
@@ -982,18 +1152,27 @@ const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
|
|
|
982
1152
|
* Also see {@link #render}.
|
|
983
1153
|
*
|
|
984
1154
|
* @param locale The localization services instance.
|
|
985
|
-
* @param linkCommand Reference to {@link module:link/linkcommand~LinkCommand}.
|
|
986
1155
|
* @param validators Form validators used by {@link #isValid}.
|
|
987
|
-
*/ constructor(locale,
|
|
1156
|
+
*/ constructor(locale, validators){
|
|
988
1157
|
super(locale);
|
|
989
|
-
const t = locale.t;
|
|
990
1158
|
this._validators = validators;
|
|
1159
|
+
// Create buttons.
|
|
1160
|
+
this.backButtonView = this._createBackButton();
|
|
1161
|
+
this.saveButtonView = this._createSaveButton();
|
|
1162
|
+
// Create input fields.
|
|
1163
|
+
this.displayedTextInputView = this._createDisplayedTextInput();
|
|
991
1164
|
this.urlInputView = this._createUrlInput();
|
|
992
|
-
this.
|
|
993
|
-
this.
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
this.
|
|
1165
|
+
this.providersListChildren = this.createCollection();
|
|
1166
|
+
this.children = this.createCollection([
|
|
1167
|
+
this._createHeaderView()
|
|
1168
|
+
]);
|
|
1169
|
+
this._createFormChildren();
|
|
1170
|
+
// Add providers list view to the children when the first item is added to the list.
|
|
1171
|
+
// This is to avoid adding the list view when the form is empty.
|
|
1172
|
+
this.listenTo(this.providersListChildren, 'add', ()=>{
|
|
1173
|
+
this.stopListening(this.providersListChildren, 'add');
|
|
1174
|
+
this.children.add(this._createProvidersListView());
|
|
1175
|
+
});
|
|
997
1176
|
this._focusCycler = new FocusCycler({
|
|
998
1177
|
focusables: this._focusables,
|
|
999
1178
|
focusTracker: this.focusTracker,
|
|
@@ -1005,36 +1184,21 @@ const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
|
|
|
1005
1184
|
focusNext: 'tab'
|
|
1006
1185
|
}
|
|
1007
1186
|
});
|
|
1008
|
-
const classList = [
|
|
1009
|
-
'ck',
|
|
1010
|
-
'ck-link-form',
|
|
1011
|
-
'ck-responsive-form'
|
|
1012
|
-
];
|
|
1013
|
-
if (linkCommand.manualDecorators.length) {
|
|
1014
|
-
classList.push('ck-link-form_layout-vertical', 'ck-vertical-form');
|
|
1015
|
-
}
|
|
1016
1187
|
this.setTemplate({
|
|
1017
1188
|
tag: 'form',
|
|
1018
1189
|
attributes: {
|
|
1019
|
-
class:
|
|
1190
|
+
class: [
|
|
1191
|
+
'ck',
|
|
1192
|
+
'ck-form',
|
|
1193
|
+
'ck-link-form',
|
|
1194
|
+
'ck-responsive-form'
|
|
1195
|
+
],
|
|
1020
1196
|
// https://github.com/ckeditor/ckeditor5-link/issues/90
|
|
1021
1197
|
tabindex: '-1'
|
|
1022
1198
|
},
|
|
1023
1199
|
children: this.children
|
|
1024
1200
|
});
|
|
1025
1201
|
}
|
|
1026
|
-
/**
|
|
1027
|
-
* Obtains the state of the {@link module:ui/button/switchbuttonview~SwitchButtonView switch buttons} representing
|
|
1028
|
-
* {@link module:link/linkcommand~LinkCommand#manualDecorators manual link decorators}
|
|
1029
|
-
* in the {@link module:link/ui/linkformview~LinkFormView}.
|
|
1030
|
-
*
|
|
1031
|
-
* @returns Key-value pairs, where the key is the name of the decorator and the value is its state.
|
|
1032
|
-
*/ getDecoratorSwitchesState() {
|
|
1033
|
-
return Array.from(this._manualDecoratorSwitches).reduce((accumulator, switchButton)=>{
|
|
1034
|
-
accumulator[switchButton.name] = switchButton.isOn;
|
|
1035
|
-
return accumulator;
|
|
1036
|
-
}, {});
|
|
1037
|
-
}
|
|
1038
1202
|
/**
|
|
1039
1203
|
* @inheritDoc
|
|
1040
1204
|
*/ render() {
|
|
@@ -1044,9 +1208,10 @@ const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
|
|
|
1044
1208
|
});
|
|
1045
1209
|
const childViews = [
|
|
1046
1210
|
this.urlInputView,
|
|
1047
|
-
...this._manualDecoratorSwitches,
|
|
1048
1211
|
this.saveButtonView,
|
|
1049
|
-
this.
|
|
1212
|
+
...this.providersListChildren,
|
|
1213
|
+
this.backButtonView,
|
|
1214
|
+
this.displayedTextInputView
|
|
1050
1215
|
];
|
|
1051
1216
|
childViews.forEach((v)=>{
|
|
1052
1217
|
// Register the view as focusable.
|
|
@@ -1093,111 +1258,104 @@ const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
|
|
|
1093
1258
|
this.urlInputView.errorText = null;
|
|
1094
1259
|
}
|
|
1095
1260
|
/**
|
|
1096
|
-
* Creates a
|
|
1097
|
-
|
|
1098
|
-
* @returns Labeled field view instance.
|
|
1099
|
-
*/ _createUrlInput() {
|
|
1261
|
+
* Creates a back button view that cancels the form.
|
|
1262
|
+
*/ _createBackButton() {
|
|
1100
1263
|
const t = this.locale.t;
|
|
1101
|
-
const
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1264
|
+
const backButton = new ButtonView(this.locale);
|
|
1265
|
+
backButton.set({
|
|
1266
|
+
class: 'ck-button-back',
|
|
1267
|
+
label: t('Back'),
|
|
1268
|
+
icon: IconPreviousArrow,
|
|
1269
|
+
tooltip: true
|
|
1270
|
+
});
|
|
1271
|
+
backButton.delegate('execute').to(this, 'cancel');
|
|
1272
|
+
return backButton;
|
|
1105
1273
|
}
|
|
1106
1274
|
/**
|
|
1107
|
-
* Creates a button view.
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1275
|
+
* Creates a save button view that inserts the link.
|
|
1276
|
+
*/ _createSaveButton() {
|
|
1277
|
+
const t = this.locale.t;
|
|
1278
|
+
const saveButton = new ButtonView(this.locale);
|
|
1279
|
+
saveButton.set({
|
|
1280
|
+
label: t('Insert'),
|
|
1281
|
+
tooltip: false,
|
|
1282
|
+
withText: true,
|
|
1283
|
+
type: 'submit',
|
|
1284
|
+
class: 'ck-button-action ck-button-bold'
|
|
1285
|
+
});
|
|
1286
|
+
return saveButton;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Creates a header view for the form.
|
|
1290
|
+
*/ _createHeaderView() {
|
|
1291
|
+
const t = this.locale.t;
|
|
1292
|
+
const header = new FormHeaderView(this.locale, {
|
|
1293
|
+
label: t('Link')
|
|
1120
1294
|
});
|
|
1121
|
-
|
|
1295
|
+
header.children.add(this.backButtonView, 0);
|
|
1296
|
+
return header;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Creates a view for the providers list.
|
|
1300
|
+
*/ _createProvidersListView() {
|
|
1301
|
+
const providersListView = new ListView(this.locale);
|
|
1302
|
+
providersListView.extendTemplate({
|
|
1122
1303
|
attributes: {
|
|
1123
|
-
class:
|
|
1304
|
+
class: [
|
|
1305
|
+
'ck-link-form__providers-list'
|
|
1306
|
+
]
|
|
1124
1307
|
}
|
|
1125
1308
|
});
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1309
|
+
providersListView.items.bindTo(this.providersListChildren).using((def)=>{
|
|
1310
|
+
const listItemView = new ListItemView(this.locale);
|
|
1311
|
+
listItemView.children.add(def);
|
|
1312
|
+
return listItemView;
|
|
1313
|
+
});
|
|
1314
|
+
return providersListView;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Creates a labeled input view for the "Displayed text" field.
|
|
1318
|
+
*/ _createDisplayedTextInput() {
|
|
1319
|
+
const t = this.locale.t;
|
|
1320
|
+
const labeledInput = new LabeledFieldView(this.locale, createLabeledInputText);
|
|
1321
|
+
labeledInput.label = t('Displayed text');
|
|
1322
|
+
labeledInput.class = 'ck-labeled-field-view_full-width';
|
|
1323
|
+
return labeledInput;
|
|
1130
1324
|
}
|
|
1131
1325
|
/**
|
|
1132
|
-
*
|
|
1133
|
-
* made based on {@link module:link/linkcommand~LinkCommand#manualDecorators}.
|
|
1326
|
+
* Creates a labeled input view for the URL field.
|
|
1134
1327
|
*
|
|
1135
|
-
* @
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
label: manualDecorator.label,
|
|
1144
|
-
withText: true
|
|
1145
|
-
});
|
|
1146
|
-
switchButton.bind('isOn').toMany([
|
|
1147
|
-
manualDecorator,
|
|
1148
|
-
linkCommand
|
|
1149
|
-
], 'value', (decoratorValue, commandValue)=>{
|
|
1150
|
-
return commandValue === undefined && decoratorValue === undefined ? !!manualDecorator.defaultValue : !!decoratorValue;
|
|
1151
|
-
});
|
|
1152
|
-
switchButton.on('execute', ()=>{
|
|
1153
|
-
manualDecorator.set('value', !switchButton.isOn);
|
|
1154
|
-
});
|
|
1155
|
-
switches.add(switchButton);
|
|
1156
|
-
}
|
|
1157
|
-
return switches;
|
|
1328
|
+
* @returns Labeled field view instance.
|
|
1329
|
+
*/ _createUrlInput() {
|
|
1330
|
+
const t = this.locale.t;
|
|
1331
|
+
const labeledInput = new LabeledFieldView(this.locale, createLabeledInputText);
|
|
1332
|
+
labeledInput.fieldView.inputMode = 'url';
|
|
1333
|
+
labeledInput.label = t('Link URL');
|
|
1334
|
+
labeledInput.class = 'ck-labeled-field-view_full-width';
|
|
1335
|
+
return labeledInput;
|
|
1158
1336
|
}
|
|
1159
1337
|
/**
|
|
1160
1338
|
* Populates the {@link #children} collection of the form.
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
attributes: {
|
|
1182
|
-
class: [
|
|
1183
|
-
'ck',
|
|
1184
|
-
'ck-list__item'
|
|
1185
|
-
]
|
|
1186
|
-
}
|
|
1187
|
-
})),
|
|
1188
|
-
attributes: {
|
|
1189
|
-
class: [
|
|
1190
|
-
'ck',
|
|
1191
|
-
'ck-reset',
|
|
1192
|
-
'ck-list'
|
|
1193
|
-
]
|
|
1194
|
-
}
|
|
1195
|
-
});
|
|
1196
|
-
children.add(additionalButtonsView);
|
|
1197
|
-
}
|
|
1198
|
-
children.add(this.saveButtonView);
|
|
1199
|
-
children.add(this.cancelButtonView);
|
|
1200
|
-
return children;
|
|
1339
|
+
*/ _createFormChildren() {
|
|
1340
|
+
this.children.add(new FormRowView(this.locale, {
|
|
1341
|
+
children: [
|
|
1342
|
+
this.displayedTextInputView
|
|
1343
|
+
],
|
|
1344
|
+
class: [
|
|
1345
|
+
'ck-form__row_large-top-padding'
|
|
1346
|
+
]
|
|
1347
|
+
}));
|
|
1348
|
+
this.children.add(new FormRowView(this.locale, {
|
|
1349
|
+
children: [
|
|
1350
|
+
this.urlInputView,
|
|
1351
|
+
this.saveButtonView
|
|
1352
|
+
],
|
|
1353
|
+
class: [
|
|
1354
|
+
'ck-form__row_with-submit',
|
|
1355
|
+
'ck-form__row_large-top-padding',
|
|
1356
|
+
'ck-form__row_large-bottom-padding'
|
|
1357
|
+
]
|
|
1358
|
+
}));
|
|
1201
1359
|
}
|
|
1202
1360
|
/**
|
|
1203
1361
|
* The native DOM `value` of the {@link #urlInputView} element.
|
|
@@ -1213,54 +1371,80 @@ const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
|
|
|
1213
1371
|
}
|
|
1214
1372
|
}
|
|
1215
1373
|
|
|
1216
|
-
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>";
|
|
1217
|
-
|
|
1218
1374
|
/**
|
|
1219
|
-
* The link
|
|
1220
|
-
|
|
1221
|
-
*/ class LinkActionsView extends View {
|
|
1375
|
+
* The link provider items view.
|
|
1376
|
+
*/ class LinkProviderItemsView extends View {
|
|
1222
1377
|
/**
|
|
1223
|
-
* Tracks information about DOM focus in the
|
|
1378
|
+
* Tracks information about DOM focus in the form.
|
|
1224
1379
|
*/ focusTracker = new FocusTracker();
|
|
1225
1380
|
/**
|
|
1226
1381
|
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
|
|
1227
1382
|
*/ keystrokes = new KeystrokeHandler();
|
|
1228
1383
|
/**
|
|
1229
|
-
* The
|
|
1230
|
-
*/
|
|
1384
|
+
* The Back button view displayed in the header.
|
|
1385
|
+
*/ backButtonView;
|
|
1386
|
+
/**
|
|
1387
|
+
* The List view of links buttons.
|
|
1388
|
+
*/ listView;
|
|
1389
|
+
/**
|
|
1390
|
+
* The collection of child views, which is bind with the `listView`.
|
|
1391
|
+
*/ listChildren;
|
|
1231
1392
|
/**
|
|
1232
|
-
* The
|
|
1233
|
-
*/
|
|
1393
|
+
* The view displayed when the list is empty.
|
|
1394
|
+
*/ emptyListInformation;
|
|
1234
1395
|
/**
|
|
1235
|
-
*
|
|
1236
|
-
*/
|
|
1396
|
+
* A collection of child views.
|
|
1397
|
+
*/ children;
|
|
1237
1398
|
/**
|
|
1238
|
-
* A collection of views that can be focused in the
|
|
1399
|
+
* A collection of views that can be focused in the form.
|
|
1239
1400
|
*/ _focusables = new ViewCollection();
|
|
1240
1401
|
/**
|
|
1241
|
-
* Helps cycling over {@link #_focusables} in the
|
|
1402
|
+
* Helps cycling over {@link #_focusables} in the form.
|
|
1242
1403
|
*/ _focusCycler;
|
|
1243
|
-
_linkConfig;
|
|
1244
|
-
_options;
|
|
1245
1404
|
/**
|
|
1246
|
-
* @
|
|
1247
|
-
|
|
1405
|
+
* Creates an instance of the {@link module:link/ui/linkprovideritemsview~LinkProviderItemsView} class.
|
|
1406
|
+
*
|
|
1407
|
+
* Also see {@link #render}.
|
|
1408
|
+
*
|
|
1409
|
+
* @param locale The localization services instance.
|
|
1410
|
+
*/ constructor(locale){
|
|
1248
1411
|
super(locale);
|
|
1249
|
-
|
|
1250
|
-
this.
|
|
1251
|
-
this.
|
|
1252
|
-
this.
|
|
1253
|
-
this.
|
|
1254
|
-
|
|
1255
|
-
|
|
1412
|
+
this.listChildren = this.createCollection();
|
|
1413
|
+
this.backButtonView = this._createBackButton();
|
|
1414
|
+
this.listView = this._createListView();
|
|
1415
|
+
this.emptyListInformation = this._createEmptyLinksListItemView();
|
|
1416
|
+
this.children = this.createCollection([
|
|
1417
|
+
this._createHeaderView(),
|
|
1418
|
+
this.emptyListInformation
|
|
1419
|
+
]);
|
|
1420
|
+
this.set('title', '');
|
|
1421
|
+
this.set('emptyListPlaceholder', '');
|
|
1422
|
+
this.set('hasItems', false);
|
|
1423
|
+
this.listenTo(this.listChildren, 'change', ()=>{
|
|
1424
|
+
this.hasItems = this.listChildren.length > 0;
|
|
1425
|
+
});
|
|
1426
|
+
this.on('change:hasItems', (evt, propName, hasItems)=>{
|
|
1427
|
+
if (hasItems) {
|
|
1428
|
+
this.children.remove(this.emptyListInformation);
|
|
1429
|
+
this.children.add(this.listView);
|
|
1430
|
+
} else {
|
|
1431
|
+
this.children.remove(this.listView);
|
|
1432
|
+
this.children.add(this.emptyListInformation);
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
// Close the panel on esc key press when the **form has focus**.
|
|
1436
|
+
this.keystrokes.set('Esc', (data, cancel)=>{
|
|
1437
|
+
this.fire('cancel');
|
|
1438
|
+
cancel();
|
|
1439
|
+
});
|
|
1256
1440
|
this._focusCycler = new FocusCycler({
|
|
1257
1441
|
focusables: this._focusables,
|
|
1258
1442
|
focusTracker: this.focusTracker,
|
|
1259
1443
|
keystrokeHandler: this.keystrokes,
|
|
1260
1444
|
actions: {
|
|
1261
|
-
// Navigate fields backwards using the Shift + Tab keystroke.
|
|
1445
|
+
// Navigate form fields backwards using the Shift + Tab keystroke.
|
|
1262
1446
|
focusPrevious: 'shift + tab',
|
|
1263
|
-
// Navigate fields forwards using the Tab key.
|
|
1447
|
+
// Navigate form fields forwards using the Tab key.
|
|
1264
1448
|
focusNext: 'tab'
|
|
1265
1449
|
}
|
|
1266
1450
|
});
|
|
@@ -1269,17 +1453,12 @@ var unlinkIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\
|
|
|
1269
1453
|
attributes: {
|
|
1270
1454
|
class: [
|
|
1271
1455
|
'ck',
|
|
1272
|
-
'ck-link-
|
|
1273
|
-
'ck-responsive-form'
|
|
1456
|
+
'ck-link-providers'
|
|
1274
1457
|
],
|
|
1275
1458
|
// https://github.com/ckeditor/ckeditor5-link/issues/90
|
|
1276
1459
|
tabindex: '-1'
|
|
1277
1460
|
},
|
|
1278
|
-
children:
|
|
1279
|
-
this.previewButtonView,
|
|
1280
|
-
this.editButtonView,
|
|
1281
|
-
this.unlinkButtonView
|
|
1282
|
-
]
|
|
1461
|
+
children: this.children
|
|
1283
1462
|
});
|
|
1284
1463
|
}
|
|
1285
1464
|
/**
|
|
@@ -1287,9 +1466,8 @@ var unlinkIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\
|
|
|
1287
1466
|
*/ render() {
|
|
1288
1467
|
super.render();
|
|
1289
1468
|
const childViews = [
|
|
1290
|
-
this.
|
|
1291
|
-
this.
|
|
1292
|
-
this.unlinkButtonView
|
|
1469
|
+
this.listView,
|
|
1470
|
+
this.backButtonView
|
|
1293
1471
|
];
|
|
1294
1472
|
childViews.forEach((v)=>{
|
|
1295
1473
|
// Register the view as focusable.
|
|
@@ -1308,75 +1486,257 @@ var unlinkIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\
|
|
|
1308
1486
|
this.keystrokes.destroy();
|
|
1309
1487
|
}
|
|
1310
1488
|
/**
|
|
1311
|
-
* Focuses the fist {@link #_focusables} in the
|
|
1489
|
+
* Focuses the fist {@link #_focusables} in the form.
|
|
1312
1490
|
*/ focus() {
|
|
1313
1491
|
this._focusCycler.focusFirst();
|
|
1314
1492
|
}
|
|
1315
1493
|
/**
|
|
1316
|
-
* Creates a
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1494
|
+
* Creates a view for the list at the bottom.
|
|
1495
|
+
*/ _createListView() {
|
|
1496
|
+
const listView = new ListView(this.locale);
|
|
1497
|
+
listView.extendTemplate({
|
|
1498
|
+
attributes: {
|
|
1499
|
+
class: [
|
|
1500
|
+
'ck-link-providers__list'
|
|
1501
|
+
]
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
listView.items.bindTo(this.listChildren).using((button)=>{
|
|
1505
|
+
const listItemView = new ListItemView(this.locale);
|
|
1506
|
+
listItemView.children.add(button);
|
|
1507
|
+
return listItemView;
|
|
1508
|
+
});
|
|
1509
|
+
return listView;
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Creates a back button view that cancels the form.
|
|
1513
|
+
*/ _createBackButton() {
|
|
1514
|
+
const t = this.locale.t;
|
|
1515
|
+
const backButton = new ButtonView(this.locale);
|
|
1516
|
+
backButton.set({
|
|
1517
|
+
class: 'ck-button-back',
|
|
1518
|
+
label: t('Back'),
|
|
1519
|
+
icon: IconPreviousArrow$1,
|
|
1327
1520
|
tooltip: true
|
|
1328
1521
|
});
|
|
1329
|
-
|
|
1330
|
-
return
|
|
1522
|
+
backButton.delegate('execute').to(this, 'cancel');
|
|
1523
|
+
return backButton;
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Creates a header view for the form.
|
|
1527
|
+
*/ _createHeaderView() {
|
|
1528
|
+
const header = new FormHeaderView(this.locale);
|
|
1529
|
+
header.bind('label').to(this, 'title');
|
|
1530
|
+
header.children.add(this.backButtonView, 0);
|
|
1531
|
+
return header;
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Creates an info view for an empty list.
|
|
1535
|
+
*/ _createEmptyLinksListItemView() {
|
|
1536
|
+
const view = new View(this.locale);
|
|
1537
|
+
view.setTemplate({
|
|
1538
|
+
tag: 'p',
|
|
1539
|
+
attributes: {
|
|
1540
|
+
class: [
|
|
1541
|
+
'ck',
|
|
1542
|
+
'ck-link__empty-list-info'
|
|
1543
|
+
]
|
|
1544
|
+
},
|
|
1545
|
+
children: [
|
|
1546
|
+
{
|
|
1547
|
+
text: this.bindTemplate.to('emptyListPlaceholder')
|
|
1548
|
+
}
|
|
1549
|
+
]
|
|
1550
|
+
});
|
|
1551
|
+
return view;
|
|
1331
1552
|
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* The link properties view controller class.
|
|
1557
|
+
*
|
|
1558
|
+
* See {@link module:link/ui/linkpropertiesview~LinkPropertiesView}.
|
|
1559
|
+
*/ class LinkPropertiesView extends View {
|
|
1560
|
+
/**
|
|
1561
|
+
* Tracks information about DOM focus in the form.
|
|
1562
|
+
*/ focusTracker = new FocusTracker();
|
|
1563
|
+
/**
|
|
1564
|
+
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
|
|
1565
|
+
*/ keystrokes = new KeystrokeHandler();
|
|
1566
|
+
/**
|
|
1567
|
+
* The Back button view displayed in the header.
|
|
1568
|
+
*/ backButtonView;
|
|
1569
|
+
/**
|
|
1570
|
+
* A collection of child views.
|
|
1571
|
+
*/ children;
|
|
1572
|
+
/**
|
|
1573
|
+
* A collection of {@link module:ui/button/switchbuttonview~SwitchButtonView},
|
|
1574
|
+
* which corresponds to {@link module:link/linkcommand~LinkCommand#manualDecorators manual decorators}
|
|
1575
|
+
* configured in the editor.
|
|
1576
|
+
*/ listChildren;
|
|
1332
1577
|
/**
|
|
1333
|
-
*
|
|
1578
|
+
* A collection of views that can be focused in the form.
|
|
1579
|
+
*/ _focusables = new ViewCollection();
|
|
1580
|
+
/**
|
|
1581
|
+
* Helps cycling over {@link #_focusables} in the form.
|
|
1582
|
+
*/ _focusCycler;
|
|
1583
|
+
/**
|
|
1584
|
+
* Creates an instance of the {@link module:link/ui/linkpropertiesview~LinkPropertiesView} class.
|
|
1334
1585
|
*
|
|
1335
|
-
*
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1586
|
+
* Also see {@link #render}.
|
|
1587
|
+
*
|
|
1588
|
+
* @param locale The localization services instance.
|
|
1589
|
+
*/ constructor(locale){
|
|
1590
|
+
super(locale);
|
|
1591
|
+
this.backButtonView = this._createBackButton();
|
|
1592
|
+
this.listChildren = this.createCollection();
|
|
1593
|
+
this.children = this.createCollection([
|
|
1594
|
+
this._createHeaderView(),
|
|
1595
|
+
this._createListView()
|
|
1596
|
+
]);
|
|
1597
|
+
this._focusCycler = new FocusCycler({
|
|
1598
|
+
focusables: this._focusables,
|
|
1599
|
+
focusTracker: this.focusTracker,
|
|
1600
|
+
keystrokeHandler: this.keystrokes,
|
|
1601
|
+
actions: {
|
|
1602
|
+
// Navigate form fields backwards using the Shift + Tab keystroke.
|
|
1603
|
+
focusPrevious: 'shift + tab',
|
|
1604
|
+
// Navigate form fields forwards using the Tab key.
|
|
1605
|
+
focusNext: 'tab'
|
|
1606
|
+
}
|
|
1342
1607
|
});
|
|
1343
|
-
|
|
1608
|
+
this.setTemplate({
|
|
1609
|
+
tag: 'div',
|
|
1344
1610
|
attributes: {
|
|
1345
1611
|
class: [
|
|
1346
1612
|
'ck',
|
|
1347
|
-
'ck-link-
|
|
1613
|
+
'ck-link-properties'
|
|
1348
1614
|
],
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
rel: 'noopener noreferrer'
|
|
1615
|
+
// https://github.com/ckeditor/ckeditor5-link/issues/90
|
|
1616
|
+
tabindex: '-1'
|
|
1352
1617
|
},
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1618
|
+
children: this.children
|
|
1619
|
+
});
|
|
1620
|
+
// Close the panel on esc key press when the **form has focus**.
|
|
1621
|
+
this.keystrokes.set('Esc', (data, cancel)=>{
|
|
1622
|
+
this.fire('back');
|
|
1623
|
+
cancel();
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* @inheritDoc
|
|
1628
|
+
*/ render() {
|
|
1629
|
+
super.render();
|
|
1630
|
+
const childViews = [
|
|
1631
|
+
...this.listChildren,
|
|
1632
|
+
this.backButtonView
|
|
1633
|
+
];
|
|
1634
|
+
childViews.forEach((v)=>{
|
|
1635
|
+
// Register the view as focusable.
|
|
1636
|
+
this._focusables.add(v);
|
|
1637
|
+
// Register the view in the focus tracker.
|
|
1638
|
+
this.focusTracker.add(v.element);
|
|
1639
|
+
});
|
|
1640
|
+
// Start listening for the keystrokes coming from #element.
|
|
1641
|
+
this.keystrokes.listenTo(this.element);
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* @inheritDoc
|
|
1645
|
+
*/ destroy() {
|
|
1646
|
+
super.destroy();
|
|
1647
|
+
this.focusTracker.destroy();
|
|
1648
|
+
this.keystrokes.destroy();
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Focuses the fist {@link #_focusables} in the form.
|
|
1652
|
+
*/ focus() {
|
|
1653
|
+
this._focusCycler.focusFirst();
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Creates a back button view.
|
|
1657
|
+
*/ _createBackButton() {
|
|
1658
|
+
const t = this.locale.t;
|
|
1659
|
+
const backButton = new ButtonView(this.locale);
|
|
1660
|
+
// TODO: maybe we should have a dedicated BackButtonView in the UI library.
|
|
1661
|
+
backButton.set({
|
|
1662
|
+
class: 'ck-button-back',
|
|
1663
|
+
label: t('Back'),
|
|
1664
|
+
icon: IconPreviousArrow$1,
|
|
1665
|
+
tooltip: true
|
|
1666
|
+
});
|
|
1667
|
+
backButton.delegate('execute').to(this, 'back');
|
|
1668
|
+
return backButton;
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Creates a header view for the form.
|
|
1672
|
+
*/ _createHeaderView() {
|
|
1673
|
+
const t = this.locale.t;
|
|
1674
|
+
const header = new FormHeaderView(this.locale, {
|
|
1675
|
+
label: t('Link properties')
|
|
1363
1676
|
});
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1677
|
+
header.children.add(this.backButtonView, 0);
|
|
1678
|
+
return header;
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Creates a form view that displays the {@link #listChildren} collection.
|
|
1682
|
+
*/ _createListView() {
|
|
1683
|
+
const listView = new ListView(this.locale);
|
|
1684
|
+
listView.extendTemplate({
|
|
1685
|
+
attributes: {
|
|
1686
|
+
class: [
|
|
1687
|
+
'ck-link__list'
|
|
1688
|
+
]
|
|
1367
1689
|
}
|
|
1368
|
-
return t('Open link in new tab');
|
|
1369
1690
|
});
|
|
1370
|
-
|
|
1371
|
-
|
|
1691
|
+
listView.items.bindTo(this.listChildren).using((item)=>{
|
|
1692
|
+
const listItemView = new ListItemView(this.locale);
|
|
1693
|
+
listItemView.children.add(item);
|
|
1694
|
+
return listItemView;
|
|
1372
1695
|
});
|
|
1373
|
-
|
|
1374
|
-
button.template.tag = 'a';
|
|
1375
|
-
return button;
|
|
1696
|
+
return listView;
|
|
1376
1697
|
}
|
|
1377
1698
|
}
|
|
1378
1699
|
|
|
1379
|
-
|
|
1700
|
+
/**
|
|
1701
|
+
* Represents a view for a dropdown menu button.
|
|
1702
|
+
*/ class LinkButtonView extends ButtonView {
|
|
1703
|
+
/**
|
|
1704
|
+
* An icon that displays an arrow to indicate a direction of the menu.
|
|
1705
|
+
*/ arrowView;
|
|
1706
|
+
/**
|
|
1707
|
+
* Creates an instance of the dropdown menu button view.
|
|
1708
|
+
*
|
|
1709
|
+
* @param locale The localization services instance.
|
|
1710
|
+
*/ constructor(locale){
|
|
1711
|
+
super(locale);
|
|
1712
|
+
this.set({
|
|
1713
|
+
withText: true
|
|
1714
|
+
});
|
|
1715
|
+
this.arrowView = this._createArrowView();
|
|
1716
|
+
this.extendTemplate({
|
|
1717
|
+
attributes: {
|
|
1718
|
+
class: [
|
|
1719
|
+
'ck-link__button'
|
|
1720
|
+
]
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* @inheritDoc
|
|
1726
|
+
*/ render() {
|
|
1727
|
+
super.render();
|
|
1728
|
+
this.children.add(this.arrowView);
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Creates the arrow view instance.
|
|
1732
|
+
*
|
|
1733
|
+
* @private
|
|
1734
|
+
*/ _createArrowView() {
|
|
1735
|
+
const arrowView = new IconView();
|
|
1736
|
+
arrowView.content = IconNextArrow;
|
|
1737
|
+
return arrowView;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1380
1740
|
|
|
1381
1741
|
const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
1382
1742
|
/**
|
|
@@ -1386,19 +1746,29 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1386
1746
|
* {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
|
|
1387
1747
|
*/ class LinkUI extends Plugin {
|
|
1388
1748
|
/**
|
|
1389
|
-
* The
|
|
1390
|
-
*/
|
|
1749
|
+
* The toolbar view displayed inside of the balloon.
|
|
1750
|
+
*/ toolbarView = null;
|
|
1391
1751
|
/**
|
|
1392
1752
|
* The form view displayed inside the balloon.
|
|
1393
1753
|
*/ formView = null;
|
|
1754
|
+
/**
|
|
1755
|
+
* The view displaying links list.
|
|
1756
|
+
*/ linkProviderItemsView = null;
|
|
1757
|
+
/**
|
|
1758
|
+
* The form view displaying properties link settings.
|
|
1759
|
+
*/ propertiesView = null;
|
|
1394
1760
|
/**
|
|
1395
1761
|
* The contextual balloon plugin instance.
|
|
1396
1762
|
*/ _balloon;
|
|
1763
|
+
/**
|
|
1764
|
+
* The collection of the link providers.
|
|
1765
|
+
*/ _linksProviders = new Collection();
|
|
1397
1766
|
/**
|
|
1398
1767
|
* @inheritDoc
|
|
1399
1768
|
*/ static get requires() {
|
|
1400
1769
|
return [
|
|
1401
|
-
ContextualBalloon
|
|
1770
|
+
ContextualBalloon,
|
|
1771
|
+
LinkEditing
|
|
1402
1772
|
];
|
|
1403
1773
|
}
|
|
1404
1774
|
/**
|
|
@@ -1416,10 +1786,12 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1416
1786
|
*/ init() {
|
|
1417
1787
|
const editor = this.editor;
|
|
1418
1788
|
const t = this.editor.t;
|
|
1789
|
+
this.set('selectedLinkableText', undefined);
|
|
1419
1790
|
editor.editing.view.addObserver(ClickObserver);
|
|
1420
1791
|
this._balloon = editor.plugins.get(ContextualBalloon);
|
|
1421
1792
|
// Create toolbar buttons.
|
|
1422
|
-
this.
|
|
1793
|
+
this._registerComponents();
|
|
1794
|
+
this._registerEditingOpeners();
|
|
1423
1795
|
this._enableBalloonActivators();
|
|
1424
1796
|
// Renders a fake visual selection marker on an expanded selection.
|
|
1425
1797
|
editor.conversion.for('editingDowncast').markerToHighlight({
|
|
@@ -1473,70 +1845,98 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1473
1845
|
*/ destroy() {
|
|
1474
1846
|
super.destroy();
|
|
1475
1847
|
// Destroy created UI components as they are not automatically destroyed (see ckeditor5#1341).
|
|
1848
|
+
if (this.propertiesView) {
|
|
1849
|
+
this.propertiesView.destroy();
|
|
1850
|
+
}
|
|
1476
1851
|
if (this.formView) {
|
|
1477
1852
|
this.formView.destroy();
|
|
1478
1853
|
}
|
|
1479
|
-
if (this.
|
|
1480
|
-
this.
|
|
1854
|
+
if (this.toolbarView) {
|
|
1855
|
+
this.toolbarView.destroy();
|
|
1856
|
+
}
|
|
1857
|
+
if (this.linkProviderItemsView) {
|
|
1858
|
+
this.linkProviderItemsView.destroy();
|
|
1481
1859
|
}
|
|
1482
1860
|
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Registers list of buttons below the link form view that
|
|
1863
|
+
* open a list of links provided by the clicked provider.
|
|
1864
|
+
*/ registerLinksListProvider(provider) {
|
|
1865
|
+
const insertIndex = this._linksProviders.filter((existing)=>(existing.order || 0) <= (provider.order || 0)).length;
|
|
1866
|
+
this._linksProviders.add(provider, insertIndex);
|
|
1867
|
+
}
|
|
1483
1868
|
/**
|
|
1484
1869
|
* Creates views.
|
|
1485
1870
|
*/ _createViews() {
|
|
1486
|
-
|
|
1871
|
+
const linkCommand = this.editor.commands.get('link');
|
|
1872
|
+
this.toolbarView = this._createToolbarView();
|
|
1487
1873
|
this.formView = this._createFormView();
|
|
1874
|
+
if (linkCommand.manualDecorators.length) {
|
|
1875
|
+
this.propertiesView = this._createPropertiesView();
|
|
1876
|
+
}
|
|
1488
1877
|
// Attach lifecycle actions to the the balloon.
|
|
1489
1878
|
this._enableUserBalloonInteractions();
|
|
1490
1879
|
}
|
|
1491
1880
|
/**
|
|
1492
|
-
* Creates the
|
|
1493
|
-
*/
|
|
1881
|
+
* Creates the ToolbarView instance.
|
|
1882
|
+
*/ _createToolbarView() {
|
|
1494
1883
|
const editor = this.editor;
|
|
1495
|
-
const
|
|
1884
|
+
const toolbarView = new ToolbarView(editor.locale);
|
|
1496
1885
|
const linkCommand = editor.commands.get('link');
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
this.listenTo(actionsView, 'unlink', ()=>{
|
|
1507
|
-
editor.execute('unlink');
|
|
1508
|
-
this._hideUI();
|
|
1509
|
-
});
|
|
1510
|
-
// Close the panel on esc key press when the **actions have focus**.
|
|
1511
|
-
actionsView.keystrokes.set('Esc', (data, cancel)=>{
|
|
1886
|
+
toolbarView.class = 'ck-link-toolbar';
|
|
1887
|
+
// Remove the linkProperties button if there are no manual decorators, as it would be useless.
|
|
1888
|
+
let toolbarItems = editor.config.get('link.toolbar');
|
|
1889
|
+
if (!linkCommand.manualDecorators.length) {
|
|
1890
|
+
toolbarItems = toolbarItems.filter((item)=>item !== 'linkProperties');
|
|
1891
|
+
}
|
|
1892
|
+
toolbarView.fillFromConfig(toolbarItems, editor.ui.componentFactory);
|
|
1893
|
+
// Close the panel on esc key press when the **link toolbar have focus**.
|
|
1894
|
+
toolbarView.keystrokes.set('Esc', (data, cancel)=>{
|
|
1512
1895
|
this._hideUI();
|
|
1513
1896
|
cancel();
|
|
1514
1897
|
});
|
|
1515
|
-
// Open the form view on Ctrl+K when the **
|
|
1516
|
-
|
|
1898
|
+
// Open the form view on Ctrl+K when the **link toolbar have focus**..
|
|
1899
|
+
toolbarView.keystrokes.set(LINK_KEYSTROKE, (data, cancel)=>{
|
|
1517
1900
|
this._addFormView();
|
|
1518
1901
|
cancel();
|
|
1519
1902
|
});
|
|
1520
|
-
|
|
1903
|
+
// Register the toolbar, so it becomes available for Alt+F10 and Esc navigation.
|
|
1904
|
+
// TODO this should be registered earlier to be able to open this toolbar without previously opening it by click or Ctrl+K
|
|
1905
|
+
editor.ui.addToolbar(toolbarView, {
|
|
1906
|
+
isContextual: true,
|
|
1907
|
+
beforeFocus: ()=>{
|
|
1908
|
+
if (this._getSelectedLinkElement() && !this._isToolbarVisible) {
|
|
1909
|
+
this._showUI(true);
|
|
1910
|
+
}
|
|
1911
|
+
},
|
|
1912
|
+
afterBlur: ()=>{
|
|
1913
|
+
this._hideUI(false);
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
return toolbarView;
|
|
1521
1917
|
}
|
|
1522
1918
|
/**
|
|
1523
1919
|
* Creates the {@link module:link/ui/linkformview~LinkFormView} instance.
|
|
1524
1920
|
*/ _createFormView() {
|
|
1525
1921
|
const editor = this.editor;
|
|
1922
|
+
const t = editor.locale.t;
|
|
1526
1923
|
const linkCommand = editor.commands.get('link');
|
|
1527
1924
|
const defaultProtocol = editor.config.get('link.defaultProtocol');
|
|
1528
|
-
const formView = new (CssTransitionDisablerMixin(LinkFormView))(editor.locale,
|
|
1529
|
-
formView.
|
|
1925
|
+
const formView = new (CssTransitionDisablerMixin(LinkFormView))(editor.locale, getFormValidators(editor));
|
|
1926
|
+
formView.displayedTextInputView.bind('isEnabled').to(this, 'selectedLinkableText', (value)=>value !== undefined);
|
|
1530
1927
|
// Form elements should be read-only when corresponding commands are disabled.
|
|
1531
1928
|
formView.urlInputView.bind('isEnabled').to(linkCommand, 'isEnabled');
|
|
1532
1929
|
// Disable the "save" button if the command is disabled.
|
|
1533
1930
|
formView.saveButtonView.bind('isEnabled').to(linkCommand, 'isEnabled');
|
|
1931
|
+
// Change the "Save" button label depending on the command state.
|
|
1932
|
+
formView.saveButtonView.bind('label').to(linkCommand, 'value', (value)=>value ? t('Update') : t('Insert'));
|
|
1534
1933
|
// Execute link command after clicking the "Save" button.
|
|
1535
1934
|
this.listenTo(formView, 'submit', ()=>{
|
|
1536
1935
|
if (formView.isValid()) {
|
|
1537
|
-
const
|
|
1538
|
-
const parsedUrl = addLinkProtocolIfApplicable(
|
|
1539
|
-
|
|
1936
|
+
const url = formView.urlInputView.fieldView.element.value;
|
|
1937
|
+
const parsedUrl = addLinkProtocolIfApplicable(url, defaultProtocol);
|
|
1938
|
+
const displayedText = formView.displayedTextInputView.fieldView.element.value;
|
|
1939
|
+
editor.execute('link', parsedUrl, this._getDecoratorSwitchesState(), displayedText !== this.selectedLinkableText ? displayedText : undefined);
|
|
1540
1940
|
this._closeFormView();
|
|
1541
1941
|
}
|
|
1542
1942
|
});
|
|
@@ -1553,12 +1953,115 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1553
1953
|
this._closeFormView();
|
|
1554
1954
|
cancel();
|
|
1555
1955
|
});
|
|
1956
|
+
// Watch adding new link providers and add them to the buttons list.
|
|
1957
|
+
formView.providersListChildren.bindTo(this._linksProviders).using((provider)=>this._createLinksListProviderButton(provider));
|
|
1556
1958
|
return formView;
|
|
1557
1959
|
}
|
|
1558
1960
|
/**
|
|
1559
|
-
* Creates a
|
|
1560
|
-
|
|
1561
|
-
|
|
1961
|
+
* Creates a sorted array of buttons with link names.
|
|
1962
|
+
*/ _createLinkProviderListView(provider) {
|
|
1963
|
+
return provider.getListItems().map(({ href, label, icon })=>{
|
|
1964
|
+
const buttonView = new ButtonView();
|
|
1965
|
+
buttonView.set({
|
|
1966
|
+
label,
|
|
1967
|
+
icon,
|
|
1968
|
+
tooltip: false,
|
|
1969
|
+
withText: true
|
|
1970
|
+
});
|
|
1971
|
+
buttonView.on('execute', ()=>{
|
|
1972
|
+
this.formView.resetFormStatus();
|
|
1973
|
+
this.formView.urlInputView.fieldView.value = href;
|
|
1974
|
+
// Set focus to the editing view to prevent from losing it while current view is removed.
|
|
1975
|
+
this.editor.editing.view.focus();
|
|
1976
|
+
this._removeLinksProviderView();
|
|
1977
|
+
// Set the focus to the URL input field.
|
|
1978
|
+
this.formView.focus();
|
|
1979
|
+
});
|
|
1980
|
+
return buttonView;
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Creates a view for links provider.
|
|
1985
|
+
*/ _createLinkProviderItemsView(provider) {
|
|
1986
|
+
const editor = this.editor;
|
|
1987
|
+
const t = editor.locale.t;
|
|
1988
|
+
const view = new LinkProviderItemsView(editor.locale);
|
|
1989
|
+
const { emptyListPlaceholder, label } = provider;
|
|
1990
|
+
view.emptyListPlaceholder = emptyListPlaceholder || t('No links available');
|
|
1991
|
+
view.title = label;
|
|
1992
|
+
// Hide the panel after clicking the "Cancel" button.
|
|
1993
|
+
this.listenTo(view, 'cancel', ()=>{
|
|
1994
|
+
// Set focus to the editing view to prevent from losing it while current view is removed.
|
|
1995
|
+
editor.editing.view.focus();
|
|
1996
|
+
this._removeLinksProviderView();
|
|
1997
|
+
// Set the focus to the URL input field.
|
|
1998
|
+
this.formView.focus();
|
|
1999
|
+
});
|
|
2000
|
+
return view;
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Creates the {@link module:link/ui/linkpropertiesview~LinkPropertiesView} instance.
|
|
2004
|
+
*/ _createPropertiesView() {
|
|
2005
|
+
const editor = this.editor;
|
|
2006
|
+
const linkCommand = this.editor.commands.get('link');
|
|
2007
|
+
const view = new (CssTransitionDisablerMixin(LinkPropertiesView))(editor.locale);
|
|
2008
|
+
// Hide the panel after clicking the back button.
|
|
2009
|
+
this.listenTo(view, 'back', ()=>{
|
|
2010
|
+
// Move focus back to the editing view to prevent from losing it while current view is removed.
|
|
2011
|
+
editor.editing.view.focus();
|
|
2012
|
+
this._removePropertiesView();
|
|
2013
|
+
});
|
|
2014
|
+
view.listChildren.bindTo(linkCommand.manualDecorators).using((manualDecorator)=>{
|
|
2015
|
+
const button = new SwitchButtonView(editor.locale);
|
|
2016
|
+
button.set({
|
|
2017
|
+
label: manualDecorator.label,
|
|
2018
|
+
withText: true
|
|
2019
|
+
});
|
|
2020
|
+
button.bind('isOn').toMany([
|
|
2021
|
+
manualDecorator,
|
|
2022
|
+
linkCommand
|
|
2023
|
+
], 'value', (decoratorValue, commandValue)=>{
|
|
2024
|
+
return commandValue === undefined && decoratorValue === undefined ? !!manualDecorator.defaultValue : !!decoratorValue;
|
|
2025
|
+
});
|
|
2026
|
+
button.on('execute', ()=>{
|
|
2027
|
+
manualDecorator.set('value', !button.isOn);
|
|
2028
|
+
editor.execute('link', linkCommand.value, this._getDecoratorSwitchesState());
|
|
2029
|
+
});
|
|
2030
|
+
return button;
|
|
2031
|
+
});
|
|
2032
|
+
return view;
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Obtains the state of the manual decorators.
|
|
2036
|
+
*/ _getDecoratorSwitchesState() {
|
|
2037
|
+
const linkCommand = this.editor.commands.get('link');
|
|
2038
|
+
return Array.from(linkCommand.manualDecorators).reduce((accumulator, manualDecorator)=>{
|
|
2039
|
+
const value = linkCommand.value === undefined && manualDecorator.value === undefined ? manualDecorator.defaultValue : manualDecorator.value;
|
|
2040
|
+
return {
|
|
2041
|
+
...accumulator,
|
|
2042
|
+
[manualDecorator.id]: !!value
|
|
2043
|
+
};
|
|
2044
|
+
}, {});
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Registers listeners used in editing plugin, used to open links.
|
|
2048
|
+
*/ _registerEditingOpeners() {
|
|
2049
|
+
const linkEditing = this.editor.plugins.get(LinkEditing);
|
|
2050
|
+
linkEditing._registerLinkOpener((href)=>{
|
|
2051
|
+
const match = this._getLinkProviderLinkByHref(href);
|
|
2052
|
+
if (!match) {
|
|
2053
|
+
return false;
|
|
2054
|
+
}
|
|
2055
|
+
const { item, provider } = match;
|
|
2056
|
+
if (provider.navigate) {
|
|
2057
|
+
return provider.navigate(item);
|
|
2058
|
+
}
|
|
2059
|
+
return false;
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Registers components in the ComponentFactory.
|
|
2064
|
+
*/ _registerComponents() {
|
|
1562
2065
|
const editor = this.editor;
|
|
1563
2066
|
editor.ui.componentFactory.add('link', ()=>{
|
|
1564
2067
|
const button = this._createButton(ButtonView);
|
|
@@ -1574,6 +2077,111 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1574
2077
|
});
|
|
1575
2078
|
return button;
|
|
1576
2079
|
});
|
|
2080
|
+
editor.ui.componentFactory.add('linkPreview', (locale)=>{
|
|
2081
|
+
const button = new LinkPreviewButtonView(locale);
|
|
2082
|
+
const allowedProtocols = editor.config.get('link.allowedProtocols');
|
|
2083
|
+
const linkCommand = editor.commands.get('link');
|
|
2084
|
+
const t = locale.t;
|
|
2085
|
+
button.bind('isEnabled').to(linkCommand, 'value', (href)=>!!href);
|
|
2086
|
+
button.bind('href').to(linkCommand, 'value', (href)=>{
|
|
2087
|
+
return href && ensureSafeUrl(href, allowedProtocols);
|
|
2088
|
+
});
|
|
2089
|
+
const setHref = (href)=>{
|
|
2090
|
+
if (!href) {
|
|
2091
|
+
button.label = undefined;
|
|
2092
|
+
button.icon = undefined;
|
|
2093
|
+
button.tooltip = t('Open link in new tab');
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
const selectedLinksProviderLink = this._getLinkProviderLinkByHref(href);
|
|
2097
|
+
if (selectedLinksProviderLink) {
|
|
2098
|
+
const { label, tooltip, icon } = selectedLinksProviderLink.item;
|
|
2099
|
+
button.label = label;
|
|
2100
|
+
button.tooltip = tooltip || false;
|
|
2101
|
+
button.icon = icon;
|
|
2102
|
+
} else {
|
|
2103
|
+
button.label = href;
|
|
2104
|
+
button.icon = undefined;
|
|
2105
|
+
button.tooltip = t('Open link in new tab');
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
setHref(linkCommand.value);
|
|
2109
|
+
this.listenTo(linkCommand, 'change:value', (evt, name, href)=>{
|
|
2110
|
+
setHref(href);
|
|
2111
|
+
});
|
|
2112
|
+
this.listenTo(button, 'navigate', (evt, href, cancel)=>{
|
|
2113
|
+
const selectedLinksProviderLink = this._getLinkProviderLinkByHref(href);
|
|
2114
|
+
if (!selectedLinksProviderLink) {
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
const { provider, item } = selectedLinksProviderLink;
|
|
2118
|
+
const { navigate } = provider;
|
|
2119
|
+
if (navigate && navigate(item)) {
|
|
2120
|
+
evt.stop();
|
|
2121
|
+
cancel();
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
return button;
|
|
2125
|
+
});
|
|
2126
|
+
editor.ui.componentFactory.add('unlink', (locale)=>{
|
|
2127
|
+
const unlinkCommand = editor.commands.get('unlink');
|
|
2128
|
+
const button = new ButtonView(locale);
|
|
2129
|
+
const t = locale.t;
|
|
2130
|
+
button.set({
|
|
2131
|
+
label: t('Unlink'),
|
|
2132
|
+
icon: IconUnlink,
|
|
2133
|
+
tooltip: true
|
|
2134
|
+
});
|
|
2135
|
+
button.bind('isEnabled').to(unlinkCommand);
|
|
2136
|
+
this.listenTo(button, 'execute', ()=>{
|
|
2137
|
+
editor.execute('unlink');
|
|
2138
|
+
this._hideUI();
|
|
2139
|
+
});
|
|
2140
|
+
return button;
|
|
2141
|
+
});
|
|
2142
|
+
editor.ui.componentFactory.add('editLink', (locale)=>{
|
|
2143
|
+
const linkCommand = editor.commands.get('link');
|
|
2144
|
+
const button = new ButtonView(locale);
|
|
2145
|
+
const t = locale.t;
|
|
2146
|
+
button.set({
|
|
2147
|
+
label: t('Edit link'),
|
|
2148
|
+
icon: IconPencil,
|
|
2149
|
+
tooltip: true
|
|
2150
|
+
});
|
|
2151
|
+
button.bind('isEnabled').to(linkCommand);
|
|
2152
|
+
this.listenTo(button, 'execute', ()=>{
|
|
2153
|
+
this._addFormView();
|
|
2154
|
+
});
|
|
2155
|
+
return button;
|
|
2156
|
+
});
|
|
2157
|
+
editor.ui.componentFactory.add('linkProperties', (locale)=>{
|
|
2158
|
+
const linkCommand = editor.commands.get('link');
|
|
2159
|
+
const button = new ButtonView(locale);
|
|
2160
|
+
const t = locale.t;
|
|
2161
|
+
button.set({
|
|
2162
|
+
label: t('Link properties'),
|
|
2163
|
+
icon: IconSettings,
|
|
2164
|
+
tooltip: true
|
|
2165
|
+
});
|
|
2166
|
+
button.bind('isEnabled').to(linkCommand, 'isEnabled', linkCommand, 'value', linkCommand, 'manualDecorators', (isEnabled, href, manualDecorators)=>isEnabled && !!href && manualDecorators.length > 0);
|
|
2167
|
+
this.listenTo(button, 'execute', ()=>{
|
|
2168
|
+
this._addPropertiesView();
|
|
2169
|
+
});
|
|
2170
|
+
return button;
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Creates a links button view.
|
|
2175
|
+
*/ _createLinksListProviderButton(linkProvider) {
|
|
2176
|
+
const locale = this.editor.locale;
|
|
2177
|
+
const linksButton = new LinkButtonView(locale);
|
|
2178
|
+
linksButton.set({
|
|
2179
|
+
label: linkProvider.label
|
|
2180
|
+
});
|
|
2181
|
+
this.listenTo(linksButton, 'execute', ()=>{
|
|
2182
|
+
this._showLinksProviderView(linkProvider);
|
|
2183
|
+
});
|
|
2184
|
+
return linksButton;
|
|
1577
2185
|
}
|
|
1578
2186
|
/**
|
|
1579
2187
|
* Creates a button for link command to use either in toolbar or in menu bar.
|
|
@@ -1585,14 +2193,21 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1585
2193
|
const t = locale.t;
|
|
1586
2194
|
view.set({
|
|
1587
2195
|
label: t('Link'),
|
|
1588
|
-
icon:
|
|
2196
|
+
icon: IconLink,
|
|
1589
2197
|
keystroke: LINK_KEYSTROKE,
|
|
1590
2198
|
isToggleable: true
|
|
1591
2199
|
});
|
|
1592
2200
|
view.bind('isEnabled').to(command, 'isEnabled');
|
|
1593
2201
|
view.bind('isOn').to(command, 'value', (value)=>!!value);
|
|
1594
2202
|
// Show the panel on button click.
|
|
1595
|
-
this.listenTo(view, 'execute', ()=>
|
|
2203
|
+
this.listenTo(view, 'execute', ()=>{
|
|
2204
|
+
this._showUI(true);
|
|
2205
|
+
// Open the form view on-top of the toolbar view if it's already visible.
|
|
2206
|
+
// It should be visible every time the link is selected.
|
|
2207
|
+
if (this._getSelectedLinkElement()) {
|
|
2208
|
+
this._addFormView();
|
|
2209
|
+
}
|
|
2210
|
+
});
|
|
1596
2211
|
return view;
|
|
1597
2212
|
}
|
|
1598
2213
|
/**
|
|
@@ -1625,8 +2240,8 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1625
2240
|
*/ _enableUserBalloonInteractions() {
|
|
1626
2241
|
// Focus the form if the balloon is visible and the Tab key has been pressed.
|
|
1627
2242
|
this.editor.keystrokes.set('Tab', (data, cancel)=>{
|
|
1628
|
-
if (this.
|
|
1629
|
-
this.
|
|
2243
|
+
if (this._isToolbarVisible && !this.toolbarView.focusTracker.isFocused) {
|
|
2244
|
+
this.toolbarView.focus();
|
|
1630
2245
|
cancel();
|
|
1631
2246
|
}
|
|
1632
2247
|
}, {
|
|
@@ -1653,19 +2268,20 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1653
2268
|
});
|
|
1654
2269
|
}
|
|
1655
2270
|
/**
|
|
1656
|
-
* Adds the {@link #
|
|
2271
|
+
* Adds the {@link #toolbarView} to the {@link #_balloon}.
|
|
1657
2272
|
*
|
|
1658
2273
|
* @internal
|
|
1659
|
-
*/
|
|
1660
|
-
if (!this.
|
|
2274
|
+
*/ _addToolbarView() {
|
|
2275
|
+
if (!this.toolbarView) {
|
|
1661
2276
|
this._createViews();
|
|
1662
2277
|
}
|
|
1663
|
-
if (this.
|
|
2278
|
+
if (this._isToolbarInPanel) {
|
|
1664
2279
|
return;
|
|
1665
2280
|
}
|
|
1666
2281
|
this._balloon.add({
|
|
1667
|
-
view: this.
|
|
1668
|
-
position: this._getBalloonPositionData()
|
|
2282
|
+
view: this.toolbarView,
|
|
2283
|
+
position: this._getBalloonPositionData(),
|
|
2284
|
+
balloonClassName: 'ck-toolbar-container'
|
|
1669
2285
|
});
|
|
1670
2286
|
}
|
|
1671
2287
|
/**
|
|
@@ -1677,20 +2293,22 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1677
2293
|
if (this._isFormInPanel) {
|
|
1678
2294
|
return;
|
|
1679
2295
|
}
|
|
1680
|
-
const
|
|
1681
|
-
const linkCommand = editor.commands.get('link');
|
|
2296
|
+
const linkCommand = this.editor.commands.get('link');
|
|
1682
2297
|
this.formView.disableCssTransitions();
|
|
1683
2298
|
this.formView.resetFormStatus();
|
|
2299
|
+
this.formView.backButtonView.isVisible = linkCommand.isEnabled && !!linkCommand.value;
|
|
1684
2300
|
this._balloon.add({
|
|
1685
2301
|
view: this.formView,
|
|
1686
2302
|
position: this._getBalloonPositionData()
|
|
1687
2303
|
});
|
|
1688
|
-
// Make sure that each time the panel shows up, the
|
|
2304
|
+
// Make sure that each time the panel shows up, the fields remains in sync with the value of
|
|
1689
2305
|
// the command. If the user typed in the input, then canceled the balloon (`urlInputView.fieldView#value` stays
|
|
1690
2306
|
// unaltered) and re-opened it without changing the value of the link command (e.g. because they
|
|
1691
2307
|
// clicked the same link), they would see the old value instead of the actual value of the command.
|
|
1692
2308
|
// https://github.com/ckeditor/ckeditor5-link/issues/78
|
|
1693
2309
|
// https://github.com/ckeditor/ckeditor5-link/issues/123
|
|
2310
|
+
this.selectedLinkableText = this._getSelectedLinkableText();
|
|
2311
|
+
this.formView.displayedTextInputView.fieldView.value = this.selectedLinkableText || '';
|
|
1694
2312
|
this.formView.urlInputView.fieldView.value = linkCommand.value || '';
|
|
1695
2313
|
// Select input when form view is currently visible.
|
|
1696
2314
|
if (this._balloon.visibleView === this.formView) {
|
|
@@ -1698,23 +2316,71 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1698
2316
|
}
|
|
1699
2317
|
this.formView.enableCssTransitions();
|
|
1700
2318
|
}
|
|
2319
|
+
/**
|
|
2320
|
+
* Adds the {@link #propertiesView} to the {@link #_balloon}.
|
|
2321
|
+
*/ _addPropertiesView() {
|
|
2322
|
+
if (!this.propertiesView) {
|
|
2323
|
+
this._createViews();
|
|
2324
|
+
}
|
|
2325
|
+
if (this._arePropertiesInPanel) {
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
this.propertiesView.disableCssTransitions();
|
|
2329
|
+
this._balloon.add({
|
|
2330
|
+
view: this.propertiesView,
|
|
2331
|
+
position: this._getBalloonPositionData()
|
|
2332
|
+
});
|
|
2333
|
+
this.propertiesView.enableCssTransitions();
|
|
2334
|
+
this.propertiesView.focus();
|
|
2335
|
+
}
|
|
2336
|
+
/**
|
|
2337
|
+
* Shows the view with links provided by the given provider.
|
|
2338
|
+
*/ _showLinksProviderView(provider) {
|
|
2339
|
+
if (this.linkProviderItemsView) {
|
|
2340
|
+
this._removeLinksProviderView();
|
|
2341
|
+
}
|
|
2342
|
+
this.linkProviderItemsView = this._createLinkProviderItemsView(provider);
|
|
2343
|
+
this._addLinkProviderItemsView(provider);
|
|
2344
|
+
}
|
|
2345
|
+
/**
|
|
2346
|
+
* Adds the {@link #linkProviderItemsView} to the {@link #_balloon}.
|
|
2347
|
+
*/ _addLinkProviderItemsView(provider) {
|
|
2348
|
+
// Clear the collection of links.
|
|
2349
|
+
this.linkProviderItemsView.listChildren.clear();
|
|
2350
|
+
// Add links to the collection.
|
|
2351
|
+
this.linkProviderItemsView.listChildren.addMany(this._createLinkProviderListView(provider));
|
|
2352
|
+
this._balloon.add({
|
|
2353
|
+
view: this.linkProviderItemsView,
|
|
2354
|
+
position: this._getBalloonPositionData()
|
|
2355
|
+
});
|
|
2356
|
+
this.linkProviderItemsView.focus();
|
|
2357
|
+
}
|
|
1701
2358
|
/**
|
|
1702
2359
|
* Closes the form view. Decides whether the balloon should be hidden completely or if the action view should be shown. This is
|
|
1703
2360
|
* decided upon the link command value (which has a value if the document selection is in the link).
|
|
1704
|
-
*
|
|
1705
|
-
* Additionally, if any {@link module:link/linkconfig~LinkConfig#decorators} are defined in the editor configuration, the state of
|
|
1706
|
-
* switch buttons responsible for manual decorator handling is restored.
|
|
1707
2361
|
*/ _closeFormView() {
|
|
1708
2362
|
const linkCommand = this.editor.commands.get('link');
|
|
1709
|
-
|
|
1710
|
-
// when the user cancels the editing form.
|
|
1711
|
-
linkCommand.restoreManualDecoratorStates();
|
|
2363
|
+
this.selectedLinkableText = undefined;
|
|
1712
2364
|
if (linkCommand.value !== undefined) {
|
|
1713
2365
|
this._removeFormView();
|
|
1714
2366
|
} else {
|
|
1715
2367
|
this._hideUI();
|
|
1716
2368
|
}
|
|
1717
2369
|
}
|
|
2370
|
+
/**
|
|
2371
|
+
* Removes the {@link #propertiesView} from the {@link #_balloon}.
|
|
2372
|
+
*/ _removePropertiesView() {
|
|
2373
|
+
if (this._arePropertiesInPanel) {
|
|
2374
|
+
this._balloon.remove(this.propertiesView);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
/**
|
|
2378
|
+
* Removes the {@link #linkProviderItemsView} from the {@link #_balloon}.
|
|
2379
|
+
*/ _removeLinksProviderView() {
|
|
2380
|
+
if (this._isLinksListInPanel) {
|
|
2381
|
+
this._balloon.remove(this.linkProviderItemsView);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
1718
2384
|
/**
|
|
1719
2385
|
* Removes the {@link #formView} from the {@link #_balloon}.
|
|
1720
2386
|
*/ _removeFormView() {
|
|
@@ -1722,7 +2388,8 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1722
2388
|
// Blur the input element before removing it from DOM to prevent issues in some browsers.
|
|
1723
2389
|
// See https://github.com/ckeditor/ckeditor5/issues/1501.
|
|
1724
2390
|
this.formView.saveButtonView.focus();
|
|
1725
|
-
// Reset
|
|
2391
|
+
// Reset fields to update the state of the submit button.
|
|
2392
|
+
this.formView.displayedTextInputView.fieldView.reset();
|
|
1726
2393
|
this.formView.urlInputView.fieldView.reset();
|
|
1727
2394
|
this._balloon.remove(this.formView);
|
|
1728
2395
|
// Because the form has an input which has focus, the focus must be brought back
|
|
@@ -1732,7 +2399,7 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1732
2399
|
}
|
|
1733
2400
|
}
|
|
1734
2401
|
/**
|
|
1735
|
-
* Shows the correct UI type. It is either {@link #formView} or {@link #
|
|
2402
|
+
* Shows the correct UI type. It is either {@link #formView} or {@link #toolbarView}.
|
|
1736
2403
|
*
|
|
1737
2404
|
* @internal
|
|
1738
2405
|
*/ _showUI(forceVisible = false) {
|
|
@@ -1744,18 +2411,18 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1744
2411
|
// Show visual selection on a text without a link when the contextual balloon is displayed.
|
|
1745
2412
|
// See https://github.com/ckeditor/ckeditor5/issues/4721.
|
|
1746
2413
|
this._showFakeVisualSelection();
|
|
1747
|
-
this.
|
|
2414
|
+
this._addToolbarView();
|
|
1748
2415
|
// Be sure panel with link is visible.
|
|
1749
2416
|
if (forceVisible) {
|
|
1750
2417
|
this._balloon.showStack('main');
|
|
1751
2418
|
}
|
|
1752
2419
|
this._addFormView();
|
|
1753
2420
|
} else {
|
|
1754
|
-
// Go to the editing UI if
|
|
1755
|
-
if (this.
|
|
2421
|
+
// Go to the editing UI if toolbar is already visible.
|
|
2422
|
+
if (this._isToolbarVisible) {
|
|
1756
2423
|
this._addFormView();
|
|
1757
2424
|
} else {
|
|
1758
|
-
this.
|
|
2425
|
+
this._addToolbarView();
|
|
1759
2426
|
}
|
|
1760
2427
|
// Be sure panel with link is visible.
|
|
1761
2428
|
if (forceVisible) {
|
|
@@ -1768,21 +2435,29 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1768
2435
|
/**
|
|
1769
2436
|
* Removes the {@link #formView} from the {@link #_balloon}.
|
|
1770
2437
|
*
|
|
1771
|
-
* See {@link #_addFormView}, {@link #
|
|
1772
|
-
*/ _hideUI() {
|
|
2438
|
+
* See {@link #_addFormView}, {@link #_addToolbarView}.
|
|
2439
|
+
*/ _hideUI(updateFocus = true) {
|
|
2440
|
+
const editor = this.editor;
|
|
1773
2441
|
if (!this._isUIInPanel) {
|
|
1774
2442
|
return;
|
|
1775
2443
|
}
|
|
1776
|
-
const editor = this.editor;
|
|
1777
2444
|
this.stopListening(editor.ui, 'update');
|
|
1778
2445
|
this.stopListening(this._balloon, 'change:visibleView');
|
|
1779
2446
|
// Make sure the focus always gets back to the editable _before_ removing the focused form view.
|
|
1780
2447
|
// Doing otherwise causes issues in some browsers. See https://github.com/ckeditor/ckeditor5-link/issues/193.
|
|
1781
|
-
|
|
1782
|
-
|
|
2448
|
+
if (updateFocus) {
|
|
2449
|
+
editor.editing.view.focus();
|
|
2450
|
+
}
|
|
2451
|
+
// If the links view is visible, remove it because it can be on top of the stack.
|
|
2452
|
+
this._removeLinksProviderView();
|
|
2453
|
+
// If the properties form view is visible, remove it because it can be on top of the stack.
|
|
2454
|
+
this._removePropertiesView();
|
|
2455
|
+
// Then remove the form view because it's beneath the properties form.
|
|
1783
2456
|
this._removeFormView();
|
|
1784
|
-
//
|
|
1785
|
-
|
|
2457
|
+
// Finally, remove the link toolbar view because it's last in the stack.
|
|
2458
|
+
if (this._isToolbarInPanel) {
|
|
2459
|
+
this._balloon.remove(this.toolbarView);
|
|
2460
|
+
}
|
|
1786
2461
|
this._hideFakeVisualSelection();
|
|
1787
2462
|
}
|
|
1788
2463
|
/**
|
|
@@ -1804,7 +2479,7 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1804
2479
|
// of the link,
|
|
1805
2480
|
// * the selection went to a different parent when creating a NEW link. E.g. someone
|
|
1806
2481
|
// else modified the document.
|
|
1807
|
-
// * the selection has expanded (e.g. displaying link
|
|
2482
|
+
// * the selection has expanded (e.g. displaying link toolbar then pressing SHIFT+Right arrow).
|
|
1808
2483
|
//
|
|
1809
2484
|
// Note: #_getSelectedLinkElement will return a link for a non-collapsed selection only
|
|
1810
2485
|
// when fully selected.
|
|
@@ -1825,33 +2500,55 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1825
2500
|
this.listenTo(editor.ui, 'update', update);
|
|
1826
2501
|
this.listenTo(this._balloon, 'change:visibleView', update);
|
|
1827
2502
|
}
|
|
2503
|
+
/**
|
|
2504
|
+
* Returns `true` when {@link #propertiesView} is in the {@link #_balloon}.
|
|
2505
|
+
*/ get _arePropertiesInPanel() {
|
|
2506
|
+
return !!this.propertiesView && this._balloon.hasView(this.propertiesView);
|
|
2507
|
+
}
|
|
2508
|
+
/**
|
|
2509
|
+
* Returns `true` when {@link #linkProviderItemsView} is in the {@link #_balloon}.
|
|
2510
|
+
*/ get _isLinksListInPanel() {
|
|
2511
|
+
return !!this.linkProviderItemsView && this._balloon.hasView(this.linkProviderItemsView);
|
|
2512
|
+
}
|
|
1828
2513
|
/**
|
|
1829
2514
|
* Returns `true` when {@link #formView} is in the {@link #_balloon}.
|
|
1830
2515
|
*/ get _isFormInPanel() {
|
|
1831
2516
|
return !!this.formView && this._balloon.hasView(this.formView);
|
|
1832
2517
|
}
|
|
1833
2518
|
/**
|
|
1834
|
-
* Returns `true` when {@link #
|
|
1835
|
-
*/ get
|
|
1836
|
-
return !!this.
|
|
2519
|
+
* Returns `true` when {@link #toolbarView} is in the {@link #_balloon}.
|
|
2520
|
+
*/ get _isToolbarInPanel() {
|
|
2521
|
+
return !!this.toolbarView && this._balloon.hasView(this.toolbarView);
|
|
1837
2522
|
}
|
|
1838
2523
|
/**
|
|
1839
|
-
* Returns `true` when {@link #
|
|
2524
|
+
* Returns `true` when {@link #propertiesView} is in the {@link #_balloon} and it is
|
|
1840
2525
|
* currently visible.
|
|
1841
|
-
*/ get
|
|
1842
|
-
return !!this.
|
|
2526
|
+
*/ get _isPropertiesVisible() {
|
|
2527
|
+
return !!this.propertiesView && this._balloon.visibleView === this.propertiesView;
|
|
1843
2528
|
}
|
|
1844
2529
|
/**
|
|
1845
|
-
* Returns `true` when {@link #
|
|
1846
|
-
|
|
1847
|
-
|
|
2530
|
+
* Returns `true` when {@link #formView} is in the {@link #_balloon} and it is
|
|
2531
|
+
* currently visible.
|
|
2532
|
+
*/ get _isFormVisible() {
|
|
2533
|
+
return !!this.formView && this._balloon.visibleView == this.formView;
|
|
1848
2534
|
}
|
|
1849
2535
|
/**
|
|
1850
|
-
* Returns `true` when {@link #
|
|
2536
|
+
* Returns `true` when {@link #toolbarView} is in the {@link #_balloon} and it is
|
|
1851
2537
|
* currently visible.
|
|
2538
|
+
*/ get _isToolbarVisible() {
|
|
2539
|
+
return !!this.toolbarView && this._balloon.visibleView === this.toolbarView;
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Returns `true` when {@link #propertiesView}, {@link #toolbarView}, {@link #linkProviderItemsView}
|
|
2543
|
+
* or {@link #formView} is in the {@link #_balloon}.
|
|
2544
|
+
*/ get _isUIInPanel() {
|
|
2545
|
+
return this._arePropertiesInPanel || this._isLinksListInPanel || this._isFormInPanel || this._isToolbarInPanel;
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Returns `true` when {@link #propertiesView}, {@link #linkProviderItemsView}, {@link #toolbarView}
|
|
2549
|
+
* or {@link #formView} is in the {@link #_balloon} and it is currently visible.
|
|
1852
2550
|
*/ get _isUIVisible() {
|
|
1853
|
-
|
|
1854
|
-
return !!this.formView && visibleView == this.formView || this._areActionsVisible;
|
|
2551
|
+
return this._isPropertiesVisible || this._isLinksListInPanel || this._isFormVisible || this._isToolbarVisible;
|
|
1855
2552
|
}
|
|
1856
2553
|
/**
|
|
1857
2554
|
* Returns positioning options for the {@link #_balloon}. They control the way the balloon is attached
|
|
@@ -1861,28 +2558,31 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1861
2558
|
* entire link element. Otherwise, it will be attached to the selection.
|
|
1862
2559
|
*/ _getBalloonPositionData() {
|
|
1863
2560
|
const view = this.editor.editing.view;
|
|
1864
|
-
const model = this.editor.model;
|
|
1865
2561
|
const viewDocument = view.document;
|
|
1866
|
-
|
|
2562
|
+
const model = this.editor.model;
|
|
1867
2563
|
if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
|
1868
2564
|
// There are cases when we highlight selection using a marker (#7705, #4721).
|
|
1869
|
-
const markerViewElements =
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
2565
|
+
const markerViewElements = this.editor.editing.mapper.markerNameToElements(VISUAL_SELECTION_MARKER_NAME);
|
|
2566
|
+
// Marker could be removed by link text override and end up in the graveyard.
|
|
2567
|
+
if (markerViewElements) {
|
|
2568
|
+
const markerViewElementsArray = Array.from(markerViewElements);
|
|
2569
|
+
const newRange = view.createRange(view.createPositionBefore(markerViewElementsArray[0]), view.createPositionAfter(markerViewElementsArray[markerViewElementsArray.length - 1]));
|
|
2570
|
+
return {
|
|
2571
|
+
target: view.domConverter.viewRangeToDom(newRange)
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
// Make sure the target is calculated on demand at the last moment because a cached DOM range
|
|
2576
|
+
// (which is very fragile) can desynchronize with the state of the editing view if there was
|
|
2577
|
+
// any rendering done in the meantime. This can happen, for instance, when an inline widget
|
|
2578
|
+
// gets unlinked.
|
|
2579
|
+
return {
|
|
2580
|
+
target: ()=>{
|
|
1878
2581
|
const targetLink = this._getSelectedLinkElement();
|
|
1879
2582
|
return targetLink ? // When selection is inside link element, then attach panel to this element.
|
|
1880
2583
|
view.domConverter.mapViewToDom(targetLink) : // Otherwise attach panel to the selection.
|
|
1881
2584
|
view.domConverter.viewRangeToDom(viewDocument.selection.getFirstRange());
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
return {
|
|
1885
|
-
target
|
|
2585
|
+
}
|
|
1886
2586
|
};
|
|
1887
2587
|
}
|
|
1888
2588
|
/**
|
|
@@ -1917,6 +2617,41 @@ const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
|
1917
2617
|
}
|
|
1918
2618
|
}
|
|
1919
2619
|
}
|
|
2620
|
+
/**
|
|
2621
|
+
* Returns selected link text content.
|
|
2622
|
+
* If link is not selected it returns the selected text.
|
|
2623
|
+
* If selection or link includes non text node (inline object or block) then returns undefined.
|
|
2624
|
+
*/ _getSelectedLinkableText() {
|
|
2625
|
+
const model = this.editor.model;
|
|
2626
|
+
const editing = this.editor.editing;
|
|
2627
|
+
const selectedLink = this._getSelectedLinkElement();
|
|
2628
|
+
if (!selectedLink) {
|
|
2629
|
+
return extractTextFromLinkRange(model.document.selection.getFirstRange());
|
|
2630
|
+
}
|
|
2631
|
+
const viewLinkRange = editing.view.createRangeOn(selectedLink);
|
|
2632
|
+
const linkRange = editing.mapper.toModelRange(viewLinkRange);
|
|
2633
|
+
return extractTextFromLinkRange(linkRange);
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Returns a provider by its URL.
|
|
2637
|
+
*
|
|
2638
|
+
* @param href URL of the link.
|
|
2639
|
+
* @returns Link provider and item or `null` if not found.
|
|
2640
|
+
*/ _getLinkProviderLinkByHref(href) {
|
|
2641
|
+
if (!href) {
|
|
2642
|
+
return null;
|
|
2643
|
+
}
|
|
2644
|
+
for (const provider of this._linksProviders){
|
|
2645
|
+
const item = provider.getItem ? provider.getItem(href) : provider.getListItems().find((item)=>item.href === href);
|
|
2646
|
+
if (item) {
|
|
2647
|
+
return {
|
|
2648
|
+
provider,
|
|
2649
|
+
item
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
return null;
|
|
2654
|
+
}
|
|
1920
2655
|
/**
|
|
1921
2656
|
* Displays a fake visual selection when the contextual balloon is displayed.
|
|
1922
2657
|
*
|
|
@@ -1999,7 +2734,8 @@ const URL_REG_EXP = new RegExp(// Group 1: Line start or after a space.
|
|
|
1999
2734
|
'(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' + '(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' + '(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' + '|' + '(' + // Do not allow `www.foo` - see https://github.com/ckeditor/ckeditor5/issues/8050.
|
|
2000
2735
|
'((?!www\\.)|(www\\.))' + // Host & domain names.
|
|
2001
2736
|
'(?![-_])(?:[-_a-z0-9\\u00a1-\\uffff]{1,63}\\.)+' + // TLD identifier name.
|
|
2002
|
-
'(?:[a-z\\u00a1-\\uffff]{2,63})' + ')' + '
|
|
2737
|
+
'(?:[a-z\\u00a1-\\uffff]{2,63})' + ')' + '|' + // Allow localhost as a valid hostname
|
|
2738
|
+
'localhost' + ')' + // port number (optional)
|
|
2003
2739
|
'(?::\\d{2,5})?' + // resource path (optional)
|
|
2004
2740
|
'(?:[/?#]\\S*)?' + ')' + '|' + // b. Short form (either www.example.com or example@example.com)
|
|
2005
2741
|
'(' + '(www.|(\\S+@))' + // Host & domain names.
|
|
@@ -2561,7 +3297,7 @@ function linkIsAlreadySet(range) {
|
|
|
2561
3297
|
* Creates a `LinkImageUI` button view.
|
|
2562
3298
|
*
|
|
2563
3299
|
* Clicking this button shows a {@link module:link/linkui~LinkUI#_balloon} attached to the selection.
|
|
2564
|
-
* When an image is already linked, the view shows {@link module:link/linkui~LinkUI#
|
|
3300
|
+
* When an image is already linked, the view shows {@link module:link/linkui~LinkUI#toolbarView} or
|
|
2565
3301
|
* {@link module:link/linkui~LinkUI#formView} if it is not.
|
|
2566
3302
|
*/ _createToolbarLinkImageButton() {
|
|
2567
3303
|
const editor = this.editor;
|
|
@@ -2573,7 +3309,7 @@ function linkIsAlreadySet(range) {
|
|
|
2573
3309
|
button.set({
|
|
2574
3310
|
isEnabled: true,
|
|
2575
3311
|
label: t('Link image'),
|
|
2576
|
-
icon:
|
|
3312
|
+
icon: IconLink,
|
|
2577
3313
|
keystroke: LINK_KEYSTROKE,
|
|
2578
3314
|
tooltip: true,
|
|
2579
3315
|
isToggleable: true
|
|
@@ -2584,7 +3320,7 @@ function linkIsAlreadySet(range) {
|
|
|
2584
3320
|
// Show the actionsView or formView (both from LinkUI) on button click depending on whether the image is linked already.
|
|
2585
3321
|
this.listenTo(button, 'execute', ()=>{
|
|
2586
3322
|
if (this._isSelectedLinkedImage(editor.model.document.selection)) {
|
|
2587
|
-
plugin.
|
|
3323
|
+
plugin._addToolbarView();
|
|
2588
3324
|
} else {
|
|
2589
3325
|
plugin._showUI(true);
|
|
2590
3326
|
}
|
|
@@ -2628,5 +3364,5 @@ function linkIsAlreadySet(range) {
|
|
|
2628
3364
|
}
|
|
2629
3365
|
}
|
|
2630
3366
|
|
|
2631
|
-
export { AutoLink, Link,
|
|
3367
|
+
export { AutoLink, Link, LinkCommand, LinkEditing, LinkFormView, LinkImage, LinkImageEditing, LinkImageUI, LinkUI, UnlinkCommand, addLinkProtocolIfApplicable, ensureSafeUrl, isLinkableElement };
|
|
2632
3368
|
//# sourceMappingURL=index.js.map
|