@ckeditor/ckeditor5-restricted-editing 47.6.1-alpha.1 → 48.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 (184) hide show
  1. package/ckeditor5-metadata.json +6 -6
  2. package/dist/index-editor.css +59 -0
  3. package/dist/index.css +61 -0
  4. package/dist/index.css.map +1 -0
  5. package/dist/index.js.map +1 -1
  6. package/{src → dist}/restrictededitingexceptionautocommand.d.ts +1 -1
  7. package/{src → dist}/restrictededitingexceptionblockcommand.d.ts +1 -1
  8. package/{src → dist}/restrictededitingexceptioncommand.d.ts +1 -1
  9. package/{src → dist}/restrictededitingmode/converters.d.ts +2 -2
  10. package/{src → dist}/restrictededitingmode/utils.d.ts +2 -2
  11. package/{src → dist}/restrictededitingmode.d.ts +1 -1
  12. package/{src → dist}/restrictededitingmodeediting.d.ts +1 -1
  13. package/{src → dist}/restrictededitingmodenavigationcommand.d.ts +1 -1
  14. package/{src → dist}/restrictededitingmodeui.d.ts +1 -1
  15. package/{src → dist}/standardeditingmode.d.ts +1 -1
  16. package/{src → dist}/standardeditingmodeediting.d.ts +1 -1
  17. package/{src → dist}/standardeditingmodeui.d.ts +1 -1
  18. package/package.json +25 -47
  19. package/build/restricted-editing.js +0 -5
  20. package/build/translations/af.js +0 -1
  21. package/build/translations/ar.js +0 -1
  22. package/build/translations/ast.js +0 -1
  23. package/build/translations/az.js +0 -1
  24. package/build/translations/be.js +0 -1
  25. package/build/translations/bg.js +0 -1
  26. package/build/translations/bn.js +0 -1
  27. package/build/translations/bs.js +0 -1
  28. package/build/translations/ca.js +0 -1
  29. package/build/translations/cs.js +0 -1
  30. package/build/translations/da.js +0 -1
  31. package/build/translations/de-ch.js +0 -1
  32. package/build/translations/de.js +0 -1
  33. package/build/translations/el.js +0 -1
  34. package/build/translations/en-au.js +0 -1
  35. package/build/translations/en-gb.js +0 -1
  36. package/build/translations/eo.js +0 -1
  37. package/build/translations/es-co.js +0 -1
  38. package/build/translations/es.js +0 -1
  39. package/build/translations/et.js +0 -1
  40. package/build/translations/eu.js +0 -1
  41. package/build/translations/fa.js +0 -1
  42. package/build/translations/fi.js +0 -1
  43. package/build/translations/fr.js +0 -1
  44. package/build/translations/gl.js +0 -1
  45. package/build/translations/gu.js +0 -1
  46. package/build/translations/he.js +0 -1
  47. package/build/translations/hi.js +0 -1
  48. package/build/translations/hr.js +0 -1
  49. package/build/translations/hu.js +0 -1
  50. package/build/translations/hy.js +0 -1
  51. package/build/translations/id.js +0 -1
  52. package/build/translations/it.js +0 -1
  53. package/build/translations/ja.js +0 -1
  54. package/build/translations/jv.js +0 -1
  55. package/build/translations/kk.js +0 -1
  56. package/build/translations/km.js +0 -1
  57. package/build/translations/kn.js +0 -1
  58. package/build/translations/ko.js +0 -1
  59. package/build/translations/ku.js +0 -1
  60. package/build/translations/lt.js +0 -1
  61. package/build/translations/lv.js +0 -1
  62. package/build/translations/ms.js +0 -1
  63. package/build/translations/nb.js +0 -1
  64. package/build/translations/ne.js +0 -1
  65. package/build/translations/nl.js +0 -1
  66. package/build/translations/no.js +0 -1
  67. package/build/translations/oc.js +0 -1
  68. package/build/translations/pl.js +0 -1
  69. package/build/translations/pt-br.js +0 -1
  70. package/build/translations/pt.js +0 -1
  71. package/build/translations/ro.js +0 -1
  72. package/build/translations/ru.js +0 -1
  73. package/build/translations/si.js +0 -1
  74. package/build/translations/sk.js +0 -1
  75. package/build/translations/sl.js +0 -1
  76. package/build/translations/sq.js +0 -1
  77. package/build/translations/sr-latn.js +0 -1
  78. package/build/translations/sr.js +0 -1
  79. package/build/translations/sv.js +0 -1
  80. package/build/translations/th.js +0 -1
  81. package/build/translations/ti.js +0 -1
  82. package/build/translations/tk.js +0 -1
  83. package/build/translations/tr.js +0 -1
  84. package/build/translations/tt.js +0 -1
  85. package/build/translations/ug.js +0 -1
  86. package/build/translations/uk.js +0 -1
  87. package/build/translations/ur.js +0 -1
  88. package/build/translations/uz.js +0 -1
  89. package/build/translations/vi.js +0 -1
  90. package/build/translations/zh-cn.js +0 -1
  91. package/build/translations/zh.js +0 -1
  92. package/lang/contexts.json +0 -11
  93. package/lang/translations/af.po +0 -48
  94. package/lang/translations/ar.po +0 -48
  95. package/lang/translations/ast.po +0 -48
  96. package/lang/translations/az.po +0 -48
  97. package/lang/translations/be.po +0 -48
  98. package/lang/translations/bg.po +0 -48
  99. package/lang/translations/bn.po +0 -48
  100. package/lang/translations/bs.po +0 -48
  101. package/lang/translations/ca.po +0 -48
  102. package/lang/translations/cs.po +0 -48
  103. package/lang/translations/da.po +0 -48
  104. package/lang/translations/de-ch.po +0 -48
  105. package/lang/translations/de.po +0 -48
  106. package/lang/translations/el.po +0 -48
  107. package/lang/translations/en-au.po +0 -48
  108. package/lang/translations/en-gb.po +0 -48
  109. package/lang/translations/en.po +0 -48
  110. package/lang/translations/eo.po +0 -48
  111. package/lang/translations/es-co.po +0 -48
  112. package/lang/translations/es.po +0 -48
  113. package/lang/translations/et.po +0 -48
  114. package/lang/translations/eu.po +0 -48
  115. package/lang/translations/fa.po +0 -48
  116. package/lang/translations/fi.po +0 -48
  117. package/lang/translations/fr.po +0 -48
  118. package/lang/translations/gl.po +0 -48
  119. package/lang/translations/gu.po +0 -48
  120. package/lang/translations/he.po +0 -48
  121. package/lang/translations/hi.po +0 -48
  122. package/lang/translations/hr.po +0 -48
  123. package/lang/translations/hu.po +0 -48
  124. package/lang/translations/hy.po +0 -48
  125. package/lang/translations/id.po +0 -48
  126. package/lang/translations/it.po +0 -48
  127. package/lang/translations/ja.po +0 -48
  128. package/lang/translations/jv.po +0 -48
  129. package/lang/translations/kk.po +0 -48
  130. package/lang/translations/km.po +0 -48
  131. package/lang/translations/kn.po +0 -48
  132. package/lang/translations/ko.po +0 -48
  133. package/lang/translations/ku.po +0 -48
  134. package/lang/translations/lt.po +0 -48
  135. package/lang/translations/lv.po +0 -48
  136. package/lang/translations/ms.po +0 -48
  137. package/lang/translations/nb.po +0 -48
  138. package/lang/translations/ne.po +0 -48
  139. package/lang/translations/nl.po +0 -48
  140. package/lang/translations/no.po +0 -48
  141. package/lang/translations/oc.po +0 -48
  142. package/lang/translations/pl.po +0 -48
  143. package/lang/translations/pt-br.po +0 -48
  144. package/lang/translations/pt.po +0 -48
  145. package/lang/translations/ro.po +0 -48
  146. package/lang/translations/ru.po +0 -48
  147. package/lang/translations/si.po +0 -48
  148. package/lang/translations/sk.po +0 -48
  149. package/lang/translations/sl.po +0 -48
  150. package/lang/translations/sq.po +0 -48
  151. package/lang/translations/sr-latn.po +0 -48
  152. package/lang/translations/sr.po +0 -48
  153. package/lang/translations/sv.po +0 -48
  154. package/lang/translations/th.po +0 -48
  155. package/lang/translations/ti.po +0 -48
  156. package/lang/translations/tk.po +0 -48
  157. package/lang/translations/tr.po +0 -48
  158. package/lang/translations/tt.po +0 -48
  159. package/lang/translations/ug.po +0 -48
  160. package/lang/translations/uk.po +0 -48
  161. package/lang/translations/ur.po +0 -48
  162. package/lang/translations/uz.po +0 -48
  163. package/lang/translations/vi.po +0 -48
  164. package/lang/translations/zh-cn.po +0 -48
  165. package/lang/translations/zh.po +0 -48
  166. package/src/augmentation.js +0 -5
  167. package/src/index.js +0 -20
  168. package/src/restrictededitingconfig.js +0 -5
  169. package/src/restrictededitingexceptionautocommand.js +0 -73
  170. package/src/restrictededitingexceptionblockcommand.js +0 -203
  171. package/src/restrictededitingexceptioncommand.js +0 -61
  172. package/src/restrictededitingmode/converters.js +0 -177
  173. package/src/restrictededitingmode/utils.js +0 -69
  174. package/src/restrictededitingmode.js +0 -39
  175. package/src/restrictededitingmodeediting.js +0 -512
  176. package/src/restrictededitingmodenavigationcommand.js +0 -99
  177. package/src/restrictededitingmodeui.js +0 -139
  178. package/src/standardeditingmode.js +0 -36
  179. package/src/standardeditingmodeediting.js +0 -193
  180. package/src/standardeditingmodeui.js +0 -150
  181. package/theme/restrictedediting.css +0 -10
  182. /package/{src → dist}/augmentation.d.ts +0 -0
  183. /package/{src → dist}/index.d.ts +0 -0
  184. /package/{src → dist}/restrictededitingconfig.d.ts +0 -0
@@ -1,512 +0,0 @@
1
- /**
2
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
- */
5
- /**
6
- * @module restricted-editing/restrictededitingmodeediting
7
- */
8
- import { Plugin } from 'ckeditor5/src/core.js';
9
- import { getCode, parseKeystroke } from 'ckeditor5/src/utils.js';
10
- import { RestrictedEditingModeNavigationCommand } from './restrictededitingmodenavigationcommand.js';
11
- import { getMarkerAtPosition, isSelectionInMarker, getExceptionRange } from './restrictededitingmode/utils.js';
12
- import { extendMarkerOnTypingPostFixer, resurrectCollapsedMarkerPostFixer, setupExceptionHighlighting, upcastHighlightToMarker } from './restrictededitingmode/converters.js';
13
- const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode';
14
- /**
15
- * The restricted editing mode editing feature.
16
- *
17
- * * It introduces the exception marker group that renders to `<span>` elements with the `restricted-editing-exception` CSS class.
18
- * * It registers the `'goToPreviousRestrictedEditingException'` and `'goToNextRestrictedEditingException'` commands.
19
- * * It also enables highlighting exception markers that are selected.
20
- */
21
- export class RestrictedEditingModeEditing extends Plugin {
22
- /**
23
- * Command names that are enabled outside the non-restricted regions.
24
- */
25
- _alwaysEnabled;
26
- /**
27
- * Commands allowed in non-restricted areas.
28
- *
29
- * Commands always enabled combine typing feature commands: `'input'`, `'insertText'`, `'delete'`, and `'deleteForward'` with
30
- * commands defined in the feature configuration.
31
- */
32
- _allowedInException;
33
- /**
34
- * @inheritDoc
35
- */
36
- static get pluginName() {
37
- return 'RestrictedEditingModeEditing';
38
- }
39
- /**
40
- * @inheritDoc
41
- * @internal
42
- */
43
- static get licenseFeatureCode() {
44
- return 'RED';
45
- }
46
- /**
47
- * @inheritDoc
48
- */
49
- static get isOfficialPlugin() {
50
- return true;
51
- }
52
- /**
53
- * @inheritDoc
54
- */
55
- static get isPremiumPlugin() {
56
- return true;
57
- }
58
- /**
59
- * @inheritDoc
60
- */
61
- constructor(editor) {
62
- super(editor);
63
- editor.config.define('restrictedEditing', {
64
- allowedCommands: ['bold', 'italic', 'link', 'unlink'],
65
- allowedAttributes: ['bold', 'italic', 'linkHref']
66
- });
67
- this._alwaysEnabled = new Set(['undo', 'redo']);
68
- this._allowedInException = new Set(['input', 'insertText', 'delete', 'deleteForward']);
69
- }
70
- /**
71
- * @inheritDoc
72
- */
73
- init() {
74
- const editor = this.editor;
75
- const editingView = editor.editing.view;
76
- const allowedCommands = editor.config.get('restrictedEditing.allowedCommands');
77
- allowedCommands.forEach(commandName => this._allowedInException.add(commandName));
78
- this._setupSchema();
79
- this._setupConversion();
80
- this._setupCommandsToggling();
81
- this._setupRestrictions();
82
- // Commands & keystrokes that allow navigation in the content.
83
- editor.commands.add('goToPreviousRestrictedEditingException', new RestrictedEditingModeNavigationCommand(editor, 'backward'));
84
- editor.commands.add('goToNextRestrictedEditingException', new RestrictedEditingModeNavigationCommand(editor, 'forward'));
85
- this.listenTo(editingView.document, 'tab', (evt, data) => {
86
- const commandName = !data.shiftKey ? 'goToNextRestrictedEditingException' : 'goToPreviousRestrictedEditingException';
87
- const command = editor.commands.get(commandName);
88
- if (command.isEnabled) {
89
- editor.execute(commandName);
90
- // Stop the event in the DOM: no listener in the web page will be triggered by this event.
91
- data.preventDefault();
92
- data.stopPropagation();
93
- }
94
- // Stop the event bubbling in the editor: no more callbacks will be executed for this keystroke.
95
- evt.stop();
96
- }, { context: '$capture' });
97
- this.listenTo(editingView.document, 'keydown', getSelectAllHandler(editor), { priority: 'high' });
98
- editingView.change(writer => {
99
- for (const root of editingView.document.roots) {
100
- writer.addClass('ck-restricted-editing_mode_restricted', root);
101
- }
102
- });
103
- // Remove existing restricted editing markers when setting new data to prevent marker resurrection.
104
- // Without this, markers from removed content would be incorrectly restored due to the resurrection mechanism.
105
- // See more: https://github.com/ckeditor/ckeditor5/issues/9646#issuecomment-843064995
106
- editor.data.on('set', () => {
107
- editor.model.change(writer => {
108
- for (const marker of editor.model.markers.getMarkersGroup('restrictedEditingException')) {
109
- writer.removeMarker(marker.name);
110
- }
111
- });
112
- }, { priority: 'high' });
113
- }
114
- /**
115
- * Makes the given command always enabled in the restricted editing mode (regardless
116
- * of selection location).
117
- *
118
- * To enable some commands in non-restricted areas of the content use
119
- * {@link module:restricted-editing/restrictededitingconfig~RestrictedEditingConfig#allowedCommands} configuration option.
120
- *
121
- * @param commandName Name of the command to enable.
122
- */
123
- enableCommand(commandName) {
124
- const command = this.editor.commands.get(commandName);
125
- command.clearForceDisabled(COMMAND_FORCE_DISABLE_ID);
126
- this._alwaysEnabled.add(commandName);
127
- }
128
- /**
129
- * Registers block exception wrapper in the schema.
130
- */
131
- _setupSchema() {
132
- const schema = this.editor.model.schema;
133
- schema.register('restrictedEditingException', {
134
- allowWhere: '$block',
135
- allowContentOf: '$container',
136
- isLimit: true
137
- });
138
- }
139
- /**
140
- * Sets up the restricted mode editing conversion:
141
- *
142
- * * ucpast & downcast converters,
143
- * * marker highlighting in the edting area,
144
- * * marker post-fixers.
145
- */
146
- _setupConversion() {
147
- const editor = this.editor;
148
- const model = editor.model;
149
- const doc = model.document;
150
- // The restricted editing does not attach additional data to the zones so there's no need for smarter markers managing.
151
- // Also, the markers will only be created when loading the data.
152
- let markerNumber = 0;
153
- editor.conversion.for('upcast').add(upcastHighlightToMarker({
154
- view: {
155
- name: 'span',
156
- classes: 'restricted-editing-exception'
157
- },
158
- model: () => {
159
- markerNumber++; // Starting from restrictedEditingException:1 marker.
160
- return `restrictedEditingException:inline:${markerNumber}`;
161
- }
162
- }));
163
- editor.conversion.for('upcast').add(upcastHighlightToMarker({
164
- view: {
165
- name: 'div',
166
- classes: 'restricted-editing-exception'
167
- },
168
- model: () => {
169
- markerNumber++; // Starting from restrictedEditingException:1 marker.
170
- return `restrictedEditingException:block:${markerNumber}`;
171
- },
172
- useWrapperElement: true
173
- }));
174
- // Block exception wrapper.
175
- editor.conversion.for('downcast').elementToElement({
176
- model: 'restrictedEditingException',
177
- view: {
178
- name: 'div',
179
- classes: 'restricted-editing-exception'
180
- }
181
- });
182
- // Currently the marker helpers are tied to other use-cases and do not render a collapsed marker as highlight.
183
- // Also, markerToHighlight cannot convert marker on an inline object. It handles only text and widgets,
184
- // but it is not a case in the data pipeline. That's why there are 3 downcast converters for them:
185
- //
186
- // 1. The custom inline item (text or inline object) converter (but not the selection).
187
- editor.conversion.for('downcast').add(dispatcher => {
188
- dispatcher.on('addMarker:restrictedEditingException:inline', (evt, data, conversionApi) => {
189
- // Only convert per-item conversion.
190
- if (!data.item) {
191
- return;
192
- }
193
- // Do not convert the selection or non-inline items.
194
- if (data.item.is('selection') || !conversionApi.schema.isInline(data.item)) {
195
- return;
196
- }
197
- if (!conversionApi.consumable.consume(data.item, evt.name)) {
198
- return;
199
- }
200
- const viewWriter = conversionApi.writer;
201
- const viewElement = viewWriter.createAttributeElement('span', {
202
- class: 'restricted-editing-exception'
203
- }, {
204
- id: data.markerName,
205
- priority: -10
206
- });
207
- const viewRange = conversionApi.mapper.toViewRange(data.range);
208
- const rangeAfterWrap = viewWriter.wrap(viewRange, viewElement);
209
- for (const element of rangeAfterWrap.getItems()) {
210
- if (element.is('attributeElement') && element.isSimilar(viewElement)) {
211
- conversionApi.mapper.bindElementToMarker(element, data.markerName);
212
- // One attribute element is enough, because all of them are bound together by the view writer.
213
- // Mapper uses this binding to get all the elements no matter how many of them are registered in the mapper.
214
- break;
215
- }
216
- }
217
- });
218
- });
219
- // 2. The marker-to-highlight converter for the document selection.
220
- editor.conversion.for('downcast').markerToHighlight({
221
- model: 'restrictedEditingException:inline',
222
- // Use callback to return new object every time new marker instance is created - otherwise it will be seen as the same marker.
223
- view: () => {
224
- return {
225
- name: 'span',
226
- classes: 'restricted-editing-exception',
227
- priority: -10
228
- };
229
- }
230
- });
231
- // 3. And for collapsed marker we need to render it as an element.
232
- // Additionally, the editing pipeline should always display a collapsed marker.
233
- editor.conversion.for('editingDowncast').markerToElement({
234
- model: 'restrictedEditingException:inline',
235
- view: (markerData, { writer }) => {
236
- return writer.createUIElement('span', {
237
- class: 'restricted-editing-exception restricted-editing-exception_collapsed'
238
- });
239
- }
240
- });
241
- editor.conversion.for('dataDowncast').markerToElement({
242
- model: 'restrictedEditingException:inline',
243
- view: (markerData, { writer }) => {
244
- return writer.createEmptyElement('span', {
245
- class: 'restricted-editing-exception'
246
- });
247
- }
248
- });
249
- doc.registerPostFixer(extendMarkerOnTypingPostFixer(editor));
250
- doc.registerPostFixer(resurrectCollapsedMarkerPostFixer(editor));
251
- doc.registerPostFixer(ensureNewMarkerIsFlatPostFixer(editor));
252
- setupExceptionHighlighting(editor);
253
- }
254
- /**
255
- * Setups additional editing restrictions beyond command toggling:
256
- *
257
- * * delete content range trimming
258
- * * disabling input command outside exception marker
259
- * * restricting clipboard holder to text only
260
- * * restricting text attributes in content
261
- */
262
- _setupRestrictions() {
263
- const editor = this.editor;
264
- const model = editor.model;
265
- const selection = model.document.selection;
266
- const viewDoc = editor.editing.view.document;
267
- const clipboard = editor.plugins.get('ClipboardPipeline');
268
- this.listenTo(model, 'deleteContent', restrictDeleteContent(editor), { priority: 'high' });
269
- const insertTextCommand = editor.commands.get('insertText');
270
- // The restricted editing might be configured without insert text support - ie allow only bolding or removing text.
271
- // This check is bit synthetic since only tests are used this way.
272
- if (insertTextCommand) {
273
- this.listenTo(insertTextCommand, 'execute', disallowInputExecForWrongRange(editor), { priority: 'high' });
274
- }
275
- // Block clipboard outside exception marker on paste and drop.
276
- this.listenTo(clipboard, 'contentInsertion', (evt, data) => {
277
- if (!isRangeInsideSingleMarker(editor, selection.getFirstRange())) {
278
- evt.stop();
279
- }
280
- const marker = getMarkerAtPosition(editor, selection.focus);
281
- // Reduce content pasted into inline exception to text nodes only. Also strip not allowed attributes.
282
- if (marker && marker.name.startsWith('restrictedEditingException:inline:')) {
283
- const allowedAttributes = editor.config.get('restrictedEditing.allowedAttributes');
284
- model.change(writer => {
285
- const content = writer.createDocumentFragment();
286
- const textNodes = Array.from(writer.createRangeIn(data.content).getItems())
287
- .filter(node => node.is('$textProxy'));
288
- for (const item of textNodes) {
289
- for (const attr of item.getAttributeKeys()) {
290
- if (!allowedAttributes.includes(attr)) {
291
- writer.removeAttribute(attr, item);
292
- }
293
- }
294
- writer.append(item, content);
295
- }
296
- data.content = content;
297
- });
298
- }
299
- });
300
- // Block clipboard outside exception marker on cut.
301
- this.listenTo(viewDoc, 'clipboardOutput', (evt, data) => {
302
- if (data.method == 'cut' && !isRangeInsideSingleMarker(editor, selection.getFirstRange())) {
303
- evt.stop();
304
- }
305
- }, { priority: 'high' });
306
- // Do not allow pasting/dropping block exception wrapper.
307
- model.schema.addChildCheck(context => {
308
- if (context.startsWith('$clipboardHolder')) {
309
- return false;
310
- }
311
- }, 'restrictedEditingException');
312
- }
313
- /**
314
- * Sets up the command toggling which enables or disables commands based on the user selection.
315
- */
316
- _setupCommandsToggling() {
317
- const editor = this.editor;
318
- const model = editor.model;
319
- const doc = model.document;
320
- this._disableCommands();
321
- this.listenTo(doc.selection, 'change', this._checkCommands.bind(this));
322
- this.listenTo(doc, 'change:data', this._checkCommands.bind(this));
323
- }
324
- /**
325
- * Checks if commands should be enabled or disabled based on the current selection.
326
- */
327
- _checkCommands() {
328
- const editor = this.editor;
329
- const selection = editor.model.document.selection;
330
- if (selection.rangeCount > 1) {
331
- this._disableCommands();
332
- return;
333
- }
334
- const marker = getMarkerAtPosition(editor, selection.focus);
335
- this._disableCommands();
336
- if (isSelectionInMarker(selection, editor.model, marker)) {
337
- this._enableCommands(marker);
338
- }
339
- }
340
- /**
341
- * Enables commands in non-restricted regions.
342
- */
343
- _enableCommands(marker) {
344
- const editor = this.editor;
345
- const selection = editor.model.document.selection;
346
- for (const [commandName, command] of editor.commands) {
347
- if (!command.affectsData || this._alwaysEnabled.has(commandName)) {
348
- continue;
349
- }
350
- // Enable ony those commands that are allowed in the exception marker.
351
- // In block exceptions all commands are enabled.
352
- if (!marker.name.startsWith('restrictedEditingException:block:') &&
353
- !this._allowedInException.has(commandName)) {
354
- continue;
355
- }
356
- // Do not enable 'delete' and 'deleteForward' commands on the exception marker boundaries.
357
- if (isDeleteCommandOnMarkerBoundaries(commandName, selection, getExceptionRange(marker, editor.model))) {
358
- continue;
359
- }
360
- command.clearForceDisabled(COMMAND_FORCE_DISABLE_ID);
361
- }
362
- }
363
- /**
364
- * Disables commands outside non-restricted regions.
365
- */
366
- _disableCommands() {
367
- const editor = this.editor;
368
- for (const [commandName, command] of editor.commands) {
369
- if (!command.affectsData || this._alwaysEnabled.has(commandName)) {
370
- continue;
371
- }
372
- command.forceDisabled(COMMAND_FORCE_DISABLE_ID);
373
- }
374
- }
375
- }
376
- /**
377
- * Helper for handling Ctrl+A keydown behaviour.
378
- */
379
- function getSelectAllHandler(editor) {
380
- return (eventInfo, domEventData) => {
381
- if (getCode(domEventData) != parseKeystroke('Ctrl+A')) {
382
- return;
383
- }
384
- const model = editor.model;
385
- const selection = editor.model.document.selection;
386
- const marker = getMarkerAtPosition(editor, selection.focus);
387
- if (!marker) {
388
- return;
389
- }
390
- // If selection range is inside a restricted editing exception, select text only within the exception.
391
- //
392
- // Note: Second Ctrl+A press is also blocked and it won't select the entire text in the editor.
393
- const selectionRange = selection.getFirstRange();
394
- const markerRange = getExceptionRange(marker, editor.model);
395
- if (markerRange.containsRange(selectionRange, true) || selection.isCollapsed) {
396
- eventInfo.stop();
397
- domEventData.preventDefault();
398
- domEventData.stopPropagation();
399
- model.change(writer => {
400
- writer.setSelection(markerRange);
401
- });
402
- }
403
- };
404
- }
405
- /**
406
- * Additional rule for enabling "delete" and "deleteForward" commands if selection is on range boundaries:
407
- *
408
- * Does not allow to enable command when selection focus is:
409
- * - is on marker start - "delete" - to prevent removing content before marker
410
- * - is on marker end - "deleteForward" - to prevent removing content after marker
411
- */
412
- function isDeleteCommandOnMarkerBoundaries(commandName, selection, markerRange) {
413
- if (commandName == 'delete' && selection.isCollapsed && markerRange.start.isTouching(selection.focus)) {
414
- return true;
415
- }
416
- // Only for collapsed selection - non-collapsed selection that extends over a marker is handled elsewhere.
417
- if (commandName == 'deleteForward' && selection.isCollapsed && markerRange.end.isTouching(selection.focus)) {
418
- return true;
419
- }
420
- return false;
421
- }
422
- /**
423
- * Ensures that model.deleteContent() does not delete outside exception markers ranges.
424
- *
425
- * The enforced restrictions are:
426
- * - only execute deleteContent() inside exception markers
427
- * - restrict passed selection to exception marker
428
- */
429
- function restrictDeleteContent(editor) {
430
- return (evt, args) => {
431
- const [selection] = args;
432
- const marker = getMarkerAtPosition(editor, selection.focus) || getMarkerAtPosition(editor, selection.anchor);
433
- // Stop method execution if marker was not found at selection focus.
434
- if (!marker) {
435
- evt.stop();
436
- return;
437
- }
438
- // Collapsed selection inside exception marker does not require fixing.
439
- if (selection.isCollapsed) {
440
- return;
441
- }
442
- // Shrink the selection to the range inside exception marker.
443
- const allowedToDelete = getExceptionRange(marker, editor.model).getIntersection(selection.getFirstRange());
444
- // Some features uses selection passed to model.deleteContent() to set the selection afterwards. For this we need to properly modify
445
- // either the document selection using change block...
446
- if (selection.is('documentSelection')) {
447
- editor.model.change(writer => {
448
- writer.setSelection(allowedToDelete);
449
- });
450
- }
451
- // ... or by modifying passed selection instance directly.
452
- else {
453
- selection.setTo(allowedToDelete);
454
- }
455
- };
456
- }
457
- /**
458
- * Ensures that input command is executed with a range that is inside exception marker.
459
- *
460
- * This restriction is due to fact that using native spell check changes text outside exception marker.
461
- */
462
- function disallowInputExecForWrongRange(editor) {
463
- return (evt, args) => {
464
- const [options] = args;
465
- const { range } = options;
466
- // Only check "input" command executed with a range value.
467
- // Selection might be set in exception marker but passed range might point elsewhere.
468
- if (!range) {
469
- return;
470
- }
471
- if (!isRangeInsideSingleMarker(editor, range)) {
472
- evt.stop();
473
- }
474
- };
475
- }
476
- function isRangeInsideSingleMarker(editor, range) {
477
- const markerAtStart = getMarkerAtPosition(editor, range.start);
478
- const markerAtEnd = getMarkerAtPosition(editor, range.end);
479
- return markerAtStart && markerAtEnd && markerAtEnd === markerAtStart;
480
- }
481
- /**
482
- * Checks if new marker range is flat. Non-flat ranges might appear during upcast conversion in nested structures, ie tables.
483
- *
484
- * Note: This marker fixer only consider case which is possible to create using StandardEditing mode plugin.
485
- * Markers created by developer in the data might break in many other ways.
486
- *
487
- * See #6003.
488
- */
489
- function ensureNewMarkerIsFlatPostFixer(editor) {
490
- return writer => {
491
- let changeApplied = false;
492
- const changedMarkers = editor.model.document.differ.getChangedMarkers();
493
- for (const { data, name } of changedMarkers) {
494
- if (!name.startsWith('restrictedEditingException')) {
495
- continue;
496
- }
497
- const newRange = data.newRange;
498
- if (!data.oldRange && !newRange.isFlat) {
499
- const start = newRange.start;
500
- const end = newRange.end;
501
- const startIsHigherInTree = start.path.length > end.path.length;
502
- const fixedStart = startIsHigherInTree ? newRange.start : writer.createPositionAt(end.parent, 0);
503
- const fixedEnd = startIsHigherInTree ? writer.createPositionAt(start.parent, 'end') : newRange.end;
504
- writer.updateMarker(name, {
505
- range: writer.createRange(fixedStart, fixedEnd)
506
- });
507
- changeApplied = true;
508
- }
509
- }
510
- return changeApplied;
511
- };
512
- }
@@ -1,99 +0,0 @@
1
- /**
2
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
- */
5
- import { Command } from 'ckeditor5/src/core.js';
6
- import { getExceptionRange } from './restrictededitingmode/utils.js';
7
- /**
8
- * The command that allows navigation across the exceptions in the edited document.
9
- */
10
- export class RestrictedEditingModeNavigationCommand extends Command {
11
- /**
12
- * The direction of the command.
13
- */
14
- _direction;
15
- /**
16
- * Creates an instance of the command.
17
- *
18
- * @param editor The editor instance.
19
- * @param direction The direction that the command works.
20
- */
21
- constructor(editor, direction) {
22
- super(editor);
23
- // It does not affect data so should be enabled in read-only mode and in restricted editing mode.
24
- this.affectsData = false;
25
- this._direction = direction;
26
- }
27
- /**
28
- * @inheritDoc
29
- */
30
- refresh() {
31
- this.isEnabled = this._checkEnabled();
32
- }
33
- /**
34
- * Executes the command.
35
- *
36
- * @fires execute
37
- */
38
- execute() {
39
- const position = getNearestExceptionRange(this.editor.model, this._direction);
40
- if (!position) {
41
- return;
42
- }
43
- this.editor.model.change(writer => {
44
- writer.setSelection(position);
45
- });
46
- }
47
- /**
48
- * Checks whether the command can be enabled in the current context.
49
- *
50
- * @returns Whether the command should be enabled.
51
- */
52
- _checkEnabled() {
53
- return !!getNearestExceptionRange(this.editor.model, this._direction);
54
- }
55
- }
56
- /**
57
- * Returns the range of the exception marker closest to the last position of the model selection.
58
- */
59
- function getNearestExceptionRange(model, direction) {
60
- const selection = model.document.selection;
61
- const selectionPosition = selection.getFirstPosition();
62
- const markerRanges = [];
63
- // Get all exception marker positions that start after/before the selection position.
64
- for (const marker of model.markers.getMarkersGroup('restrictedEditingException')) {
65
- const markerRange = getExceptionRange(marker, model);
66
- // Checking parent because there two positions <paragraph>foo^</paragraph><paragraph>^bar</paragraph>
67
- // are touching but they will represent different markers.
68
- const isMarkerRangeTouching = selectionPosition.isTouching(markerRange.start) && selectionPosition.hasSameParentAs(markerRange.start) ||
69
- selectionPosition.isTouching(markerRange.end) && selectionPosition.hasSameParentAs(markerRange.end);
70
- // <paragraph>foo <marker≥b[]ar</marker> baz</paragraph>
71
- // <paragraph>foo <marker≥b[ar</marker> ba]z</paragraph>
72
- // <paragraph>foo <marker≥bar</marker>[] baz</paragraph>
73
- // <paragraph>foo []<marker≥bar</marker> baz</paragraph>
74
- if (markerRange.containsPosition(selectionPosition) || isMarkerRangeTouching) {
75
- continue;
76
- }
77
- if (direction === 'forward' && markerRange.start.isAfter(selectionPosition)) {
78
- markerRanges.push(markerRange);
79
- }
80
- else if (direction === 'backward' && markerRange.end.isBefore(selectionPosition)) {
81
- markerRanges.push(markerRange);
82
- }
83
- }
84
- if (!markerRanges.length) {
85
- return;
86
- }
87
- // Get the marker closest to the selection position among many. To know that, we need to sort
88
- // them first.
89
- return markerRanges
90
- .sort((rangeA, rangeB) => {
91
- if (direction === 'forward') {
92
- return rangeA.start.isAfter(rangeB.start) ? 1 : -1;
93
- }
94
- else {
95
- return rangeA.start.isBefore(rangeB.start) ? 1 : -1;
96
- }
97
- })
98
- .shift();
99
- }