@ckeditor/ckeditor5-source-editing 38.0.1 → 38.1.1
Sign up to get free protection for your applications and to get access to all the features.
- package/build/source-editing.js +1 -1
- package/build/source-editing.js.map +1 -0
- package/package.json +3 -24
- package/src/augmentation.d.ts +10 -10
- package/src/augmentation.js +5 -5
- package/src/index.d.ts +9 -9
- package/src/index.js +9 -9
- package/src/sourceediting.d.ts +102 -102
- package/src/sourceediting.js +298 -298
- package/src/utils/formathtml.d.ts +19 -19
- package/src/utils/formathtml.js +128 -128
package/src/sourceediting.js
CHANGED
@@ -1,298 +1,298 @@
|
|
1
|
-
/**
|
2
|
-
* @license Copyright (c) 2003-2023, 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
|
-
/**
|
6
|
-
* @module source-editing/sourceediting
|
7
|
-
*/
|
8
|
-
/* global console */
|
9
|
-
import { Plugin, PendingActions } from 'ckeditor5/src/core';
|
10
|
-
import { ButtonView } from 'ckeditor5/src/ui';
|
11
|
-
import { createElement, ElementReplacer } from 'ckeditor5/src/utils';
|
12
|
-
import { formatHtml } from './utils/formathtml';
|
13
|
-
import '../theme/sourceediting.css';
|
14
|
-
import sourceEditingIcon from '../theme/icons/source-editing.svg';
|
15
|
-
const COMMAND_FORCE_DISABLE_ID = 'SourceEditingMode';
|
16
|
-
/**
|
17
|
-
* The source editing feature.
|
18
|
-
*
|
19
|
-
* It provides the possibility to view and edit the source of the document.
|
20
|
-
*
|
21
|
-
* For a detailed overview, check the {@glink features/source-editing source editing feature documentation} and the
|
22
|
-
* {@glink api/source-editing package page}.
|
23
|
-
*/
|
24
|
-
export default class SourceEditing extends Plugin {
|
25
|
-
/**
|
26
|
-
* @inheritDoc
|
27
|
-
*/
|
28
|
-
static get pluginName() {
|
29
|
-
return 'SourceEditing';
|
30
|
-
}
|
31
|
-
/**
|
32
|
-
* @inheritDoc
|
33
|
-
*/
|
34
|
-
static get requires() {
|
35
|
-
return [PendingActions];
|
36
|
-
}
|
37
|
-
/**
|
38
|
-
* @inheritDoc
|
39
|
-
*/
|
40
|
-
constructor(editor) {
|
41
|
-
super(editor);
|
42
|
-
this.set('isSourceEditingMode', false);
|
43
|
-
this._elementReplacer = new ElementReplacer();
|
44
|
-
this._replacedRoots = new Map();
|
45
|
-
this._dataFromRoots = new Map();
|
46
|
-
}
|
47
|
-
/**
|
48
|
-
* @inheritDoc
|
49
|
-
*/
|
50
|
-
init() {
|
51
|
-
const editor = this.editor;
|
52
|
-
const t = editor.t;
|
53
|
-
editor.ui.componentFactory.add('sourceEditing', locale => {
|
54
|
-
const buttonView = new ButtonView(locale);
|
55
|
-
buttonView.set({
|
56
|
-
label: t('Source'),
|
57
|
-
icon: sourceEditingIcon,
|
58
|
-
tooltip: true,
|
59
|
-
withText: true,
|
60
|
-
class: 'ck-source-editing-button'
|
61
|
-
});
|
62
|
-
buttonView.bind('isOn').to(this, 'isSourceEditingMode');
|
63
|
-
// The button should be disabled if one of the following conditions is met:
|
64
|
-
buttonView.bind('isEnabled').to(this, 'isEnabled', editor, 'isReadOnly', editor.plugins.get(PendingActions), 'hasAny', (isEnabled, isEditorReadOnly, hasAnyPendingActions) => {
|
65
|
-
// (1) The plugin itself is disabled.
|
66
|
-
if (!isEnabled) {
|
67
|
-
return false;
|
68
|
-
}
|
69
|
-
// (2) The editor is in read-only mode.
|
70
|
-
if (isEditorReadOnly) {
|
71
|
-
return false;
|
72
|
-
}
|
73
|
-
// (3) Any pending action is scheduled. It may change the model, so modifying the document source should be prevented
|
74
|
-
// until the model is finally set.
|
75
|
-
if (hasAnyPendingActions) {
|
76
|
-
return false;
|
77
|
-
}
|
78
|
-
return true;
|
79
|
-
});
|
80
|
-
this.listenTo(buttonView, 'execute', () => {
|
81
|
-
this.isSourceEditingMode = !this.isSourceEditingMode;
|
82
|
-
});
|
83
|
-
return buttonView;
|
84
|
-
});
|
85
|
-
// Currently, the plugin handles the source editing mode by itself only for the classic editor. To use this plugin with other
|
86
|
-
// integrations, listen to the `change:isSourceEditingMode` event and act accordingly.
|
87
|
-
if (this._isAllowedToHandleSourceEditingMode()) {
|
88
|
-
this.on('change:isSourceEditingMode', (evt, name, isSourceEditingMode) => {
|
89
|
-
if (isSourceEditingMode) {
|
90
|
-
this._showSourceEditing();
|
91
|
-
this._disableCommands();
|
92
|
-
}
|
93
|
-
else {
|
94
|
-
this._hideSourceEditing();
|
95
|
-
this._enableCommands();
|
96
|
-
}
|
97
|
-
});
|
98
|
-
this.on('change:isEnabled', (evt, name, isEnabled) => this._handleReadOnlyMode(!isEnabled));
|
99
|
-
this.listenTo(editor, 'change:isReadOnly', (evt, name, isReadOnly) => this._handleReadOnlyMode(isReadOnly));
|
100
|
-
}
|
101
|
-
// Update the editor data while calling editor.getData() in the source editing mode.
|
102
|
-
editor.data.on('get', () => {
|
103
|
-
if (this.isSourceEditingMode) {
|
104
|
-
this.updateEditorData();
|
105
|
-
}
|
106
|
-
}, { priority: 'high' });
|
107
|
-
}
|
108
|
-
/**
|
109
|
-
* @inheritDoc
|
110
|
-
*/
|
111
|
-
afterInit() {
|
112
|
-
const editor = this.editor;
|
113
|
-
const collaborationPluginNamesToWarn = [
|
114
|
-
'RealTimeCollaborativeEditing',
|
115
|
-
'CommentsEditing',
|
116
|
-
'TrackChangesEditing',
|
117
|
-
'RevisionHistory'
|
118
|
-
];
|
119
|
-
// Currently, the basic integration with Collaboration Features is to display a warning in the console.
|
120
|
-
if (collaborationPluginNamesToWarn.some(pluginName => editor.plugins.has(pluginName))) {
|
121
|
-
console.warn('You initialized the editor with the source editing feature and at least one of the collaboration features. ' +
|
122
|
-
'Please be advised that the source editing feature may not work, and be careful when editing document source ' +
|
123
|
-
'that contains markers created by the collaboration features.');
|
124
|
-
}
|
125
|
-
// Restricted Editing integration can also lead to problems. Warn the user accordingly.
|
126
|
-
if (editor.plugins.has('RestrictedEditingModeEditing')) {
|
127
|
-
console.warn('You initialized the editor with the source editing feature and restricted editing feature. ' +
|
128
|
-
'Please be advised that the source editing feature may not work, and be careful when editing document source ' +
|
129
|
-
'that contains markers created by the restricted editing feature.');
|
130
|
-
}
|
131
|
-
}
|
132
|
-
/**
|
133
|
-
* Updates the source data in all hidden editing roots.
|
134
|
-
*/
|
135
|
-
updateEditorData() {
|
136
|
-
const editor = this.editor;
|
137
|
-
const data = {};
|
138
|
-
for (const [rootName, domSourceEditingElementWrapper] of this._replacedRoots) {
|
139
|
-
const oldData = this._dataFromRoots.get(rootName);
|
140
|
-
const newData = domSourceEditingElementWrapper.dataset.value;
|
141
|
-
// Do not set the data unless some changes have been made in the meantime.
|
142
|
-
// This prevents empty undo steps after switching to the normal editor.
|
143
|
-
if (oldData !== newData) {
|
144
|
-
data[rootName] = newData;
|
145
|
-
}
|
146
|
-
}
|
147
|
-
if (Object.keys(data).length) {
|
148
|
-
editor.data.set(data, { batchType: { isUndoable: true } });
|
149
|
-
}
|
150
|
-
}
|
151
|
-
/**
|
152
|
-
* Creates source editing wrappers that replace each editing root. Each wrapper contains the document source from the corresponding
|
153
|
-
* root.
|
154
|
-
*
|
155
|
-
* The wrapper element contains a textarea and it solves the problem, that the textarea element cannot auto expand its height based on
|
156
|
-
* 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
|
157
|
-
* needs to, in order to display the whole document source without scrolling. The wrapper element is a parent for the textarea and for
|
158
|
-
* the pseudo-element `::after`, that replicates the look, content, and position of the textarea. The pseudo-element replica is hidden,
|
159
|
-
* 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
|
160
|
-
* of its children (the textarea and the `::after` pseudo-element) are positioned within a CSS grid to occupy the same grid cell. The
|
161
|
-
* content in the pseudo-element `::after` is set in CSS and it stretches the grid to the appropriate size based on the textarea value.
|
162
|
-
* Since both children occupy the same grid cell, both have always the same height.
|
163
|
-
*/
|
164
|
-
_showSourceEditing() {
|
165
|
-
const editor = this.editor;
|
166
|
-
const editingView = editor.editing.view;
|
167
|
-
const model = editor.model;
|
168
|
-
model.change(writer => {
|
169
|
-
writer.setSelection(null);
|
170
|
-
writer.removeSelectionAttribute(model.document.selection.getAttributeKeys());
|
171
|
-
});
|
172
|
-
// It is not needed to iterate through all editing roots, as currently the plugin supports only the Classic Editor with a single
|
173
|
-
// main root, but this code may help understand and use this feature in external integrations.
|
174
|
-
for (const [rootName, domRootElement] of editingView.domRoots) {
|
175
|
-
const data = formatSource(editor.data.get({ rootName }));
|
176
|
-
const domSourceEditingElementTextarea = createElement(domRootElement.ownerDocument, 'textarea', {
|
177
|
-
rows: '1',
|
178
|
-
'aria-label': 'Source code editing area'
|
179
|
-
});
|
180
|
-
const domSourceEditingElementWrapper = createElement(domRootElement.ownerDocument, 'div', {
|
181
|
-
class: 'ck-source-editing-area',
|
182
|
-
'data-value': data
|
183
|
-
}, [domSourceEditingElementTextarea]);
|
184
|
-
domSourceEditingElementTextarea.value = data;
|
185
|
-
// Setting a value to textarea moves the input cursor to the end. We want the selection at the beginning.
|
186
|
-
domSourceEditingElementTextarea.setSelectionRange(0, 0);
|
187
|
-
// Bind the textarea's value to the wrapper's `data-value` property. Each change of the textarea's value updates the
|
188
|
-
// wrapper's `data-value` property.
|
189
|
-
domSourceEditingElementTextarea.addEventListener('input', () => {
|
190
|
-
domSourceEditingElementWrapper.dataset.value = domSourceEditingElementTextarea.value;
|
191
|
-
editor.ui.update();
|
192
|
-
});
|
193
|
-
editingView.change(writer => {
|
194
|
-
const viewRoot = editingView.document.getRoot(rootName);
|
195
|
-
writer.addClass('ck-hidden', viewRoot);
|
196
|
-
});
|
197
|
-
// Register the element so it becomes available for Alt+F10 and Esc navigation.
|
198
|
-
editor.ui.setEditableElement('sourceEditing:' + rootName, domSourceEditingElementTextarea);
|
199
|
-
this._replacedRoots.set(rootName, domSourceEditingElementWrapper);
|
200
|
-
this._elementReplacer.replace(domRootElement, domSourceEditingElementWrapper);
|
201
|
-
this._dataFromRoots.set(rootName, data);
|
202
|
-
}
|
203
|
-
this._focusSourceEditing();
|
204
|
-
}
|
205
|
-
/**
|
206
|
-
* Restores all hidden editing roots and sets the source data in them.
|
207
|
-
*/
|
208
|
-
_hideSourceEditing() {
|
209
|
-
const editor = this.editor;
|
210
|
-
const editingView = editor.editing.view;
|
211
|
-
this.updateEditorData();
|
212
|
-
editingView.change(writer => {
|
213
|
-
for (const [rootName] of this._replacedRoots) {
|
214
|
-
writer.removeClass('ck-hidden', editingView.document.getRoot(rootName));
|
215
|
-
}
|
216
|
-
});
|
217
|
-
this._elementReplacer.restore();
|
218
|
-
this._replacedRoots.clear();
|
219
|
-
this._dataFromRoots.clear();
|
220
|
-
editingView.focus();
|
221
|
-
}
|
222
|
-
/**
|
223
|
-
* Focuses the textarea containing document source from the first editing root.
|
224
|
-
*/
|
225
|
-
_focusSourceEditing() {
|
226
|
-
const editor = this.editor;
|
227
|
-
const [domSourceEditingElementWrapper] = this._replacedRoots.values();
|
228
|
-
const textarea = domSourceEditingElementWrapper.querySelector('textarea');
|
229
|
-
// The FocusObserver was disabled by View.render() while the DOM root was getting hidden and the replacer
|
230
|
-
// revealed the textarea. So it couldn't notice that the DOM root got blurred in the process.
|
231
|
-
// Let's sync this state manually here because otherwise Renderer will attempt to render selection
|
232
|
-
// in an invisible DOM root.
|
233
|
-
editor.editing.view.document.isFocused = false;
|
234
|
-
textarea.focus();
|
235
|
-
}
|
236
|
-
/**
|
237
|
-
* Disables all commands.
|
238
|
-
*/
|
239
|
-
_disableCommands() {
|
240
|
-
const editor = this.editor;
|
241
|
-
for (const command of editor.commands.commands()) {
|
242
|
-
command.forceDisabled(COMMAND_FORCE_DISABLE_ID);
|
243
|
-
}
|
244
|
-
}
|
245
|
-
/**
|
246
|
-
* Clears forced disable for all commands, that was previously set through {@link #_disableCommands}.
|
247
|
-
*/
|
248
|
-
_enableCommands() {
|
249
|
-
const editor = this.editor;
|
250
|
-
for (const command of editor.commands.commands()) {
|
251
|
-
command.clearForceDisabled(COMMAND_FORCE_DISABLE_ID);
|
252
|
-
}
|
253
|
-
}
|
254
|
-
/**
|
255
|
-
* Adds or removes the `readonly` attribute from the textarea from all roots, if document source mode is active.
|
256
|
-
*
|
257
|
-
* @param isReadOnly Indicates whether all textarea elements should be read-only.
|
258
|
-
*/
|
259
|
-
_handleReadOnlyMode(isReadOnly) {
|
260
|
-
if (!this.isSourceEditingMode) {
|
261
|
-
return;
|
262
|
-
}
|
263
|
-
for (const [, domSourceEditingElementWrapper] of this._replacedRoots) {
|
264
|
-
domSourceEditingElementWrapper.querySelector('textarea').readOnly = isReadOnly;
|
265
|
-
}
|
266
|
-
}
|
267
|
-
/**
|
268
|
-
* Checks, if the plugin is allowed to handle the source editing mode by itself. Currently, the source editing mode is supported only
|
269
|
-
* for the {@link module:editor-classic/classiceditor~ClassicEditor classic editor}.
|
270
|
-
*/
|
271
|
-
_isAllowedToHandleSourceEditingMode() {
|
272
|
-
const editor = this.editor;
|
273
|
-
const editable = editor.ui.view.editable;
|
274
|
-
// Checks, if the editor's editable belongs to the editor's DOM tree.
|
275
|
-
return editable && !editable.hasExternalElement;
|
276
|
-
}
|
277
|
-
}
|
278
|
-
/**
|
279
|
-
* Formats the content for a better readability.
|
280
|
-
*
|
281
|
-
* For a non-HTML source the unchanged input string is returned.
|
282
|
-
*
|
283
|
-
* @param input Input string to check.
|
284
|
-
*/
|
285
|
-
function formatSource(input) {
|
286
|
-
if (!isHtml(input)) {
|
287
|
-
return input;
|
288
|
-
}
|
289
|
-
return formatHtml(input);
|
290
|
-
}
|
291
|
-
/**
|
292
|
-
* Checks, if the document source is HTML. It is sufficient to just check the first character from the document data.
|
293
|
-
*
|
294
|
-
* @param input Input string to check.
|
295
|
-
*/
|
296
|
-
function isHtml(input) {
|
297
|
-
return input.startsWith('<');
|
298
|
-
}
|
1
|
+
/**
|
2
|
+
* @license Copyright (c) 2003-2023, 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
|
+
/**
|
6
|
+
* @module source-editing/sourceediting
|
7
|
+
*/
|
8
|
+
/* global console */
|
9
|
+
import { Plugin, PendingActions } from 'ckeditor5/src/core';
|
10
|
+
import { ButtonView } from 'ckeditor5/src/ui';
|
11
|
+
import { createElement, ElementReplacer } from 'ckeditor5/src/utils';
|
12
|
+
import { formatHtml } from './utils/formathtml';
|
13
|
+
import '../theme/sourceediting.css';
|
14
|
+
import sourceEditingIcon from '../theme/icons/source-editing.svg';
|
15
|
+
const COMMAND_FORCE_DISABLE_ID = 'SourceEditingMode';
|
16
|
+
/**
|
17
|
+
* The source editing feature.
|
18
|
+
*
|
19
|
+
* It provides the possibility to view and edit the source of the document.
|
20
|
+
*
|
21
|
+
* For a detailed overview, check the {@glink features/source-editing source editing feature documentation} and the
|
22
|
+
* {@glink api/source-editing package page}.
|
23
|
+
*/
|
24
|
+
export default class SourceEditing extends Plugin {
|
25
|
+
/**
|
26
|
+
* @inheritDoc
|
27
|
+
*/
|
28
|
+
static get pluginName() {
|
29
|
+
return 'SourceEditing';
|
30
|
+
}
|
31
|
+
/**
|
32
|
+
* @inheritDoc
|
33
|
+
*/
|
34
|
+
static get requires() {
|
35
|
+
return [PendingActions];
|
36
|
+
}
|
37
|
+
/**
|
38
|
+
* @inheritDoc
|
39
|
+
*/
|
40
|
+
constructor(editor) {
|
41
|
+
super(editor);
|
42
|
+
this.set('isSourceEditingMode', false);
|
43
|
+
this._elementReplacer = new ElementReplacer();
|
44
|
+
this._replacedRoots = new Map();
|
45
|
+
this._dataFromRoots = new Map();
|
46
|
+
}
|
47
|
+
/**
|
48
|
+
* @inheritDoc
|
49
|
+
*/
|
50
|
+
init() {
|
51
|
+
const editor = this.editor;
|
52
|
+
const t = editor.t;
|
53
|
+
editor.ui.componentFactory.add('sourceEditing', locale => {
|
54
|
+
const buttonView = new ButtonView(locale);
|
55
|
+
buttonView.set({
|
56
|
+
label: t('Source'),
|
57
|
+
icon: sourceEditingIcon,
|
58
|
+
tooltip: true,
|
59
|
+
withText: true,
|
60
|
+
class: 'ck-source-editing-button'
|
61
|
+
});
|
62
|
+
buttonView.bind('isOn').to(this, 'isSourceEditingMode');
|
63
|
+
// The button should be disabled if one of the following conditions is met:
|
64
|
+
buttonView.bind('isEnabled').to(this, 'isEnabled', editor, 'isReadOnly', editor.plugins.get(PendingActions), 'hasAny', (isEnabled, isEditorReadOnly, hasAnyPendingActions) => {
|
65
|
+
// (1) The plugin itself is disabled.
|
66
|
+
if (!isEnabled) {
|
67
|
+
return false;
|
68
|
+
}
|
69
|
+
// (2) The editor is in read-only mode.
|
70
|
+
if (isEditorReadOnly) {
|
71
|
+
return false;
|
72
|
+
}
|
73
|
+
// (3) Any pending action is scheduled. It may change the model, so modifying the document source should be prevented
|
74
|
+
// until the model is finally set.
|
75
|
+
if (hasAnyPendingActions) {
|
76
|
+
return false;
|
77
|
+
}
|
78
|
+
return true;
|
79
|
+
});
|
80
|
+
this.listenTo(buttonView, 'execute', () => {
|
81
|
+
this.isSourceEditingMode = !this.isSourceEditingMode;
|
82
|
+
});
|
83
|
+
return buttonView;
|
84
|
+
});
|
85
|
+
// Currently, the plugin handles the source editing mode by itself only for the classic editor. To use this plugin with other
|
86
|
+
// integrations, listen to the `change:isSourceEditingMode` event and act accordingly.
|
87
|
+
if (this._isAllowedToHandleSourceEditingMode()) {
|
88
|
+
this.on('change:isSourceEditingMode', (evt, name, isSourceEditingMode) => {
|
89
|
+
if (isSourceEditingMode) {
|
90
|
+
this._showSourceEditing();
|
91
|
+
this._disableCommands();
|
92
|
+
}
|
93
|
+
else {
|
94
|
+
this._hideSourceEditing();
|
95
|
+
this._enableCommands();
|
96
|
+
}
|
97
|
+
});
|
98
|
+
this.on('change:isEnabled', (evt, name, isEnabled) => this._handleReadOnlyMode(!isEnabled));
|
99
|
+
this.listenTo(editor, 'change:isReadOnly', (evt, name, isReadOnly) => this._handleReadOnlyMode(isReadOnly));
|
100
|
+
}
|
101
|
+
// Update the editor data while calling editor.getData() in the source editing mode.
|
102
|
+
editor.data.on('get', () => {
|
103
|
+
if (this.isSourceEditingMode) {
|
104
|
+
this.updateEditorData();
|
105
|
+
}
|
106
|
+
}, { priority: 'high' });
|
107
|
+
}
|
108
|
+
/**
|
109
|
+
* @inheritDoc
|
110
|
+
*/
|
111
|
+
afterInit() {
|
112
|
+
const editor = this.editor;
|
113
|
+
const collaborationPluginNamesToWarn = [
|
114
|
+
'RealTimeCollaborativeEditing',
|
115
|
+
'CommentsEditing',
|
116
|
+
'TrackChangesEditing',
|
117
|
+
'RevisionHistory'
|
118
|
+
];
|
119
|
+
// Currently, the basic integration with Collaboration Features is to display a warning in the console.
|
120
|
+
if (collaborationPluginNamesToWarn.some(pluginName => editor.plugins.has(pluginName))) {
|
121
|
+
console.warn('You initialized the editor with the source editing feature and at least one of the collaboration features. ' +
|
122
|
+
'Please be advised that the source editing feature may not work, and be careful when editing document source ' +
|
123
|
+
'that contains markers created by the collaboration features.');
|
124
|
+
}
|
125
|
+
// Restricted Editing integration can also lead to problems. Warn the user accordingly.
|
126
|
+
if (editor.plugins.has('RestrictedEditingModeEditing')) {
|
127
|
+
console.warn('You initialized the editor with the source editing feature and restricted editing feature. ' +
|
128
|
+
'Please be advised that the source editing feature may not work, and be careful when editing document source ' +
|
129
|
+
'that contains markers created by the restricted editing feature.');
|
130
|
+
}
|
131
|
+
}
|
132
|
+
/**
|
133
|
+
* Updates the source data in all hidden editing roots.
|
134
|
+
*/
|
135
|
+
updateEditorData() {
|
136
|
+
const editor = this.editor;
|
137
|
+
const data = {};
|
138
|
+
for (const [rootName, domSourceEditingElementWrapper] of this._replacedRoots) {
|
139
|
+
const oldData = this._dataFromRoots.get(rootName);
|
140
|
+
const newData = domSourceEditingElementWrapper.dataset.value;
|
141
|
+
// Do not set the data unless some changes have been made in the meantime.
|
142
|
+
// This prevents empty undo steps after switching to the normal editor.
|
143
|
+
if (oldData !== newData) {
|
144
|
+
data[rootName] = newData;
|
145
|
+
}
|
146
|
+
}
|
147
|
+
if (Object.keys(data).length) {
|
148
|
+
editor.data.set(data, { batchType: { isUndoable: true } });
|
149
|
+
}
|
150
|
+
}
|
151
|
+
/**
|
152
|
+
* Creates source editing wrappers that replace each editing root. Each wrapper contains the document source from the corresponding
|
153
|
+
* root.
|
154
|
+
*
|
155
|
+
* The wrapper element contains a textarea and it solves the problem, that the textarea element cannot auto expand its height based on
|
156
|
+
* 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
|
157
|
+
* needs to, in order to display the whole document source without scrolling. The wrapper element is a parent for the textarea and for
|
158
|
+
* the pseudo-element `::after`, that replicates the look, content, and position of the textarea. The pseudo-element replica is hidden,
|
159
|
+
* 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
|
160
|
+
* of its children (the textarea and the `::after` pseudo-element) are positioned within a CSS grid to occupy the same grid cell. The
|
161
|
+
* content in the pseudo-element `::after` is set in CSS and it stretches the grid to the appropriate size based on the textarea value.
|
162
|
+
* Since both children occupy the same grid cell, both have always the same height.
|
163
|
+
*/
|
164
|
+
_showSourceEditing() {
|
165
|
+
const editor = this.editor;
|
166
|
+
const editingView = editor.editing.view;
|
167
|
+
const model = editor.model;
|
168
|
+
model.change(writer => {
|
169
|
+
writer.setSelection(null);
|
170
|
+
writer.removeSelectionAttribute(model.document.selection.getAttributeKeys());
|
171
|
+
});
|
172
|
+
// It is not needed to iterate through all editing roots, as currently the plugin supports only the Classic Editor with a single
|
173
|
+
// main root, but this code may help understand and use this feature in external integrations.
|
174
|
+
for (const [rootName, domRootElement] of editingView.domRoots) {
|
175
|
+
const data = formatSource(editor.data.get({ rootName }));
|
176
|
+
const domSourceEditingElementTextarea = createElement(domRootElement.ownerDocument, 'textarea', {
|
177
|
+
rows: '1',
|
178
|
+
'aria-label': 'Source code editing area'
|
179
|
+
});
|
180
|
+
const domSourceEditingElementWrapper = createElement(domRootElement.ownerDocument, 'div', {
|
181
|
+
class: 'ck-source-editing-area',
|
182
|
+
'data-value': data
|
183
|
+
}, [domSourceEditingElementTextarea]);
|
184
|
+
domSourceEditingElementTextarea.value = data;
|
185
|
+
// Setting a value to textarea moves the input cursor to the end. We want the selection at the beginning.
|
186
|
+
domSourceEditingElementTextarea.setSelectionRange(0, 0);
|
187
|
+
// Bind the textarea's value to the wrapper's `data-value` property. Each change of the textarea's value updates the
|
188
|
+
// wrapper's `data-value` property.
|
189
|
+
domSourceEditingElementTextarea.addEventListener('input', () => {
|
190
|
+
domSourceEditingElementWrapper.dataset.value = domSourceEditingElementTextarea.value;
|
191
|
+
editor.ui.update();
|
192
|
+
});
|
193
|
+
editingView.change(writer => {
|
194
|
+
const viewRoot = editingView.document.getRoot(rootName);
|
195
|
+
writer.addClass('ck-hidden', viewRoot);
|
196
|
+
});
|
197
|
+
// Register the element so it becomes available for Alt+F10 and Esc navigation.
|
198
|
+
editor.ui.setEditableElement('sourceEditing:' + rootName, domSourceEditingElementTextarea);
|
199
|
+
this._replacedRoots.set(rootName, domSourceEditingElementWrapper);
|
200
|
+
this._elementReplacer.replace(domRootElement, domSourceEditingElementWrapper);
|
201
|
+
this._dataFromRoots.set(rootName, data);
|
202
|
+
}
|
203
|
+
this._focusSourceEditing();
|
204
|
+
}
|
205
|
+
/**
|
206
|
+
* Restores all hidden editing roots and sets the source data in them.
|
207
|
+
*/
|
208
|
+
_hideSourceEditing() {
|
209
|
+
const editor = this.editor;
|
210
|
+
const editingView = editor.editing.view;
|
211
|
+
this.updateEditorData();
|
212
|
+
editingView.change(writer => {
|
213
|
+
for (const [rootName] of this._replacedRoots) {
|
214
|
+
writer.removeClass('ck-hidden', editingView.document.getRoot(rootName));
|
215
|
+
}
|
216
|
+
});
|
217
|
+
this._elementReplacer.restore();
|
218
|
+
this._replacedRoots.clear();
|
219
|
+
this._dataFromRoots.clear();
|
220
|
+
editingView.focus();
|
221
|
+
}
|
222
|
+
/**
|
223
|
+
* Focuses the textarea containing document source from the first editing root.
|
224
|
+
*/
|
225
|
+
_focusSourceEditing() {
|
226
|
+
const editor = this.editor;
|
227
|
+
const [domSourceEditingElementWrapper] = this._replacedRoots.values();
|
228
|
+
const textarea = domSourceEditingElementWrapper.querySelector('textarea');
|
229
|
+
// The FocusObserver was disabled by View.render() while the DOM root was getting hidden and the replacer
|
230
|
+
// revealed the textarea. So it couldn't notice that the DOM root got blurred in the process.
|
231
|
+
// Let's sync this state manually here because otherwise Renderer will attempt to render selection
|
232
|
+
// in an invisible DOM root.
|
233
|
+
editor.editing.view.document.isFocused = false;
|
234
|
+
textarea.focus();
|
235
|
+
}
|
236
|
+
/**
|
237
|
+
* Disables all commands.
|
238
|
+
*/
|
239
|
+
_disableCommands() {
|
240
|
+
const editor = this.editor;
|
241
|
+
for (const command of editor.commands.commands()) {
|
242
|
+
command.forceDisabled(COMMAND_FORCE_DISABLE_ID);
|
243
|
+
}
|
244
|
+
}
|
245
|
+
/**
|
246
|
+
* Clears forced disable for all commands, that was previously set through {@link #_disableCommands}.
|
247
|
+
*/
|
248
|
+
_enableCommands() {
|
249
|
+
const editor = this.editor;
|
250
|
+
for (const command of editor.commands.commands()) {
|
251
|
+
command.clearForceDisabled(COMMAND_FORCE_DISABLE_ID);
|
252
|
+
}
|
253
|
+
}
|
254
|
+
/**
|
255
|
+
* Adds or removes the `readonly` attribute from the textarea from all roots, if document source mode is active.
|
256
|
+
*
|
257
|
+
* @param isReadOnly Indicates whether all textarea elements should be read-only.
|
258
|
+
*/
|
259
|
+
_handleReadOnlyMode(isReadOnly) {
|
260
|
+
if (!this.isSourceEditingMode) {
|
261
|
+
return;
|
262
|
+
}
|
263
|
+
for (const [, domSourceEditingElementWrapper] of this._replacedRoots) {
|
264
|
+
domSourceEditingElementWrapper.querySelector('textarea').readOnly = isReadOnly;
|
265
|
+
}
|
266
|
+
}
|
267
|
+
/**
|
268
|
+
* Checks, if the plugin is allowed to handle the source editing mode by itself. Currently, the source editing mode is supported only
|
269
|
+
* for the {@link module:editor-classic/classiceditor~ClassicEditor classic editor}.
|
270
|
+
*/
|
271
|
+
_isAllowedToHandleSourceEditingMode() {
|
272
|
+
const editor = this.editor;
|
273
|
+
const editable = editor.ui.view.editable;
|
274
|
+
// Checks, if the editor's editable belongs to the editor's DOM tree.
|
275
|
+
return editable && !editable.hasExternalElement;
|
276
|
+
}
|
277
|
+
}
|
278
|
+
/**
|
279
|
+
* Formats the content for a better readability.
|
280
|
+
*
|
281
|
+
* For a non-HTML source the unchanged input string is returned.
|
282
|
+
*
|
283
|
+
* @param input Input string to check.
|
284
|
+
*/
|
285
|
+
function formatSource(input) {
|
286
|
+
if (!isHtml(input)) {
|
287
|
+
return input;
|
288
|
+
}
|
289
|
+
return formatHtml(input);
|
290
|
+
}
|
291
|
+
/**
|
292
|
+
* Checks, if the document source is HTML. It is sufficient to just check the first character from the document data.
|
293
|
+
*
|
294
|
+
* @param input Input string to check.
|
295
|
+
*/
|
296
|
+
function isHtml(input) {
|
297
|
+
return input.startsWith('<');
|
298
|
+
}
|
@@ -1,19 +1,19 @@
|
|
1
|
-
/**
|
2
|
-
* @license Copyright (c) 2003-2023, 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
|
-
/**
|
6
|
-
* @module source-editing/utils/formathtml
|
7
|
-
*/
|
8
|
-
/**
|
9
|
-
* A simple (and naive) HTML code formatter that returns a formatted HTML markup that can be easily
|
10
|
-
* parsed by human eyes. It beautifies the HTML code by adding new lines between elements that behave like block elements
|
11
|
-
* (https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
|
12
|
-
* and a few more like `tr`, `td`, and similar ones) and inserting indents for nested content.
|
13
|
-
*
|
14
|
-
* WARNING: This function works only on a text that does not contain any indentations or new lines.
|
15
|
-
* Calling this function on the already formatted text will damage the formatting.
|
16
|
-
*
|
17
|
-
* @param input An HTML string to format.
|
18
|
-
*/
|
19
|
-
export declare function formatHtml(input: string): string;
|
1
|
+
/**
|
2
|
+
* @license Copyright (c) 2003-2023, 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
|
+
/**
|
6
|
+
* @module source-editing/utils/formathtml
|
7
|
+
*/
|
8
|
+
/**
|
9
|
+
* A simple (and naive) HTML code formatter that returns a formatted HTML markup that can be easily
|
10
|
+
* parsed by human eyes. It beautifies the HTML code by adding new lines between elements that behave like block elements
|
11
|
+
* (https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
|
12
|
+
* and a few more like `tr`, `td`, and similar ones) and inserting indents for nested content.
|
13
|
+
*
|
14
|
+
* WARNING: This function works only on a text that does not contain any indentations or new lines.
|
15
|
+
* Calling this function on the already formatted text will damage the formatting.
|
16
|
+
*
|
17
|
+
* @param input An HTML string to format.
|
18
|
+
*/
|
19
|
+
export declare function formatHtml(input: string): string;
|