@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.
- package/dist/index-content.css +4 -0
- package/dist/index-editor.css +43 -0
- package/dist/index.css +87 -0
- package/dist/index.css.map +1 -0
- package/dist/index.js +583 -0
- package/dist/index.js.map +1 -0
- package/dist/translations/ar.d.ts +8 -0
- package/dist/translations/ar.js +5 -0
- package/dist/translations/bg.d.ts +8 -0
- package/dist/translations/bg.js +5 -0
- package/dist/translations/bn.d.ts +8 -0
- package/dist/translations/bn.js +5 -0
- package/dist/translations/ca.d.ts +8 -0
- package/dist/translations/ca.js +5 -0
- package/dist/translations/cs.d.ts +8 -0
- package/dist/translations/cs.js +5 -0
- package/dist/translations/da.d.ts +8 -0
- package/dist/translations/da.js +5 -0
- package/dist/translations/de.d.ts +8 -0
- package/dist/translations/de.js +5 -0
- package/dist/translations/el.d.ts +8 -0
- package/dist/translations/el.js +5 -0
- package/dist/translations/en-au.d.ts +8 -0
- package/dist/translations/en-au.js +5 -0
- package/dist/translations/en.d.ts +8 -0
- package/dist/translations/en.js +5 -0
- package/dist/translations/es.d.ts +8 -0
- package/dist/translations/es.js +5 -0
- package/dist/translations/et.d.ts +8 -0
- package/dist/translations/et.js +5 -0
- package/dist/translations/fi.d.ts +8 -0
- package/dist/translations/fi.js +5 -0
- package/dist/translations/fr.d.ts +8 -0
- package/dist/translations/fr.js +5 -0
- package/dist/translations/gl.d.ts +8 -0
- package/dist/translations/gl.js +5 -0
- package/dist/translations/he.d.ts +8 -0
- package/dist/translations/he.js +5 -0
- package/dist/translations/hi.d.ts +8 -0
- package/dist/translations/hi.js +5 -0
- package/dist/translations/hr.d.ts +8 -0
- package/dist/translations/hr.js +5 -0
- package/dist/translations/hu.d.ts +8 -0
- package/dist/translations/hu.js +5 -0
- package/dist/translations/id.d.ts +8 -0
- package/dist/translations/id.js +5 -0
- package/dist/translations/it.d.ts +8 -0
- package/dist/translations/it.js +5 -0
- package/dist/translations/ja.d.ts +8 -0
- package/dist/translations/ja.js +5 -0
- package/dist/translations/ko.d.ts +8 -0
- package/dist/translations/ko.js +5 -0
- package/dist/translations/lt.d.ts +8 -0
- package/dist/translations/lt.js +5 -0
- package/dist/translations/lv.d.ts +8 -0
- package/dist/translations/lv.js +5 -0
- package/dist/translations/ms.d.ts +8 -0
- package/dist/translations/ms.js +5 -0
- package/dist/translations/nl.d.ts +8 -0
- package/dist/translations/nl.js +5 -0
- package/dist/translations/no.d.ts +8 -0
- package/dist/translations/no.js +5 -0
- package/dist/translations/pl.d.ts +8 -0
- package/dist/translations/pl.js +5 -0
- package/dist/translations/pt-br.d.ts +8 -0
- package/dist/translations/pt-br.js +5 -0
- package/dist/translations/pt.d.ts +8 -0
- package/dist/translations/pt.js +5 -0
- package/dist/translations/ro.d.ts +8 -0
- package/dist/translations/ro.js +5 -0
- package/dist/translations/ru.d.ts +8 -0
- package/dist/translations/ru.js +5 -0
- package/dist/translations/sk.d.ts +8 -0
- package/dist/translations/sk.js +5 -0
- package/dist/translations/sr-latn.d.ts +8 -0
- package/dist/translations/sr-latn.js +5 -0
- package/dist/translations/sr.d.ts +8 -0
- package/dist/translations/sr.js +5 -0
- package/dist/translations/sv.d.ts +8 -0
- package/dist/translations/sv.js +5 -0
- package/dist/translations/th.d.ts +8 -0
- package/dist/translations/th.js +5 -0
- package/dist/translations/tr.d.ts +8 -0
- package/dist/translations/tr.js +5 -0
- package/dist/translations/ug.d.ts +8 -0
- package/dist/translations/ug.js +5 -0
- package/dist/translations/uk.d.ts +8 -0
- package/dist/translations/uk.js +5 -0
- package/dist/translations/ur.d.ts +8 -0
- package/dist/translations/ur.js +5 -0
- package/dist/translations/vi.d.ts +8 -0
- package/dist/translations/vi.js +5 -0
- package/dist/translations/zh-cn.d.ts +8 -0
- package/dist/translations/zh-cn.js +5 -0
- package/dist/translations/zh.d.ts +8 -0
- package/dist/translations/zh.js +5 -0
- package/dist/types/augmentation.d.ts +22 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/sourceediting.d.ts +108 -0
- package/dist/types/sourceeditingconfig.d.ts +38 -0
- package/dist/types/utils/formathtml.d.ts +23 -0
- 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
|