@ckeditor/ckeditor5-source-editing 41.3.1 → 41.4.0-alpha.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. package/dist/index-content.css +4 -0
  2. package/dist/index-editor.css +43 -0
  3. package/dist/index.css +87 -0
  4. package/dist/index.css.map +1 -0
  5. package/dist/index.js +583 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/translations/ar.d.ts +8 -0
  8. package/dist/translations/ar.js +5 -0
  9. package/dist/translations/bg.d.ts +8 -0
  10. package/dist/translations/bg.js +5 -0
  11. package/dist/translations/bn.d.ts +8 -0
  12. package/dist/translations/bn.js +5 -0
  13. package/dist/translations/ca.d.ts +8 -0
  14. package/dist/translations/ca.js +5 -0
  15. package/dist/translations/cs.d.ts +8 -0
  16. package/dist/translations/cs.js +5 -0
  17. package/dist/translations/da.d.ts +8 -0
  18. package/dist/translations/da.js +5 -0
  19. package/dist/translations/de.d.ts +8 -0
  20. package/dist/translations/de.js +5 -0
  21. package/dist/translations/el.d.ts +8 -0
  22. package/dist/translations/el.js +5 -0
  23. package/dist/translations/en-au.d.ts +8 -0
  24. package/dist/translations/en-au.js +5 -0
  25. package/dist/translations/en.d.ts +8 -0
  26. package/dist/translations/en.js +5 -0
  27. package/dist/translations/es.d.ts +8 -0
  28. package/dist/translations/es.js +5 -0
  29. package/dist/translations/et.d.ts +8 -0
  30. package/dist/translations/et.js +5 -0
  31. package/dist/translations/fi.d.ts +8 -0
  32. package/dist/translations/fi.js +5 -0
  33. package/dist/translations/fr.d.ts +8 -0
  34. package/dist/translations/fr.js +5 -0
  35. package/dist/translations/gl.d.ts +8 -0
  36. package/dist/translations/gl.js +5 -0
  37. package/dist/translations/he.d.ts +8 -0
  38. package/dist/translations/he.js +5 -0
  39. package/dist/translations/hi.d.ts +8 -0
  40. package/dist/translations/hi.js +5 -0
  41. package/dist/translations/hr.d.ts +8 -0
  42. package/dist/translations/hr.js +5 -0
  43. package/dist/translations/hu.d.ts +8 -0
  44. package/dist/translations/hu.js +5 -0
  45. package/dist/translations/id.d.ts +8 -0
  46. package/dist/translations/id.js +5 -0
  47. package/dist/translations/it.d.ts +8 -0
  48. package/dist/translations/it.js +5 -0
  49. package/dist/translations/ja.d.ts +8 -0
  50. package/dist/translations/ja.js +5 -0
  51. package/dist/translations/ko.d.ts +8 -0
  52. package/dist/translations/ko.js +5 -0
  53. package/dist/translations/lt.d.ts +8 -0
  54. package/dist/translations/lt.js +5 -0
  55. package/dist/translations/lv.d.ts +8 -0
  56. package/dist/translations/lv.js +5 -0
  57. package/dist/translations/ms.d.ts +8 -0
  58. package/dist/translations/ms.js +5 -0
  59. package/dist/translations/nl.d.ts +8 -0
  60. package/dist/translations/nl.js +5 -0
  61. package/dist/translations/no.d.ts +8 -0
  62. package/dist/translations/no.js +5 -0
  63. package/dist/translations/pl.d.ts +8 -0
  64. package/dist/translations/pl.js +5 -0
  65. package/dist/translations/pt-br.d.ts +8 -0
  66. package/dist/translations/pt-br.js +5 -0
  67. package/dist/translations/pt.d.ts +8 -0
  68. package/dist/translations/pt.js +5 -0
  69. package/dist/translations/ro.d.ts +8 -0
  70. package/dist/translations/ro.js +5 -0
  71. package/dist/translations/ru.d.ts +8 -0
  72. package/dist/translations/ru.js +5 -0
  73. package/dist/translations/sk.d.ts +8 -0
  74. package/dist/translations/sk.js +5 -0
  75. package/dist/translations/sr-latn.d.ts +8 -0
  76. package/dist/translations/sr-latn.js +5 -0
  77. package/dist/translations/sr.d.ts +8 -0
  78. package/dist/translations/sr.js +5 -0
  79. package/dist/translations/sv.d.ts +8 -0
  80. package/dist/translations/sv.js +5 -0
  81. package/dist/translations/th.d.ts +8 -0
  82. package/dist/translations/th.js +5 -0
  83. package/dist/translations/tr.d.ts +8 -0
  84. package/dist/translations/tr.js +5 -0
  85. package/dist/translations/ug.d.ts +8 -0
  86. package/dist/translations/ug.js +5 -0
  87. package/dist/translations/uk.d.ts +8 -0
  88. package/dist/translations/uk.js +5 -0
  89. package/dist/translations/ur.d.ts +8 -0
  90. package/dist/translations/ur.js +5 -0
  91. package/dist/translations/vi.d.ts +8 -0
  92. package/dist/translations/vi.js +5 -0
  93. package/dist/translations/zh-cn.d.ts +8 -0
  94. package/dist/translations/zh-cn.js +5 -0
  95. package/dist/translations/zh.d.ts +8 -0
  96. package/dist/translations/zh.js +5 -0
  97. package/dist/types/augmentation.d.ts +22 -0
  98. package/dist/types/index.d.ts +14 -0
  99. package/dist/types/sourceediting.d.ts +108 -0
  100. package/dist/types/sourceeditingconfig.d.ts +38 -0
  101. package/dist/types/utils/formathtml.d.ts +23 -0
  102. package/package.json +4 -3
package/dist/index.js ADDED
@@ -0,0 +1,583 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ import { Plugin, PendingActions } from '@ckeditor/ckeditor5-core/dist/index.js';
6
+ import { ButtonView, MenuBarMenuListItemButtonView } from '@ckeditor/ckeditor5-ui/dist/index.js';
7
+ import { CKEditorError, createElement, ElementReplacer } from '@ckeditor/ckeditor5-utils/dist/index.js';
8
+
9
+ /**
10
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
11
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
12
+ */ /**
13
+ * @module source-editing/utils/formathtml
14
+ */ /**
15
+ * A simple (and naive) HTML code formatter that returns a formatted HTML markup that can be easily
16
+ * parsed by human eyes. It beautifies the HTML code by adding new lines between elements that behave like block elements
17
+ * (https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
18
+ * and a few more like `tr`, `td`, and similar ones) and inserting indents for nested content.
19
+ *
20
+ * WARNING: This function works only on a text that does not contain any indentations or new lines.
21
+ * Calling this function on the already formatted text will damage the formatting.
22
+ *
23
+ * @param input An HTML string to format.
24
+ */ function formatHtml(input) {
25
+ // A list of block-like elements around which the new lines should be inserted, and within which
26
+ // the indentation of their children should be increased.
27
+ // The list is partially based on https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements that contains
28
+ // a full list of HTML block-level elements.
29
+ // A void element is an element that cannot have any child - https://html.spec.whatwg.org/multipage/syntax.html#void-elements.
30
+ // Note that <pre> element is not listed on this list to avoid breaking whitespace formatting.
31
+ // Note that <br> element is not listed and handled separately so no additional white spaces are injected.
32
+ const elementsToFormat = [
33
+ {
34
+ name: 'address',
35
+ isVoid: false
36
+ },
37
+ {
38
+ name: 'article',
39
+ isVoid: false
40
+ },
41
+ {
42
+ name: 'aside',
43
+ isVoid: false
44
+ },
45
+ {
46
+ name: 'blockquote',
47
+ isVoid: false
48
+ },
49
+ {
50
+ name: 'details',
51
+ isVoid: false
52
+ },
53
+ {
54
+ name: 'dialog',
55
+ isVoid: false
56
+ },
57
+ {
58
+ name: 'dd',
59
+ isVoid: false
60
+ },
61
+ {
62
+ name: 'div',
63
+ isVoid: false
64
+ },
65
+ {
66
+ name: 'dl',
67
+ isVoid: false
68
+ },
69
+ {
70
+ name: 'dt',
71
+ isVoid: false
72
+ },
73
+ {
74
+ name: 'fieldset',
75
+ isVoid: false
76
+ },
77
+ {
78
+ name: 'figcaption',
79
+ isVoid: false
80
+ },
81
+ {
82
+ name: 'figure',
83
+ isVoid: false
84
+ },
85
+ {
86
+ name: 'footer',
87
+ isVoid: false
88
+ },
89
+ {
90
+ name: 'form',
91
+ isVoid: false
92
+ },
93
+ {
94
+ name: 'h1',
95
+ isVoid: false
96
+ },
97
+ {
98
+ name: 'h2',
99
+ isVoid: false
100
+ },
101
+ {
102
+ name: 'h3',
103
+ isVoid: false
104
+ },
105
+ {
106
+ name: 'h4',
107
+ isVoid: false
108
+ },
109
+ {
110
+ name: 'h5',
111
+ isVoid: false
112
+ },
113
+ {
114
+ name: 'h6',
115
+ isVoid: false
116
+ },
117
+ {
118
+ name: 'header',
119
+ isVoid: false
120
+ },
121
+ {
122
+ name: 'hgroup',
123
+ isVoid: false
124
+ },
125
+ {
126
+ name: 'hr',
127
+ isVoid: true
128
+ },
129
+ {
130
+ name: 'li',
131
+ isVoid: false
132
+ },
133
+ {
134
+ name: 'main',
135
+ isVoid: false
136
+ },
137
+ {
138
+ name: 'nav',
139
+ isVoid: false
140
+ },
141
+ {
142
+ name: 'ol',
143
+ isVoid: false
144
+ },
145
+ {
146
+ name: 'p',
147
+ isVoid: false
148
+ },
149
+ {
150
+ name: 'section',
151
+ isVoid: false
152
+ },
153
+ {
154
+ name: 'table',
155
+ isVoid: false
156
+ },
157
+ {
158
+ name: 'tbody',
159
+ isVoid: false
160
+ },
161
+ {
162
+ name: 'td',
163
+ isVoid: false
164
+ },
165
+ {
166
+ name: 'th',
167
+ isVoid: false
168
+ },
169
+ {
170
+ name: 'thead',
171
+ isVoid: false
172
+ },
173
+ {
174
+ name: 'tr',
175
+ isVoid: false
176
+ },
177
+ {
178
+ name: 'ul',
179
+ isVoid: false
180
+ }
181
+ ];
182
+ const elementNamesToFormat = elementsToFormat.map((element)=>element.name).join('|');
183
+ // It is not the fastest way to format the HTML markup but the performance should be good enough.
184
+ const lines = input// Add new line before and after `<tag>` and `</tag>`.
185
+ // It may separate individual elements with two new lines, but this will be fixed below.
186
+ .replace(new RegExp(`</?(${elementNamesToFormat})( .*?)?>`, 'g'), '\n$&\n')// Keep `<br>`s at the end of line to avoid adding additional whitespaces before `<br>`.
187
+ .replace(/<br[^>]*>/g, '$&\n')// Divide input string into lines, which start with either an opening tag, a closing tag, or just a text.
188
+ .split('\n');
189
+ let indentCount = 0;
190
+ let isPreformattedLine = false;
191
+ return lines.filter((line)=>line.length).map((line)=>{
192
+ isPreformattedLine = isPreformattedBlockLine(line, isPreformattedLine);
193
+ if (isNonVoidOpeningTag(line, elementsToFormat)) {
194
+ return indentLine(line, indentCount++);
195
+ }
196
+ if (isClosingTag(line, elementsToFormat)) {
197
+ return indentLine(line, --indentCount);
198
+ }
199
+ if (isPreformattedLine === 'middle' || isPreformattedLine === 'last') {
200
+ return line;
201
+ }
202
+ return indentLine(line, indentCount);
203
+ }).join('\n');
204
+ }
205
+ /**
206
+ * Checks, if an argument is an opening tag of a non-void element to be formatted.
207
+ *
208
+ * @param line String to check.
209
+ * @param elementsToFormat Elements to be formatted.
210
+ */ function isNonVoidOpeningTag(line, elementsToFormat) {
211
+ return elementsToFormat.some((element)=>{
212
+ if (element.isVoid) {
213
+ return false;
214
+ }
215
+ if (!new RegExp(`<${element.name}( .*?)?>`).test(line)) {
216
+ return false;
217
+ }
218
+ return true;
219
+ });
220
+ }
221
+ /**
222
+ * Checks, if an argument is a closing tag.
223
+ *
224
+ * @param line String to check.
225
+ * @param elementsToFormat Elements to be formatted.
226
+ */ function isClosingTag(line, elementsToFormat) {
227
+ return elementsToFormat.some((element)=>{
228
+ return new RegExp(`</${element.name}>`).test(line);
229
+ });
230
+ }
231
+ /**
232
+ * Indents a line by a specified number of characters.
233
+ *
234
+ * @param line Line to indent.
235
+ * @param indentCount Number of characters to use for indentation.
236
+ * @param indentChar Indentation character(s). 4 spaces by default.
237
+ */ function indentLine(line, indentCount, indentChar = ' ') {
238
+ // More about Math.max() here in https://github.com/ckeditor/ckeditor5/issues/10698.
239
+ return `${indentChar.repeat(Math.max(0, indentCount))}${line}`;
240
+ }
241
+ /**
242
+ * Checks whether a line belongs to a preformatted (`<pre>`) block.
243
+ *
244
+ * @param line Line to check.
245
+ * @param isPreviousLinePreFormatted Information on whether the previous line was preformatted (and how).
246
+ */ function isPreformattedBlockLine(line, isPreviousLinePreFormatted) {
247
+ if (new RegExp('<pre( .*?)?>').test(line)) {
248
+ return 'first';
249
+ } else if (new RegExp('</pre>').test(line)) {
250
+ return 'last';
251
+ } else if (isPreviousLinePreFormatted === 'first' || isPreviousLinePreFormatted === 'middle') {
252
+ return 'middle';
253
+ } else {
254
+ return false;
255
+ }
256
+ }
257
+
258
+ var sourceEditingIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m12.5 0 5 4.5v15.003h-16V0h11zM3 1.5v3.25l-1.497 1-.003 8 1.5 1v3.254L7.685 18l-.001 1.504H17.5V8.002L16 9.428l-.004-4.22-4.222-3.692L3 1.5z\"/><path d=\"M4.06 6.64a.75.75 0 0 1 .958 1.15l-.085.07L2.29 9.75l2.646 1.89c.302.216.4.62.232.951l-.058.095a.75.75 0 0 1-.951.232l-.095-.058-3.5-2.5V9.14l3.496-2.5zm4.194 6.22a.75.75 0 0 1-.958-1.149l.085-.07 2.643-1.89-2.646-1.89a.75.75 0 0 1-.232-.952l.058-.095a.75.75 0 0 1 .95-.232l.096.058 3.5 2.5v1.22l-3.496 2.5zm7.644-.836 2.122 2.122-5.825 5.809-2.125-.005.003-2.116zm2.539-1.847 1.414 1.414a.5.5 0 0 1 0 .707l-1.06 1.06-2.122-2.12 1.061-1.061a.5.5 0 0 1 .707 0z\"/></svg>";
259
+
260
+ const COMMAND_FORCE_DISABLE_ID = 'SourceEditingMode';
261
+ class SourceEditing extends Plugin {
262
+ /**
263
+ * @inheritDoc
264
+ */ static get pluginName() {
265
+ return 'SourceEditing';
266
+ }
267
+ /**
268
+ * @inheritDoc
269
+ */ static get requires() {
270
+ return [
271
+ PendingActions
272
+ ];
273
+ }
274
+ /**
275
+ * @inheritDoc
276
+ */ init() {
277
+ this._checkCompatibility();
278
+ const editor = this.editor;
279
+ const t = editor.locale.t;
280
+ editor.ui.componentFactory.add('sourceEditing', ()=>{
281
+ const buttonView = this._createButton(ButtonView);
282
+ buttonView.set({
283
+ label: t('Source'),
284
+ icon: sourceEditingIcon,
285
+ tooltip: true,
286
+ class: 'ck-source-editing-button'
287
+ });
288
+ return buttonView;
289
+ });
290
+ editor.ui.componentFactory.add('menuBar:sourceEditing', ()=>{
291
+ const buttonView = this._createButton(MenuBarMenuListItemButtonView);
292
+ buttonView.set({
293
+ label: t('Show source')
294
+ });
295
+ return buttonView;
296
+ });
297
+ // Currently, the plugin handles the source editing mode by itself only for the classic editor. To use this plugin with other
298
+ // integrations, listen to the `change:isSourceEditingMode` event and act accordingly.
299
+ if (this._isAllowedToHandleSourceEditingMode()) {
300
+ this.on('change:isSourceEditingMode', (evt, name, isSourceEditingMode)=>{
301
+ if (isSourceEditingMode) {
302
+ this._hideVisibleDialog();
303
+ this._showSourceEditing();
304
+ this._disableCommands();
305
+ } else {
306
+ this._hideSourceEditing();
307
+ this._enableCommands();
308
+ }
309
+ });
310
+ this.on('change:isEnabled', (evt, name, isEnabled)=>this._handleReadOnlyMode(!isEnabled));
311
+ this.listenTo(editor, 'change:isReadOnly', (evt, name, isReadOnly)=>this._handleReadOnlyMode(isReadOnly));
312
+ }
313
+ // Update the editor data while calling editor.getData() in the source editing mode.
314
+ editor.data.on('get', ()=>{
315
+ if (this.isSourceEditingMode) {
316
+ this.updateEditorData();
317
+ }
318
+ }, {
319
+ priority: 'high'
320
+ });
321
+ }
322
+ /**
323
+ * Updates the source data in all hidden editing roots.
324
+ */ updateEditorData() {
325
+ const editor = this.editor;
326
+ const data = {};
327
+ for (const [rootName, domSourceEditingElementWrapper] of this._replacedRoots){
328
+ const oldData = this._dataFromRoots.get(rootName);
329
+ const newData = domSourceEditingElementWrapper.dataset.value;
330
+ // Do not set the data unless some changes have been made in the meantime.
331
+ // This prevents empty undo steps after switching to the normal editor.
332
+ if (oldData !== newData) {
333
+ data[rootName] = newData;
334
+ this._dataFromRoots.set(rootName, newData);
335
+ }
336
+ }
337
+ if (Object.keys(data).length) {
338
+ editor.data.set(data, {
339
+ batchType: {
340
+ isUndoable: true
341
+ },
342
+ suppressErrorInCollaboration: true
343
+ });
344
+ }
345
+ }
346
+ _checkCompatibility() {
347
+ const editor = this.editor;
348
+ const allowCollaboration = editor.config.get('sourceEditing.allowCollaborationFeatures');
349
+ if (!allowCollaboration && editor.plugins.has('RealTimeCollaborativeEditing')) {
350
+ /**
351
+ * Source editing feature is not fully compatible with real-time collaboration,
352
+ * and using it may lead to data loss. Please read
353
+ * {@glink features/source-editing#limitations-and-incompatibilities source editing feature guide} to learn more.
354
+ *
355
+ * If you understand the possible risk of data loss, you can enable the source editing
356
+ * by setting the
357
+ * {@link module:source-editing/sourceeditingconfig~SourceEditingConfig#allowCollaborationFeatures}
358
+ * configuration flag to `true`.
359
+ *
360
+ * @error source-editing-incompatible-with-real-time-collaboration
361
+ */ throw new CKEditorError('source-editing-incompatible-with-real-time-collaboration', null);
362
+ }
363
+ const collaborationPluginNamesToWarn = [
364
+ 'CommentsEditing',
365
+ 'TrackChangesEditing',
366
+ 'RevisionHistory'
367
+ ];
368
+ // Currently, the basic integration with Collaboration Features is to display a warning in the console.
369
+ //
370
+ // If `allowCollaboration` flag is set, do not show these warnings. If the flag is set, we assume that the integrator read
371
+ // appropriate section of the guide so there's no use to spam the console with warnings.
372
+ //
373
+ if (!allowCollaboration && collaborationPluginNamesToWarn.some((pluginName)=>editor.plugins.has(pluginName))) {
374
+ console.warn('You initialized the editor with the source editing feature and at least one of the collaboration features. ' + 'Please be advised that the source editing feature may not work, and be careful when editing document source ' + 'that contains markers created by the collaboration features.');
375
+ }
376
+ // Restricted Editing integration can also lead to problems. Warn the user accordingly.
377
+ if (editor.plugins.has('RestrictedEditingModeEditing')) {
378
+ console.warn('You initialized the editor with the source editing feature and restricted editing feature. ' + 'Please be advised that the source editing feature may not work, and be careful when editing document source ' + 'that contains markers created by the restricted editing feature.');
379
+ }
380
+ }
381
+ /**
382
+ * Creates source editing wrappers that replace each editing root. Each wrapper contains the document source from the corresponding
383
+ * root.
384
+ *
385
+ * The wrapper element contains a textarea and it solves the problem, that the textarea element cannot auto expand its height based on
386
+ * the content it contains. The solution is to make the textarea more like a plain div element, which expands in height as much as it
387
+ * needs to, in order to display the whole document source without scrolling. The wrapper element is a parent for the textarea and for
388
+ * the pseudo-element `::after`, that replicates the look, content, and position of the textarea. The pseudo-element replica is hidden,
389
+ * but it is styled to be an identical visual copy of the textarea with the same content. Then, the wrapper is a grid container and both
390
+ * of its children (the textarea and the `::after` pseudo-element) are positioned within a CSS grid to occupy the same grid cell. The
391
+ * content in the pseudo-element `::after` is set in CSS and it stretches the grid to the appropriate size based on the textarea value.
392
+ * Since both children occupy the same grid cell, both have always the same height.
393
+ */ _showSourceEditing() {
394
+ const editor = this.editor;
395
+ const editingView = editor.editing.view;
396
+ const model = editor.model;
397
+ model.change((writer)=>{
398
+ writer.setSelection(null);
399
+ writer.removeSelectionAttribute(model.document.selection.getAttributeKeys());
400
+ });
401
+ // It is not needed to iterate through all editing roots, as currently the plugin supports only the Classic Editor with a single
402
+ // main root, but this code may help understand and use this feature in external integrations.
403
+ for (const [rootName, domRootElement] of editingView.domRoots){
404
+ const data = formatSource(editor.data.get({
405
+ rootName
406
+ }));
407
+ const domSourceEditingElementTextarea = createElement(domRootElement.ownerDocument, 'textarea', {
408
+ rows: '1',
409
+ 'aria-label': 'Source code editing area'
410
+ });
411
+ const domSourceEditingElementWrapper = createElement(domRootElement.ownerDocument, 'div', {
412
+ class: 'ck-source-editing-area',
413
+ 'data-value': data
414
+ }, [
415
+ domSourceEditingElementTextarea
416
+ ]);
417
+ domSourceEditingElementTextarea.value = data;
418
+ // Setting a value to textarea moves the input cursor to the end. We want the selection at the beginning.
419
+ domSourceEditingElementTextarea.setSelectionRange(0, 0);
420
+ // Bind the textarea's value to the wrapper's `data-value` property. Each change of the textarea's value updates the
421
+ // wrapper's `data-value` property.
422
+ domSourceEditingElementTextarea.addEventListener('input', ()=>{
423
+ domSourceEditingElementWrapper.dataset.value = domSourceEditingElementTextarea.value;
424
+ editor.ui.update();
425
+ });
426
+ editingView.change((writer)=>{
427
+ const viewRoot = editingView.document.getRoot(rootName);
428
+ writer.addClass('ck-hidden', viewRoot);
429
+ });
430
+ // Register the element so it becomes available for Alt+F10 and Esc navigation.
431
+ editor.ui.setEditableElement('sourceEditing:' + rootName, domSourceEditingElementTextarea);
432
+ this._replacedRoots.set(rootName, domSourceEditingElementWrapper);
433
+ this._elementReplacer.replace(domRootElement, domSourceEditingElementWrapper);
434
+ this._dataFromRoots.set(rootName, data);
435
+ }
436
+ this._focusSourceEditing();
437
+ }
438
+ /**
439
+ * Restores all hidden editing roots and sets the source data in them.
440
+ */ _hideSourceEditing() {
441
+ const editor = this.editor;
442
+ const editingView = editor.editing.view;
443
+ this.updateEditorData();
444
+ editingView.change((writer)=>{
445
+ for (const [rootName] of this._replacedRoots){
446
+ writer.removeClass('ck-hidden', editingView.document.getRoot(rootName));
447
+ }
448
+ });
449
+ this._elementReplacer.restore();
450
+ this._replacedRoots.clear();
451
+ this._dataFromRoots.clear();
452
+ editingView.focus();
453
+ }
454
+ /**
455
+ * Focuses the textarea containing document source from the first editing root.
456
+ */ _focusSourceEditing() {
457
+ const editor = this.editor;
458
+ const [domSourceEditingElementWrapper] = this._replacedRoots.values();
459
+ const textarea = domSourceEditingElementWrapper.querySelector('textarea');
460
+ // The FocusObserver was disabled by View.render() while the DOM root was getting hidden and the replacer
461
+ // revealed the textarea. So it couldn't notice that the DOM root got blurred in the process.
462
+ // Let's sync this state manually here because otherwise Renderer will attempt to render selection
463
+ // in an invisible DOM root.
464
+ editor.editing.view.document.isFocused = false;
465
+ textarea.focus();
466
+ }
467
+ /**
468
+ * Disables all commands.
469
+ */ _disableCommands() {
470
+ const editor = this.editor;
471
+ for (const command of editor.commands.commands()){
472
+ command.forceDisabled(COMMAND_FORCE_DISABLE_ID);
473
+ }
474
+ // Comments archive UI plugin will be disabled manually too.
475
+ if (editor.plugins.has('CommentsArchiveUI')) {
476
+ editor.plugins.get('CommentsArchiveUI').forceDisabled(COMMAND_FORCE_DISABLE_ID);
477
+ }
478
+ }
479
+ /**
480
+ * Clears forced disable for all commands, that was previously set through {@link #_disableCommands}.
481
+ */ _enableCommands() {
482
+ const editor = this.editor;
483
+ for (const command of editor.commands.commands()){
484
+ command.clearForceDisabled(COMMAND_FORCE_DISABLE_ID);
485
+ }
486
+ // Comments archive UI plugin will be enabled manually too.
487
+ if (editor.plugins.has('CommentsArchiveUI')) {
488
+ editor.plugins.get('CommentsArchiveUI').clearForceDisabled(COMMAND_FORCE_DISABLE_ID);
489
+ }
490
+ }
491
+ /**
492
+ * Adds or removes the `readonly` attribute from the textarea from all roots, if document source mode is active.
493
+ *
494
+ * @param isReadOnly Indicates whether all textarea elements should be read-only.
495
+ */ _handleReadOnlyMode(isReadOnly) {
496
+ if (!this.isSourceEditingMode) {
497
+ return;
498
+ }
499
+ for (const [, domSourceEditingElementWrapper] of this._replacedRoots){
500
+ domSourceEditingElementWrapper.querySelector('textarea').readOnly = isReadOnly;
501
+ }
502
+ }
503
+ /**
504
+ * Checks, if the plugin is allowed to handle the source editing mode by itself. Currently, the source editing mode is supported only
505
+ * for the {@link module:editor-classic/classiceditor~ClassicEditor classic editor}.
506
+ */ _isAllowedToHandleSourceEditingMode() {
507
+ const editor = this.editor;
508
+ const editable = editor.ui.view.editable;
509
+ // Checks, if the editor's editable belongs to the editor's DOM tree.
510
+ return editable && !editable.hasExternalElement;
511
+ }
512
+ /**
513
+ * If any {@link module:ui/dialog/dialogview~DialogView editor dialog} is currently visible, hide it.
514
+ */ _hideVisibleDialog() {
515
+ if (this.editor.plugins.has('Dialog')) {
516
+ const dialogPlugin = this.editor.plugins.get('Dialog');
517
+ if (dialogPlugin.isOpen) {
518
+ dialogPlugin.hide();
519
+ }
520
+ }
521
+ }
522
+ _createButton(ButtonClass) {
523
+ const editor = this.editor;
524
+ const buttonView = new ButtonClass(editor.locale);
525
+ buttonView.set({
526
+ withText: true
527
+ });
528
+ buttonView.bind('isOn').to(this, 'isSourceEditingMode');
529
+ // The button should be disabled if one of the following conditions is met:
530
+ buttonView.bind('isEnabled').to(this, 'isEnabled', editor, 'isReadOnly', editor.plugins.get(PendingActions), 'hasAny', (isEnabled, isEditorReadOnly, hasAnyPendingActions)=>{
531
+ // (1) The plugin itself is disabled.
532
+ if (!isEnabled) {
533
+ return false;
534
+ }
535
+ // (2) The editor is in read-only mode.
536
+ if (isEditorReadOnly) {
537
+ return false;
538
+ }
539
+ // (3) Any pending action is scheduled. It may change the model, so modifying the document source should be prevented
540
+ // until the model is finally set.
541
+ if (hasAnyPendingActions) {
542
+ return false;
543
+ }
544
+ return true;
545
+ });
546
+ this.listenTo(buttonView, 'execute', ()=>{
547
+ this.isSourceEditingMode = !this.isSourceEditingMode;
548
+ });
549
+ return buttonView;
550
+ }
551
+ /**
552
+ * @inheritDoc
553
+ */ constructor(editor){
554
+ super(editor);
555
+ this.set('isSourceEditingMode', false);
556
+ this._elementReplacer = new ElementReplacer();
557
+ this._replacedRoots = new Map();
558
+ this._dataFromRoots = new Map();
559
+ editor.config.define('sourceEditing.allowCollaborationFeatures', false);
560
+ }
561
+ }
562
+ /**
563
+ * Formats the content for a better readability.
564
+ *
565
+ * For a non-HTML source the unchanged input string is returned.
566
+ *
567
+ * @param input Input string to check.
568
+ */ function formatSource(input) {
569
+ if (!isHtml(input)) {
570
+ return input;
571
+ }
572
+ return formatHtml(input);
573
+ }
574
+ /**
575
+ * Checks, if the document source is HTML. It is sufficient to just check the first character from the document data.
576
+ *
577
+ * @param input Input string to check.
578
+ */ function isHtml(input) {
579
+ return input.startsWith('<');
580
+ }
581
+
582
+ export { SourceEditing };
583
+ //# sourceMappingURL=index.js.map