@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
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, icons } from '@ckeditor/ckeditor5-core/dist/index.js';
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 { upperFirst } from 'lodash-es';
10
- import { ClickObserver, Matcher } from '@ckeditor/ckeditor5-engine/dist/index.js';
11
- import { View, ViewCollection, FocusCycler, submitHandler, LabeledFieldView, createLabeledInputText, ButtonView, SwitchButtonView, ContextualBalloon, CssTransitionDisablerMixin, MenuBarMenuListItemButtonView, clickOutsideHandler } from '@ckeditor/ckeditor5-ui/dist/index.js';
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
- * Creates the bookmark callbacks for handling link opening experience.
266
- */ function createBookmarkCallbacks(editor) {
267
- const bookmarkEditing = editor.plugins.has('BookmarkEditing') ? editor.plugins.get('BookmarkEditing') : null;
268
- /**
269
- * Returns `true` when bookmark `id` matches the hash from `link`.
270
- */ function isScrollableToTarget(link) {
271
- return !!link && link.startsWith('#') && !!bookmarkEditing && !!bookmarkEditing.getElementForBookmarkId(link.slice(1));
272
- }
273
- /**
274
- * Scrolls the view to the desired bookmark or open a link in new window.
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
- */ execute(href, manualDecoratorIds = {}) {
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 linkText = extractTextFromSelection(selection);
415
- // Then update `linkHref` value.
416
- let linkRange = findAttributeRange(position, 'linkHref', selection.getAttribute('linkHref'), model);
417
- if (selection.getAttribute('linkHref') === linkText) {
418
- linkRange = this._updateLinkContent(model, writer, linkRange, href);
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 { end: positionAfter } = model.insertContent(writer.createText(href, attributes), position);
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
- writer.setSelection(positionAfter);
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 ranges = model.schema.getValidRanges(selection.getRanges(), 'linkHref');
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
- for (const range of rangesToUpdate){
470
- let linkRange = range;
471
- if (rangesToUpdate.length === 1) {
472
- // Current text of the link in the document.
473
- const linkText = extractTextFromSelection(selection);
474
- if (selection.getAttribute('linkHref') === linkText) {
475
- linkRange = this._updateLinkContent(model, writer, range, href);
476
- writer.setSelection(writer.createSelection(linkRange));
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
- // Returns a text of a link under the collapsed selection or a selection that contains the entire link.
535
- function extractTextFromSelection(selection) {
536
- if (selection.isCollapsed) {
537
- const firstPosition = selection.getFirstPosition();
538
- return firstPosition.textNode && firstPosition.textNode.data;
539
- } else {
540
- const rangeItems = Array.from(selection.getFirstRange().getItems());
541
- if (rangeItems.length > 1) {
542
- return null;
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
- const firstNode = rangeItems[0];
545
- if (firstNode.is('$text') || firstNode.is('$textProxy')) {
546
- return firstNode.data;
643
+ // Increment appropriate counter for the current operation.
644
+ if (action) {
645
+ counter[action]++;
547
646
  }
548
- return null;
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 bookmarkCallbacks = createBookmarkCallbacks(editor);
845
- function handleLinkOpening(url) {
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 form view controller class.
1078
+ * The link button class. Rendered as an `<a>` tag with link opening in a new tab.
944
1079
  *
945
- * See {@link module:link/ui/linkformview~LinkFormView}.
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 URL input view.
955
- */ urlInputView;
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 Cancel button view.
961
- */ cancelButtonView;
1129
+ * The "Displayed text" input view.
1130
+ */ displayedTextInputView;
962
1131
  /**
963
- * A collection of {@link module:ui/button/switchbuttonview~SwitchButtonView},
964
- * which corresponds to {@link module:link/linkcommand~LinkCommand#manualDecorators manual decorators}
965
- * configured in the editor.
966
- */ _manualDecoratorSwitches;
1132
+ * The URL input view.
1133
+ */ urlInputView;
967
1134
  /**
968
- * A collection of child views in the form.
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, linkCommand, validators){
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.saveButtonView = this._createButton(t('Save'), icons.check, 'ck-button-save');
993
- this.saveButtonView.type = 'submit';
994
- this.cancelButtonView = this._createButton(t('Cancel'), icons.cancel, 'ck-button-cancel', 'cancel');
995
- this._manualDecoratorSwitches = this._createManualDecoratorSwitches(linkCommand);
996
- this.children = this._createFormChildren(linkCommand.manualDecorators);
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: classList,
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.cancelButtonView
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 labeled input view.
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 labeledInput = new LabeledFieldView(this.locale, createLabeledInputText);
1102
- labeledInput.fieldView.inputMode = 'url';
1103
- labeledInput.label = t('Link URL');
1104
- return labeledInput;
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
- * @param label The button label.
1110
- * @param icon The button icon.
1111
- * @param className The additional button CSS class name.
1112
- * @param eventName An event name that the `ButtonView#execute` event will be delegated to.
1113
- * @returns The button view instance.
1114
- */ _createButton(label, icon, className, eventName) {
1115
- const button = new ButtonView(this.locale);
1116
- button.set({
1117
- label,
1118
- icon,
1119
- tooltip: true
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
- button.extendTemplate({
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: className
1304
+ class: [
1305
+ 'ck-link-form__providers-list'
1306
+ ]
1124
1307
  }
1125
1308
  });
1126
- if (eventName) {
1127
- button.delegate('execute').to(this, eventName);
1128
- }
1129
- return button;
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
- * Populates {@link module:ui/viewcollection~ViewCollection} of {@link module:ui/button/switchbuttonview~SwitchButtonView}
1133
- * made based on {@link module:link/linkcommand~LinkCommand#manualDecorators}.
1326
+ * Creates a labeled input view for the URL field.
1134
1327
  *
1135
- * @param linkCommand A reference to the link command.
1136
- * @returns ViewCollection of switch buttons.
1137
- */ _createManualDecoratorSwitches(linkCommand) {
1138
- const switches = this.createCollection();
1139
- for (const manualDecorator of linkCommand.manualDecorators){
1140
- const switchButton = new SwitchButtonView(this.locale);
1141
- switchButton.set({
1142
- name: manualDecorator.id,
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
- * If {@link module:link/linkcommand~LinkCommand#manualDecorators manual decorators} are configured in the editor, it creates an
1163
- * additional `View` wrapping all {@link #_manualDecoratorSwitches} switch buttons corresponding
1164
- * to these decorators.
1165
- *
1166
- * @param manualDecorators A reference to
1167
- * the collection of manual decorators stored in the link command.
1168
- * @returns The children of link form view.
1169
- */ _createFormChildren(manualDecorators) {
1170
- const children = this.createCollection();
1171
- children.add(this.urlInputView);
1172
- if (manualDecorators.length) {
1173
- const additionalButtonsView = new View();
1174
- additionalButtonsView.setTemplate({
1175
- tag: 'ul',
1176
- children: this._manualDecoratorSwitches.map((switchButton)=>({
1177
- tag: 'li',
1178
- children: [
1179
- switchButton
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 actions view class. This view displays the link preview, allows
1220
- * unlinking or editing the link.
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 actions.
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 href preview view.
1230
- */ previewButtonView;
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 unlink button view.
1233
- */ unlinkButtonView;
1393
+ * The view displayed when the list is empty.
1394
+ */ emptyListInformation;
1234
1395
  /**
1235
- * The edit link button view.
1236
- */ editButtonView;
1396
+ * A collection of child views.
1397
+ */ children;
1237
1398
  /**
1238
- * A collection of views that can be focused in the view.
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 view.
1402
+ * Helps cycling over {@link #_focusables} in the form.
1242
1403
  */ _focusCycler;
1243
- _linkConfig;
1244
- _options;
1245
1404
  /**
1246
- * @inheritDoc
1247
- */ constructor(locale, linkConfig = {}, options){
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
- const t = locale.t;
1250
- this._options = options;
1251
- this.previewButtonView = this._createPreviewButton();
1252
- this.unlinkButtonView = this._createButton(t('Unlink'), unlinkIcon, 'unlink');
1253
- this.editButtonView = this._createButton(t('Edit link'), icons.pencil, 'edit');
1254
- this.set('href', undefined);
1255
- this._linkConfig = linkConfig;
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-actions',
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.previewButtonView,
1291
- this.editButtonView,
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 actions.
1489
+ * Focuses the fist {@link #_focusables} in the form.
1312
1490
  */ focus() {
1313
1491
  this._focusCycler.focusFirst();
1314
1492
  }
1315
1493
  /**
1316
- * Creates a button view.
1317
- *
1318
- * @param label The button label.
1319
- * @param icon The button icon.
1320
- * @param eventName An event name that the `ButtonView#execute` event will be delegated to.
1321
- * @returns The button view instance.
1322
- */ _createButton(label, icon, eventName) {
1323
- const button = new ButtonView(this.locale);
1324
- button.set({
1325
- label,
1326
- icon,
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
- button.delegate('execute').to(this, eventName);
1330
- return button;
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
- * Creates a link href preview button.
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
- * @returns The button view instance.
1336
- */ _createPreviewButton() {
1337
- const button = new ButtonView(this.locale);
1338
- const bind = this.bindTemplate;
1339
- const t = this.t;
1340
- button.set({
1341
- withText: true
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
- button.extendTemplate({
1608
+ this.setTemplate({
1609
+ tag: 'div',
1344
1610
  attributes: {
1345
1611
  class: [
1346
1612
  'ck',
1347
- 'ck-link-actions__preview'
1613
+ 'ck-link-properties'
1348
1614
  ],
1349
- href: bind.to('href', (href)=>href && ensureSafeUrl(href, this._linkConfig.allowedProtocols)),
1350
- target: '_blank',
1351
- rel: 'noopener noreferrer'
1615
+ // https://github.com/ckeditor/ckeditor5-link/issues/90
1616
+ tabindex: '-1'
1352
1617
  },
1353
- on: {
1354
- click: bind.to((evt)=>{
1355
- if (this._options && this._options.isScrollableToTarget(this.href)) {
1356
- evt.preventDefault();
1357
- this._options.scrollToTarget(this.href);
1358
- } else {
1359
- openLink(this.href);
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
- button.bind('tooltip').to(this, 'href', (href)=>{
1365
- if (this._options && this._options.isScrollableToTarget(href)) {
1366
- return t('Scroll to target');
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
- button.bind('label').to(this, 'href', (href)=>{
1371
- return href || t('This link has no URL');
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
- button.bind('isEnabled').to(this, 'href', (href)=>!!href);
1374
- button.template.tag = 'a';
1375
- return button;
1696
+ return listView;
1376
1697
  }
1377
1698
  }
1378
1699
 
1379
- var linkIcon = "<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-.184z\"/></svg>";
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 actions view displayed inside of the balloon.
1390
- */ actionsView = null;
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._createToolbarLinkButton();
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.actionsView) {
1480
- this.actionsView.destroy();
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
- this.actionsView = this._createActionsView();
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 {@link module:link/ui/linkactionsview~LinkActionsView} instance.
1493
- */ _createActionsView() {
1881
+ * Creates the ToolbarView instance.
1882
+ */ _createToolbarView() {
1494
1883
  const editor = this.editor;
1495
- const actionsView = new LinkActionsView(editor.locale, editor.config.get('link'), createBookmarkCallbacks(editor));
1884
+ const toolbarView = new ToolbarView(editor.locale);
1496
1885
  const linkCommand = editor.commands.get('link');
1497
- const unlinkCommand = editor.commands.get('unlink');
1498
- actionsView.bind('href').to(linkCommand, 'value');
1499
- actionsView.editButtonView.bind('isEnabled').to(linkCommand);
1500
- actionsView.unlinkButtonView.bind('isEnabled').to(unlinkCommand);
1501
- // Execute unlink command after clicking on the "Edit" button.
1502
- this.listenTo(actionsView, 'edit', ()=>{
1503
- this._addFormView();
1504
- });
1505
- // Execute unlink command after clicking on the "Unlink" button.
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 **actions have focus**..
1516
- actionsView.keystrokes.set(LINK_KEYSTROKE, (data, cancel)=>{
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
- return actionsView;
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, linkCommand, getFormValidators(editor));
1529
- formView.urlInputView.fieldView.bind('value').to(linkCommand, 'value');
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 { value } = formView.urlInputView.fieldView.element;
1538
- const parsedUrl = addLinkProtocolIfApplicable(value, defaultProtocol);
1539
- editor.execute('link', parsedUrl, formView.getDecoratorSwitchesState());
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 toolbar Link button. Clicking this button will show
1560
- * a {@link #_balloon} attached to the selection.
1561
- */ _createToolbarLinkButton() {
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: linkIcon,
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', ()=>this._showUI(true));
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._areActionsVisible && !this.actionsView.focusTracker.isFocused) {
1629
- this.actionsView.focus();
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 #actionsView} to the {@link #_balloon}.
2271
+ * Adds the {@link #toolbarView} to the {@link #_balloon}.
1657
2272
  *
1658
2273
  * @internal
1659
- */ _addActionsView() {
1660
- if (!this.actionsView) {
2274
+ */ _addToolbarView() {
2275
+ if (!this.toolbarView) {
1661
2276
  this._createViews();
1662
2277
  }
1663
- if (this._areActionsInPanel) {
2278
+ if (this._isToolbarInPanel) {
1664
2279
  return;
1665
2280
  }
1666
2281
  this._balloon.add({
1667
- view: this.actionsView,
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 editor = this.editor;
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 URL field remains in sync with the value of
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
- // Restore manual decorator states to represent the current model state. This case is important to reset the switch buttons
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 the URL field to update the state of the submit button.
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 #actionsView}.
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._addActionsView();
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 actions are already visible.
1755
- if (this._areActionsVisible) {
2421
+ // Go to the editing UI if toolbar is already visible.
2422
+ if (this._isToolbarVisible) {
1756
2423
  this._addFormView();
1757
2424
  } else {
1758
- this._addActionsView();
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 #_addActionsView}.
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
- editor.editing.view.focus();
1782
- // Remove form first because it's on top of the stack.
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
- // Then remove the actions view because it's beneath the form.
1785
- this._balloon.remove(this.actionsView);
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 actions then pressing SHIFT+Right arrow).
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 #actionsView} is in the {@link #_balloon}.
1835
- */ get _areActionsInPanel() {
1836
- return !!this.actionsView && this._balloon.hasView(this.actionsView);
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 #actionsView} is in the {@link #_balloon} and it is
2524
+ * Returns `true` when {@link #propertiesView} is in the {@link #_balloon} and it is
1840
2525
  * currently visible.
1841
- */ get _areActionsVisible() {
1842
- return !!this.actionsView && this._balloon.visibleView === this.actionsView;
2526
+ */ get _isPropertiesVisible() {
2527
+ return !!this.propertiesView && this._balloon.visibleView === this.propertiesView;
1843
2528
  }
1844
2529
  /**
1845
- * Returns `true` when {@link #actionsView} or {@link #formView} is in the {@link #_balloon}.
1846
- */ get _isUIInPanel() {
1847
- return this._isFormInPanel || this._areActionsInPanel;
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 #actionsView} or {@link #formView} is in the {@link #_balloon} and it is
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
- const visibleView = this._balloon.visibleView;
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
- let target;
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 = Array.from(this.editor.editing.mapper.markerNameToElements(VISUAL_SELECTION_MARKER_NAME));
1870
- const newRange = view.createRange(view.createPositionBefore(markerViewElements[0]), view.createPositionAfter(markerViewElements[markerViewElements.length - 1]));
1871
- target = view.domConverter.viewRangeToDom(newRange);
1872
- } else {
1873
- // Make sure the target is calculated on demand at the last moment because a cached DOM range
1874
- // (which is very fragile) can desynchronize with the state of the editing view if there was
1875
- // any rendering done in the meantime. This can happen, for instance, when an inline widget
1876
- // gets unlinked.
1877
- target = ()=>{
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})' + ')' + ')' + // port number (optional)
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#actionsView} or
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: linkIcon,
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._addActionsView();
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, LinkActionsView, LinkCommand, LinkEditing, LinkFormView, LinkImage, LinkImageEditing, LinkImageUI, LinkUI, UnlinkCommand, addLinkProtocolIfApplicable, ensureSafeUrl, isLinkableElement };
3367
+ export { AutoLink, Link, LinkCommand, LinkEditing, LinkFormView, LinkImage, LinkImageEditing, LinkImageUI, LinkUI, UnlinkCommand, addLinkProtocolIfApplicable, ensureSafeUrl, isLinkableElement };
2632
3368
  //# sourceMappingURL=index.js.map