@ckeditor/ckeditor5-html-embed 48.2.0-alpha.7 → 48.3.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.
package/dist/index.js CHANGED
@@ -2,541 +2,522 @@
2
2
  * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
4
  */
5
- import { Command, Plugin } from '@ckeditor/ckeditor5-core/dist/index.js';
6
- import { findOptimalInsertionRange, toWidget, Widget } from '@ckeditor/ckeditor5-widget/dist/index.js';
7
- import { ButtonView, MenuBarMenuListItemButtonView } from '@ckeditor/ckeditor5-ui/dist/index.js';
8
- import { logWarning, createElement } from '@ckeditor/ckeditor5-utils/dist/index.js';
9
- import { IconPencil, IconCheck, IconCancel, IconHtml } from '@ckeditor/ckeditor5-icons/dist/index.js';
5
+ import { Command, Plugin } from "@ckeditor/ckeditor5-core";
6
+ import { Widget, findOptimalInsertionRange, toWidget } from "@ckeditor/ckeditor5-widget";
7
+ import { ButtonView, MenuBarMenuListItemButtonView } from "@ckeditor/ckeditor5-ui";
8
+ import { createElement, logWarning } from "@ckeditor/ckeditor5-utils";
9
+ import { IconCancel, IconCheck, IconHtml, IconPencil } from "@ckeditor/ckeditor5-icons";
10
10
 
11
11
  /**
12
- * The insert HTML embed element command.
13
- *
14
- * The command is registered by {@link module:html-embed/htmlembedediting~HtmlEmbedEditing} as `'htmlEmbed'`.
15
- *
16
- * To insert an empty HTML embed element at the current selection, execute the command:
17
- *
18
- * ```ts
19
- * editor.execute( 'htmlEmbed' );
20
- * ```
21
- *
22
- * You can specify the initial content of a new HTML embed in the argument:
23
- *
24
- * ```ts
25
- * editor.execute( 'htmlEmbed', '<b>Initial content.</b>' );
26
- * ```
27
- *
28
- * To update the content of the HTML embed, select it in the model and pass the content in the argument:
29
- *
30
- * ```ts
31
- * editor.execute( 'htmlEmbed', '<b>New content of an existing embed.</b>' );
32
- * ```
33
- */ class HtmlEmbedCommand extends Command {
34
- /**
35
- * @inheritDoc
36
- */ refresh() {
37
- const model = this.editor.model;
38
- const schema = model.schema;
39
- const selection = model.document.selection;
40
- const selectedRawHtmlElement = getSelectedRawHtmlModelWidget(selection);
41
- this.isEnabled = isHtmlEmbedAllowedInParent(selection, schema, model);
42
- this.value = selectedRawHtmlElement ? selectedRawHtmlElement.getAttribute('value') || '' : null;
43
- }
44
- /**
45
- * Executes the command, which either:
46
- *
47
- * * creates and inserts a new HTML embed element if none was selected,
48
- * * updates the content of the HTML embed if one was selected.
49
- *
50
- * @fires execute
51
- * @param value When passed, the value (content) will be set on a new embed or a selected one.
52
- */ execute(value) {
53
- const model = this.editor.model;
54
- const selection = model.document.selection;
55
- model.change((writer)=>{
56
- let htmlEmbedElement;
57
- // If the command has a non-null value, there must be some HTML embed selected in the model.
58
- if (this.value !== null) {
59
- htmlEmbedElement = getSelectedRawHtmlModelWidget(selection);
60
- } else {
61
- htmlEmbedElement = writer.createElement('rawHtml');
62
- model.insertObject(htmlEmbedElement, null, null, {
63
- setSelection: 'on'
64
- });
65
- }
66
- writer.setAttribute('value', value, htmlEmbedElement);
67
- });
68
- }
69
- }
12
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
13
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
14
+ */
15
+ /**
16
+ * The insert HTML embed element command.
17
+ *
18
+ * The command is registered by {@link module:html-embed/htmlembedediting~HtmlEmbedEditing} as `'htmlEmbed'`.
19
+ *
20
+ * To insert an empty HTML embed element at the current selection, execute the command:
21
+ *
22
+ * ```ts
23
+ * editor.execute( 'htmlEmbed' );
24
+ * ```
25
+ *
26
+ * You can specify the initial content of a new HTML embed in the argument:
27
+ *
28
+ * ```ts
29
+ * editor.execute( 'htmlEmbed', '<b>Initial content.</b>' );
30
+ * ```
31
+ *
32
+ * To update the content of the HTML embed, select it in the model and pass the content in the argument:
33
+ *
34
+ * ```ts
35
+ * editor.execute( 'htmlEmbed', '<b>New content of an existing embed.</b>' );
36
+ * ```
37
+ */
38
+ var HtmlEmbedCommand = class extends Command {
39
+ /**
40
+ * @inheritDoc
41
+ */
42
+ refresh() {
43
+ const model = this.editor.model;
44
+ const schema = model.schema;
45
+ const selection = model.document.selection;
46
+ const selectedRawHtmlElement = getSelectedRawHtmlModelWidget(selection);
47
+ this.isEnabled = isHtmlEmbedAllowedInParent(selection, schema, model);
48
+ this.value = selectedRawHtmlElement ? selectedRawHtmlElement.getAttribute("value") || "" : null;
49
+ }
50
+ /**
51
+ * Executes the command, which either:
52
+ *
53
+ * * creates and inserts a new HTML embed element if none was selected,
54
+ * * updates the content of the HTML embed if one was selected.
55
+ *
56
+ * @fires execute
57
+ * @param value When passed, the value (content) will be set on a new embed or a selected one.
58
+ */
59
+ execute(value) {
60
+ const model = this.editor.model;
61
+ const selection = model.document.selection;
62
+ model.change((writer) => {
63
+ let htmlEmbedElement;
64
+ if (this.value !== null) htmlEmbedElement = getSelectedRawHtmlModelWidget(selection);
65
+ else {
66
+ htmlEmbedElement = writer.createElement("rawHtml");
67
+ model.insertObject(htmlEmbedElement, null, null, { setSelection: "on" });
68
+ }
69
+ writer.setAttribute("value", value, htmlEmbedElement);
70
+ });
71
+ }
72
+ };
70
73
  /**
71
- * Checks if an HTML embed is allowed by the schema in the optimal insertion parent.
72
- */ function isHtmlEmbedAllowedInParent(selection, schema, model) {
73
- const parent = getInsertHtmlEmbedParent(selection, model);
74
- return schema.checkChild(parent, 'rawHtml');
74
+ * Checks if an HTML embed is allowed by the schema in the optimal insertion parent.
75
+ */
76
+ function isHtmlEmbedAllowedInParent(selection, schema, model) {
77
+ const parent = getInsertHtmlEmbedParent(selection, model);
78
+ return schema.checkChild(parent, "rawHtml");
75
79
  }
76
80
  /**
77
- * Returns a node that will be used to insert a html embed with `model.insertContent` to check if a html embed element can be placed there.
78
- */ function getInsertHtmlEmbedParent(selection, model) {
79
- const insertionRange = findOptimalInsertionRange(selection, model);
80
- const parent = insertionRange.start.parent;
81
- if (parent.isEmpty && !parent.is('rootElement')) {
82
- return parent.parent;
83
- }
84
- return parent;
81
+ * Returns a node that will be used to insert a html embed with `model.insertContent` to check if a html embed element can be placed there.
82
+ */
83
+ function getInsertHtmlEmbedParent(selection, model) {
84
+ const parent = findOptimalInsertionRange(selection, model).start.parent;
85
+ if (parent.isEmpty && !parent.is("rootElement")) return parent.parent;
86
+ return parent;
85
87
  }
86
88
  /**
87
- * Returns the selected HTML embed element in the model, if any.
88
- */ function getSelectedRawHtmlModelWidget(selection) {
89
- const selectedElement = selection.getSelectedElement();
90
- if (selectedElement && selectedElement.is('element', 'rawHtml')) {
91
- return selectedElement;
92
- }
93
- return null;
89
+ * Returns the selected HTML embed element in the model, if any.
90
+ */
91
+ function getSelectedRawHtmlModelWidget(selection) {
92
+ const selectedElement = selection.getSelectedElement();
93
+ if (selectedElement && selectedElement.is("element", "rawHtml")) return selectedElement;
94
+ return null;
94
95
  }
95
96
 
96
97
  /**
97
- * The HTML embed editing feature.
98
- */ class HtmlEmbedEditing extends Plugin {
99
- /**
100
- * Keeps references to {@link module:ui/button/buttonview~ButtonView edit, save, and cancel} button instances created for
101
- * each widget so they can be destroyed if they are no longer in DOM after the editing view was re-rendered.
102
- */ _widgetButtonViewReferences = new Set();
103
- /**
104
- * @inheritDoc
105
- */ static get pluginName() {
106
- return 'HtmlEmbedEditing';
107
- }
108
- /**
109
- * @inheritDoc
110
- * @internal
111
- */ static get licenseFeatureCode() {
112
- return 'HE';
113
- }
114
- /**
115
- * @inheritDoc
116
- */ static get isOfficialPlugin() {
117
- return true;
118
- }
119
- /**
120
- * @inheritDoc
121
- */ static get isPremiumPlugin() {
122
- return true;
123
- }
124
- /**
125
- * @inheritDoc
126
- */ constructor(editor){
127
- super(editor);
128
- editor.config.define('htmlEmbed', {
129
- showPreviews: false,
130
- sanitizeHtml: (rawHtml)=>{
131
- /**
132
- * When using the HTML embed feature with the `config.htmlEmbed.showPreviews` set to `true`, it is strongly recommended to
133
- * define a sanitize function that will clean up the input HTML in order to avoid XSS vulnerability.
134
- *
135
- * For a detailed overview, check the {@glink features/html/html-embed HTML embed feature} documentation.
136
- *
137
- * @error html-embed-provide-sanitize-function
138
- */ logWarning('html-embed-provide-sanitize-function');
139
- return {
140
- html: rawHtml,
141
- hasChanged: false
142
- };
143
- }
144
- });
145
- }
146
- /**
147
- * @inheritDoc
148
- */ init() {
149
- const editor = this.editor;
150
- const schema = editor.model.schema;
151
- schema.register('rawHtml', {
152
- inheritAllFrom: '$blockObject',
153
- allowAttributes: [
154
- 'value'
155
- ]
156
- });
157
- editor.commands.add('htmlEmbed', new HtmlEmbedCommand(editor));
158
- this._setupConversion();
159
- }
160
- /**
161
- * Prepares converters for the feature.
162
- */ _setupConversion() {
163
- const editor = this.editor;
164
- const t = editor.t;
165
- const view = editor.editing.view;
166
- const widgetButtonViewReferences = this._widgetButtonViewReferences;
167
- const htmlEmbedConfig = editor.config.get('htmlEmbed');
168
- // Destroy UI buttons created for widgets that have been removed from the view document (e.g. in the previous conversion).
169
- // This prevents unexpected memory leaks from UI views.
170
- this.editor.editing.view.on('render', ()=>{
171
- for (const buttonView of widgetButtonViewReferences){
172
- if (buttonView.element && buttonView.element.isConnected) {
173
- return;
174
- }
175
- buttonView.destroy();
176
- widgetButtonViewReferences.delete(buttonView);
177
- }
178
- }, {
179
- priority: 'lowest'
180
- });
181
- // Register div.raw-html-embed as a raw content element so all of it's content will be provided
182
- // as a view element's custom property while data upcasting.
183
- editor.data.registerRawContentMatcher({
184
- name: 'div',
185
- classes: 'raw-html-embed'
186
- });
187
- editor.conversion.for('upcast').elementToElement({
188
- view: {
189
- name: 'div',
190
- classes: 'raw-html-embed'
191
- },
192
- model: (viewElement, { writer })=>{
193
- // The div.raw-html-embed is registered as a raw content element,
194
- // so all it's content is available in a custom property.
195
- return writer.createElement('rawHtml', {
196
- value: viewElement.getCustomProperty('$rawContent')
197
- });
198
- }
199
- });
200
- editor.conversion.for('dataDowncast').elementToElement({
201
- model: 'rawHtml',
202
- view: (modelElement, { writer })=>{
203
- return writer.createRawElement('div', {
204
- class: 'raw-html-embed'
205
- }, function(domElement) {
206
- domElement.innerHTML = modelElement.getAttribute('value') || '';
207
- });
208
- }
209
- });
210
- editor.conversion.for('editingDowncast').elementToStructure({
211
- model: {
212
- name: 'rawHtml',
213
- attributes: [
214
- 'value'
215
- ]
216
- },
217
- view: (modelElement, { writer })=>{
218
- let domContentWrapper;
219
- let state;
220
- let props;
221
- const viewContentWrapper = writer.createRawElement('div', {
222
- class: 'raw-html-embed__content-wrapper'
223
- }, function(domElement) {
224
- domContentWrapper = domElement;
225
- renderContent({
226
- editor,
227
- domElement,
228
- state,
229
- props
230
- });
231
- // Since there is a `data-cke-ignore-events` attribute set on the wrapper element in the editable mode,
232
- // the explicit `mousedown` handler on the `capture` phase is needed to move the selection onto the whole
233
- // HTML embed widget.
234
- domContentWrapper.addEventListener('mousedown', ()=>{
235
- if (state.isEditable) {
236
- const model = editor.model;
237
- const selectedElement = model.document.selection.getSelectedElement();
238
- // Move the selection onto the whole HTML embed widget if it's currently not selected.
239
- if (selectedElement !== modelElement) {
240
- model.change((writer)=>writer.setSelection(modelElement, 'on'));
241
- }
242
- }
243
- }, true);
244
- });
245
- // API exposed on each raw HTML embed widget so other features can control a particular widget.
246
- const rawHtmlApi = {
247
- makeEditable () {
248
- state = Object.assign({}, state, {
249
- isEditable: true
250
- });
251
- renderContent({
252
- domElement: domContentWrapper,
253
- editor,
254
- state,
255
- props
256
- });
257
- view.change((writer)=>{
258
- writer.setAttribute('data-cke-ignore-events', 'true', viewContentWrapper);
259
- });
260
- // This could be potentially pulled to a separate method called focusTextarea().
261
- domContentWrapper.querySelector('textarea').focus();
262
- },
263
- save (newValue) {
264
- // If the value didn't change, we just cancel. If it changed,
265
- // it's enough to update the model – the entire widget will be reconverted.
266
- if (newValue !== state.getRawHtmlValue()) {
267
- editor.execute('htmlEmbed', newValue);
268
- editor.editing.view.focus();
269
- } else {
270
- this.cancel();
271
- }
272
- },
273
- cancel () {
274
- state = Object.assign({}, state, {
275
- isEditable: false
276
- });
277
- renderContent({
278
- domElement: domContentWrapper,
279
- editor,
280
- state,
281
- props
282
- });
283
- editor.editing.view.focus();
284
- view.change((writer)=>{
285
- writer.removeAttribute('data-cke-ignore-events', viewContentWrapper);
286
- });
287
- }
288
- };
289
- state = {
290
- showPreviews: htmlEmbedConfig.showPreviews,
291
- isEditable: false,
292
- getRawHtmlValue: ()=>modelElement.getAttribute('value') || ''
293
- };
294
- props = {
295
- sanitizeHtml: htmlEmbedConfig.sanitizeHtml,
296
- textareaPlaceholder: t('Paste raw HTML here...'),
297
- onEditClick () {
298
- rawHtmlApi.makeEditable();
299
- },
300
- onSaveClick (newValue) {
301
- rawHtmlApi.save(newValue);
302
- },
303
- onCancelClick () {
304
- rawHtmlApi.cancel();
305
- }
306
- };
307
- const viewContainer = writer.createContainerElement('div', {
308
- class: 'raw-html-embed',
309
- 'data-html-embed-label': t('HTML snippet'),
310
- dir: editor.locale.uiLanguageDirection
311
- }, viewContentWrapper);
312
- writer.setCustomProperty('rawHtmlApi', rawHtmlApi, viewContainer);
313
- writer.setCustomProperty('rawHtml', true, viewContainer);
314
- return toWidget(viewContainer, writer, {
315
- label: t('HTML snippet'),
316
- hasSelectionHandle: true
317
- });
318
- }
319
- });
320
- function renderContent({ editor, domElement, state, props }) {
321
- // Remove all children;
322
- domElement.textContent = '';
323
- const domDocument = domElement.ownerDocument;
324
- let domTextarea;
325
- if (state.isEditable) {
326
- const textareaProps = {
327
- isDisabled: false,
328
- placeholder: props.textareaPlaceholder
329
- };
330
- domTextarea = createDomTextarea({
331
- domDocument,
332
- state,
333
- props: textareaProps
334
- });
335
- domElement.append(domTextarea);
336
- } else if (state.showPreviews) {
337
- const previewContainerProps = {
338
- sanitizeHtml: props.sanitizeHtml
339
- };
340
- domElement.append(createPreviewContainer({
341
- domDocument,
342
- state,
343
- props: previewContainerProps,
344
- editor
345
- }));
346
- } else {
347
- const textareaProps = {
348
- isDisabled: true,
349
- placeholder: props.textareaPlaceholder
350
- };
351
- domElement.append(createDomTextarea({
352
- domDocument,
353
- state,
354
- props: textareaProps
355
- }));
356
- }
357
- const buttonsWrapperProps = {
358
- onEditClick: props.onEditClick,
359
- onSaveClick: ()=>{
360
- props.onSaveClick(domTextarea.value);
361
- },
362
- onCancelClick: props.onCancelClick
363
- };
364
- domElement.prepend(createDomButtonsWrapper({
365
- editor,
366
- domDocument,
367
- state,
368
- props: buttonsWrapperProps
369
- }));
370
- }
371
- function createDomButtonsWrapper({ editor, domDocument, state, props }) {
372
- const domButtonsWrapper = createElement(domDocument, 'div', {
373
- class: 'raw-html-embed__buttons-wrapper'
374
- });
375
- if (state.isEditable) {
376
- const saveButtonView = createUIButton(editor, 'save', props.onSaveClick);
377
- const cancelButtonView = createUIButton(editor, 'cancel', props.onCancelClick);
378
- domButtonsWrapper.append(saveButtonView.element, cancelButtonView.element);
379
- widgetButtonViewReferences.add(saveButtonView).add(cancelButtonView);
380
- } else {
381
- const editButtonView = createUIButton(editor, 'edit', props.onEditClick);
382
- domButtonsWrapper.append(editButtonView.element);
383
- widgetButtonViewReferences.add(editButtonView);
384
- }
385
- return domButtonsWrapper;
386
- }
387
- function createDomTextarea({ domDocument, state, props }) {
388
- const domTextarea = createElement(domDocument, 'textarea', {
389
- placeholder: props.placeholder,
390
- class: 'ck ck-reset ck-input ck-input-text raw-html-embed__source'
391
- });
392
- domTextarea.disabled = props.isDisabled;
393
- domTextarea.value = state.getRawHtmlValue();
394
- return domTextarea;
395
- }
396
- function createPreviewContainer({ editor, domDocument, state, props }) {
397
- const sanitizedOutput = props.sanitizeHtml(state.getRawHtmlValue());
398
- const placeholderText = state.getRawHtmlValue().length > 0 ? t('No preview available') : t('Empty snippet content');
399
- const domPreviewPlaceholder = createElement(domDocument, 'div', {
400
- class: 'ck ck-reset_all raw-html-embed__preview-placeholder'
401
- }, placeholderText);
402
- const domPreviewContent = createElement(domDocument, 'div', {
403
- class: 'raw-html-embed__preview-content',
404
- dir: editor.locale.contentLanguageDirection
405
- });
406
- // Creating a contextual document fragment allows executing scripts when inserting into the preview element.
407
- // See: https://github.com/ckeditor/ckeditor5/issues/8326.
408
- const domRange = domDocument.createRange();
409
- const domDocumentFragment = domRange.createContextualFragment(sanitizedOutput.html);
410
- domPreviewContent.appendChild(domDocumentFragment);
411
- const domPreviewContainer = createElement(domDocument, 'div', {
412
- class: 'raw-html-embed__preview'
413
- }, [
414
- domPreviewPlaceholder,
415
- domPreviewContent
416
- ]);
417
- return domPreviewContainer;
418
- }
419
- }
420
- }
98
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
99
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
100
+ */
101
+ /**
102
+ * @module html-embed/htmlembedediting
103
+ */
421
104
  /**
422
- * Returns a UI button view that can be used in conversion.
423
- */ function createUIButton(editor, type, onClick) {
424
- const { t } = editor.locale;
425
- const buttonView = new ButtonView(editor.locale);
426
- const command = editor.commands.get('htmlEmbed');
427
- buttonView.set({
428
- class: `raw-html-embed__${type}-button`,
429
- icon: IconPencil,
430
- tooltip: true,
431
- tooltipPosition: editor.locale.uiLanguageDirection === 'rtl' ? 'e' : 'w'
432
- });
433
- buttonView.render();
434
- if (type === 'edit') {
435
- buttonView.set({
436
- icon: IconPencil,
437
- label: t('Edit source')
438
- });
439
- buttonView.bind('isEnabled').to(command);
440
- } else if (type === 'save') {
441
- buttonView.set({
442
- icon: IconCheck,
443
- label: t('Save changes')
444
- });
445
- buttonView.bind('isEnabled').to(command);
446
- } else {
447
- buttonView.set({
448
- icon: IconCancel,
449
- label: t('Cancel')
450
- });
451
- }
452
- buttonView.on('execute', onClick);
453
- return buttonView;
105
+ * The HTML embed editing feature.
106
+ */
107
+ var HtmlEmbedEditing = class extends Plugin {
108
+ /**
109
+ * Keeps references to {@link module:ui/button/buttonview~ButtonView edit, save, and cancel} button instances created for
110
+ * each widget so they can be destroyed if they are no longer in DOM after the editing view was re-rendered.
111
+ */
112
+ _widgetButtonViewReferences = /* @__PURE__ */ new Set();
113
+ /**
114
+ * @inheritDoc
115
+ */
116
+ static get pluginName() {
117
+ return "HtmlEmbedEditing";
118
+ }
119
+ /**
120
+ * @inheritDoc
121
+ * @internal
122
+ */
123
+ static get licenseFeatureCode() {
124
+ return "HE";
125
+ }
126
+ /**
127
+ * @inheritDoc
128
+ */
129
+ static get isOfficialPlugin() {
130
+ return true;
131
+ }
132
+ /**
133
+ * @inheritDoc
134
+ */
135
+ static get isPremiumPlugin() {
136
+ return true;
137
+ }
138
+ /**
139
+ * @inheritDoc
140
+ */
141
+ constructor(editor) {
142
+ super(editor);
143
+ editor.config.define("htmlEmbed", {
144
+ showPreviews: false,
145
+ sanitizeHtml: (rawHtml) => {
146
+ /**
147
+ * When using the HTML embed feature with the `config.htmlEmbed.showPreviews` set to `true`, it is strongly recommended to
148
+ * define a sanitize function that will clean up the input HTML in order to avoid XSS vulnerability.
149
+ *
150
+ * For a detailed overview, check the {@glink features/html/html-embed HTML embed feature} documentation.
151
+ *
152
+ * @error html-embed-provide-sanitize-function
153
+ */
154
+ logWarning("html-embed-provide-sanitize-function");
155
+ return {
156
+ html: rawHtml,
157
+ hasChanged: false
158
+ };
159
+ }
160
+ });
161
+ }
162
+ /**
163
+ * @inheritDoc
164
+ */
165
+ init() {
166
+ const editor = this.editor;
167
+ editor.model.schema.register("rawHtml", {
168
+ inheritAllFrom: "$blockObject",
169
+ allowAttributes: ["value"]
170
+ });
171
+ editor.commands.add("htmlEmbed", new HtmlEmbedCommand(editor));
172
+ this._setupConversion();
173
+ }
174
+ /**
175
+ * Prepares converters for the feature.
176
+ */
177
+ _setupConversion() {
178
+ const editor = this.editor;
179
+ const t = editor.t;
180
+ const view = editor.editing.view;
181
+ const widgetButtonViewReferences = this._widgetButtonViewReferences;
182
+ const htmlEmbedConfig = editor.config.get("htmlEmbed");
183
+ this.editor.editing.view.on("render", () => {
184
+ for (const buttonView of widgetButtonViewReferences) {
185
+ if (buttonView.element && buttonView.element.isConnected) return;
186
+ buttonView.destroy();
187
+ widgetButtonViewReferences.delete(buttonView);
188
+ }
189
+ }, { priority: "lowest" });
190
+ editor.data.registerRawContentMatcher({
191
+ name: "div",
192
+ classes: "raw-html-embed"
193
+ });
194
+ editor.conversion.for("upcast").elementToElement({
195
+ view: {
196
+ name: "div",
197
+ classes: "raw-html-embed"
198
+ },
199
+ model: (viewElement, { writer }) => {
200
+ return writer.createElement("rawHtml", { value: viewElement.getCustomProperty("$rawContent") });
201
+ }
202
+ });
203
+ editor.conversion.for("dataDowncast").elementToElement({
204
+ model: "rawHtml",
205
+ view: (modelElement, { writer }) => {
206
+ return writer.createRawElement("div", { class: "raw-html-embed" }, function(domElement) {
207
+ domElement.innerHTML = modelElement.getAttribute("value") || "";
208
+ });
209
+ }
210
+ });
211
+ editor.conversion.for("editingDowncast").elementToStructure({
212
+ model: {
213
+ name: "rawHtml",
214
+ attributes: ["value"]
215
+ },
216
+ view: (modelElement, { writer }) => {
217
+ let domContentWrapper;
218
+ let state;
219
+ let props;
220
+ const viewContentWrapper = writer.createRawElement("div", { class: "raw-html-embed__content-wrapper" }, function(domElement) {
221
+ domContentWrapper = domElement;
222
+ renderContent({
223
+ editor,
224
+ domElement,
225
+ state,
226
+ props
227
+ });
228
+ domContentWrapper.addEventListener("mousedown", () => {
229
+ if (state.isEditable) {
230
+ const model = editor.model;
231
+ if (model.document.selection.getSelectedElement() !== modelElement) model.change((writer) => writer.setSelection(modelElement, "on"));
232
+ }
233
+ }, true);
234
+ });
235
+ const rawHtmlApi = {
236
+ makeEditable() {
237
+ state = Object.assign({}, state, { isEditable: true });
238
+ renderContent({
239
+ domElement: domContentWrapper,
240
+ editor,
241
+ state,
242
+ props
243
+ });
244
+ view.change((writer) => {
245
+ writer.setAttribute("data-cke-ignore-events", "true", viewContentWrapper);
246
+ });
247
+ domContentWrapper.querySelector("textarea").focus();
248
+ },
249
+ save(newValue) {
250
+ if (newValue !== state.getRawHtmlValue()) {
251
+ editor.execute("htmlEmbed", newValue);
252
+ editor.editing.view.focus();
253
+ } else this.cancel();
254
+ },
255
+ cancel() {
256
+ state = Object.assign({}, state, { isEditable: false });
257
+ renderContent({
258
+ domElement: domContentWrapper,
259
+ editor,
260
+ state,
261
+ props
262
+ });
263
+ editor.editing.view.focus();
264
+ view.change((writer) => {
265
+ writer.removeAttribute("data-cke-ignore-events", viewContentWrapper);
266
+ });
267
+ }
268
+ };
269
+ state = {
270
+ showPreviews: htmlEmbedConfig.showPreviews,
271
+ isEditable: false,
272
+ getRawHtmlValue: () => modelElement.getAttribute("value") || ""
273
+ };
274
+ props = {
275
+ sanitizeHtml: htmlEmbedConfig.sanitizeHtml,
276
+ textareaPlaceholder: t("Paste raw HTML here..."),
277
+ onEditClick() {
278
+ rawHtmlApi.makeEditable();
279
+ },
280
+ onSaveClick(newValue) {
281
+ rawHtmlApi.save(newValue);
282
+ },
283
+ onCancelClick() {
284
+ rawHtmlApi.cancel();
285
+ }
286
+ };
287
+ const viewContainer = writer.createContainerElement("div", {
288
+ class: "raw-html-embed",
289
+ "data-html-embed-label": t("HTML snippet"),
290
+ dir: editor.locale.uiLanguageDirection
291
+ }, viewContentWrapper);
292
+ writer.setCustomProperty("rawHtmlApi", rawHtmlApi, viewContainer);
293
+ writer.setCustomProperty("rawHtml", true, viewContainer);
294
+ return toWidget(viewContainer, writer, {
295
+ label: t("HTML snippet"),
296
+ hasSelectionHandle: true
297
+ });
298
+ }
299
+ });
300
+ function renderContent({ editor, domElement, state, props }) {
301
+ domElement.textContent = "";
302
+ const domDocument = domElement.ownerDocument;
303
+ let domTextarea;
304
+ if (state.isEditable) {
305
+ domTextarea = createDomTextarea({
306
+ domDocument,
307
+ state,
308
+ props: {
309
+ isDisabled: false,
310
+ placeholder: props.textareaPlaceholder
311
+ }
312
+ });
313
+ domElement.append(domTextarea);
314
+ } else if (state.showPreviews) {
315
+ const previewContainerProps = { sanitizeHtml: props.sanitizeHtml };
316
+ domElement.append(createPreviewContainer({
317
+ domDocument,
318
+ state,
319
+ props: previewContainerProps,
320
+ editor
321
+ }));
322
+ } else {
323
+ const textareaProps = {
324
+ isDisabled: true,
325
+ placeholder: props.textareaPlaceholder
326
+ };
327
+ domElement.append(createDomTextarea({
328
+ domDocument,
329
+ state,
330
+ props: textareaProps
331
+ }));
332
+ }
333
+ const buttonsWrapperProps = {
334
+ onEditClick: props.onEditClick,
335
+ onSaveClick: () => {
336
+ props.onSaveClick(domTextarea.value);
337
+ },
338
+ onCancelClick: props.onCancelClick
339
+ };
340
+ domElement.prepend(createDomButtonsWrapper({
341
+ editor,
342
+ domDocument,
343
+ state,
344
+ props: buttonsWrapperProps
345
+ }));
346
+ }
347
+ function createDomButtonsWrapper({ editor, domDocument, state, props }) {
348
+ const domButtonsWrapper = createElement(domDocument, "div", { class: "raw-html-embed__buttons-wrapper" });
349
+ if (state.isEditable) {
350
+ const saveButtonView = createUIButton(editor, "save", props.onSaveClick);
351
+ const cancelButtonView = createUIButton(editor, "cancel", props.onCancelClick);
352
+ domButtonsWrapper.append(saveButtonView.element, cancelButtonView.element);
353
+ widgetButtonViewReferences.add(saveButtonView).add(cancelButtonView);
354
+ } else {
355
+ const editButtonView = createUIButton(editor, "edit", props.onEditClick);
356
+ domButtonsWrapper.append(editButtonView.element);
357
+ widgetButtonViewReferences.add(editButtonView);
358
+ }
359
+ return domButtonsWrapper;
360
+ }
361
+ function createDomTextarea({ domDocument, state, props }) {
362
+ const domTextarea = createElement(domDocument, "textarea", {
363
+ placeholder: props.placeholder,
364
+ class: "ck ck-reset ck-input ck-input-text raw-html-embed__source"
365
+ });
366
+ domTextarea.disabled = props.isDisabled;
367
+ domTextarea.value = state.getRawHtmlValue();
368
+ return domTextarea;
369
+ }
370
+ function createPreviewContainer({ editor, domDocument, state, props }) {
371
+ const sanitizedOutput = props.sanitizeHtml(state.getRawHtmlValue());
372
+ const domPreviewPlaceholder = createElement(domDocument, "div", { class: "ck ck-reset_all raw-html-embed__preview-placeholder" }, state.getRawHtmlValue().length > 0 ? t("No preview available") : t("Empty snippet content"));
373
+ const domPreviewContent = createElement(domDocument, "div", {
374
+ class: "raw-html-embed__preview-content",
375
+ dir: editor.locale.contentLanguageDirection
376
+ });
377
+ const domDocumentFragment = domDocument.createRange().createContextualFragment(sanitizedOutput.html);
378
+ domPreviewContent.appendChild(domDocumentFragment);
379
+ return createElement(domDocument, "div", { class: "raw-html-embed__preview" }, [domPreviewPlaceholder, domPreviewContent]);
380
+ }
381
+ }
382
+ };
383
+ /**
384
+ * Returns a UI button view that can be used in conversion.
385
+ */
386
+ function createUIButton(editor, type, onClick) {
387
+ const { t } = editor.locale;
388
+ const buttonView = new ButtonView(editor.locale);
389
+ const command = editor.commands.get("htmlEmbed");
390
+ buttonView.set({
391
+ class: `raw-html-embed__${type}-button`,
392
+ icon: IconPencil,
393
+ tooltip: true,
394
+ tooltipPosition: editor.locale.uiLanguageDirection === "rtl" ? "e" : "w"
395
+ });
396
+ buttonView.render();
397
+ if (type === "edit") {
398
+ buttonView.set({
399
+ icon: IconPencil,
400
+ label: t("Edit source")
401
+ });
402
+ buttonView.bind("isEnabled").to(command);
403
+ } else if (type === "save") {
404
+ buttonView.set({
405
+ icon: IconCheck,
406
+ label: t("Save changes")
407
+ });
408
+ buttonView.bind("isEnabled").to(command);
409
+ } else buttonView.set({
410
+ icon: IconCancel,
411
+ label: t("Cancel")
412
+ });
413
+ buttonView.on("execute", onClick);
414
+ return buttonView;
454
415
  }
455
416
 
456
417
  /**
457
- * The HTML embed UI plugin.
458
- */ class HtmlEmbedUI extends Plugin {
459
- /**
460
- * @inheritDoc
461
- */ static get pluginName() {
462
- return 'HtmlEmbedUI';
463
- }
464
- /**
465
- * @inheritDoc
466
- */ static get isOfficialPlugin() {
467
- return true;
468
- }
469
- /**
470
- * @inheritDoc
471
- */ init() {
472
- const editor = this.editor;
473
- const locale = editor.locale;
474
- const t = locale.t;
475
- // Add the `htmlEmbed` button to feature components.
476
- editor.ui.componentFactory.add('htmlEmbed', ()=>{
477
- const buttonView = this._createButton(ButtonView);
478
- buttonView.set({
479
- tooltip: true,
480
- label: t('Insert HTML')
481
- });
482
- return buttonView;
483
- });
484
- editor.ui.componentFactory.add('menuBar:htmlEmbed', ()=>{
485
- const buttonView = this._createButton(MenuBarMenuListItemButtonView);
486
- buttonView.set({
487
- label: t('HTML snippet')
488
- });
489
- return buttonView;
490
- });
491
- }
492
- /**
493
- * Creates a button for html embed command to use either in toolbar or in menu bar.
494
- */ _createButton(ButtonClass) {
495
- const editor = this.editor;
496
- const command = editor.commands.get('htmlEmbed');
497
- const view = new ButtonClass(editor.locale);
498
- view.set({
499
- icon: IconHtml
500
- });
501
- view.bind('isEnabled').to(command, 'isEnabled');
502
- // Execute the command.
503
- this.listenTo(view, 'execute', ()=>{
504
- editor.execute('htmlEmbed');
505
- editor.editing.view.focus();
506
- const rawHtmlApi = editor.editing.view.document.selection.getSelectedElement().getCustomProperty('rawHtmlApi');
507
- rawHtmlApi.makeEditable();
508
- });
509
- return view;
510
- }
511
- }
418
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
419
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
420
+ */
421
+ /**
422
+ * @module html-embed/htmlembedui
423
+ */
424
+ /**
425
+ * The HTML embed UI plugin.
426
+ */
427
+ var HtmlEmbedUI = class extends Plugin {
428
+ /**
429
+ * @inheritDoc
430
+ */
431
+ static get pluginName() {
432
+ return "HtmlEmbedUI";
433
+ }
434
+ /**
435
+ * @inheritDoc
436
+ */
437
+ static get isOfficialPlugin() {
438
+ return true;
439
+ }
440
+ /**
441
+ * @inheritDoc
442
+ */
443
+ init() {
444
+ const editor = this.editor;
445
+ const t = editor.locale.t;
446
+ editor.ui.componentFactory.add("htmlEmbed", () => {
447
+ const buttonView = this._createButton(ButtonView);
448
+ buttonView.set({
449
+ tooltip: true,
450
+ label: t("Insert HTML")
451
+ });
452
+ return buttonView;
453
+ });
454
+ editor.ui.componentFactory.add("menuBar:htmlEmbed", () => {
455
+ const buttonView = this._createButton(MenuBarMenuListItemButtonView);
456
+ buttonView.set({ label: t("HTML snippet") });
457
+ return buttonView;
458
+ });
459
+ }
460
+ /**
461
+ * Creates a button for html embed command to use either in toolbar or in menu bar.
462
+ */
463
+ _createButton(ButtonClass) {
464
+ const editor = this.editor;
465
+ const command = editor.commands.get("htmlEmbed");
466
+ const view = new ButtonClass(editor.locale);
467
+ view.set({ icon: IconHtml });
468
+ view.bind("isEnabled").to(command, "isEnabled");
469
+ this.listenTo(view, "execute", () => {
470
+ editor.execute("htmlEmbed");
471
+ editor.editing.view.focus();
472
+ editor.editing.view.document.selection.getSelectedElement().getCustomProperty("rawHtmlApi").makeEditable();
473
+ });
474
+ return view;
475
+ }
476
+ };
512
477
 
513
478
  /**
514
- * The HTML embed feature.
515
- *
516
- * It allows inserting HTML snippets directly into the editor.
517
- *
518
- * For a detailed overview, check the {@glink features/html/html-embed HTML embed feature} documentation.
519
- */ class HtmlEmbed extends Plugin {
520
- /**
521
- * @inheritDoc
522
- */ static get requires() {
523
- return [
524
- HtmlEmbedEditing,
525
- HtmlEmbedUI,
526
- Widget
527
- ];
528
- }
529
- /**
530
- * @inheritDoc
531
- */ static get pluginName() {
532
- return 'HtmlEmbed';
533
- }
534
- /**
535
- * @inheritDoc
536
- */ static get isOfficialPlugin() {
537
- return true;
538
- }
539
- }
479
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
480
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
481
+ */
482
+ /**
483
+ * @module html-embed/htmlembed
484
+ */
485
+ /**
486
+ * The HTML embed feature.
487
+ *
488
+ * It allows inserting HTML snippets directly into the editor.
489
+ *
490
+ * For a detailed overview, check the {@glink features/html/html-embed HTML embed feature} documentation.
491
+ */
492
+ var HtmlEmbed = class extends Plugin {
493
+ /**
494
+ * @inheritDoc
495
+ */
496
+ static get requires() {
497
+ return [
498
+ HtmlEmbedEditing,
499
+ HtmlEmbedUI,
500
+ Widget
501
+ ];
502
+ }
503
+ /**
504
+ * @inheritDoc
505
+ */
506
+ static get pluginName() {
507
+ return "HtmlEmbed";
508
+ }
509
+ /**
510
+ * @inheritDoc
511
+ */
512
+ static get isOfficialPlugin() {
513
+ return true;
514
+ }
515
+ };
516
+
517
+ /**
518
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
519
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
520
+ */
540
521
 
541
522
  export { HtmlEmbed, HtmlEmbedCommand, HtmlEmbedEditing, HtmlEmbedUI };
542
- //# sourceMappingURL=index.js.map
523
+ //# sourceMappingURL=index.js.map