@ckeditor/ckeditor5-link 44.3.0-alpha.7 → 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/src/linkcommand.js
CHANGED
|
@@ -7,28 +7,26 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Command } from 'ckeditor5/src/core.js';
|
|
9
9
|
import { findAttributeRange } from 'ckeditor5/src/typing.js';
|
|
10
|
-
import { Collection, first, toMap } from 'ckeditor5/src/utils.js';
|
|
10
|
+
import { Collection, diff, first, toMap } from 'ckeditor5/src/utils.js';
|
|
11
|
+
import { LivePosition } from 'ckeditor5/src/engine.js';
|
|
11
12
|
import AutomaticDecorators from './utils/automaticdecorators.js';
|
|
12
|
-
import { isLinkableElement } from './utils.js';
|
|
13
|
+
import { extractTextFromLinkRange, isLinkableElement } from './utils.js';
|
|
13
14
|
/**
|
|
14
15
|
* The link command. It is used by the {@link module:link/link~Link link feature}.
|
|
15
16
|
*/
|
|
16
17
|
export default class LinkCommand extends Command {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*/
|
|
30
|
-
this.automaticDecorators = new AutomaticDecorators();
|
|
31
|
-
}
|
|
18
|
+
/**
|
|
19
|
+
* A collection of {@link module:link/utils/manualdecorator~ManualDecorator manual decorators}
|
|
20
|
+
* corresponding to the {@link module:link/linkconfig~LinkConfig#decorators decorator configuration}.
|
|
21
|
+
*
|
|
22
|
+
* You can consider it a model with states of manual decorators added to the currently selected link.
|
|
23
|
+
*/
|
|
24
|
+
manualDecorators = new Collection();
|
|
25
|
+
/**
|
|
26
|
+
* An instance of the helper that ties together all {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition}
|
|
27
|
+
* that are used by the {@glink features/link link} and the {@glink features/images/images-linking linking images} features.
|
|
28
|
+
*/
|
|
29
|
+
automaticDecorators = new AutomaticDecorators();
|
|
32
30
|
/**
|
|
33
31
|
* Synchronizes the state of {@link #manualDecorators} with the currently present elements in the model.
|
|
34
32
|
*/
|
|
@@ -119,11 +117,27 @@ export default class LinkCommand extends Command {
|
|
|
119
117
|
* **Note**: {@link module:link/unlinkcommand~UnlinkCommand#execute `UnlinkCommand#execute()`} removes all
|
|
120
118
|
* decorator attributes.
|
|
121
119
|
*
|
|
120
|
+
* An optional parameter called `displayedText` is to add or update text of the link that represents the `href`. For example:
|
|
121
|
+
*
|
|
122
|
+
* ```ts
|
|
123
|
+
* const linkCommand = editor.commands.get( 'link' );
|
|
124
|
+
*
|
|
125
|
+
* // Adding a new link with `displayedText` attribute.
|
|
126
|
+
* linkCommand.execute( 'http://example.com', {}, 'Example' );
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* The above code will create an anchor like this:
|
|
130
|
+
*
|
|
131
|
+
* ```html
|
|
132
|
+
* <a href="http://example.com">Example</a>
|
|
133
|
+
* ```
|
|
134
|
+
*
|
|
122
135
|
* @fires execute
|
|
123
136
|
* @param href Link destination.
|
|
124
137
|
* @param manualDecoratorIds The information about manual decorator attributes to be applied or removed upon execution.
|
|
138
|
+
* @param displayedText Text of the link.
|
|
125
139
|
*/
|
|
126
|
-
execute(href, manualDecoratorIds = {}) {
|
|
140
|
+
execute(href, manualDecoratorIds = {}, displayedText) {
|
|
127
141
|
const model = this.editor.model;
|
|
128
142
|
const selection = model.document.selection;
|
|
129
143
|
// Stores information about manual decorators to turn them on/off when command is applied.
|
|
@@ -138,26 +152,82 @@ export default class LinkCommand extends Command {
|
|
|
138
152
|
}
|
|
139
153
|
}
|
|
140
154
|
model.change(writer => {
|
|
155
|
+
const updateLinkAttributes = (itemOrRange) => {
|
|
156
|
+
writer.setAttribute('linkHref', href, itemOrRange);
|
|
157
|
+
truthyManualDecorators.forEach(item => writer.setAttribute(item, true, itemOrRange));
|
|
158
|
+
falsyManualDecorators.forEach(item => writer.removeAttribute(item, itemOrRange));
|
|
159
|
+
};
|
|
160
|
+
const updateLinkTextIfNeeded = (range, linkHref) => {
|
|
161
|
+
const linkText = extractTextFromLinkRange(range);
|
|
162
|
+
if (!linkText) {
|
|
163
|
+
return range;
|
|
164
|
+
}
|
|
165
|
+
// Make a copy not to override the command param value.
|
|
166
|
+
let newText = displayedText;
|
|
167
|
+
if (!newText) {
|
|
168
|
+
// Replace the link text with the new href if previously href was equal to text.
|
|
169
|
+
// For example: `<a href="http://ckeditor.com/">http://ckeditor.com/</a>`.
|
|
170
|
+
newText = linkHref && linkHref == linkText ? href : linkText;
|
|
171
|
+
}
|
|
172
|
+
// Only if needed.
|
|
173
|
+
if (newText != linkText) {
|
|
174
|
+
const changes = findChanges(linkText, newText);
|
|
175
|
+
let insertsLength = 0;
|
|
176
|
+
for (const { offset, actual, expected } of changes) {
|
|
177
|
+
const updatedOffset = offset + insertsLength;
|
|
178
|
+
const subRange = writer.createRange(range.start.getShiftedBy(updatedOffset), range.start.getShiftedBy(updatedOffset + actual.length));
|
|
179
|
+
// Collect formatting attributes from replaced text.
|
|
180
|
+
const textNode = getLinkPartTextNode(subRange, range);
|
|
181
|
+
const attributes = textNode.getAttributes();
|
|
182
|
+
const formattingAttributes = Array
|
|
183
|
+
.from(attributes)
|
|
184
|
+
.filter(([key]) => model.schema.getAttributeProperties(key).isFormatting);
|
|
185
|
+
// Create a new text node.
|
|
186
|
+
const newTextNode = writer.createText(expected, formattingAttributes);
|
|
187
|
+
// Set link attributes before inserting to document to avoid Differ attributes edge case.
|
|
188
|
+
updateLinkAttributes(newTextNode);
|
|
189
|
+
// Replace text with formatting.
|
|
190
|
+
model.insertContent(newTextNode, subRange);
|
|
191
|
+
// Sum of all previous inserts.
|
|
192
|
+
insertsLength += expected.length;
|
|
193
|
+
}
|
|
194
|
+
return writer.createRange(range.start, range.start.getShiftedBy(newText.length));
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const collapseSelectionAtLinkEnd = (linkRange) => {
|
|
198
|
+
const { plugins } = this.editor;
|
|
199
|
+
writer.setSelection(linkRange.end);
|
|
200
|
+
if (plugins.has('TwoStepCaretMovement')) {
|
|
201
|
+
// After replacing the text of the link, we need to move the caret to the end of the link,
|
|
202
|
+
// override it's gravity to forward to prevent keeping e.g. bold attribute on the caret
|
|
203
|
+
// which was previously inside the link.
|
|
204
|
+
//
|
|
205
|
+
// If the plugin is not available, the caret will be placed at the end of the link and the
|
|
206
|
+
// bold attribute will be kept even if command moved caret outside the link.
|
|
207
|
+
plugins.get('TwoStepCaretMovement')._handleForwardMovement();
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// Remove the `linkHref` attribute and all link decorators from the selection.
|
|
211
|
+
// It stops adding a new content into the link element.
|
|
212
|
+
for (const key of ['linkHref', ...truthyManualDecorators, ...falsyManualDecorators]) {
|
|
213
|
+
writer.removeSelectionAttribute(key);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
141
217
|
// If selection is collapsed then update selected link or insert new one at the place of caret.
|
|
142
218
|
if (selection.isCollapsed) {
|
|
143
219
|
const position = selection.getFirstPosition();
|
|
144
220
|
// When selection is inside text with `linkHref` attribute.
|
|
145
221
|
if (selection.hasAttribute('linkHref')) {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
222
|
+
const linkHref = selection.getAttribute('linkHref');
|
|
223
|
+
const linkRange = findAttributeRange(position, 'linkHref', linkHref, model);
|
|
224
|
+
const newLinkRange = updateLinkTextIfNeeded(linkRange, linkHref);
|
|
225
|
+
updateLinkAttributes(newLinkRange || linkRange);
|
|
226
|
+
// Put the selection at the end of the updated link only when text was changed.
|
|
227
|
+
// When text was not altered we keep the original selection.
|
|
228
|
+
if (newLinkRange) {
|
|
229
|
+
collapseSelectionAtLinkEnd(newLinkRange);
|
|
151
230
|
}
|
|
152
|
-
writer.setAttribute('linkHref', href, linkRange);
|
|
153
|
-
truthyManualDecorators.forEach(item => {
|
|
154
|
-
writer.setAttribute(item, true, linkRange);
|
|
155
|
-
});
|
|
156
|
-
falsyManualDecorators.forEach(item => {
|
|
157
|
-
writer.removeAttribute(item, linkRange);
|
|
158
|
-
});
|
|
159
|
-
// Put the selection at the end of the updated link.
|
|
160
|
-
writer.setSelection(writer.createPositionAfter(linkRange.end.nodeBefore));
|
|
161
231
|
}
|
|
162
232
|
// If not then insert text node with `linkHref` attribute in place of caret.
|
|
163
233
|
// However, since selection is collapsed, attribute value will be used as data for text node.
|
|
@@ -168,21 +238,18 @@ export default class LinkCommand extends Command {
|
|
|
168
238
|
truthyManualDecorators.forEach(item => {
|
|
169
239
|
attributes.set(item, true);
|
|
170
240
|
});
|
|
171
|
-
const
|
|
241
|
+
const newLinkRange = model.insertContent(writer.createText(displayedText || href, attributes), position);
|
|
172
242
|
// Put the selection at the end of the inserted link.
|
|
173
243
|
// Using end of range returned from insertContent in case nodes with the same attributes got merged.
|
|
174
|
-
|
|
244
|
+
collapseSelectionAtLinkEnd(newLinkRange);
|
|
175
245
|
}
|
|
176
|
-
// Remove the `linkHref` attribute and all link decorators from the selection.
|
|
177
|
-
// It stops adding a new content into the link element.
|
|
178
|
-
['linkHref', ...truthyManualDecorators, ...falsyManualDecorators].forEach(item => {
|
|
179
|
-
writer.removeSelectionAttribute(item);
|
|
180
|
-
});
|
|
181
246
|
}
|
|
182
247
|
else {
|
|
248
|
+
// Non-collapsed selection.
|
|
183
249
|
// If selection has non-collapsed ranges, we change attribute on nodes inside those ranges
|
|
184
250
|
// omitting nodes where the `linkHref` attribute is disallowed.
|
|
185
|
-
const
|
|
251
|
+
const selectionRanges = Array.from(selection.getRanges());
|
|
252
|
+
const ranges = model.schema.getValidRanges(selectionRanges, 'linkHref');
|
|
186
253
|
// But for the first, check whether the `linkHref` attribute is allowed on selected blocks (e.g. the "image" element).
|
|
187
254
|
const allowedRanges = [];
|
|
188
255
|
for (const element of selection.getSelectedBlocks()) {
|
|
@@ -199,24 +266,25 @@ export default class LinkCommand extends Command {
|
|
|
199
266
|
rangesToUpdate.push(range);
|
|
200
267
|
}
|
|
201
268
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
writer.setAttribute('linkHref', href, linkRange);
|
|
213
|
-
truthyManualDecorators.forEach(item => {
|
|
214
|
-
writer.setAttribute(item, true, linkRange);
|
|
215
|
-
});
|
|
216
|
-
falsyManualDecorators.forEach(item => {
|
|
217
|
-
writer.removeAttribute(item, linkRange);
|
|
218
|
-
});
|
|
269
|
+
// Store the selection ranges in a pseudo live range array (stickiness to the outside of the range).
|
|
270
|
+
const stickyPseudoRanges = selectionRanges.map(range => ({
|
|
271
|
+
start: LivePosition.fromPosition(range.start, 'toPrevious'),
|
|
272
|
+
end: LivePosition.fromPosition(range.end, 'toNext')
|
|
273
|
+
}));
|
|
274
|
+
// Update or set links (including text update if needed).
|
|
275
|
+
for (let range of rangesToUpdate) {
|
|
276
|
+
const linkHref = (range.start.textNode || range.start.nodeAfter).getAttribute('linkHref');
|
|
277
|
+
range = updateLinkTextIfNeeded(range, linkHref) || range;
|
|
278
|
+
updateLinkAttributes(range);
|
|
219
279
|
}
|
|
280
|
+
// The original selection got trimmed by replacing content so we need to restore it.
|
|
281
|
+
writer.setSelection(stickyPseudoRanges.map(pseudoRange => {
|
|
282
|
+
const start = pseudoRange.start.toPosition();
|
|
283
|
+
const end = pseudoRange.end.toPosition();
|
|
284
|
+
pseudoRange.start.detach();
|
|
285
|
+
pseudoRange.end.detach();
|
|
286
|
+
return model.createRange(start, end);
|
|
287
|
+
}));
|
|
220
288
|
}
|
|
221
289
|
});
|
|
222
290
|
}
|
|
@@ -252,34 +320,96 @@ export default class LinkCommand extends Command {
|
|
|
252
320
|
}
|
|
253
321
|
return true;
|
|
254
322
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Compares two strings and returns an array of changes needed to transform one into another.
|
|
326
|
+
* Uses the diff utility to find the differences and groups them into chunks containing information
|
|
327
|
+
* about the offset and actual/expected content.
|
|
328
|
+
*
|
|
329
|
+
* @param oldText The original text to compare.
|
|
330
|
+
* @param newText The new text to compare against.
|
|
331
|
+
* @returns Array of change objects containing offset and actual/expected content.
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* findChanges( 'hello world', 'hi there' );
|
|
335
|
+
*
|
|
336
|
+
* Returns:
|
|
337
|
+
* [
|
|
338
|
+
* {
|
|
339
|
+
* "offset": 1,
|
|
340
|
+
* "actual": "ello",
|
|
341
|
+
* "expected": "i"
|
|
342
|
+
* },
|
|
343
|
+
* {
|
|
344
|
+
* "offset": 2,
|
|
345
|
+
* "actual": "wo",
|
|
346
|
+
* "expected": "the"
|
|
347
|
+
* },
|
|
348
|
+
* {
|
|
349
|
+
* "offset": 3,
|
|
350
|
+
* "actual": "ld",
|
|
351
|
+
* "expected": "e"
|
|
352
|
+
* }
|
|
353
|
+
* ]
|
|
354
|
+
*/
|
|
355
|
+
function findChanges(oldText, newText) {
|
|
356
|
+
// Get array of operations (insert/delete/equal) needed to transform oldText into newText.
|
|
357
|
+
// Example: diff('abc', 'abxc') returns ['equal', 'equal', 'insert', 'equal']
|
|
358
|
+
const changes = diff(oldText, newText);
|
|
359
|
+
// Track position in both strings based on operation type.
|
|
360
|
+
const counter = { equal: 0, insert: 0, delete: 0 };
|
|
361
|
+
const result = [];
|
|
362
|
+
// Accumulate consecutive changes into slices before creating change objects.
|
|
363
|
+
let actualSlice = '';
|
|
364
|
+
let expectedSlice = '';
|
|
365
|
+
// Adding null as sentinel value to handle final accumulated changes.
|
|
366
|
+
for (const action of [...changes, null]) {
|
|
367
|
+
if (action == 'insert') {
|
|
368
|
+
// Example: for 'abc' -> 'abxc', at insert position, adds 'x' to expectedSlice.
|
|
369
|
+
expectedSlice += newText[counter.equal + counter.insert];
|
|
370
|
+
}
|
|
371
|
+
else if (action == 'delete') {
|
|
372
|
+
// Example: for 'abc' -> 'ac', at delete position, adds 'b' to actualSlice.
|
|
373
|
+
actualSlice += oldText[counter.equal + counter.delete];
|
|
374
|
+
}
|
|
375
|
+
else if (actualSlice.length || expectedSlice.length) {
|
|
376
|
+
// On 'equal' or end: bundle accumulated changes into a single change object.
|
|
377
|
+
// Example: { offset: 2, actual: "", expected: "x" }
|
|
378
|
+
result.push({
|
|
379
|
+
offset: counter.equal,
|
|
380
|
+
actual: actualSlice,
|
|
381
|
+
expected: expectedSlice
|
|
382
|
+
});
|
|
383
|
+
actualSlice = '';
|
|
384
|
+
expectedSlice = '';
|
|
385
|
+
}
|
|
386
|
+
// Increment appropriate counter for the current operation.
|
|
387
|
+
if (action) {
|
|
388
|
+
counter[action]++;
|
|
389
|
+
}
|
|
266
390
|
}
|
|
391
|
+
return result;
|
|
267
392
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
393
|
+
/**
|
|
394
|
+
* Returns text node withing the link range that should be updated.
|
|
395
|
+
*
|
|
396
|
+
* @param range Partial link range.
|
|
397
|
+
* @param linkRange Range of the entire link.
|
|
398
|
+
* @returns Text node.
|
|
399
|
+
*/
|
|
400
|
+
function getLinkPartTextNode(range, linkRange) {
|
|
401
|
+
if (!range.isCollapsed) {
|
|
402
|
+
return first(range.getItems());
|
|
403
|
+
}
|
|
404
|
+
const position = range.start;
|
|
405
|
+
if (position.textNode) {
|
|
406
|
+
return position.textNode;
|
|
407
|
+
}
|
|
408
|
+
// If the range is at the start of a link range then prefer node inside a link range.
|
|
409
|
+
if (!position.nodeBefore || position.isEqual(linkRange.start)) {
|
|
410
|
+
return position.nodeAfter;
|
|
273
411
|
}
|
|
274
412
|
else {
|
|
275
|
-
|
|
276
|
-
if (rangeItems.length > 1) {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
const firstNode = rangeItems[0];
|
|
280
|
-
if (firstNode.is('$text') || firstNode.is('$textProxy')) {
|
|
281
|
-
return firstNode.data;
|
|
282
|
-
}
|
|
283
|
-
return null;
|
|
413
|
+
return position.nodeBefore;
|
|
284
414
|
}
|
|
285
415
|
}
|
package/src/linkconfig.d.ts
CHANGED
|
@@ -172,6 +172,34 @@ export interface LinkConfig {
|
|
|
172
172
|
* See also the {@glink features/link#custom-link-attributes-decorators link feature guide} for more information.
|
|
173
173
|
*/
|
|
174
174
|
decorators?: Record<string, LinkDecoratorDefinition>;
|
|
175
|
+
/**
|
|
176
|
+
* Items to be placed in the link contextual toolbar.
|
|
177
|
+
*
|
|
178
|
+
* Assuming that you use the {@link module:link/linkui~LinkUI} feature, the following toolbar items will be available
|
|
179
|
+
* in {@link module:ui/componentfactory~ComponentFactory}:
|
|
180
|
+
*
|
|
181
|
+
* * `'linkPreview'`,
|
|
182
|
+
* * `'editLink'`,
|
|
183
|
+
* * `'linkProperties'`
|
|
184
|
+
* * `'unlink'`.
|
|
185
|
+
*
|
|
186
|
+
* The default configuration for link toolbar is:
|
|
187
|
+
*
|
|
188
|
+
* ```ts
|
|
189
|
+
* const linkConfig = {
|
|
190
|
+
* toolbar: [ 'linkPreview', '|', 'editLink', 'linkProperties', 'unlink' ]
|
|
191
|
+
* };
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* The `linkProperties` toolbar item is only available when at least one manual decorator is defined in the
|
|
195
|
+
* {@link module:link/linkconfig~LinkConfig#decorators decorators configuration}.
|
|
196
|
+
*
|
|
197
|
+
* Of course, the same buttons can also be used in the
|
|
198
|
+
* {@link module:core/editor/editorconfig~EditorConfig#toolbar main editor toolbar}.
|
|
199
|
+
*
|
|
200
|
+
* Read more about configuring the toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
|
|
201
|
+
*/
|
|
202
|
+
toolbar?: Array<string>;
|
|
175
203
|
}
|
|
176
204
|
/**
|
|
177
205
|
* A link decorator definition. Two types implement this definition:
|
package/src/linkediting.d.ts
CHANGED
|
@@ -16,6 +16,10 @@ import '../theme/link.css';
|
|
|
16
16
|
* as well as `'link'` and `'unlink'` commands.
|
|
17
17
|
*/
|
|
18
18
|
export default class LinkEditing extends Plugin {
|
|
19
|
+
/**
|
|
20
|
+
* A list of functions that handles opening links. If any of them returns `true`, the link is considered to be opened.
|
|
21
|
+
*/
|
|
22
|
+
private readonly _linkOpeners;
|
|
19
23
|
/**
|
|
20
24
|
* @inheritDoc
|
|
21
25
|
*/
|
|
@@ -36,6 +40,13 @@ export default class LinkEditing extends Plugin {
|
|
|
36
40
|
* @inheritDoc
|
|
37
41
|
*/
|
|
38
42
|
init(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Registers a function that opens links in a new browser tab.
|
|
45
|
+
*
|
|
46
|
+
* @param linkOpener The function that opens a link in a new browser tab.
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
_registerLinkOpener(linkOpener: LinkOpener): void;
|
|
39
50
|
/**
|
|
40
51
|
* Processes an array of configured {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition automatic decorators}
|
|
41
52
|
* and registers a {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast dispatcher}
|
|
@@ -72,3 +83,10 @@ export default class LinkEditing extends Plugin {
|
|
|
72
83
|
*/
|
|
73
84
|
private _enableClipboardIntegration;
|
|
74
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* A function that handles opening links. It may be used to define custom link handlers.
|
|
88
|
+
*
|
|
89
|
+
* @returns `true` if the link was opened successfully.
|
|
90
|
+
*/
|
|
91
|
+
type LinkOpener = (url: string) => boolean;
|
|
92
|
+
export {};
|
package/src/linkediting.js
CHANGED
|
@@ -12,7 +12,7 @@ import { keyCodes, env } from 'ckeditor5/src/utils.js';
|
|
|
12
12
|
import LinkCommand from './linkcommand.js';
|
|
13
13
|
import UnlinkCommand from './unlinkcommand.js';
|
|
14
14
|
import ManualDecorator from './utils/manualdecorator.js';
|
|
15
|
-
import { createLinkElement, ensureSafeUrl, getLocalizedDecorators, normalizeDecorators, addLinkProtocolIfApplicable,
|
|
15
|
+
import { createLinkElement, ensureSafeUrl, getLocalizedDecorators, normalizeDecorators, addLinkProtocolIfApplicable, openLink } from './utils.js';
|
|
16
16
|
import '../theme/link.css';
|
|
17
17
|
const HIGHLIGHT_CLASS = 'ck-link_selected';
|
|
18
18
|
const DECORATOR_AUTOMATIC = 'automatic';
|
|
@@ -25,6 +25,10 @@ const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
|
|
|
25
25
|
* as well as `'link'` and `'unlink'` commands.
|
|
26
26
|
*/
|
|
27
27
|
export default class LinkEditing extends Plugin {
|
|
28
|
+
/**
|
|
29
|
+
* A list of functions that handles opening links. If any of them returns `true`, the link is considered to be opened.
|
|
30
|
+
*/
|
|
31
|
+
_linkOpeners = [];
|
|
28
32
|
/**
|
|
29
33
|
* @inheritDoc
|
|
30
34
|
*/
|
|
@@ -51,7 +55,8 @@ export default class LinkEditing extends Plugin {
|
|
|
51
55
|
super(editor);
|
|
52
56
|
editor.config.define('link', {
|
|
53
57
|
allowCreatingEmptyLinks: false,
|
|
54
|
-
addTargetToExternalLinks: false
|
|
58
|
+
addTargetToExternalLinks: false,
|
|
59
|
+
toolbar: ['linkPreview', '|', 'editLink', 'linkProperties', 'unlink']
|
|
55
60
|
});
|
|
56
61
|
}
|
|
57
62
|
/**
|
|
@@ -101,6 +106,15 @@ export default class LinkEditing extends Plugin {
|
|
|
101
106
|
// Handle adding default protocol to pasted links.
|
|
102
107
|
this._enableClipboardIntegration();
|
|
103
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Registers a function that opens links in a new browser tab.
|
|
111
|
+
*
|
|
112
|
+
* @param linkOpener The function that opens a link in a new browser tab.
|
|
113
|
+
* @internal
|
|
114
|
+
*/
|
|
115
|
+
_registerLinkOpener(linkOpener) {
|
|
116
|
+
this._linkOpeners.push(linkOpener);
|
|
117
|
+
}
|
|
104
118
|
/**
|
|
105
119
|
* Processes an array of configured {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition automatic decorators}
|
|
106
120
|
* and registers a {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast dispatcher}
|
|
@@ -193,15 +207,11 @@ export default class LinkEditing extends Plugin {
|
|
|
193
207
|
const editor = this.editor;
|
|
194
208
|
const view = editor.editing.view;
|
|
195
209
|
const viewDocument = view.document;
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
if (bookmarkCallbacks.isScrollableToTarget(url)) {
|
|
199
|
-
bookmarkCallbacks.scrollToTarget(url);
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
210
|
+
const handleLinkOpening = (url) => {
|
|
211
|
+
if (!this._linkOpeners.some(opener => opener(url))) {
|
|
202
212
|
openLink(url);
|
|
203
213
|
}
|
|
204
|
-
}
|
|
214
|
+
};
|
|
205
215
|
this.listenTo(viewDocument, 'click', (evt, data) => {
|
|
206
216
|
const shouldOpen = env.isMac ? data.domEvent.metaKey : data.domEvent.ctrlKey;
|
|
207
217
|
if (!shouldOpen) {
|
package/src/linkimageui.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export default class LinkImageUI extends Plugin {
|
|
|
32
32
|
* Creates a `LinkImageUI` button view.
|
|
33
33
|
*
|
|
34
34
|
* Clicking this button shows a {@link module:link/linkui~LinkUI#_balloon} attached to the selection.
|
|
35
|
-
* When an image is already linked, the view shows {@link module:link/linkui~LinkUI#
|
|
35
|
+
* When an image is already linked, the view shows {@link module:link/linkui~LinkUI#toolbarView} or
|
|
36
36
|
* {@link module:link/linkui~LinkUI#formView} if it is not.
|
|
37
37
|
*/
|
|
38
38
|
private _createToolbarLinkImageButton;
|
package/src/linkimageui.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { ButtonView } from 'ckeditor5/src/ui.js';
|
|
9
9
|
import { Plugin } from 'ckeditor5/src/core.js';
|
|
10
|
+
import { IconLink } from 'ckeditor5/src/icons.js';
|
|
10
11
|
import LinkUI from './linkui.js';
|
|
11
12
|
import LinkEditing from './linkediting.js';
|
|
12
13
|
import { LINK_KEYSTROKE } from './utils.js';
|
|
13
|
-
import linkIcon from '../theme/icons/link.svg';
|
|
14
14
|
/**
|
|
15
15
|
* The link image UI plugin.
|
|
16
16
|
*
|
|
@@ -57,7 +57,7 @@ export default class LinkImageUI extends Plugin {
|
|
|
57
57
|
* Creates a `LinkImageUI` button view.
|
|
58
58
|
*
|
|
59
59
|
* Clicking this button shows a {@link module:link/linkui~LinkUI#_balloon} attached to the selection.
|
|
60
|
-
* When an image is already linked, the view shows {@link module:link/linkui~LinkUI#
|
|
60
|
+
* When an image is already linked, the view shows {@link module:link/linkui~LinkUI#toolbarView} or
|
|
61
61
|
* {@link module:link/linkui~LinkUI#formView} if it is not.
|
|
62
62
|
*/
|
|
63
63
|
_createToolbarLinkImageButton() {
|
|
@@ -70,7 +70,7 @@ export default class LinkImageUI extends Plugin {
|
|
|
70
70
|
button.set({
|
|
71
71
|
isEnabled: true,
|
|
72
72
|
label: t('Link image'),
|
|
73
|
-
icon:
|
|
73
|
+
icon: IconLink,
|
|
74
74
|
keystroke: LINK_KEYSTROKE,
|
|
75
75
|
tooltip: true,
|
|
76
76
|
isToggleable: true
|
|
@@ -81,7 +81,7 @@ export default class LinkImageUI extends Plugin {
|
|
|
81
81
|
// Show the actionsView or formView (both from LinkUI) on button click depending on whether the image is linked already.
|
|
82
82
|
this.listenTo(button, 'execute', () => {
|
|
83
83
|
if (this._isSelectedLinkedImage(editor.model.document.selection)) {
|
|
84
|
-
plugin.
|
|
84
|
+
plugin._addToolbarView();
|
|
85
85
|
}
|
|
86
86
|
else {
|
|
87
87
|
plugin._showUI(true);
|