@ckeditor/ckeditor5-restricted-editing 47.1.0 → 47.2.0-alpha.1

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 (313) hide show
  1. package/README.md +1 -1
  2. package/build/restricted-editing.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 -1
  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 +20 -2
  76. package/dist/index.js +564 -105
  77. package/dist/index.js.map +1 -1
  78. package/dist/translations/af.js +1 -1
  79. package/dist/translations/af.umd.js +1 -1
  80. package/dist/translations/ar.js +1 -1
  81. package/dist/translations/ar.umd.js +1 -1
  82. package/dist/translations/ast.js +1 -1
  83. package/dist/translations/ast.umd.js +1 -1
  84. package/dist/translations/az.js +1 -1
  85. package/dist/translations/az.umd.js +1 -1
  86. package/dist/translations/be.js +1 -1
  87. package/dist/translations/be.umd.js +1 -1
  88. package/dist/translations/bg.js +1 -1
  89. package/dist/translations/bg.umd.js +1 -1
  90. package/dist/translations/bn.js +1 -1
  91. package/dist/translations/bn.umd.js +1 -1
  92. package/dist/translations/bs.js +1 -1
  93. package/dist/translations/bs.umd.js +1 -1
  94. package/dist/translations/ca.js +1 -1
  95. package/dist/translations/ca.umd.js +1 -1
  96. package/dist/translations/cs.js +1 -1
  97. package/dist/translations/cs.umd.js +1 -1
  98. package/dist/translations/da.js +1 -1
  99. package/dist/translations/da.umd.js +1 -1
  100. package/dist/translations/de-ch.js +1 -1
  101. package/dist/translations/de-ch.umd.js +1 -1
  102. package/dist/translations/de.js +1 -1
  103. package/dist/translations/de.umd.js +1 -1
  104. package/dist/translations/el.js +1 -1
  105. package/dist/translations/el.umd.js +1 -1
  106. package/dist/translations/en-au.js +1 -1
  107. package/dist/translations/en-au.umd.js +1 -1
  108. package/dist/translations/en-gb.js +1 -1
  109. package/dist/translations/en-gb.umd.js +1 -1
  110. package/dist/translations/en.js +1 -1
  111. package/dist/translations/en.umd.js +1 -1
  112. package/dist/translations/eo.js +1 -1
  113. package/dist/translations/eo.umd.js +1 -1
  114. package/dist/translations/es-co.js +1 -1
  115. package/dist/translations/es-co.umd.js +1 -1
  116. package/dist/translations/es.js +1 -1
  117. package/dist/translations/es.umd.js +1 -1
  118. package/dist/translations/et.js +1 -1
  119. package/dist/translations/et.umd.js +1 -1
  120. package/dist/translations/eu.js +1 -1
  121. package/dist/translations/eu.umd.js +1 -1
  122. package/dist/translations/fa.js +1 -1
  123. package/dist/translations/fa.umd.js +1 -1
  124. package/dist/translations/fi.js +1 -1
  125. package/dist/translations/fi.umd.js +1 -1
  126. package/dist/translations/fr.js +1 -1
  127. package/dist/translations/fr.umd.js +1 -1
  128. package/dist/translations/gl.js +1 -1
  129. package/dist/translations/gl.umd.js +1 -1
  130. package/dist/translations/gu.js +1 -1
  131. package/dist/translations/gu.umd.js +1 -1
  132. package/dist/translations/he.js +1 -1
  133. package/dist/translations/he.umd.js +1 -1
  134. package/dist/translations/hi.js +1 -1
  135. package/dist/translations/hi.umd.js +1 -1
  136. package/dist/translations/hr.js +1 -1
  137. package/dist/translations/hr.umd.js +1 -1
  138. package/dist/translations/hu.js +1 -1
  139. package/dist/translations/hu.umd.js +1 -1
  140. package/dist/translations/hy.js +1 -1
  141. package/dist/translations/hy.umd.js +1 -1
  142. package/dist/translations/id.js +1 -1
  143. package/dist/translations/id.umd.js +1 -1
  144. package/dist/translations/it.js +1 -1
  145. package/dist/translations/it.umd.js +1 -1
  146. package/dist/translations/ja.js +1 -1
  147. package/dist/translations/ja.umd.js +1 -1
  148. package/dist/translations/jv.js +1 -1
  149. package/dist/translations/jv.umd.js +1 -1
  150. package/dist/translations/kk.js +1 -1
  151. package/dist/translations/kk.umd.js +1 -1
  152. package/dist/translations/km.js +1 -1
  153. package/dist/translations/km.umd.js +1 -1
  154. package/dist/translations/kn.js +1 -1
  155. package/dist/translations/kn.umd.js +1 -1
  156. package/dist/translations/ko.js +1 -1
  157. package/dist/translations/ko.umd.js +1 -1
  158. package/dist/translations/ku.js +1 -1
  159. package/dist/translations/ku.umd.js +1 -1
  160. package/dist/translations/lt.js +1 -1
  161. package/dist/translations/lt.umd.js +1 -1
  162. package/dist/translations/lv.js +1 -1
  163. package/dist/translations/lv.umd.js +1 -1
  164. package/dist/translations/ms.js +1 -1
  165. package/dist/translations/ms.umd.js +1 -1
  166. package/dist/translations/nb.js +1 -1
  167. package/dist/translations/nb.umd.js +1 -1
  168. package/dist/translations/ne.js +1 -1
  169. package/dist/translations/ne.umd.js +1 -1
  170. package/dist/translations/nl.js +1 -1
  171. package/dist/translations/nl.umd.js +1 -1
  172. package/dist/translations/no.js +1 -1
  173. package/dist/translations/no.umd.js +1 -1
  174. package/dist/translations/oc.js +1 -1
  175. package/dist/translations/oc.umd.js +1 -1
  176. package/dist/translations/pl.js +1 -1
  177. package/dist/translations/pl.umd.js +1 -1
  178. package/dist/translations/pt-br.js +1 -1
  179. package/dist/translations/pt-br.umd.js +1 -1
  180. package/dist/translations/pt.js +1 -1
  181. package/dist/translations/pt.umd.js +1 -1
  182. package/dist/translations/ro.js +1 -1
  183. package/dist/translations/ro.umd.js +1 -1
  184. package/dist/translations/ru.js +1 -1
  185. package/dist/translations/ru.umd.js +1 -1
  186. package/dist/translations/si.js +1 -1
  187. package/dist/translations/si.umd.js +1 -1
  188. package/dist/translations/sk.js +1 -1
  189. package/dist/translations/sk.umd.js +1 -1
  190. package/dist/translations/sl.js +1 -1
  191. package/dist/translations/sl.umd.js +1 -1
  192. package/dist/translations/sq.js +1 -1
  193. package/dist/translations/sq.umd.js +1 -1
  194. package/dist/translations/sr-latn.js +1 -1
  195. package/dist/translations/sr-latn.umd.js +1 -1
  196. package/dist/translations/sr.js +1 -1
  197. package/dist/translations/sr.umd.js +1 -1
  198. package/dist/translations/sv.js +1 -1
  199. package/dist/translations/sv.umd.js +1 -1
  200. package/dist/translations/th.js +1 -1
  201. package/dist/translations/th.umd.js +1 -1
  202. package/dist/translations/ti.js +1 -1
  203. package/dist/translations/ti.umd.js +1 -1
  204. package/dist/translations/tk.js +1 -1
  205. package/dist/translations/tk.umd.js +1 -1
  206. package/dist/translations/tr.js +1 -1
  207. package/dist/translations/tr.umd.js +1 -1
  208. package/dist/translations/tt.js +1 -1
  209. package/dist/translations/tt.umd.js +1 -1
  210. package/dist/translations/ug.js +1 -1
  211. package/dist/translations/ug.umd.js +1 -1
  212. package/dist/translations/uk.js +1 -1
  213. package/dist/translations/uk.umd.js +1 -1
  214. package/dist/translations/ur.js +1 -1
  215. package/dist/translations/ur.umd.js +1 -1
  216. package/dist/translations/uz.js +1 -1
  217. package/dist/translations/uz.umd.js +1 -1
  218. package/dist/translations/vi.js +1 -1
  219. package/dist/translations/vi.umd.js +1 -1
  220. package/dist/translations/zh-cn.js +1 -1
  221. package/dist/translations/zh-cn.umd.js +1 -1
  222. package/dist/translations/zh.js +1 -1
  223. package/dist/translations/zh.umd.js +1 -1
  224. package/lang/contexts.json +4 -1
  225. package/lang/translations/af.po +16 -4
  226. package/lang/translations/ar.po +16 -4
  227. package/lang/translations/ast.po +16 -4
  228. package/lang/translations/az.po +16 -4
  229. package/lang/translations/be.po +16 -4
  230. package/lang/translations/bg.po +16 -4
  231. package/lang/translations/bn.po +16 -4
  232. package/lang/translations/bs.po +16 -4
  233. package/lang/translations/ca.po +16 -4
  234. package/lang/translations/cs.po +16 -4
  235. package/lang/translations/da.po +16 -4
  236. package/lang/translations/de-ch.po +16 -4
  237. package/lang/translations/de.po +16 -4
  238. package/lang/translations/el.po +16 -4
  239. package/lang/translations/en-au.po +16 -4
  240. package/lang/translations/en-gb.po +16 -4
  241. package/lang/translations/en.po +16 -4
  242. package/lang/translations/eo.po +16 -4
  243. package/lang/translations/es-co.po +16 -4
  244. package/lang/translations/es.po +16 -4
  245. package/lang/translations/et.po +16 -4
  246. package/lang/translations/eu.po +16 -4
  247. package/lang/translations/fa.po +16 -4
  248. package/lang/translations/fi.po +16 -4
  249. package/lang/translations/fr.po +16 -4
  250. package/lang/translations/gl.po +16 -4
  251. package/lang/translations/gu.po +16 -4
  252. package/lang/translations/he.po +16 -4
  253. package/lang/translations/hi.po +16 -4
  254. package/lang/translations/hr.po +16 -4
  255. package/lang/translations/hu.po +16 -4
  256. package/lang/translations/hy.po +16 -4
  257. package/lang/translations/id.po +16 -4
  258. package/lang/translations/it.po +16 -4
  259. package/lang/translations/ja.po +16 -4
  260. package/lang/translations/jv.po +16 -4
  261. package/lang/translations/kk.po +16 -4
  262. package/lang/translations/km.po +16 -4
  263. package/lang/translations/kn.po +16 -4
  264. package/lang/translations/ko.po +16 -4
  265. package/lang/translations/ku.po +16 -4
  266. package/lang/translations/lt.po +16 -4
  267. package/lang/translations/lv.po +16 -4
  268. package/lang/translations/ms.po +16 -4
  269. package/lang/translations/nb.po +16 -4
  270. package/lang/translations/ne.po +16 -4
  271. package/lang/translations/nl.po +16 -4
  272. package/lang/translations/no.po +16 -4
  273. package/lang/translations/oc.po +16 -4
  274. package/lang/translations/pl.po +16 -4
  275. package/lang/translations/pt-br.po +16 -4
  276. package/lang/translations/pt.po +16 -4
  277. package/lang/translations/ro.po +16 -4
  278. package/lang/translations/ru.po +16 -4
  279. package/lang/translations/si.po +16 -4
  280. package/lang/translations/sk.po +16 -4
  281. package/lang/translations/sl.po +16 -4
  282. package/lang/translations/sq.po +16 -4
  283. package/lang/translations/sr-latn.po +16 -4
  284. package/lang/translations/sr.po +16 -4
  285. package/lang/translations/sv.po +16 -4
  286. package/lang/translations/th.po +16 -4
  287. package/lang/translations/ti.po +16 -4
  288. package/lang/translations/tk.po +16 -4
  289. package/lang/translations/tr.po +16 -4
  290. package/lang/translations/tt.po +16 -4
  291. package/lang/translations/ug.po +16 -4
  292. package/lang/translations/uk.po +16 -4
  293. package/lang/translations/ur.po +16 -4
  294. package/lang/translations/uz.po +16 -4
  295. package/lang/translations/vi.po +16 -4
  296. package/lang/translations/zh-cn.po +16 -4
  297. package/lang/translations/zh.po +16 -4
  298. package/package.json +7 -7
  299. package/src/augmentation.d.ts +2 -1
  300. package/src/index.d.ts +1 -0
  301. package/src/index.js +1 -0
  302. package/src/restrictededitingconfig.d.ts +2 -2
  303. package/src/restrictededitingexceptionblockcommand.d.ts +57 -0
  304. package/src/restrictededitingexceptionblockcommand.js +203 -0
  305. package/src/restrictededitingmode/converters.d.ts +1 -0
  306. package/src/restrictededitingmode/converters.js +33 -6
  307. package/src/restrictededitingmode/utils.d.ts +8 -2
  308. package/src/restrictededitingmode/utils.js +16 -3
  309. package/src/restrictededitingmodeediting.d.ts +4 -0
  310. package/src/restrictededitingmodeediting.js +83 -35
  311. package/src/restrictededitingmodenavigationcommand.js +2 -1
  312. package/src/standardeditingmodeediting.js +135 -3
  313. package/src/standardeditingmodeui.js +75 -11
package/dist/index.js CHANGED
@@ -3,10 +3,72 @@
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
4
  */
5
5
  import { Command, Plugin } from '@ckeditor/ckeditor5-core/dist/index.js';
6
+ import { getCode, parseKeystroke, Collection, first } from '@ckeditor/ckeditor5-utils/dist/index.js';
6
7
  import { Matcher } from '@ckeditor/ckeditor5-engine/dist/index.js';
7
8
  import { IconContentLock, IconContentUnlock } from '@ckeditor/ckeditor5-icons/dist/index.js';
8
- import { createDropdown, addListToDropdown, MenuBarMenuView, MenuBarMenuListView, MenuBarMenuListItemView, MenuBarMenuListItemButtonView, UIModel, ButtonView } from '@ckeditor/ckeditor5-ui/dist/index.js';
9
- import { Collection } from '@ckeditor/ckeditor5-utils/dist/index.js';
9
+ import { createDropdown, addListToDropdown, MenuBarMenuView, MenuBarMenuListView, MenuBarMenuListItemView, MenuBarMenuListItemButtonView, UIModel, addToolbarToDropdown, ButtonView } from '@ckeditor/ckeditor5-ui/dist/index.js';
10
+
11
+ /**
12
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
13
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
14
+ */ /**
15
+ * @module restricted-editing/restrictededitingmode/utils
16
+ */ /**
17
+ * Returns a single "restricted-editing-exception" marker at a given position. Contrary to
18
+ * {@link module:engine/model/markercollection~MarkerCollection#getMarkersAtPosition}, it returnd a marker also when the postion is
19
+ * equal to one of the marker's start or end positions.
20
+ *
21
+ * @internal
22
+ */ function getMarkerAtPosition(editor, position) {
23
+ for (const marker of editor.model.markers){
24
+ const markerRange = getExceptionRange(marker, editor.model);
25
+ if (isPositionInRangeBoundaries(markerRange, position)) {
26
+ if (marker.name.startsWith('restrictedEditingException:')) {
27
+ return marker;
28
+ }
29
+ }
30
+ }
31
+ }
32
+ /**
33
+ * Checks if the position is fully contained in the range. Positions equal to range start or end are considered "in".
34
+ *
35
+ * @internal
36
+ */ function isPositionInRangeBoundaries(range, position) {
37
+ return range.containsPosition(position) || range.end.isEqual(position) || range.start.isEqual(position);
38
+ }
39
+ /**
40
+ * Checks if the selection is fully contained in the marker. Positions on marker boundaries are considered "in".
41
+ *
42
+ * ```xml
43
+ * <marker>[]foo</marker> -> true
44
+ * <marker>f[oo]</marker> -> true
45
+ * <marker>f[oo</marker> ba]r -> false
46
+ * <marker>foo</marker> []bar -> false
47
+ * ```
48
+ *
49
+ * @internal
50
+ */ function isSelectionInMarker(selection, model, marker) {
51
+ if (!marker) {
52
+ return false;
53
+ }
54
+ const markerRange = getExceptionRange(marker, model);
55
+ if (selection.isCollapsed) {
56
+ return isPositionInRangeBoundaries(markerRange, selection.focus);
57
+ }
58
+ return markerRange.containsRange(selection.getFirstRange(), true);
59
+ }
60
+ /**
61
+ * Returns the marker range asjusted to the inside of exception wrapper element if needed.
62
+ *
63
+ * @internal
64
+ */ function getExceptionRange(marker, model) {
65
+ const markerRange = marker.getRange();
66
+ const wrapperElement = markerRange.getContainedElement();
67
+ if (wrapperElement && wrapperElement.is('element', 'restrictedEditingException')) {
68
+ return model.createRangeIn(wrapperElement);
69
+ }
70
+ return markerRange;
71
+ }
10
72
 
11
73
  /**
12
74
  * The command that allows navigation across the exceptions in the edited document.
@@ -59,7 +121,7 @@ import { Collection } from '@ckeditor/ckeditor5-utils/dist/index.js';
59
121
  const markerRanges = [];
60
122
  // Get all exception marker positions that start after/before the selection position.
61
123
  for (const marker of model.markers.getMarkersGroup('restrictedEditingException')){
62
- const markerRange = marker.getRange();
124
+ const markerRange = getExceptionRange(marker, model);
63
125
  // Checking parent because there two positions <paragraph>foo^</paragraph><paragraph>^bar</paragraph>
64
126
  // are touching but they will represent different markers.
65
127
  const isMarkerRangeTouching = selectionPosition.isTouching(markerRange.start) && selectionPosition.hasSameParentAs(markerRange.start) || selectionPosition.isTouching(markerRange.end) && selectionPosition.hasSameParentAs(markerRange.end);
@@ -90,56 +152,6 @@ import { Collection } from '@ckeditor/ckeditor5-utils/dist/index.js';
90
152
  }).shift();
91
153
  }
92
154
 
93
- /**
94
- * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
95
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
96
- */ /**
97
- * @module restricted-editing/restrictededitingmode/utils
98
- */ /**
99
- * Returns a single "restricted-editing-exception" marker at a given position. Contrary to
100
- * {@link module:engine/model/markercollection~MarkerCollection#getMarkersAtPosition}, it returnd a marker also when the postion is
101
- * equal to one of the marker's start or end positions.
102
- *
103
- * @internal
104
- */ function getMarkerAtPosition(editor, position) {
105
- for (const marker of editor.model.markers){
106
- const markerRange = marker.getRange();
107
- if (isPositionInRangeBoundaries(markerRange, position)) {
108
- if (marker.name.startsWith('restrictedEditingException:')) {
109
- return marker;
110
- }
111
- }
112
- }
113
- }
114
- /**
115
- * Checks if the position is fully contained in the range. Positions equal to range start or end are considered "in".
116
- *
117
- * @internal
118
- */ function isPositionInRangeBoundaries(range, position) {
119
- return range.containsPosition(position) || range.end.isEqual(position) || range.start.isEqual(position);
120
- }
121
- /**
122
- * Checks if the selection is fully contained in the marker. Positions on marker boundaries are considered "in".
123
- *
124
- * ```xml
125
- * <marker>[]foo</marker> -> true
126
- * <marker>f[oo]</marker> -> true
127
- * <marker>f[oo</marker> ba]r -> false
128
- * <marker>foo</marker> []bar -> false
129
- * ```
130
- *
131
- * @internal
132
- */ function isSelectionInMarker(selection, marker) {
133
- if (!marker) {
134
- return false;
135
- }
136
- const markerRange = marker.getRange();
137
- if (selection.isCollapsed) {
138
- return isPositionInRangeBoundaries(markerRange, selection.focus);
139
- }
140
- return markerRange.containsRange(selection.getFirstRange(), true);
141
- }
142
-
143
155
  const HIGHLIGHT_CLASS = 'restricted-editing-exception_selected';
144
156
  /**
145
157
  * Adds a visual highlight style to a restricted editing exception that the selection is anchored to.
@@ -165,9 +177,16 @@ const HIGHLIGHT_CLASS = 'restricted-editing-exception_selected';
165
177
  if (!marker) {
166
178
  return false;
167
179
  }
168
- for (const viewElement of editor.editing.mapper.markerNameToElements(marker.name)){
180
+ const modelWrapperElement = marker.getRange().getContainedElement();
181
+ if (modelWrapperElement && modelWrapperElement.is('element', 'restrictedEditingException')) {
182
+ const viewElement = editor.editing.mapper.toViewElement(modelWrapperElement);
169
183
  writer.addClass(HIGHLIGHT_CLASS, viewElement);
170
184
  highlightedMarkers.add(viewElement);
185
+ } else {
186
+ for (const viewElement of editor.editing.mapper.markerNameToElements(marker.name)){
187
+ writer.addClass(HIGHLIGHT_CLASS, viewElement);
188
+ highlightedMarkers.add(viewElement);
189
+ }
171
190
  }
172
191
  return false;
173
192
  });
@@ -237,7 +256,7 @@ const HIGHLIGHT_CLASS = 'restricted-editing-exception_selected';
237
256
  * @param config Conversion configuration.
238
257
  * @internal
239
258
  */ function upcastHighlightToMarker(config) {
240
- return (dispatcher)=>dispatcher.on('element:span', (evt, data, conversionApi)=>{
259
+ return (dispatcher)=>dispatcher.on('element', (evt, data, conversionApi)=>{
241
260
  const { writer } = conversionApi;
242
261
  const matcher = new Matcher(config.view);
243
262
  const matcherResult = matcher.match(data.viewItem);
@@ -248,7 +267,20 @@ const HIGHLIGHT_CLASS = 'restricted-editing-exception_selected';
248
267
  const match = matcherResult.match;
249
268
  // Force consuming element's name (taken from upcast helpers elementToElement converter).
250
269
  match.name = true;
251
- const { modelRange: convertedChildrenRange } = conversionApi.convertChildren(data.viewItem, data.modelCursor);
270
+ if (!conversionApi.consumable.test(data.viewItem, match)) {
271
+ return;
272
+ }
273
+ let position = data.modelCursor;
274
+ let wrapperElement = null;
275
+ if (config.useWrapperElement) {
276
+ if (!conversionApi.schema.checkChild(position, 'restrictedEditingException')) {
277
+ return;
278
+ }
279
+ wrapperElement = writer.createElement('restrictedEditingException');
280
+ writer.insert(wrapperElement, position);
281
+ position = writer.createPositionAt(wrapperElement, 0);
282
+ }
283
+ const { modelRange: convertedChildrenRange } = conversionApi.convertChildren(data.viewItem, position);
252
284
  conversionApi.consumable.consume(data.viewItem, match);
253
285
  const markerName = config.model();
254
286
  const fakeMarkerStart = writer.createElement('$marker', {
@@ -257,9 +289,14 @@ const HIGHLIGHT_CLASS = 'restricted-editing-exception_selected';
257
289
  const fakeMarkerEnd = writer.createElement('$marker', {
258
290
  'data-name': markerName
259
291
  });
260
- // Insert in reverse order to use converter content positions directly (without recalculating).
261
- writer.insert(fakeMarkerEnd, convertedChildrenRange.end);
262
- writer.insert(fakeMarkerStart, convertedChildrenRange.start);
292
+ if (wrapperElement) {
293
+ writer.insert(fakeMarkerStart, writer.createPositionBefore(wrapperElement));
294
+ writer.insert(fakeMarkerEnd, writer.createPositionAfter(wrapperElement));
295
+ } else {
296
+ // Insert in reverse order to use converter content positions directly (without recalculating).
297
+ writer.insert(fakeMarkerEnd, convertedChildrenRange.end);
298
+ writer.insert(fakeMarkerStart, convertedChildrenRange.start);
299
+ }
263
300
  data.modelRange = writer.createRange(writer.createPositionBefore(fakeMarkerStart), writer.createPositionAfter(fakeMarkerEnd));
264
301
  data.modelCursor = data.modelRange.end;
265
302
  });
@@ -362,6 +399,7 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
362
399
  const editingView = editor.editing.view;
363
400
  const allowedCommands = editor.config.get('restrictedEditing.allowedCommands');
364
401
  allowedCommands.forEach((commandName)=>this._allowedInException.add(commandName));
402
+ this._setupSchema();
365
403
  this._setupConversion();
366
404
  this._setupCommandsToggling();
367
405
  this._setupRestrictions();
@@ -382,7 +420,9 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
382
420
  }, {
383
421
  context: '$capture'
384
422
  });
385
- editor.keystrokes.set('Ctrl+A', getSelectAllHandler(editor));
423
+ this.listenTo(editingView.document, 'keydown', getSelectAllHandler(editor), {
424
+ priority: 'high'
425
+ });
386
426
  editingView.change((writer)=>{
387
427
  for (const root of editingView.document.roots){
388
428
  writer.addClass('ck-restricted-editing_mode_restricted', root);
@@ -414,6 +454,16 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
414
454
  command.clearForceDisabled(COMMAND_FORCE_DISABLE_ID);
415
455
  this._alwaysEnabled.add(commandName);
416
456
  }
457
+ /**
458
+ * Registers block exception wrapper in the schema.
459
+ */ _setupSchema() {
460
+ const schema = this.editor.model.schema;
461
+ schema.register('restrictedEditingException', {
462
+ allowWhere: '$block',
463
+ allowContentOf: '$container',
464
+ isLimit: true
465
+ });
466
+ }
417
467
  /**
418
468
  * Sets up the restricted mode editing conversion:
419
469
  *
@@ -434,16 +484,35 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
434
484
  },
435
485
  model: ()=>{
436
486
  markerNumber++; // Starting from restrictedEditingException:1 marker.
437
- return `restrictedEditingException:${markerNumber}`;
487
+ return `restrictedEditingException:inline:${markerNumber}`;
438
488
  }
439
489
  }));
490
+ editor.conversion.for('upcast').add(upcastHighlightToMarker({
491
+ view: {
492
+ name: 'div',
493
+ classes: 'restricted-editing-exception'
494
+ },
495
+ model: ()=>{
496
+ markerNumber++; // Starting from restrictedEditingException:1 marker.
497
+ return `restrictedEditingException:block:${markerNumber}`;
498
+ },
499
+ useWrapperElement: true
500
+ }));
501
+ // Block exception wrapper.
502
+ editor.conversion.for('downcast').elementToElement({
503
+ model: 'restrictedEditingException',
504
+ view: {
505
+ name: 'div',
506
+ classes: 'restricted-editing-exception'
507
+ }
508
+ });
440
509
  // Currently the marker helpers are tied to other use-cases and do not render a collapsed marker as highlight.
441
510
  // Also, markerToHighlight cannot convert marker on an inline object. It handles only text and widgets,
442
511
  // but it is not a case in the data pipeline. That's why there are 3 downcast converters for them:
443
512
  //
444
513
  // 1. The custom inline item (text or inline object) converter (but not the selection).
445
514
  editor.conversion.for('downcast').add((dispatcher)=>{
446
- dispatcher.on('addMarker:restrictedEditingException', (evt, data, conversionApi)=>{
515
+ dispatcher.on('addMarker:restrictedEditingException:inline', (evt, data, conversionApi)=>{
447
516
  // Only convert per-item conversion.
448
517
  if (!data.item) {
449
518
  return;
@@ -474,7 +543,7 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
474
543
  });
475
544
  // 2. The marker-to-highlight converter for the document selection.
476
545
  editor.conversion.for('downcast').markerToHighlight({
477
- model: 'restrictedEditingException',
546
+ model: 'restrictedEditingException:inline',
478
547
  // Use callback to return new object every time new marker instance is created - otherwise it will be seen as the same marker.
479
548
  view: ()=>{
480
549
  return {
@@ -487,7 +556,7 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
487
556
  // 3. And for collapsed marker we need to render it as an element.
488
557
  // Additionally, the editing pipeline should always display a collapsed marker.
489
558
  editor.conversion.for('editingDowncast').markerToElement({
490
- model: 'restrictedEditingException',
559
+ model: 'restrictedEditingException:inline',
491
560
  view: (markerData, { writer })=>{
492
561
  return writer.createUIElement('span', {
493
562
  class: 'restricted-editing-exception restricted-editing-exception_collapsed'
@@ -495,7 +564,7 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
495
564
  }
496
565
  });
497
566
  editor.conversion.for('dataDowncast').markerToElement({
498
- model: 'restrictedEditingException',
567
+ model: 'restrictedEditingException:inline',
499
568
  view: (markerData, { writer })=>{
500
569
  return writer.createEmptyElement('span', {
501
570
  class: 'restricted-editing-exception'
@@ -532,10 +601,28 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
532
601
  });
533
602
  }
534
603
  // Block clipboard outside exception marker on paste and drop.
535
- this.listenTo(clipboard, 'contentInsertion', (evt)=>{
604
+ this.listenTo(clipboard, 'contentInsertion', (evt, data)=>{
536
605
  if (!isRangeInsideSingleMarker(editor, selection.getFirstRange())) {
537
606
  evt.stop();
538
607
  }
608
+ const marker = getMarkerAtPosition(editor, selection.focus);
609
+ // Reduce content pasted into inline exception to text nodes only. Also strip not allowed attributes.
610
+ if (marker && marker.name.startsWith('restrictedEditingException:inline:')) {
611
+ const allowedAttributes = editor.config.get('restrictedEditing.allowedAttributes');
612
+ model.change((writer)=>{
613
+ const content = writer.createDocumentFragment();
614
+ const textNodes = Array.from(writer.createRangeIn(data.content).getItems()).filter((node)=>node.is('$textProxy'));
615
+ for (const item of textNodes){
616
+ for (const attr of item.getAttributeKeys()){
617
+ if (!allowedAttributes.includes(attr)) {
618
+ writer.removeAttribute(attr, item);
619
+ }
620
+ }
621
+ writer.append(item, content);
622
+ }
623
+ data.content = content;
624
+ });
625
+ }
539
626
  });
540
627
  // Block clipboard outside exception marker on cut.
541
628
  this.listenTo(viewDoc, 'clipboardOutput', (evt, data)=>{
@@ -545,9 +632,12 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
545
632
  }, {
546
633
  priority: 'high'
547
634
  });
548
- const allowedAttributes = editor.config.get('restrictedEditing.allowedAttributes');
549
- model.schema.addAttributeCheck(onlyAllowAttributesFromList(allowedAttributes));
550
- model.schema.addChildCheck(allowTextOnlyInClipboardHolder());
635
+ // Do not allow pasting/dropping block exception wrapper.
636
+ model.schema.addChildCheck((context)=>{
637
+ if (context.startsWith('$clipboardHolder')) {
638
+ return false;
639
+ }
640
+ }, 'restrictedEditingException');
551
641
  }
552
642
  /**
553
643
  * Sets up the command toggling which enables or disables commands based on the user selection.
@@ -570,7 +660,7 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
570
660
  }
571
661
  const marker = getMarkerAtPosition(editor, selection.focus);
572
662
  this._disableCommands();
573
- if (isSelectionInMarker(selection, marker)) {
663
+ if (isSelectionInMarker(selection, editor.model, marker)) {
574
664
  this._enableCommands(marker);
575
665
  }
576
666
  }
@@ -578,16 +668,18 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
578
668
  * Enables commands in non-restricted regions.
579
669
  */ _enableCommands(marker) {
580
670
  const editor = this.editor;
671
+ const selection = editor.model.document.selection;
581
672
  for (const [commandName, command] of editor.commands){
582
673
  if (!command.affectsData || this._alwaysEnabled.has(commandName)) {
583
674
  continue;
584
675
  }
585
676
  // Enable ony those commands that are allowed in the exception marker.
586
- if (!this._allowedInException.has(commandName)) {
677
+ // In block exceptions all commands are enabled.
678
+ if (!marker.name.startsWith('restrictedEditingException:block:') && !this._allowedInException.has(commandName)) {
587
679
  continue;
588
680
  }
589
681
  // Do not enable 'delete' and 'deleteForward' commands on the exception marker boundaries.
590
- if (isDeleteCommandOnMarkerBoundaries(commandName, editor.model.document.selection, marker.getRange())) {
682
+ if (isDeleteCommandOnMarkerBoundaries(commandName, selection, getExceptionRange(marker, editor.model))) {
591
683
  continue;
592
684
  }
593
685
  command.clearForceDisabled(COMMAND_FORCE_DISABLE_ID);
@@ -608,7 +700,10 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
608
700
  /**
609
701
  * Helper for handling Ctrl+A keydown behaviour.
610
702
  */ function getSelectAllHandler(editor) {
611
- return (_, cancel)=>{
703
+ return (eventInfo, domEventData)=>{
704
+ if (getCode(domEventData) != parseKeystroke('Ctrl+A')) {
705
+ return;
706
+ }
612
707
  const model = editor.model;
613
708
  const selection = editor.model.document.selection;
614
709
  const marker = getMarkerAtPosition(editor, selection.focus);
@@ -619,11 +714,13 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
619
714
  //
620
715
  // Note: Second Ctrl+A press is also blocked and it won't select the entire text in the editor.
621
716
  const selectionRange = selection.getFirstRange();
622
- const markerRange = marker.getRange();
717
+ const markerRange = getExceptionRange(marker, editor.model);
623
718
  if (markerRange.containsRange(selectionRange, true) || selection.isCollapsed) {
624
- cancel();
719
+ eventInfo.stop();
720
+ domEventData.preventDefault();
721
+ domEventData.stopPropagation();
625
722
  model.change((writer)=>{
626
- writer.setSelection(marker.getRange());
723
+ writer.setSelection(markerRange);
627
724
  });
628
725
  }
629
726
  };
@@ -635,11 +732,11 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
635
732
  * - is on marker start - "delete" - to prevent removing content before marker
636
733
  * - is on marker end - "deleteForward" - to prevent removing content after marker
637
734
  */ function isDeleteCommandOnMarkerBoundaries(commandName, selection, markerRange) {
638
- if (commandName == 'delete' && markerRange.start.isEqual(selection.focus)) {
735
+ if (commandName == 'delete' && selection.isCollapsed && markerRange.start.isTouching(selection.focus)) {
639
736
  return true;
640
737
  }
641
738
  // Only for collapsed selection - non-collapsed selection that extends over a marker is handled elsewhere.
642
- if (commandName == 'deleteForward' && selection.isCollapsed && markerRange.end.isEqual(selection.focus)) {
739
+ if (commandName == 'deleteForward' && selection.isCollapsed && markerRange.end.isTouching(selection.focus)) {
643
740
  return true;
644
741
  }
645
742
  return false;
@@ -664,7 +761,7 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
664
761
  return;
665
762
  }
666
763
  // Shrink the selection to the range inside exception marker.
667
- const allowedToDelete = marker.getRange().getIntersection(selection.getFirstRange());
764
+ const allowedToDelete = getExceptionRange(marker, editor.model).getIntersection(selection.getFirstRange());
668
765
  // Some features uses selection passed to model.deleteContent() to set the selection afterwards. For this we need to properly modify
669
766
  // either the document selection using change block...
670
767
  if (selection.is('documentSelection')) {
@@ -730,20 +827,6 @@ function isRangeInsideSingleMarker(editor, range) {
730
827
  return changeApplied;
731
828
  };
732
829
  }
733
- function onlyAllowAttributesFromList(allowedAttributes) {
734
- return (context, attributeName)=>{
735
- if (context.startsWith('$clipboardHolder')) {
736
- return allowedAttributes.includes(attributeName);
737
- }
738
- };
739
- }
740
- function allowTextOnlyInClipboardHolder() {
741
- return (context, childDefinition)=>{
742
- if (context.startsWith('$clipboardHolder')) {
743
- return childDefinition.name === '$text';
744
- }
745
- };
746
- }
747
830
 
748
831
  /**
749
832
  * The restricted editing mode UI feature.
@@ -953,6 +1036,193 @@ function allowTextOnlyInClipboardHolder() {
953
1036
  }
954
1037
  }
955
1038
 
1039
+ /**
1040
+ * The command that toggles exception blocks for the restricted editing.
1041
+ */ class RestrictedEditingExceptionBlockCommand extends Command {
1042
+ /**
1043
+ * @inheritDoc
1044
+ */ refresh() {
1045
+ this.value = this._getValue();
1046
+ this.isEnabled = this._checkEnabled();
1047
+ }
1048
+ /**
1049
+ * Wraps or unwraps the selected blocks with non-restricted area.
1050
+ *
1051
+ * @fires execute
1052
+ * @param options Command options.
1053
+ * @param options.forceValue If set, it will force the command behavior. If `true`, the command will apply a block exception,
1054
+ * otherwise the command will remove the block exception. If not set, the command will act basing on its current value.
1055
+ */ execute(options = {}) {
1056
+ const model = this.editor.model;
1057
+ const schema = model.schema;
1058
+ const selection = model.document.selection;
1059
+ const blocks = Array.from(selection.getSelectedBlocks());
1060
+ const value = options.forceValue === undefined ? !this.value : options.forceValue;
1061
+ model.change((writer)=>{
1062
+ if (!value) {
1063
+ const blocksToUnwrap = blocks.map((block)=>{
1064
+ // Find blocks directly nested inside an exception.
1065
+ return findExceptionContentBlock(block);
1066
+ }).filter((exception)=>!!exception);
1067
+ this._removeException(writer, blocksToUnwrap);
1068
+ } else {
1069
+ const blocksToWrap = blocks.filter((block)=>{
1070
+ // Already wrapped blocks needs to be considered while wrapping too
1071
+ // in order to reuse their wrapper elements.
1072
+ return findException(block) || checkCanBeWrapped(schema, block);
1073
+ });
1074
+ this._applyException(writer, blocksToWrap);
1075
+ }
1076
+ });
1077
+ }
1078
+ /**
1079
+ * Checks the command's {@link #value}.
1080
+ */ _getValue() {
1081
+ const selection = this.editor.model.document.selection;
1082
+ const firstBlock = first(selection.getSelectedBlocks());
1083
+ return !!(firstBlock && findException(firstBlock));
1084
+ }
1085
+ /**
1086
+ * Checks whether the command can be enabled in the current context.
1087
+ *
1088
+ * @returns Whether the command should be enabled.
1089
+ */ _checkEnabled() {
1090
+ if (this.value) {
1091
+ return true;
1092
+ }
1093
+ const selection = this.editor.model.document.selection;
1094
+ const schema = this.editor.model.schema;
1095
+ const firstBlock = first(selection.getSelectedBlocks());
1096
+ if (!firstBlock) {
1097
+ return false;
1098
+ }
1099
+ return checkCanBeWrapped(schema, firstBlock);
1100
+ }
1101
+ /**
1102
+ * Unwraps the exception from given blocks.
1103
+ *
1104
+ * If blocks which are supposed to be unwrapped are in the middle of an exception,
1105
+ * start it or end it, then the exception will be split (if needed) and the blocks
1106
+ * will be moved out of it, so other exception blocks remained wrapped.
1107
+ */ _removeException(writer, blocks) {
1108
+ // Unwrap all groups of block. Iterate in the reverse order to not break following ranges.
1109
+ getRangesOfBlockGroups(writer, blocks).reverse().forEach((groupRange)=>{
1110
+ if (groupRange.start.isAtStart && groupRange.end.isAtEnd) {
1111
+ writer.unwrap(groupRange.start.parent);
1112
+ return;
1113
+ }
1114
+ // The group of blocks are at the beginning of an exception so let's move them left (out of the exception).
1115
+ if (groupRange.start.isAtStart) {
1116
+ const positionBefore = writer.createPositionBefore(groupRange.start.parent);
1117
+ writer.move(groupRange, positionBefore);
1118
+ return;
1119
+ }
1120
+ // The blocks are in the middle of an exception so we need to split the exception after the last block
1121
+ // so we move the items there.
1122
+ if (!groupRange.end.isAtEnd) {
1123
+ writer.split(groupRange.end);
1124
+ }
1125
+ // Now we are sure that groupRange.end.isAtEnd is true, so let's move the blocks right.
1126
+ const positionAfter = writer.createPositionAfter(groupRange.end.parent);
1127
+ writer.move(groupRange, positionAfter);
1128
+ });
1129
+ }
1130
+ /**
1131
+ * Applies the exception to given blocks.
1132
+ */ _applyException(writer, blocks) {
1133
+ const schema = this.editor.model.schema;
1134
+ const exceptionsToMerge = [];
1135
+ // Wrap all groups of block. Iterate in the reverse order to not break following ranges.
1136
+ getRangesOfBlockGroups(writer, blocks).reverse().forEach((groupRange)=>{
1137
+ let exception = findException(groupRange.start);
1138
+ if (!exception) {
1139
+ exception = writer.createElement('restrictedEditingException');
1140
+ writer.wrap(groupRange, exception);
1141
+ }
1142
+ exceptionsToMerge.push(exception);
1143
+ });
1144
+ // Merge subsequent exception elements. Reverse the order again because this time we want to go through
1145
+ // the exception elements in the source order (due to how merge works – it moves the right element's content
1146
+ // to the first element and removes the right one. Since we may need to merge a couple of subsequent exception elements
1147
+ // we want to keep the reference to the first (furthest left) one.
1148
+ exceptionsToMerge.reverse();
1149
+ // But first add any neighbouring block exceptions to the list.
1150
+ if (exceptionsToMerge.length) {
1151
+ const previousSibling = exceptionsToMerge.at(0).previousSibling;
1152
+ const nextSibling = exceptionsToMerge.at(-1).nextSibling;
1153
+ if (previousSibling?.is('element', 'restrictedEditingException')) {
1154
+ exceptionsToMerge.unshift(previousSibling);
1155
+ }
1156
+ if (nextSibling?.is('element', 'restrictedEditingException')) {
1157
+ exceptionsToMerge.push(nextSibling);
1158
+ }
1159
+ }
1160
+ // Merge subsequent exceptions.
1161
+ exceptionsToMerge.reduce((currentException, nextException)=>{
1162
+ if (currentException.nextSibling == nextException) {
1163
+ writer.merge(writer.createPositionAfter(currentException));
1164
+ return currentException;
1165
+ }
1166
+ return nextException;
1167
+ });
1168
+ // Remove inline exceptions from block exception.
1169
+ schema.removeDisallowedAttributes(blocks, writer);
1170
+ }
1171
+ }
1172
+ function findException(elementOrPosition) {
1173
+ return elementOrPosition.findAncestor('restrictedEditingException', {
1174
+ includeSelf: true
1175
+ });
1176
+ }
1177
+ function findExceptionContentBlock(element) {
1178
+ let node = element;
1179
+ while(node.parent){
1180
+ if (node.parent.name == 'restrictedEditingException') {
1181
+ return node;
1182
+ }
1183
+ node = node.parent;
1184
+ }
1185
+ return null;
1186
+ }
1187
+ /**
1188
+ * Returns a minimal array of ranges containing groups of subsequent blocks.
1189
+ *
1190
+ * content: abcdefgh
1191
+ * blocks: [ a, b, d, f, g, h ]
1192
+ * output ranges: [ab]c[d]e[fgh]
1193
+ */ function getRangesOfBlockGroups(writer, blocks) {
1194
+ let startPosition;
1195
+ let i = 0;
1196
+ const ranges = [];
1197
+ while(i < blocks.length){
1198
+ const block = blocks[i];
1199
+ const nextBlock = blocks[i + 1];
1200
+ if (!startPosition) {
1201
+ startPosition = writer.createPositionBefore(block);
1202
+ }
1203
+ if (!nextBlock || block.nextSibling != nextBlock) {
1204
+ ranges.push(writer.createRange(startPosition, writer.createPositionAfter(block)));
1205
+ startPosition = null;
1206
+ }
1207
+ i++;
1208
+ }
1209
+ return ranges;
1210
+ }
1211
+ /**
1212
+ * Checks whether exception can wrap the block.
1213
+ */ function checkCanBeWrapped(schema, block) {
1214
+ const parentContext = schema.createContext(block.parent);
1215
+ // Is block exception allowed in parent of block.
1216
+ if (!schema.checkChild(parentContext, 'restrictedEditingException')) {
1217
+ return false;
1218
+ }
1219
+ // Is block allowed inside block exception.
1220
+ if (!schema.checkChild(parentContext.push('restrictedEditingException'), block)) {
1221
+ return false;
1222
+ }
1223
+ return true;
1224
+ }
1225
+
956
1226
  /**
957
1227
  * The standard editing mode editing feature.
958
1228
  *
@@ -974,18 +1244,107 @@ function allowTextOnlyInClipboardHolder() {
974
1244
  * @inheritDoc
975
1245
  */ init() {
976
1246
  const editor = this.editor;
977
- editor.model.schema.extend('$text', {
1247
+ const schema = editor.model.schema;
1248
+ schema.extend('$text', {
978
1249
  allowAttributes: [
979
1250
  'restrictedEditingException'
980
1251
  ]
981
1252
  });
1253
+ schema.register('restrictedEditingException', {
1254
+ allowWhere: '$container',
1255
+ allowContentOf: '$container'
1256
+ });
1257
+ // Don't allow nesting of block exceptions.
1258
+ schema.addChildCheck((context)=>{
1259
+ for (const item of context){
1260
+ if (item.name == 'restrictedEditingException') {
1261
+ return false;
1262
+ }
1263
+ }
1264
+ }, 'restrictedEditingException');
1265
+ // Don't allow nesting inline exceptions inside block exceptions.
1266
+ schema.addAttributeCheck((context)=>{
1267
+ for (const item of context){
1268
+ if (item.name == 'restrictedEditingException') {
1269
+ return false;
1270
+ }
1271
+ }
1272
+ }, 'restrictedEditingException');
1273
+ // Post-fixer to ensure proper structure.
1274
+ editor.model.document.registerPostFixer((writer)=>{
1275
+ const changes = editor.model.document.differ.getChanges();
1276
+ const unwrap = new Set();
1277
+ const remove = new Set();
1278
+ const merge = new Set();
1279
+ let changed = false;
1280
+ for (const entry of changes){
1281
+ if (entry.type == 'insert') {
1282
+ const range = writer.createRange(entry.position, entry.position.getShiftedBy(entry.length));
1283
+ for (const child of range.getItems()){
1284
+ if (child.is('element', 'restrictedEditingException')) {
1285
+ // Make sure that block exception is not nested or added in invalid place.
1286
+ if (!schema.checkChild(writer.createPositionBefore(child), child)) {
1287
+ unwrap.add(child);
1288
+ } else if (child.isEmpty) {
1289
+ remove.add(child);
1290
+ } else {
1291
+ merge.add(child);
1292
+ }
1293
+ } else if (child.is('$textProxy') && child.hasAttribute('restrictedEditingException') && !schema.checkAttribute(child, 'restrictedEditingException')) {
1294
+ writer.removeAttribute('restrictedEditingException', child);
1295
+ changed = true;
1296
+ }
1297
+ }
1298
+ } else if (entry.type == 'remove') {
1299
+ const parent = entry.position.parent;
1300
+ if (parent.is('element', 'restrictedEditingException') && parent.isEmpty) {
1301
+ remove.add(parent);
1302
+ }
1303
+ // Verify if some block exceptions are siblings now after element removed between.
1304
+ for (const child of parent.getChildren()){
1305
+ if (child.is('element', 'restrictedEditingException')) {
1306
+ merge.add(child);
1307
+ }
1308
+ }
1309
+ }
1310
+ }
1311
+ for (const child of unwrap){
1312
+ writer.unwrap(child);
1313
+ changed = true;
1314
+ }
1315
+ for (const child of remove){
1316
+ writer.remove(child);
1317
+ changed = true;
1318
+ }
1319
+ for (const child of merge){
1320
+ if (child.root.rootName == '$graveyard') {
1321
+ continue;
1322
+ }
1323
+ const nodeBefore = child.previousSibling;
1324
+ const nodeAfter = child.nextSibling;
1325
+ if (nodeBefore && nodeBefore.is('element', 'restrictedEditingException')) {
1326
+ writer.merge(writer.createPositionBefore(child));
1327
+ }
1328
+ if (nodeAfter && nodeAfter.is('element', 'restrictedEditingException')) {
1329
+ writer.merge(writer.createPositionAfter(child));
1330
+ }
1331
+ }
1332
+ return changed;
1333
+ });
982
1334
  editor.conversion.for('upcast').elementToAttribute({
983
1335
  model: 'restrictedEditingException',
984
1336
  view: {
985
1337
  name: 'span',
986
1338
  classes: 'restricted-editing-exception'
987
1339
  }
1340
+ }).elementToElement({
1341
+ model: 'restrictedEditingException',
1342
+ view: {
1343
+ name: 'div',
1344
+ classes: 'restricted-editing-exception'
1345
+ }
988
1346
  });
1347
+ registerFallbackUpcastConverter(editor);
989
1348
  editor.conversion.for('downcast').attributeToElement({
990
1349
  model: 'restrictedEditingException',
991
1350
  view: (modelAttributeValue, { writer })=>{
@@ -998,8 +1357,15 @@ function allowTextOnlyInClipboardHolder() {
998
1357
  });
999
1358
  }
1000
1359
  }
1360
+ }).elementToElement({
1361
+ model: 'restrictedEditingException',
1362
+ view: {
1363
+ name: 'div',
1364
+ classes: 'restricted-editing-exception'
1365
+ }
1001
1366
  });
1002
1367
  editor.commands.add('restrictedEditingException', new RestrictedEditingExceptionCommand(editor));
1368
+ editor.commands.add('restrictedEditingExceptionBlock', new RestrictedEditingExceptionBlockCommand(editor));
1003
1369
  editor.editing.view.change((writer)=>{
1004
1370
  for (const root of editor.editing.view.document.roots){
1005
1371
  writer.addClass('ck-restricted-editing_mode_standard', root);
@@ -1007,6 +1373,36 @@ function allowTextOnlyInClipboardHolder() {
1007
1373
  });
1008
1374
  }
1009
1375
  }
1376
+ /**
1377
+ * Fallback upcast converter for empty exception span inside a table cell.
1378
+ */ function registerFallbackUpcastConverter(editor) {
1379
+ const matcher = new Matcher({
1380
+ name: 'span',
1381
+ classes: 'restricted-editing-exception'
1382
+ });
1383
+ // See: https://github.com/ckeditor/ckeditor5/issues/16376.
1384
+ editor.conversion.for('upcast').add((dispatcher)=>dispatcher.on('element:span', (evt, data, conversionApi)=>{
1385
+ const matcherResult = matcher.match(data.viewItem);
1386
+ if (!matcherResult) {
1387
+ return;
1388
+ }
1389
+ const match = matcherResult.match;
1390
+ if (!conversionApi.consumable.test(data.viewItem, match)) {
1391
+ return;
1392
+ }
1393
+ const modelText = conversionApi.writer.createText(' ', {
1394
+ restrictedEditingException: true
1395
+ });
1396
+ if (!conversionApi.safeInsert(modelText, data.modelCursor)) {
1397
+ return;
1398
+ }
1399
+ conversionApi.consumable.consume(data.viewItem, match);
1400
+ data.modelRange = conversionApi.writer.createRange(data.modelCursor, data.modelCursor.getShiftedBy(modelText.offsetSize));
1401
+ data.modelCursor = data.modelRange.end;
1402
+ }, {
1403
+ priority: 'low'
1404
+ }));
1405
+ }
1010
1406
 
1011
1407
  /**
1012
1408
  * The standard editing mode UI feature.
@@ -1027,34 +1423,97 @@ function allowTextOnlyInClipboardHolder() {
1027
1423
  * @inheritDoc
1028
1424
  */ init() {
1029
1425
  const editor = this.editor;
1030
- editor.ui.componentFactory.add('restrictedEditingException', ()=>{
1031
- const button = this._createButton(ButtonView);
1426
+ const componentFactory = editor.ui.componentFactory;
1427
+ componentFactory.add('restrictedEditingException:dropdown', (locale)=>{
1428
+ const dropdownView = createDropdown(locale);
1429
+ const t = locale.t;
1430
+ const buttons = [
1431
+ componentFactory.create('restrictedEditingException:inline'),
1432
+ componentFactory.create('restrictedEditingException:block')
1433
+ ];
1434
+ for (const button of buttons){
1435
+ button.set({
1436
+ withText: true,
1437
+ tooltip: false
1438
+ });
1439
+ }
1440
+ addToolbarToDropdown(dropdownView, buttons, {
1441
+ enableActiveItemFocusOnDropdownOpen: true,
1442
+ isVertical: true,
1443
+ ariaLabel: t('Enable editing')
1444
+ });
1445
+ dropdownView.buttonView.set({
1446
+ label: t('Enable editing'),
1447
+ icon: IconContentUnlock,
1448
+ tooltip: true
1449
+ });
1450
+ dropdownView.extendTemplate({
1451
+ attributes: {
1452
+ class: 'ck-restricted-editing-dropdown'
1453
+ }
1454
+ });
1455
+ // Enable button if any of the buttons is enabled.
1456
+ dropdownView.bind('isEnabled').toMany(buttons, 'isEnabled', (...areEnabled)=>{
1457
+ return areEnabled.some((isEnabled)=>isEnabled);
1458
+ });
1459
+ // Focus the editable after executing the command.
1460
+ this.listenTo(dropdownView, 'execute', ()=>{
1461
+ editor.editing.view.focus();
1462
+ });
1463
+ return dropdownView;
1464
+ });
1465
+ componentFactory.add('restrictedEditingException:inline', ()=>{
1466
+ const button = this._createButton('restrictedEditingException', ButtonView);
1032
1467
  button.set({
1033
1468
  tooltip: true,
1034
1469
  isToggleable: true
1035
1470
  });
1036
1471
  return button;
1037
1472
  });
1038
- editor.ui.componentFactory.add('menuBar:restrictedEditingException', ()=>{
1039
- return this._createButton(MenuBarMenuListItemButtonView);
1473
+ componentFactory.add('restrictedEditingException:block', ()=>{
1474
+ const button = this._createButton('restrictedEditingExceptionBlock', ButtonView);
1475
+ button.set({
1476
+ tooltip: true,
1477
+ isToggleable: true
1478
+ });
1479
+ return button;
1480
+ });
1481
+ componentFactory.add('menuBar:restrictedEditingException:inline', ()=>{
1482
+ return this._createButton('restrictedEditingException', MenuBarMenuListItemButtonView);
1483
+ });
1484
+ componentFactory.add('menuBar:restrictedEditingException:block', ()=>{
1485
+ return this._createButton('restrictedEditingExceptionBlock', MenuBarMenuListItemButtonView);
1486
+ });
1487
+ // Aliases for backward compatibility.
1488
+ componentFactory.add('restrictedEditingException', ()=>{
1489
+ return componentFactory.create('restrictedEditingException:inline');
1490
+ });
1491
+ componentFactory.add('menuBar:restrictedEditingException', ()=>{
1492
+ return componentFactory.create('menuBar:restrictedEditingException:inline');
1040
1493
  });
1041
1494
  }
1042
1495
  /**
1043
1496
  * Creates a button for restricted editing exception command to use either in toolbar or in menu bar.
1044
- */ _createButton(ButtonClass) {
1497
+ */ _createButton(commandName, ButtonClass) {
1045
1498
  const editor = this.editor;
1046
1499
  const locale = editor.locale;
1047
- const command = this.editor.commands.get('restrictedEditingException');
1500
+ const command = this.editor.commands.get(commandName);
1048
1501
  const view = new ButtonClass(locale);
1049
1502
  const t = locale.t;
1050
1503
  view.icon = IconContentUnlock;
1051
1504
  view.bind('isOn', 'isEnabled').to(command, 'value', 'isEnabled');
1052
- view.bind('label').to(command, 'value', (value)=>{
1053
- return value ? t('Disable editing') : t('Enable editing');
1054
- });
1505
+ if (commandName == 'restrictedEditingExceptionBlock') {
1506
+ view.bind('label').to(command, 'value', (value)=>{
1507
+ return value ? t('Disable block editing') : t('Enable block editing');
1508
+ });
1509
+ } else {
1510
+ view.bind('label').to(command, 'value', (value)=>{
1511
+ return value ? t('Disable inline editing') : t('Enable inline editing');
1512
+ });
1513
+ }
1055
1514
  // Execute the command.
1056
1515
  this.listenTo(view, 'execute', ()=>{
1057
- editor.execute('restrictedEditingException');
1516
+ editor.execute(commandName);
1058
1517
  editor.editing.view.focus();
1059
1518
  });
1060
1519
  return view;
@@ -1087,5 +1546,5 @@ function allowTextOnlyInClipboardHolder() {
1087
1546
  }
1088
1547
  }
1089
1548
 
1090
- export { RestrictedEditingExceptionCommand, RestrictedEditingMode, RestrictedEditingModeEditing, RestrictedEditingModeNavigationCommand, RestrictedEditingModeUI, StandardEditingMode, StandardEditingModeEditing, StandardEditingModeUI, extendMarkerOnTypingPostFixer as _extendRestrictedEditingMarkerOnTypingPostFixer, getMarkerAtPosition as _getRestrictedEditingMarkerAtPosition, isPositionInRangeBoundaries as _isRestrictedEditingPositionInRangeBoundaries, isSelectionInMarker as _isRestrictedEditingSelectionInMarker, resurrectCollapsedMarkerPostFixer as _resurrectRestrictedEditingCollapsedMarkerPostFixer, setupExceptionHighlighting as _setupRestrictedEditingExceptionHighlighting, upcastHighlightToMarker as _upcastRestrictedEditingHighlightToMarker };
1549
+ export { RestrictedEditingExceptionBlockCommand, RestrictedEditingExceptionCommand, RestrictedEditingMode, RestrictedEditingModeEditing, RestrictedEditingModeNavigationCommand, RestrictedEditingModeUI, StandardEditingMode, StandardEditingModeEditing, StandardEditingModeUI, extendMarkerOnTypingPostFixer as _extendRestrictedEditingMarkerOnTypingPostFixer, getMarkerAtPosition as _getRestrictedEditingMarkerAtPosition, isPositionInRangeBoundaries as _isRestrictedEditingPositionInRangeBoundaries, isSelectionInMarker as _isRestrictedEditingSelectionInMarker, resurrectCollapsedMarkerPostFixer as _resurrectRestrictedEditingCollapsedMarkerPostFixer, setupExceptionHighlighting as _setupRestrictedEditingExceptionHighlighting, upcastHighlightToMarker as _upcastRestrictedEditingHighlightToMarker };
1091
1550
  //# sourceMappingURL=index.js.map