@ckeditor/ckeditor5-html-embed 30.0.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/LICENSE.md +17 -0
- package/README.md +20 -0
- package/build/html-embed.js +5 -0
- package/build/translations/cs.js +1 -0
- package/build/translations/de-ch.js +1 -0
- package/build/translations/de.js +1 -0
- package/build/translations/en-au.js +1 -0
- package/build/translations/es.js +1 -0
- package/build/translations/fr.js +1 -0
- package/build/translations/gl.js +1 -0
- package/build/translations/hr.js +1 -0
- package/build/translations/hu.js +1 -0
- package/build/translations/id.js +1 -0
- package/build/translations/it.js +1 -0
- package/build/translations/nl.js +1 -0
- package/build/translations/pl.js +1 -0
- package/build/translations/pt-br.js +1 -0
- package/build/translations/ro.js +1 -0
- package/build/translations/ru.js +1 -0
- package/build/translations/sk.js +1 -0
- package/build/translations/sl.js +1 -0
- package/build/translations/sr-latn.js +1 -0
- package/build/translations/sr.js +1 -0
- package/build/translations/uk.js +1 -0
- package/build/translations/vi.js +1 -0
- package/build/translations/zh-cn.js +1 -0
- package/ckeditor5-metadata.json +30 -0
- package/lang/contexts.json +9 -0
- package/lang/translations/cs.po +45 -0
- package/lang/translations/de-ch.po +45 -0
- package/lang/translations/de.po +45 -0
- package/lang/translations/en-au.po +45 -0
- package/lang/translations/en.po +45 -0
- package/lang/translations/es.po +45 -0
- package/lang/translations/fr.po +45 -0
- package/lang/translations/gl.po +45 -0
- package/lang/translations/hr.po +45 -0
- package/lang/translations/hu.po +45 -0
- package/lang/translations/id.po +45 -0
- package/lang/translations/it.po +45 -0
- package/lang/translations/nl.po +45 -0
- package/lang/translations/pl.po +45 -0
- package/lang/translations/pt-br.po +45 -0
- package/lang/translations/ro.po +45 -0
- package/lang/translations/ru.po +45 -0
- package/lang/translations/sk.po +45 -0
- package/lang/translations/sl.po +45 -0
- package/lang/translations/sr-latn.po +45 -0
- package/lang/translations/sr.po +45 -0
- package/lang/translations/uk.po +45 -0
- package/lang/translations/vi.po +45 -0
- package/lang/translations/zh-cn.po +45 -0
- package/package.json +58 -0
- package/src/htmlembed.js +124 -0
- package/src/htmlembedediting.js +400 -0
- package/src/htmlembedui.js +61 -0
- package/src/index.js +12 -0
- package/src/inserthtmlembedcommand.js +79 -0
- package/src/updatehtmlembedcommand.js +64 -0
- package/theme/htmlembed.css +68 -0
- package/theme/icons/html.svg +1 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# !!! IMPORTANT !!!
|
|
4
|
+
#
|
|
5
|
+
# Before you edit this file, please keep in mind that contributing to the project
|
|
6
|
+
# translations is possible ONLY via the Transifex online service.
|
|
7
|
+
#
|
|
8
|
+
# To submit your translations, visit https://www.transifex.com/ckeditor/ckeditor5.
|
|
9
|
+
#
|
|
10
|
+
# To learn more, check out the official contributor's guide:
|
|
11
|
+
# https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html
|
|
12
|
+
#
|
|
13
|
+
msgid ""
|
|
14
|
+
msgstr ""
|
|
15
|
+
"Language-Team: Vietnamese (https://www.transifex.com/ckeditor/teams/11143/vi/)\n"
|
|
16
|
+
"Language: vi\n"
|
|
17
|
+
"Plural-Forms: nplurals=1; plural=0;\n"
|
|
18
|
+
|
|
19
|
+
msgctxt "Toolbar button tooltip for the HTML embed feature."
|
|
20
|
+
msgid "Insert HTML"
|
|
21
|
+
msgstr "Chèn HTML"
|
|
22
|
+
|
|
23
|
+
msgctxt "The HTML snippet."
|
|
24
|
+
msgid "HTML snippet"
|
|
25
|
+
msgstr "Mẫu HTML"
|
|
26
|
+
|
|
27
|
+
msgctxt "A placeholder that will be displayed in the raw HTML textarea field."
|
|
28
|
+
msgid "Paste raw HTML here..."
|
|
29
|
+
msgstr "Dán mã HTML nguyên bản tại đây..."
|
|
30
|
+
|
|
31
|
+
msgctxt "A label of a button that switches the HTML embed to the source editing mode."
|
|
32
|
+
msgid "Edit source"
|
|
33
|
+
msgstr "Sửa nguồn"
|
|
34
|
+
|
|
35
|
+
msgctxt "A label of a button that saves the HTML embed content and navigates back to the preview."
|
|
36
|
+
msgid "Save changes"
|
|
37
|
+
msgstr "Lưu thay đổi"
|
|
38
|
+
|
|
39
|
+
msgctxt "An information displayed in the HTML embed preview if the content is not previewable."
|
|
40
|
+
msgid "No preview available"
|
|
41
|
+
msgstr ""
|
|
42
|
+
|
|
43
|
+
msgctxt "An information displayed in the HTML embed preview if the HTML snippet has no content."
|
|
44
|
+
msgid "Empty snippet content"
|
|
45
|
+
msgstr ""
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# !!! IMPORTANT !!!
|
|
4
|
+
#
|
|
5
|
+
# Before you edit this file, please keep in mind that contributing to the project
|
|
6
|
+
# translations is possible ONLY via the Transifex online service.
|
|
7
|
+
#
|
|
8
|
+
# To submit your translations, visit https://www.transifex.com/ckeditor/ckeditor5.
|
|
9
|
+
#
|
|
10
|
+
# To learn more, check out the official contributor's guide:
|
|
11
|
+
# https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html
|
|
12
|
+
#
|
|
13
|
+
msgid ""
|
|
14
|
+
msgstr ""
|
|
15
|
+
"Language-Team: Chinese (China) (https://www.transifex.com/ckeditor/teams/11143/zh_CN/)\n"
|
|
16
|
+
"Language: zh_CN\n"
|
|
17
|
+
"Plural-Forms: nplurals=1; plural=0;\n"
|
|
18
|
+
|
|
19
|
+
msgctxt "Toolbar button tooltip for the HTML embed feature."
|
|
20
|
+
msgid "Insert HTML"
|
|
21
|
+
msgstr "插入 HTML"
|
|
22
|
+
|
|
23
|
+
msgctxt "The HTML snippet."
|
|
24
|
+
msgid "HTML snippet"
|
|
25
|
+
msgstr "HTML 代码片段"
|
|
26
|
+
|
|
27
|
+
msgctxt "A placeholder that will be displayed in the raw HTML textarea field."
|
|
28
|
+
msgid "Paste raw HTML here..."
|
|
29
|
+
msgstr "在这里粘贴 HTML 源代码"
|
|
30
|
+
|
|
31
|
+
msgctxt "A label of a button that switches the HTML embed to the source editing mode."
|
|
32
|
+
msgid "Edit source"
|
|
33
|
+
msgstr "编辑源代码"
|
|
34
|
+
|
|
35
|
+
msgctxt "A label of a button that saves the HTML embed content and navigates back to the preview."
|
|
36
|
+
msgid "Save changes"
|
|
37
|
+
msgstr "保存更改"
|
|
38
|
+
|
|
39
|
+
msgctxt "An information displayed in the HTML embed preview if the content is not previewable."
|
|
40
|
+
msgid "No preview available"
|
|
41
|
+
msgstr "预览不可用"
|
|
42
|
+
|
|
43
|
+
msgctxt "An information displayed in the HTML embed preview if the HTML snippet has no content."
|
|
44
|
+
msgid "Empty snippet content"
|
|
45
|
+
msgstr "空片段内容"
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ckeditor/ckeditor5-html-embed",
|
|
3
|
+
"version": "30.0.0",
|
|
4
|
+
"description": "HTML embed feature for CKEditor 5.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ckeditor",
|
|
7
|
+
"ckeditor5",
|
|
8
|
+
"ckeditor 5",
|
|
9
|
+
"ckeditor5-feature",
|
|
10
|
+
"ckeditor5-plugin",
|
|
11
|
+
"ckeditor5-dll"
|
|
12
|
+
],
|
|
13
|
+
"main": "src/index.js",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"ckeditor5": "^30.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@ckeditor/ckeditor5-basic-styles": "^30.0.0",
|
|
19
|
+
"@ckeditor/ckeditor5-core": "^30.0.0",
|
|
20
|
+
"@ckeditor/ckeditor5-dev-utils": "^25.4.0",
|
|
21
|
+
"@ckeditor/ckeditor5-clipboard": "^30.0.0",
|
|
22
|
+
"@ckeditor/ckeditor5-editor-classic": "^30.0.0",
|
|
23
|
+
"@ckeditor/ckeditor5-engine": "^30.0.0",
|
|
24
|
+
"@ckeditor/ckeditor5-media-embed": "^30.0.0",
|
|
25
|
+
"@ckeditor/ckeditor5-paragraph": "^30.0.0",
|
|
26
|
+
"@ckeditor/ckeditor5-table": "^30.0.0",
|
|
27
|
+
"@ckeditor/ckeditor5-ui": "^30.0.0",
|
|
28
|
+
"@ckeditor/ckeditor5-theme-lark": "^30.0.0",
|
|
29
|
+
"@ckeditor/ckeditor5-widget": "^30.0.0",
|
|
30
|
+
"lodash-es": "^4.17.15",
|
|
31
|
+
"sanitize-html": "^2.1.0",
|
|
32
|
+
"webpack": "^4.43.0",
|
|
33
|
+
"webpack-cli": "^3.3.11"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=12.0.0",
|
|
37
|
+
"npm": ">=5.7.1"
|
|
38
|
+
},
|
|
39
|
+
"author": "CKSource (http://cksource.com/)",
|
|
40
|
+
"license": "GPL-2.0-or-later",
|
|
41
|
+
"homepage": "https://ckeditor.com",
|
|
42
|
+
"bugs": "https://github.com/ckeditor/ckeditor5/issues",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/ckeditor/ckeditor5.git",
|
|
46
|
+
"directory": "packages/ckeditor5-html-embed"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"lang",
|
|
50
|
+
"src",
|
|
51
|
+
"theme",
|
|
52
|
+
"build",
|
|
53
|
+
"ckeditor5-metadata.json"
|
|
54
|
+
],
|
|
55
|
+
"scripts": {
|
|
56
|
+
"dll:build": "webpack"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/htmlembed.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module html-embed/htmlembed
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Plugin } from 'ckeditor5/src/core';
|
|
11
|
+
import { Widget } from 'ckeditor5/src/widget';
|
|
12
|
+
|
|
13
|
+
import HtmlEmbedEditing from './htmlembedediting';
|
|
14
|
+
import HtmlEmbedUI from './htmlembedui';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The HTML embed feature.
|
|
18
|
+
*
|
|
19
|
+
* It allows inserting HTML snippets directly into the editor.
|
|
20
|
+
*
|
|
21
|
+
* For a detailed overview, check the {@glink features/html-embed HTML embed feature} documentation.
|
|
22
|
+
*
|
|
23
|
+
* @extends module:core/plugin~Plugin
|
|
24
|
+
*/
|
|
25
|
+
export default class HtmlEmbed extends Plugin {
|
|
26
|
+
/**
|
|
27
|
+
* @inheritDoc
|
|
28
|
+
*/
|
|
29
|
+
static get requires() {
|
|
30
|
+
return [ HtmlEmbedEditing, HtmlEmbedUI, Widget ];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @inheritDoc
|
|
35
|
+
*/
|
|
36
|
+
static get pluginName() {
|
|
37
|
+
return 'HtmlEmbed';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The configuration of the HTML embed feature.
|
|
43
|
+
*
|
|
44
|
+
* ClassicEditor
|
|
45
|
+
* .create( editorElement, {
|
|
46
|
+
* htmlEmbed: ... // HTML embed feature options.
|
|
47
|
+
* } )
|
|
48
|
+
* .then( ... )
|
|
49
|
+
* .catch( ... );
|
|
50
|
+
*
|
|
51
|
+
* See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
|
|
52
|
+
*
|
|
53
|
+
* @interface HtmlEmbedConfig
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether the feature should render previews of the embedded HTML.
|
|
58
|
+
*
|
|
59
|
+
* When set to `true`, the feature will produce a preview of the inserted HTML based on a sanitized
|
|
60
|
+
* version of the HTML provided by the user.
|
|
61
|
+
*
|
|
62
|
+
* The function responsible for sanitizing the HTML needs to be specified in
|
|
63
|
+
* {@link module:html-embed/htmlembed~HtmlEmbedConfig#sanitizeHtml `config.htmlEmbed.sanitizeHtml()`}.
|
|
64
|
+
*
|
|
65
|
+
* Read more about the security aspect of this feature in the {@glink features/html-embed#security "Security"} section of
|
|
66
|
+
* the {@glink features/html-embed HTML embed} feature guide.
|
|
67
|
+
*
|
|
68
|
+
* @member {Boolean} [module:html-embed/htmlembed~HtmlEmbedConfig#showPreviews=false]
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Callback used to sanitize the HTML provided by the user when generating previews of it in the editor.
|
|
73
|
+
*
|
|
74
|
+
* We strongly recommend overwriting the default function to avoid XSS vulnerabilities.
|
|
75
|
+
*
|
|
76
|
+
* Read more about the security aspect of this feature in the {@glink features/html-embed#security "Security"} section of
|
|
77
|
+
* the {@glink features/html-embed HTML embed} feature guide.
|
|
78
|
+
*
|
|
79
|
+
* The function receives the input HTML (as a string), and should return an object
|
|
80
|
+
* that matches the {@link module:html-embed/htmlembed~HtmlEmbedSanitizeOutput} interface.
|
|
81
|
+
*
|
|
82
|
+
* ClassicEditor
|
|
83
|
+
* .create( editorElement, {
|
|
84
|
+
* htmlEmbed: {
|
|
85
|
+
* showPreviews: true,
|
|
86
|
+
* sanitizeHtml( inputHtml ) {
|
|
87
|
+
* // Strip unsafe elements and attributes, e.g.:
|
|
88
|
+
* // the `<script>` elements and `on*` attributes.
|
|
89
|
+
* const outputHtml = sanitize( inputHtml );
|
|
90
|
+
*
|
|
91
|
+
* return {
|
|
92
|
+
* html: outputHtml,
|
|
93
|
+
* // true or false depending on whether the sanitizer stripped anything.
|
|
94
|
+
* hasChanged: ...
|
|
95
|
+
* };
|
|
96
|
+
* },
|
|
97
|
+
* }
|
|
98
|
+
* } )
|
|
99
|
+
* .then( ... )
|
|
100
|
+
* .catch( ... );
|
|
101
|
+
*
|
|
102
|
+
* **Note:** The function is used only when the feature
|
|
103
|
+
* {@link module:html-embed/htmlembed~HtmlEmbedConfig#showPreviews is configured to render previews}.
|
|
104
|
+
*
|
|
105
|
+
* @member {Function} [module:html-embed/htmlembed~HtmlEmbedConfig#sanitizeHtml]
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* An object returned by the {@link module:html-embed/htmlembed~HtmlEmbedConfig#sanitizeHtml} function.
|
|
110
|
+
*
|
|
111
|
+
* @interface HtmlEmbedSanitizeOutput
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* An output (safe) HTML that will be inserted into the {@glink framework/guides/architecture/editing-engine editing view}.
|
|
116
|
+
*
|
|
117
|
+
* @member {String} module:html-embed/htmlembed~HtmlEmbedSanitizeOutput#html
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A flag that indicates whether the output HTML is different than the input value.
|
|
122
|
+
*
|
|
123
|
+
* @member {Boolean} [module:html-embed/htmlembed~HtmlEmbedSanitizeOutput#hasChanged]
|
|
124
|
+
*/
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module html-embed/htmlembedediting
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Plugin, icons } from 'ckeditor5/src/core';
|
|
11
|
+
import { ButtonView } from 'ckeditor5/src/ui';
|
|
12
|
+
import { toWidget } from 'ckeditor5/src/widget';
|
|
13
|
+
import { logWarning, createElement } from 'ckeditor5/src/utils';
|
|
14
|
+
|
|
15
|
+
import InsertHtmlEmbedCommand from './inserthtmlembedcommand';
|
|
16
|
+
import UpdateHtmlEmbedCommand from './updatehtmlembedcommand';
|
|
17
|
+
|
|
18
|
+
import '../theme/htmlembed.css';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The HTML embed editing feature.
|
|
22
|
+
*
|
|
23
|
+
* @extends module:core/plugin~Plugin
|
|
24
|
+
*/
|
|
25
|
+
export default class HtmlEmbedEditing extends Plugin {
|
|
26
|
+
/**
|
|
27
|
+
* @inheritDoc
|
|
28
|
+
*/
|
|
29
|
+
static get pluginName() {
|
|
30
|
+
return 'HtmlEmbedEditing';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @inheritDoc
|
|
35
|
+
*/
|
|
36
|
+
constructor( editor ) {
|
|
37
|
+
super( editor );
|
|
38
|
+
|
|
39
|
+
editor.config.define( 'htmlEmbed', {
|
|
40
|
+
showPreviews: false,
|
|
41
|
+
sanitizeHtml: rawHtml => {
|
|
42
|
+
/**
|
|
43
|
+
* When using the HTML embed feature with the `htmlEmbed.showPreviews=true` option, it is strongly recommended to
|
|
44
|
+
* define a sanitize function that will clean up the input HTML in order to avoid XSS vulnerability.
|
|
45
|
+
*
|
|
46
|
+
* For a detailed overview, check the {@glink features/html-embed HTML embed feature} documentation.
|
|
47
|
+
*
|
|
48
|
+
* @error html-embed-provide-sanitize-function
|
|
49
|
+
*/
|
|
50
|
+
logWarning( 'html-embed-provide-sanitize-function' );
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
html: rawHtml,
|
|
54
|
+
hasChanged: false
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
} );
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @inheritDoc
|
|
62
|
+
*/
|
|
63
|
+
init() {
|
|
64
|
+
const editor = this.editor;
|
|
65
|
+
const schema = editor.model.schema;
|
|
66
|
+
|
|
67
|
+
schema.register( 'rawHtml', {
|
|
68
|
+
isObject: true,
|
|
69
|
+
allowWhere: '$block',
|
|
70
|
+
allowAttributes: [ 'value' ]
|
|
71
|
+
} );
|
|
72
|
+
|
|
73
|
+
editor.commands.add( 'updateHtmlEmbed', new UpdateHtmlEmbedCommand( editor ) );
|
|
74
|
+
editor.commands.add( 'insertHtmlEmbed', new InsertHtmlEmbedCommand( editor ) );
|
|
75
|
+
|
|
76
|
+
this._setupConversion();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Prepares converters for the feature.
|
|
81
|
+
*
|
|
82
|
+
* @private
|
|
83
|
+
*/
|
|
84
|
+
_setupConversion() {
|
|
85
|
+
const editor = this.editor;
|
|
86
|
+
const t = editor.t;
|
|
87
|
+
const view = editor.editing.view;
|
|
88
|
+
|
|
89
|
+
const htmlEmbedConfig = editor.config.get( 'htmlEmbed' );
|
|
90
|
+
|
|
91
|
+
// Register div.raw-html-embed as a raw content element so all of it's content will be provided
|
|
92
|
+
// as a view element's custom property while data upcasting.
|
|
93
|
+
editor.data.registerRawContentMatcher( {
|
|
94
|
+
name: 'div',
|
|
95
|
+
classes: 'raw-html-embed'
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
editor.conversion.for( 'upcast' ).elementToElement( {
|
|
99
|
+
view: {
|
|
100
|
+
name: 'div',
|
|
101
|
+
classes: 'raw-html-embed'
|
|
102
|
+
},
|
|
103
|
+
model: ( viewElement, { writer } ) => {
|
|
104
|
+
// The div.raw-html-embed is registered as a raw content element,
|
|
105
|
+
// so all it's content is available in a custom property.
|
|
106
|
+
return writer.createElement( 'rawHtml', {
|
|
107
|
+
value: viewElement.getCustomProperty( '$rawContent' )
|
|
108
|
+
} );
|
|
109
|
+
}
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
editor.conversion.for( 'dataDowncast' ).elementToElement( {
|
|
113
|
+
model: 'rawHtml',
|
|
114
|
+
view: ( modelElement, { writer } ) => {
|
|
115
|
+
return writer.createRawElement( 'div', { class: 'raw-html-embed' }, function( domElement ) {
|
|
116
|
+
domElement.innerHTML = modelElement.getAttribute( 'value' ) || '';
|
|
117
|
+
} );
|
|
118
|
+
}
|
|
119
|
+
} );
|
|
120
|
+
|
|
121
|
+
editor.conversion.for( 'editingDowncast' ).elementToElement( {
|
|
122
|
+
triggerBy: {
|
|
123
|
+
attributes: [ 'value' ]
|
|
124
|
+
},
|
|
125
|
+
model: 'rawHtml',
|
|
126
|
+
view: ( modelElement, { writer } ) => {
|
|
127
|
+
let domContentWrapper, state, props;
|
|
128
|
+
|
|
129
|
+
const viewContainer = writer.createContainerElement( 'div', {
|
|
130
|
+
class: 'raw-html-embed',
|
|
131
|
+
'data-html-embed-label': t( 'HTML snippet' ),
|
|
132
|
+
dir: editor.locale.uiLanguageDirection
|
|
133
|
+
} );
|
|
134
|
+
// Widget cannot be a raw element because the widget system would not be able
|
|
135
|
+
// to add its UI to it. Thus, we need this wrapper.
|
|
136
|
+
const viewContentWrapper = writer.createRawElement( 'div', {
|
|
137
|
+
class: 'raw-html-embed__content-wrapper'
|
|
138
|
+
}, function( domElement ) {
|
|
139
|
+
domContentWrapper = domElement;
|
|
140
|
+
|
|
141
|
+
renderContent( { domElement, editor, state, props } );
|
|
142
|
+
|
|
143
|
+
// Since there is a `data-cke-ignore-events` attribute set on the wrapper element in the editable mode,
|
|
144
|
+
// the explicit `mousedown` handler on the `capture` phase is needed to move the selection onto the whole
|
|
145
|
+
// HTML embed widget.
|
|
146
|
+
domContentWrapper.addEventListener( 'mousedown', () => {
|
|
147
|
+
if ( state.isEditable ) {
|
|
148
|
+
const model = editor.model;
|
|
149
|
+
const selectedElement = model.document.selection.getSelectedElement();
|
|
150
|
+
|
|
151
|
+
// Move the selection onto the whole HTML embed widget if it's currently not selected.
|
|
152
|
+
if ( selectedElement !== modelElement ) {
|
|
153
|
+
model.change( writer => writer.setSelection( modelElement, 'on' ) );
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}, true );
|
|
157
|
+
} );
|
|
158
|
+
|
|
159
|
+
// API exposed on each raw HTML embed widget so other features can control a particular widget.
|
|
160
|
+
const rawHtmlApi = {
|
|
161
|
+
makeEditable() {
|
|
162
|
+
state = Object.assign( {}, state, {
|
|
163
|
+
isEditable: true
|
|
164
|
+
} );
|
|
165
|
+
|
|
166
|
+
renderContent( { domElement: domContentWrapper, editor, state, props } );
|
|
167
|
+
|
|
168
|
+
view.change( writer => {
|
|
169
|
+
writer.setAttribute( 'data-cke-ignore-events', 'true', viewContentWrapper );
|
|
170
|
+
} );
|
|
171
|
+
|
|
172
|
+
// This could be potentially pulled to a separate method called focusTextarea().
|
|
173
|
+
domContentWrapper.querySelector( 'textarea' ).focus();
|
|
174
|
+
},
|
|
175
|
+
save( newValue ) {
|
|
176
|
+
// If the value didn't change, we just cancel. If it changed,
|
|
177
|
+
// it's enough to update the model – the entire widget will be reconverted.
|
|
178
|
+
if ( newValue !== state.getRawHtmlValue() ) {
|
|
179
|
+
editor.execute( 'updateHtmlEmbed', newValue );
|
|
180
|
+
editor.editing.view.focus();
|
|
181
|
+
} else {
|
|
182
|
+
this.cancel();
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
cancel() {
|
|
186
|
+
state = Object.assign( {}, state, {
|
|
187
|
+
isEditable: false
|
|
188
|
+
} );
|
|
189
|
+
|
|
190
|
+
renderContent( { domElement: domContentWrapper, editor, state, props } );
|
|
191
|
+
editor.editing.view.focus();
|
|
192
|
+
|
|
193
|
+
view.change( writer => {
|
|
194
|
+
writer.removeAttribute( 'data-cke-ignore-events', viewContentWrapper );
|
|
195
|
+
} );
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
state = {
|
|
200
|
+
showPreviews: htmlEmbedConfig.showPreviews,
|
|
201
|
+
isEditable: false,
|
|
202
|
+
getRawHtmlValue: () => modelElement.getAttribute( 'value' ) || ''
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
props = {
|
|
206
|
+
sanitizeHtml: htmlEmbedConfig.sanitizeHtml,
|
|
207
|
+
textareaPlaceholder: t( 'Paste raw HTML here...' ),
|
|
208
|
+
|
|
209
|
+
onEditClick() {
|
|
210
|
+
rawHtmlApi.makeEditable();
|
|
211
|
+
},
|
|
212
|
+
onSaveClick( newValue ) {
|
|
213
|
+
rawHtmlApi.save( newValue );
|
|
214
|
+
},
|
|
215
|
+
onCancelClick() {
|
|
216
|
+
rawHtmlApi.cancel();
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
writer.insert( writer.createPositionAt( viewContainer, 0 ), viewContentWrapper );
|
|
221
|
+
|
|
222
|
+
writer.setCustomProperty( 'rawHtmlApi', rawHtmlApi, viewContainer );
|
|
223
|
+
writer.setCustomProperty( 'rawHtml', true, viewContainer );
|
|
224
|
+
|
|
225
|
+
return toWidget( viewContainer, writer, {
|
|
226
|
+
widgetLabel: t( 'HTML snippet' ),
|
|
227
|
+
hasSelectionHandle: true
|
|
228
|
+
} );
|
|
229
|
+
}
|
|
230
|
+
} );
|
|
231
|
+
|
|
232
|
+
function renderContent( { domElement, editor, state, props } ) {
|
|
233
|
+
// Remove all children;
|
|
234
|
+
domElement.textContent = '';
|
|
235
|
+
|
|
236
|
+
const domDocument = domElement.ownerDocument;
|
|
237
|
+
let domTextarea;
|
|
238
|
+
|
|
239
|
+
if ( state.isEditable ) {
|
|
240
|
+
const textareaProps = {
|
|
241
|
+
isDisabled: false,
|
|
242
|
+
placeholder: props.textareaPlaceholder
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
domTextarea = createDomTextarea( { domDocument, state, props: textareaProps } );
|
|
246
|
+
|
|
247
|
+
domElement.append( domTextarea );
|
|
248
|
+
} else if ( state.showPreviews ) {
|
|
249
|
+
const previewContainerProps = {
|
|
250
|
+
sanitizeHtml: props.sanitizeHtml
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
domElement.append( createPreviewContainer( { domDocument, state, props: previewContainerProps, editor } ) );
|
|
254
|
+
} else {
|
|
255
|
+
const textareaProps = {
|
|
256
|
+
isDisabled: true,
|
|
257
|
+
placeholder: props.textareaPlaceholder
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
domElement.append( createDomTextarea( { domDocument, state, props: textareaProps } ) );
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const buttonsWrapperProps = {
|
|
264
|
+
onEditClick: props.onEditClick,
|
|
265
|
+
onSaveClick: () => {
|
|
266
|
+
props.onSaveClick( domTextarea.value );
|
|
267
|
+
},
|
|
268
|
+
onCancelClick: props.onCancelClick
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
domElement.prepend( createDomButtonsWrapper( { editor, domDocument, state, props: buttonsWrapperProps } ) );
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function createDomButtonsWrapper( { editor, domDocument, state, props } ) {
|
|
275
|
+
const domButtonsWrapper = createElement( domDocument, 'div', {
|
|
276
|
+
class: 'raw-html-embed__buttons-wrapper'
|
|
277
|
+
} );
|
|
278
|
+
// TODO these should be cached and we should only clone here these cached nodes!
|
|
279
|
+
const domEditButton = createDomButton( editor, 'edit' );
|
|
280
|
+
const domSaveButton = createDomButton( editor, 'save' );
|
|
281
|
+
const domCancelButton = createDomButton( editor, 'cancel' );
|
|
282
|
+
|
|
283
|
+
if ( state.isEditable ) {
|
|
284
|
+
const clonedDomSaveButton = domSaveButton.cloneNode( true );
|
|
285
|
+
const clonedDomCancelButton = domCancelButton.cloneNode( true );
|
|
286
|
+
|
|
287
|
+
clonedDomSaveButton.addEventListener( 'click', evt => {
|
|
288
|
+
evt.preventDefault();
|
|
289
|
+
props.onSaveClick( );
|
|
290
|
+
} );
|
|
291
|
+
|
|
292
|
+
clonedDomCancelButton.addEventListener( 'click', evt => {
|
|
293
|
+
evt.preventDefault();
|
|
294
|
+
props.onCancelClick( );
|
|
295
|
+
} );
|
|
296
|
+
|
|
297
|
+
domButtonsWrapper.appendChild( clonedDomSaveButton );
|
|
298
|
+
domButtonsWrapper.appendChild( clonedDomCancelButton );
|
|
299
|
+
} else {
|
|
300
|
+
const clonedDomEditButton = domEditButton.cloneNode( true );
|
|
301
|
+
|
|
302
|
+
clonedDomEditButton.addEventListener( 'click', evt => {
|
|
303
|
+
evt.preventDefault();
|
|
304
|
+
props.onEditClick();
|
|
305
|
+
} );
|
|
306
|
+
|
|
307
|
+
domButtonsWrapper.appendChild( clonedDomEditButton );
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return domButtonsWrapper;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function createDomTextarea( { domDocument, state, props } ) {
|
|
314
|
+
const domTextarea = createElement( domDocument, 'textarea', {
|
|
315
|
+
placeholder: props.placeholder,
|
|
316
|
+
class: 'ck ck-reset ck-input ck-input-text raw-html-embed__source'
|
|
317
|
+
} );
|
|
318
|
+
|
|
319
|
+
domTextarea.disabled = props.isDisabled;
|
|
320
|
+
domTextarea.value = state.getRawHtmlValue();
|
|
321
|
+
|
|
322
|
+
return domTextarea;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function createPreviewContainer( { domDocument, state, props, editor } ) {
|
|
326
|
+
const sanitizedOutput = props.sanitizeHtml( state.getRawHtmlValue() );
|
|
327
|
+
const placeholderText = state.getRawHtmlValue().length > 0 ?
|
|
328
|
+
t( 'No preview available' ) :
|
|
329
|
+
t( 'Empty snippet content' );
|
|
330
|
+
|
|
331
|
+
const domPreviewPlaceholder = createElement( domDocument, 'div', {
|
|
332
|
+
class: 'ck ck-reset_all raw-html-embed__preview-placeholder'
|
|
333
|
+
}, placeholderText );
|
|
334
|
+
|
|
335
|
+
const domPreviewContent = createElement( domDocument, 'div', {
|
|
336
|
+
class: 'raw-html-embed__preview-content',
|
|
337
|
+
dir: editor.locale.contentLanguageDirection
|
|
338
|
+
} );
|
|
339
|
+
|
|
340
|
+
// Creating a contextual document fragment allows executing scripts when inserting into the preview element.
|
|
341
|
+
// See: #8326.
|
|
342
|
+
const domRange = domDocument.createRange();
|
|
343
|
+
const domDocumentFragment = domRange.createContextualFragment( sanitizedOutput.html );
|
|
344
|
+
|
|
345
|
+
domPreviewContent.appendChild( domDocumentFragment );
|
|
346
|
+
|
|
347
|
+
const domPreviewContainer = createElement( domDocument, 'div', {
|
|
348
|
+
class: 'raw-html-embed__preview'
|
|
349
|
+
}, [
|
|
350
|
+
domPreviewPlaceholder, domPreviewContent
|
|
351
|
+
] );
|
|
352
|
+
|
|
353
|
+
return domPreviewContainer;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Returns a toggle mode button DOM element that can be cloned and used in conversion.
|
|
359
|
+
//
|
|
360
|
+
// @param {module:utils/locale~Locale} locale Editor locale.
|
|
361
|
+
// @param {'edit'|'save'|'cancel'} type Type of button to create.
|
|
362
|
+
// @returns {HTMLElement}
|
|
363
|
+
function createDomButton( editor, type ) {
|
|
364
|
+
const t = editor.locale.t;
|
|
365
|
+
const buttonView = new ButtonView( editor.locale );
|
|
366
|
+
const command = editor.commands.get( 'updateHtmlEmbed' );
|
|
367
|
+
|
|
368
|
+
buttonView.set( {
|
|
369
|
+
tooltipPosition: editor.locale.uiLanguageDirection === 'rtl' ? 'e' : 'w',
|
|
370
|
+
icon: icons.pencil,
|
|
371
|
+
tooltip: true
|
|
372
|
+
} );
|
|
373
|
+
|
|
374
|
+
buttonView.render();
|
|
375
|
+
|
|
376
|
+
if ( type === 'edit' ) {
|
|
377
|
+
buttonView.set( {
|
|
378
|
+
icon: icons.pencil,
|
|
379
|
+
label: t( 'Edit source' ),
|
|
380
|
+
class: 'raw-html-embed__edit-button'
|
|
381
|
+
} );
|
|
382
|
+
} else if ( type === 'save' ) {
|
|
383
|
+
buttonView.set( {
|
|
384
|
+
icon: icons.check,
|
|
385
|
+
label: t( 'Save changes' ),
|
|
386
|
+
class: 'raw-html-embed__save-button'
|
|
387
|
+
} );
|
|
388
|
+
buttonView.bind( 'isEnabled' ).to( command, 'isEnabled' );
|
|
389
|
+
} else {
|
|
390
|
+
buttonView.set( {
|
|
391
|
+
icon: icons.cancel,
|
|
392
|
+
label: t( 'Cancel' ),
|
|
393
|
+
class: 'raw-html-embed__cancel-button'
|
|
394
|
+
} );
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
buttonView.destroy();
|
|
398
|
+
|
|
399
|
+
return buttonView.element.cloneNode( true );
|
|
400
|
+
}
|