@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.
Files changed (337) hide show
  1. package/LICENSE.md +1 -1
  2. package/build/link.js +2 -2
  3. package/build/translations/af.js +1 -1
  4. package/build/translations/ar.js +1 -1
  5. package/build/translations/ast.js +1 -1
  6. package/build/translations/az.js +1 -1
  7. package/build/translations/be.js +1 -0
  8. package/build/translations/bg.js +1 -1
  9. package/build/translations/bn.js +1 -1
  10. package/build/translations/bs.js +1 -1
  11. package/build/translations/ca.js +1 -1
  12. package/build/translations/cs.js +1 -1
  13. package/build/translations/da.js +1 -1
  14. package/build/translations/de-ch.js +1 -1
  15. package/build/translations/de.js +1 -1
  16. package/build/translations/el.js +1 -1
  17. package/build/translations/en-au.js +1 -1
  18. package/build/translations/en-gb.js +1 -1
  19. package/build/translations/eo.js +1 -1
  20. package/build/translations/es-co.js +1 -1
  21. package/build/translations/es.js +1 -1
  22. package/build/translations/et.js +1 -1
  23. package/build/translations/eu.js +1 -1
  24. package/build/translations/fa.js +1 -1
  25. package/build/translations/fi.js +1 -1
  26. package/build/translations/fr.js +1 -1
  27. package/build/translations/gl.js +1 -1
  28. package/build/translations/gu.js +1 -1
  29. package/build/translations/he.js +1 -1
  30. package/build/translations/hi.js +1 -1
  31. package/build/translations/hr.js +1 -1
  32. package/build/translations/hu.js +1 -1
  33. package/build/translations/hy.js +1 -1
  34. package/build/translations/id.js +1 -1
  35. package/build/translations/it.js +1 -1
  36. package/build/translations/ja.js +1 -1
  37. package/build/translations/jv.js +1 -1
  38. package/build/translations/kk.js +1 -1
  39. package/build/translations/km.js +1 -1
  40. package/build/translations/kn.js +1 -1
  41. package/build/translations/ko.js +1 -1
  42. package/build/translations/ku.js +1 -1
  43. package/build/translations/lt.js +1 -1
  44. package/build/translations/lv.js +1 -1
  45. package/build/translations/ms.js +1 -1
  46. package/build/translations/nb.js +1 -1
  47. package/build/translations/ne.js +1 -1
  48. package/build/translations/nl.js +1 -1
  49. package/build/translations/no.js +1 -1
  50. package/build/translations/oc.js +1 -1
  51. package/build/translations/pl.js +1 -1
  52. package/build/translations/pt-br.js +1 -1
  53. package/build/translations/pt.js +1 -1
  54. package/build/translations/ro.js +1 -1
  55. package/build/translations/ru.js +1 -1
  56. package/build/translations/si.js +1 -1
  57. package/build/translations/sk.js +1 -1
  58. package/build/translations/sl.js +1 -1
  59. package/build/translations/sq.js +1 -1
  60. package/build/translations/sr-latn.js +1 -1
  61. package/build/translations/sr.js +1 -1
  62. package/build/translations/sv.js +1 -1
  63. package/build/translations/th.js +1 -1
  64. package/build/translations/ti.js +1 -1
  65. package/build/translations/tk.js +1 -1
  66. package/build/translations/tr.js +1 -1
  67. package/build/translations/tt.js +1 -1
  68. package/build/translations/ug.js +1 -1
  69. package/build/translations/uk.js +1 -1
  70. package/build/translations/ur.js +1 -1
  71. package/build/translations/uz.js +1 -1
  72. package/build/translations/vi.js +1 -1
  73. package/build/translations/zh-cn.js +1 -1
  74. package/build/translations/zh.js +1 -1
  75. package/ckeditor5-metadata.json +2 -2
  76. package/dist/index-editor.css +87 -47
  77. package/dist/index.css +108 -58
  78. package/dist/index.css.map +1 -1
  79. package/dist/index.js +1161 -425
  80. package/dist/index.js.map +1 -1
  81. package/dist/translations/af.js +1 -1
  82. package/dist/translations/af.umd.js +1 -1
  83. package/dist/translations/ar.js +1 -1
  84. package/dist/translations/ar.umd.js +1 -1
  85. package/dist/translations/ast.js +1 -1
  86. package/dist/translations/ast.umd.js +1 -1
  87. package/dist/translations/az.js +1 -1
  88. package/dist/translations/az.umd.js +1 -1
  89. package/dist/translations/be.d.ts +8 -0
  90. package/dist/translations/be.js +5 -0
  91. package/dist/translations/be.umd.js +11 -0
  92. package/dist/translations/bg.js +1 -1
  93. package/dist/translations/bg.umd.js +1 -1
  94. package/dist/translations/bn.js +1 -1
  95. package/dist/translations/bn.umd.js +1 -1
  96. package/dist/translations/bs.js +1 -1
  97. package/dist/translations/bs.umd.js +1 -1
  98. package/dist/translations/ca.js +1 -1
  99. package/dist/translations/ca.umd.js +1 -1
  100. package/dist/translations/cs.js +1 -1
  101. package/dist/translations/cs.umd.js +1 -1
  102. package/dist/translations/da.js +1 -1
  103. package/dist/translations/da.umd.js +1 -1
  104. package/dist/translations/de-ch.js +1 -1
  105. package/dist/translations/de-ch.umd.js +1 -1
  106. package/dist/translations/de.js +1 -1
  107. package/dist/translations/de.umd.js +1 -1
  108. package/dist/translations/el.js +1 -1
  109. package/dist/translations/el.umd.js +1 -1
  110. package/dist/translations/en-au.js +1 -1
  111. package/dist/translations/en-au.umd.js +1 -1
  112. package/dist/translations/en-gb.js +1 -1
  113. package/dist/translations/en-gb.umd.js +1 -1
  114. package/dist/translations/en.js +1 -1
  115. package/dist/translations/en.umd.js +1 -1
  116. package/dist/translations/eo.js +1 -1
  117. package/dist/translations/eo.umd.js +1 -1
  118. package/dist/translations/es-co.js +1 -1
  119. package/dist/translations/es-co.umd.js +1 -1
  120. package/dist/translations/es.js +1 -1
  121. package/dist/translations/es.umd.js +1 -1
  122. package/dist/translations/et.js +1 -1
  123. package/dist/translations/et.umd.js +1 -1
  124. package/dist/translations/eu.js +1 -1
  125. package/dist/translations/eu.umd.js +1 -1
  126. package/dist/translations/fa.js +1 -1
  127. package/dist/translations/fa.umd.js +1 -1
  128. package/dist/translations/fi.js +1 -1
  129. package/dist/translations/fi.umd.js +1 -1
  130. package/dist/translations/fr.js +1 -1
  131. package/dist/translations/fr.umd.js +1 -1
  132. package/dist/translations/gl.js +1 -1
  133. package/dist/translations/gl.umd.js +1 -1
  134. package/dist/translations/gu.js +1 -1
  135. package/dist/translations/gu.umd.js +1 -1
  136. package/dist/translations/he.js +1 -1
  137. package/dist/translations/he.umd.js +1 -1
  138. package/dist/translations/hi.js +1 -1
  139. package/dist/translations/hi.umd.js +1 -1
  140. package/dist/translations/hr.js +1 -1
  141. package/dist/translations/hr.umd.js +1 -1
  142. package/dist/translations/hu.js +1 -1
  143. package/dist/translations/hu.umd.js +1 -1
  144. package/dist/translations/hy.js +1 -1
  145. package/dist/translations/hy.umd.js +1 -1
  146. package/dist/translations/id.js +1 -1
  147. package/dist/translations/id.umd.js +1 -1
  148. package/dist/translations/it.js +1 -1
  149. package/dist/translations/it.umd.js +1 -1
  150. package/dist/translations/ja.js +1 -1
  151. package/dist/translations/ja.umd.js +1 -1
  152. package/dist/translations/jv.js +1 -1
  153. package/dist/translations/jv.umd.js +1 -1
  154. package/dist/translations/kk.js +1 -1
  155. package/dist/translations/kk.umd.js +1 -1
  156. package/dist/translations/km.js +1 -1
  157. package/dist/translations/km.umd.js +1 -1
  158. package/dist/translations/kn.js +1 -1
  159. package/dist/translations/kn.umd.js +1 -1
  160. package/dist/translations/ko.js +1 -1
  161. package/dist/translations/ko.umd.js +1 -1
  162. package/dist/translations/ku.js +1 -1
  163. package/dist/translations/ku.umd.js +1 -1
  164. package/dist/translations/lt.js +1 -1
  165. package/dist/translations/lt.umd.js +1 -1
  166. package/dist/translations/lv.js +1 -1
  167. package/dist/translations/lv.umd.js +1 -1
  168. package/dist/translations/ms.js +1 -1
  169. package/dist/translations/ms.umd.js +1 -1
  170. package/dist/translations/nb.js +1 -1
  171. package/dist/translations/nb.umd.js +1 -1
  172. package/dist/translations/ne.js +1 -1
  173. package/dist/translations/ne.umd.js +1 -1
  174. package/dist/translations/nl.js +1 -1
  175. package/dist/translations/nl.umd.js +1 -1
  176. package/dist/translations/no.js +1 -1
  177. package/dist/translations/no.umd.js +1 -1
  178. package/dist/translations/oc.js +1 -1
  179. package/dist/translations/oc.umd.js +1 -1
  180. package/dist/translations/pl.js +1 -1
  181. package/dist/translations/pl.umd.js +1 -1
  182. package/dist/translations/pt-br.js +1 -1
  183. package/dist/translations/pt-br.umd.js +1 -1
  184. package/dist/translations/pt.js +1 -1
  185. package/dist/translations/pt.umd.js +1 -1
  186. package/dist/translations/ro.js +1 -1
  187. package/dist/translations/ro.umd.js +1 -1
  188. package/dist/translations/ru.js +1 -1
  189. package/dist/translations/ru.umd.js +1 -1
  190. package/dist/translations/si.js +1 -1
  191. package/dist/translations/si.umd.js +1 -1
  192. package/dist/translations/sk.js +1 -1
  193. package/dist/translations/sk.umd.js +1 -1
  194. package/dist/translations/sl.js +1 -1
  195. package/dist/translations/sl.umd.js +1 -1
  196. package/dist/translations/sq.js +1 -1
  197. package/dist/translations/sq.umd.js +1 -1
  198. package/dist/translations/sr-latn.js +1 -1
  199. package/dist/translations/sr-latn.umd.js +1 -1
  200. package/dist/translations/sr.js +1 -1
  201. package/dist/translations/sr.umd.js +1 -1
  202. package/dist/translations/sv.js +1 -1
  203. package/dist/translations/sv.umd.js +1 -1
  204. package/dist/translations/th.js +1 -1
  205. package/dist/translations/th.umd.js +1 -1
  206. package/dist/translations/ti.js +1 -1
  207. package/dist/translations/ti.umd.js +1 -1
  208. package/dist/translations/tk.js +1 -1
  209. package/dist/translations/tk.umd.js +1 -1
  210. package/dist/translations/tr.js +1 -1
  211. package/dist/translations/tr.umd.js +1 -1
  212. package/dist/translations/tt.js +1 -1
  213. package/dist/translations/tt.umd.js +1 -1
  214. package/dist/translations/ug.js +1 -1
  215. package/dist/translations/ug.umd.js +1 -1
  216. package/dist/translations/uk.js +1 -1
  217. package/dist/translations/uk.umd.js +1 -1
  218. package/dist/translations/ur.js +1 -1
  219. package/dist/translations/ur.umd.js +1 -1
  220. package/dist/translations/uz.js +1 -1
  221. package/dist/translations/uz.umd.js +1 -1
  222. package/dist/translations/vi.js +1 -1
  223. package/dist/translations/vi.umd.js +1 -1
  224. package/dist/translations/zh-cn.js +1 -1
  225. package/dist/translations/zh-cn.umd.js +1 -1
  226. package/dist/translations/zh.js +1 -1
  227. package/dist/translations/zh.umd.js +1 -1
  228. package/lang/contexts.json +4 -3
  229. package/lang/translations/af.po +10 -6
  230. package/lang/translations/ar.po +11 -7
  231. package/lang/translations/ast.po +10 -6
  232. package/lang/translations/az.po +10 -6
  233. package/lang/translations/be.po +68 -0
  234. package/lang/translations/bg.po +11 -7
  235. package/lang/translations/bn.po +11 -7
  236. package/lang/translations/bs.po +10 -6
  237. package/lang/translations/ca.po +11 -7
  238. package/lang/translations/cs.po +11 -7
  239. package/lang/translations/da.po +11 -7
  240. package/lang/translations/de-ch.po +10 -6
  241. package/lang/translations/de.po +11 -7
  242. package/lang/translations/el.po +11 -7
  243. package/lang/translations/en-au.po +11 -7
  244. package/lang/translations/en-gb.po +11 -7
  245. package/lang/translations/en.po +11 -7
  246. package/lang/translations/eo.po +10 -6
  247. package/lang/translations/es-co.po +10 -6
  248. package/lang/translations/es.po +11 -7
  249. package/lang/translations/et.po +11 -7
  250. package/lang/translations/eu.po +10 -6
  251. package/lang/translations/fa.po +10 -6
  252. package/lang/translations/fi.po +11 -7
  253. package/lang/translations/fr.po +11 -7
  254. package/lang/translations/gl.po +10 -6
  255. package/lang/translations/gu.po +10 -6
  256. package/lang/translations/he.po +11 -7
  257. package/lang/translations/hi.po +11 -7
  258. package/lang/translations/hr.po +10 -6
  259. package/lang/translations/hu.po +11 -7
  260. package/lang/translations/hy.po +10 -6
  261. package/lang/translations/id.po +11 -7
  262. package/lang/translations/it.po +11 -7
  263. package/lang/translations/ja.po +11 -7
  264. package/lang/translations/jv.po +10 -6
  265. package/lang/translations/kk.po +10 -6
  266. package/lang/translations/km.po +10 -6
  267. package/lang/translations/kn.po +10 -6
  268. package/lang/translations/ko.po +11 -7
  269. package/lang/translations/ku.po +10 -6
  270. package/lang/translations/lt.po +11 -7
  271. package/lang/translations/lv.po +11 -7
  272. package/lang/translations/ms.po +11 -7
  273. package/lang/translations/nb.po +10 -6
  274. package/lang/translations/ne.po +10 -6
  275. package/lang/translations/nl.po +11 -7
  276. package/lang/translations/no.po +11 -7
  277. package/lang/translations/oc.po +10 -6
  278. package/lang/translations/pl.po +11 -7
  279. package/lang/translations/pt-br.po +11 -7
  280. package/lang/translations/pt.po +11 -7
  281. package/lang/translations/ro.po +11 -7
  282. package/lang/translations/ru.po +11 -7
  283. package/lang/translations/si.po +10 -6
  284. package/lang/translations/sk.po +11 -7
  285. package/lang/translations/sl.po +10 -6
  286. package/lang/translations/sq.po +10 -6
  287. package/lang/translations/sr-latn.po +10 -6
  288. package/lang/translations/sr.po +11 -7
  289. package/lang/translations/sv.po +11 -7
  290. package/lang/translations/th.po +11 -7
  291. package/lang/translations/ti.po +10 -6
  292. package/lang/translations/tk.po +10 -6
  293. package/lang/translations/tr.po +11 -7
  294. package/lang/translations/tt.po +10 -6
  295. package/lang/translations/ug.po +10 -6
  296. package/lang/translations/uk.po +11 -7
  297. package/lang/translations/ur.po +10 -6
  298. package/lang/translations/uz.po +10 -6
  299. package/lang/translations/vi.po +11 -7
  300. package/lang/translations/zh-cn.po +11 -7
  301. package/lang/translations/zh.po +11 -7
  302. package/package.json +12 -12
  303. package/src/autolink.js +3 -0
  304. package/src/index.d.ts +1 -2
  305. package/src/index.js +0 -1
  306. package/src/linkcommand.d.ts +17 -10
  307. package/src/linkcommand.js +212 -82
  308. package/src/linkconfig.d.ts +28 -0
  309. package/src/linkediting.d.ts +18 -0
  310. package/src/linkediting.js +19 -9
  311. package/src/linkimageui.d.ts +1 -1
  312. package/src/linkimageui.js +4 -4
  313. package/src/linkui.d.ts +215 -24
  314. package/src/linkui.js +517 -109
  315. package/src/ui/linkbuttonview.d.ts +31 -0
  316. package/src/ui/linkbuttonview.js +54 -0
  317. package/src/ui/linkformview.d.ts +34 -49
  318. package/src/ui/linkformview.js +163 -134
  319. package/src/ui/linkpreviewbuttonview.d.ts +35 -0
  320. package/src/ui/linkpreviewbuttonview.js +43 -0
  321. package/src/ui/linkpropertiesview.d.ts +88 -0
  322. package/src/ui/linkpropertiesview.js +170 -0
  323. package/src/ui/linkprovideritemsview.d.ts +114 -0
  324. package/src/ui/linkprovideritemsview.js +207 -0
  325. package/src/utils/automaticdecorators.js +5 -7
  326. package/src/utils/manualdecorator.js +27 -0
  327. package/src/utils.d.ts +5 -5
  328. package/src/utils.js +12 -32
  329. package/theme/linkform.css +11 -33
  330. package/theme/linkproperties.css +4 -0
  331. package/theme/linkprovideritems.css +18 -0
  332. package/theme/linktoolbar.css +12 -0
  333. package/src/ui/linkactionsview.d.ts +0 -117
  334. package/src/ui/linkactionsview.js +0 -173
  335. package/theme/icons/link.svg +0 -1
  336. package/theme/icons/unlink.svg +0 -1
  337. package/theme/linkactions.css +0 -32
@@ -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
- constructor() {
18
- super(...arguments);
19
- /**
20
- * A collection of {@link module:link/utils/manualdecorator~ManualDecorator manual decorators}
21
- * corresponding to the {@link module:link/linkconfig~LinkConfig#decorators decorator configuration}.
22
- *
23
- * You can consider it a model with states of manual decorators added to the currently selected link.
24
- */
25
- this.manualDecorators = new Collection();
26
- /**
27
- * An instance of the helper that ties together all {@link module:link/linkconfig~LinkDecoratorAutomaticDefinition}
28
- * that are used by the {@glink features/link link} and the {@glink features/images/images-linking linking images} features.
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 linkText = extractTextFromSelection(selection);
147
- // Then update `linkHref` value.
148
- let linkRange = findAttributeRange(position, 'linkHref', selection.getAttribute('linkHref'), model);
149
- if (selection.getAttribute('linkHref') === linkText) {
150
- linkRange = this._updateLinkContent(model, writer, linkRange, href);
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 { end: positionAfter } = model.insertContent(writer.createText(href, attributes), position);
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
- writer.setSelection(positionAfter);
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 ranges = model.schema.getValidRanges(selection.getRanges(), 'linkHref');
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
- for (const range of rangesToUpdate) {
203
- let linkRange = range;
204
- if (rangesToUpdate.length === 1) {
205
- // Current text of the link in the document.
206
- const linkText = extractTextFromSelection(selection);
207
- if (selection.getAttribute('linkHref') === linkText) {
208
- linkRange = this._updateLinkContent(model, writer, range, href);
209
- writer.setSelection(writer.createSelection(linkRange));
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
- * Updates selected link with a new value as its content and as its href attribute.
257
- *
258
- * @param model Model is need to insert content.
259
- * @param writer Writer is need to create text element in model.
260
- * @param range A range where should be inserted content.
261
- * @param href A link value which should be in the href attribute and in the content.
262
- */
263
- _updateLinkContent(model, writer, range, href) {
264
- const text = writer.createText(href, { linkHref: href });
265
- return model.insertContent(text, range);
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
- // Returns a text of a link under the collapsed selection or a selection that contains the entire link.
269
- function extractTextFromSelection(selection) {
270
- if (selection.isCollapsed) {
271
- const firstPosition = selection.getFirstPosition();
272
- return firstPosition.textNode && firstPosition.textNode.data;
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
- const rangeItems = Array.from(selection.getFirstRange().getItems());
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
  }
@@ -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:
@@ -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 {};
@@ -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, createBookmarkCallbacks, openLink } from './utils.js';
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 bookmarkCallbacks = createBookmarkCallbacks(editor);
197
- function handleLinkOpening(url) {
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) {
@@ -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#actionsView} or
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;
@@ -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#actionsView} or
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: linkIcon,
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._addActionsView();
84
+ plugin._addToolbarView();
85
85
  }
86
86
  else {
87
87
  plugin._showUI(true);