Procházet zdrojové kódy

Merge pull request #8301 from ckeditor/i/8291

Internal (html-embed): Editing UI for Raw HTML embedding feature. Closes #8291.

Tests (theme-lark): Added new icons to the manual test.
Kamil Piechaczek před 5 roky
rodič
revize
ea1a1221e8
25 změnil soubory, kde provedl 817 přidání a 582 odebrání
  1. 24 2
      packages/ckeditor5-html-embed/docs/_snippets/features/html-embed.html
  2. 49 13
      packages/ckeditor5-html-embed/docs/_snippets/features/html-embed.js
  3. 7 2
      packages/ckeditor5-html-embed/docs/features/html-embed.md
  4. 4 1
      packages/ckeditor5-html-embed/lang/contexts.json
  5. 60 3
      packages/ckeditor5-html-embed/src/htmlembed.js
  6. 217 109
      packages/ckeditor5-html-embed/src/htmlembedediting.js
  7. 5 9
      packages/ckeditor5-html-embed/src/htmlembedui.js
  8. 4 4
      packages/ckeditor5-html-embed/src/htmlembedinsertcommand.js
  9. 4 4
      packages/ckeditor5-html-embed/src/htmlembedupdatecommand.js
  10. 163 228
      packages/ckeditor5-html-embed/tests/htmlembedediting.js
  11. 6 9
      packages/ckeditor5-html-embed/tests/htmlembedui.js
  12. 2 2
      packages/ckeditor5-html-embed/tests/htmlembedinsertcommand.js
  13. 138 138
      packages/ckeditor5-html-embed/tests/manual/htmlembed.html
  14. 6 6
      packages/ckeditor5-html-embed/tests/manual/htmlembed.js
  15. 1 1
      packages/ckeditor5-html-embed/tests/manual/htmlembed.md
  16. 2 2
      packages/ckeditor5-html-embed/tests/htmlembedupdatecommand.js
  17. 19 0
      packages/ckeditor5-html-embed/theme/htmlembed.css
  18. 1 0
      packages/ckeditor5-html-embed/theme/icons/html.svg
  19. 0 1
      packages/ckeditor5-html-embed/theme/icons/htmlembed.svg
  20. 0 6
      packages/ckeditor5-html-embed/theme/icons/htmlembedmode.svg
  21. 1 0
      packages/ckeditor5-theme-lark/package.json
  22. 5 0
      packages/ckeditor5-theme-lark/tests/manual/iconset.js
  23. 60 36
      packages/ckeditor5-theme-lark/theme/ckeditor5-html-embed/htmlembed.css
  24. 31 4
      packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/tooltip/tooltip.css
  25. 8 2
      packages/ckeditor5-ui/src/tooltip/tooltipview.js

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 24 - 2
packages/ckeditor5-html-embed/docs/_snippets/features/html-embed.html


+ 49 - 13
packages/ckeditor5-html-embed/docs/_snippets/features/html-embed.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* globals window, document, console */
+/* globals window, document */
 
 import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
 import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
@@ -11,8 +11,50 @@ import HtmlEmbed from '@ckeditor/ckeditor5-html-embed/src/htmlembed';
 
 ClassicEditor.builtinPlugins.push( HtmlEmbed );
 
-ClassicEditor
-	.create( document.querySelector( '#snippet-html-embed' ), {
+const previewsModeButton = document.getElementById( 'raw-html-previews-enabled' );
+const noPreviewsModeButton = document.getElementById( 'raw-html-previews-disabled' );
+
+previewsModeButton.addEventListener( 'change', handleModeChange );
+noPreviewsModeButton.addEventListener( 'change', handleModeChange );
+
+startMode( document.querySelector( 'input[name="mode"]:checked' ).value );
+
+async function handleModeChange( evt ) {
+	await startMode( evt.target.value );
+}
+
+async function startMode( selectedMode ) {
+	if ( selectedMode === 'enabled' ) {
+		await startEnabledPreviewsMode();
+	} else {
+		await startDisabledPreviewsMode();
+	}
+}
+
+async function startEnabledPreviewsMode() {
+	await reloadEditor( {
+		htmlEmbed: {
+			showPreviews: true,
+			sanitizeHtml( rawHtml ) {
+				return {
+					html: rawHtml,
+					hasChanged: false
+				};
+			}
+		}
+	} );
+}
+
+async function startDisabledPreviewsMode() {
+	await reloadEditor();
+}
+
+async function reloadEditor( config = {} ) {
+	if ( window.editor ) {
+		await window.editor.destroy();
+	}
+
+	config = Object.assign( config, {
 		toolbar: {
 			items: [
 				'heading',
@@ -25,15 +67,12 @@ ClassicEditor
 				'outdent',
 				'indent',
 				'|',
-				'alignment',
-				'|',
-				'horizontalLine',
 				'blockQuote',
 				'link',
 				'imageUpload',
 				'mediaEmbed',
-				'htmlEmbed',
 				'insertTable',
+				'htmlEmbed',
 				'|',
 				'undo',
 				'redo'
@@ -58,10 +97,7 @@ ClassicEditor
 			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
 		},
 		cloudServices: CS_CONFIG
-	} )
-	.then( editor => {
-		window.editor = editor;
-	} )
-	.catch( err => {
-		console.error( err.stack );
 	} );
+
+	window.editor = await ClassicEditor.create( document.querySelector( '#snippet-html-embed' ), config );
+}

+ 7 - 2
packages/ckeditor5-html-embed/docs/features/html-embed.md

@@ -39,12 +39,17 @@ ClassicEditor
 	Read more about {@link builds/guides/integration/installing-plugins installing plugins}.
 </info-box>
 
+### Security
+
+TODO
+Note: it's mentioned in the config.htmlEmbed.* options so if we'll decide to rename this section we'll need to change it there.
+
 ## Common API
 
 The {@link module:html-embed/htmlembed~HtmlEmbed} plugin registers:
 * the UI button component (`'htmlEmbed'`),
-* the `'htmlEmbedUpdate'` command implemented by {@link module:html-embed/htmlembedupdatecommand~HtmlEmbedUpdateCommand}.
-* the `'htmlEmbedInsert'` command implemented by {@link module:html-embed/htmlembedinsertcommand~HtmlEmbedInsertCommand}.
+* the `'updateHtmlEmbed'` command implemented by {@link module:html-embed/updatehtmlembedcommand~UpdateHtmlEmbedCommand}.
+* the `'insertHtmlEmbed'` command implemented by {@link module:html-embed/inserthtmlembedcommand~InsertHtmlEmbedCommand}.
 
 The command can be executed using the {@link module:core/editor/editor~Editor#execute `editor.execute()`} method:
 

+ 4 - 1
packages/ckeditor5-html-embed/lang/contexts.json

@@ -1,5 +1,8 @@
 {
   "Insert HTML": "Toolbar button tooltip for the HTML embed feature.",
   "HTML snippet": "The HTML snippet.",
-  "Paste the raw code here.": "A placeholder that will be displayed in the raw HTML textarea field."
+  "Paste raw HTML here...": "A placeholder that will be displayed in the raw HTML textarea field.",
+  "Edit source": "A label of a button that switches the HTML embed to the source editing mode.",
+  "Save changes": "A label of a button that saves the HTML embed content and navigates back to the preview.",
+  "Cancel": "A label of a button that discards changes in the HTML embed content and navigates back to the preview."
 }

+ 60 - 3
packages/ckeditor5-html-embed/src/htmlembed.js

@@ -52,13 +52,70 @@ export default class HtmlEmbed extends Plugin {
  */
 
 /**
- * TODO
+ * Whether the feature should render previews of the the embedded HTML.
  *
- * @member {Boolean} [module:html-embed/htmlembed~HtmlEmbedConfig#previewsInData=false]
+ * When set to `true`, the feature will produce a preview of the inserted HTML based on a sanitized
+ * version of the HTML provided by the user.
+ *
+ * The function responsible for sanitizing the HTML needs to be specified in
+ * {@link module:html-embed/htmlembed~HtmlEmbedConfig#sanitizeHtml `config.htmlEmbed.sanitizeHtml()`}.
+ *
+ * Read more about the security aspect of this feature in the {@glink features/html-embed#security "Security"} section of
+ * the {@glink features/html-embed HTML embed} feature guide.
+ *
+ * @member {Boolean} [module:html-embed/htmlembed~HtmlEmbedConfig#showPreviews=false]
  */
 
 /**
- * TODO
+ * Callback used to sanitize HTML provided by the user when generating previews of it in the editor.
+ *
+ * We strongly recommend to overwrite the default function to avoid XSS vulnerabilities.
+ *
+ * Read more about the security aspect of this feature in the {@glink features/html-embed#security "Security"} section of
+ * the {@glink features/html-embed HTML embed} feature guide.
+ *
+ * The function receives the input HTML (as a string), and should return an object
+ * that matches the {@link module:html-embed/htmlembed~HtmlEmbedSanitizeOutput} interface.
+ *
+ *  	ClassicEditor
+ * 			.create( editorElement, {
+ * 				htmlEmbed: {
+ * 					showPreviews: true,
+ * 					sanitizeHtml( inputHtml ) {
+ * 						// Strip unsafe elements and attributes, e.g.:
+ * 						// the `<script>` elements and `on*` attributes.
+ * 						const outputHtml = sanitize( inputHtml );
+ *
+ * 						return {
+ * 							html: outputHtml,
+ * 							hasChanged: ...
+ * 						}
+ * 					},
+ * 				}
+ * 			} )
+ * 			.then( ... )
+ * 			.catch( ... );
+ *
+ * **Note:** The function is used only when the feature
+ * {@link module:html-embed/htmlembed~HtmlEmbedConfig#showPreviews is configured to render previews}.
  *
  * @member {Function} [module:html-embed/htmlembed~HtmlEmbedConfig#sanitizeHtml]
  */
+
+/**
+ * An object returned by the {@link module:html-embed/htmlembed~HtmlEmbedConfig#sanitizeHtml} function.
+ *
+ * @interface HtmlEmbedSanitizeOutput
+ */
+
+/**
+ * An output (safe) HTML that will be inserted into the {@glink framework/guides/architecture/editing-engine editing view}.
+ *
+ * @member {String} module:html-embed/htmlembed~HtmlEmbedSanitizeOutput#html
+ */
+
+/**
+ * A flag that indicates whether the output HTML is different than the input value.
+ *
+ * @member {Boolean} [module:html-embed/htmlembed~HtmlEmbedSanitizeOutput#hasChanged]
+ */

+ 217 - 109
packages/ckeditor5-html-embed/src/htmlembedediting.js

@@ -12,14 +12,17 @@ import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/html
 import UpcastWriter from '@ckeditor/ckeditor5-engine/src/view/upcastwriter';
 import { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
-import HtmlEmbedInsertCommand from './htmlembedinsertcommand';
-import HtmlEmbedUpdateCommand from './htmlembedupdatecommand';
+import InsertHtmlEmbedCommand from './inserthtmlembedcommand';
+import UpdateHtmlEmbedCommand from './updatehtmlembedcommand';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 
-import htmlEmbedModeIcon from '../theme/icons/htmlembedmode.svg';
+import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
 
-import '../theme/htmlembed.css';
+import pencilIcon from '@ckeditor/ckeditor5-core/theme/icons/pencil.svg';
+import checkIcon from '@ckeditor/ckeditor5-core/theme/icons/check.svg';
+import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg';
 
-const DISPLAY_PREVIEW_CLASS = 'raw-html--display-preview';
+import '../theme/htmlembed.css';
 
 /**
  * The HTML embed editing feature.
@@ -41,10 +44,10 @@ export default class HtmlEmbedEditing extends Plugin {
 		super( editor );
 
 		editor.config.define( 'htmlEmbed', {
-			previewsInData: false,
+			showPreviews: false,
 			sanitizeHtml: rawHtml => {
 				/**
-				 * When using the HTML embed feature with `htmlEmbed.previewsInData=true` option, it is strongly recommended to
+				 * When using the HTML embed feature with `htmlEmbed.showPreviews=true` option, it is strongly recommended to
 				 * define a sanitize function that will clean up an input HTML in order to avoid XSS vulnerability.
 				 *
 				 * For a detailed overview, check the {@glink features/html-embed HTML embed feature} documentation.
@@ -55,7 +58,7 @@ export default class HtmlEmbedEditing extends Plugin {
 
 				return {
 					html: rawHtml,
-					hasModified: false
+					hasChanged: false
 				};
 			}
 		} );
@@ -74,8 +77,8 @@ export default class HtmlEmbedEditing extends Plugin {
 			allowAttributes: [ 'value' ]
 		} );
 
-		editor.commands.add( 'htmlEmbedUpdate', new HtmlEmbedUpdateCommand( editor ) );
-		editor.commands.add( 'htmlEmbedInsert', new HtmlEmbedInsertCommand( editor ) );
+		editor.commands.add( 'updateHtmlEmbed', new UpdateHtmlEmbedCommand( editor ) );
+		editor.commands.add( 'insertHtmlEmbed', new InsertHtmlEmbedCommand( editor ) );
 
 		this._setupConversion();
 	}
@@ -121,135 +124,240 @@ export default class HtmlEmbedEditing extends Plugin {
 		} );
 
 		editor.conversion.for( 'editingDowncast' ).elementToElement( {
+			triggerBy: {
+				attributes: [ 'value' ]
+			},
 			model: 'rawHtml',
 			view: ( modelElement, { writer } ) => {
-				const widgetLabel = t( 'HTML snippet' );
-				const placeholder = t( 'Paste the raw code here.' );
+				let domContentWrapper, state, props;
 
-				const widgetView = writer.createContainerElement( 'div', {
-					class: 'raw-html'
+				const viewContainer = writer.createContainerElement( 'div', {
+					class: 'raw-html-embed'
 				} );
-
-				const rawHtmlContainer = writer.createContainerElement( 'div', {
-					'data-cke-ignore-events': true
+				// Widget cannot be a raw element because the widget system would not be able
+				// to add its UI to it. Thus, we need this wrapper.
+				const viewContentWrapper = writer.createRawElement( 'div', {
+					class: 'raw-html-embed__content-wrapper'
+				}, function( domElement ) {
+					domContentWrapper = domElement;
+
+					renderContent( { domElement, editor, state, props } );
 				} );
 
-				// Whether to show a preview mode or editing area.
-				writer.setCustomProperty( 'isEditingSourceActive', false, rawHtmlContainer );
+				// API exposed on each raw HTML embed widget so other features can control a particular widget.
+				const rawHtmlApi = {
+					makeEditable() {
+						state = Object.assign( {}, state, {
+							isEditable: true
+						} );
 
-				const textareaAttributes = {
-					placeholder,
-					disabled: true,
-					class: 'raw-html__source'
-				};
+						renderContent( { domElement: domContentWrapper, editor, state, props } );
 
-				// The editing raw HTML field.
-				const sourceElement = writer.createUIElement( 'textarea', textareaAttributes, function( domDocument ) {
-					const root = this.toDomElement( domDocument );
+						view.change( writer => {
+							writer.setAttribute( 'data-cke-ignore-events', 'true', viewContentWrapper );
+						} );
 
-					writer.setCustomProperty( 'domElement', root, sourceElement );
-					root.value = modelElement.getAttribute( 'value' ) || '';
+						// This could be potentially pulled to a separate method called focusTextarea().
+						domContentWrapper.querySelector( 'textarea' ).focus();
+					},
+					save( newValue ) {
+						// If the value didn't change, we just cancel. If it changed,
+						// it's enough to update the model – the entire widget will be reconverted.
+						if ( newValue !== state.getRawHtmlValue() ) {
+							editor.execute( 'updateHtmlEmbed', newValue );
+						} else {
+							this.cancel();
+						}
+					},
+					cancel() {
+						state = Object.assign( {}, state, {
+							isEditable: false
+						} );
 
-					root.addEventListener( 'input', () => {
-						editor.execute( 'htmlEmbedUpdate', root.value );
-					} );
+						renderContent( { domElement: domContentWrapper, editor, state, props } );
 
-					return root;
-				} );
+						view.change( writer => {
+							writer.removeAttribute( 'data-cke-ignore-events', viewContentWrapper );
+						} );
+					}
+				};
 
-				// The switch button between preview and editing HTML.
-				const toggleButton = writer.createUIElement( 'div', { class: 'raw-html__switch-mode' }, function( domDocument ) {
-					const root = this.toDomElement( domDocument );
+				state = {
+					showPreviews: htmlEmbedConfig.showPreviews,
+					isEditable: false,
+					getRawHtmlValue: () => modelElement.getAttribute( 'value' ) || ''
+				};
 
-					root.innerHTML = htmlEmbedModeIcon;
-					writer.setCustomProperty( 'domElement', root, toggleButton );
+				props = {
+					sanitizeHtml: htmlEmbedConfig.sanitizeHtml,
+					textareaPlaceholder: t( 'Paste raw HTML here...' ),
+
+					onEditClick() {
+						rawHtmlApi.makeEditable();
+					},
+					onSaveClick( newValue ) {
+						rawHtmlApi.save( newValue );
+					},
+					onCancelClick() {
+						rawHtmlApi.cancel();
+					}
+				};
 
-					root.addEventListener( 'click', evt => {
-						view.change( writer => {
-							if ( htmlEmbedConfig.previewsInData ) {
-								if ( widgetView.hasClass( DISPLAY_PREVIEW_CLASS ) ) {
-									writer.removeClass( DISPLAY_PREVIEW_CLASS, widgetView );
-								} else {
-									writer.addClass( DISPLAY_PREVIEW_CLASS, widgetView );
-								}
-							}
-
-							const textarea = sourceElement.getCustomProperty( 'domElement' );
-							textarea.disabled = !textarea.disabled;
-						} );
+				writer.insert( writer.createPositionAt( viewContainer, 0 ), viewContentWrapper );
 
-						evt.preventDefault();
-					} );
+				writer.setCustomProperty( 'rawHtmlApi', rawHtmlApi, viewContainer );
+				writer.setCustomProperty( 'rawHtml', true, viewContainer );
 
-					return root;
+				return toWidget( viewContainer, writer, {
+					widgetLabel: t( 'HTML snippet' ),
+					hasSelectionHandle: true
 				} );
+			}
+		} );
+
+		function renderContent( { domElement, editor, state, props } ) {
+			// Remove all children;
+			domElement.textContent = '';
 
-				writer.insert( writer.createPositionAt( widgetView, 0 ), rawHtmlContainer );
-				writer.insert( writer.createPositionAt( rawHtmlContainer, 0 ), toggleButton );
-				writer.insert( writer.createPositionAt( rawHtmlContainer, 1 ), sourceElement );
+			const domDocument = domElement.ownerDocument;
+			let domTextarea;
 
-				// The container that renders the HTML should be created only when `htmlEmbed.previewsInData=true` in the config.
-				if ( htmlEmbedConfig.previewsInData ) {
-					writer.addClass( [ 'raw-html--preview-enabled', DISPLAY_PREVIEW_CLASS ], widgetView );
+			if ( state.isEditable ) {
+				const textareaProps = {
+					isDisabled: false,
+					placeholder: props.textareaPlaceholder
+				};
 
-					const previewContainer = writer.createRawElement( 'div', { class: 'raw-html__preview' }, function( domElement ) {
-						writer.setCustomProperty( 'domElement', domElement, previewContainer );
+				domTextarea = createDomTextarea( { domDocument, state, props: textareaProps } );
 
-						const sanitizeOutput = htmlEmbedConfig.sanitizeHtml( modelElement.getAttribute( 'value' ) || '' );
-						domElement.innerHTML = sanitizeOutput.html;
-					} );
+				domElement.append( domTextarea );
+			} else if ( state.showPreviews ) {
+				const previewContainerProps = {
+					sanitizeHtml: props.sanitizeHtml
+				};
 
-					writer.insert( writer.createPositionAt( rawHtmlContainer, 2 ), previewContainer );
-				}
+				domElement.append( createPreviewContainer( { domDocument, state, props: previewContainerProps } ) );
+			} else {
+				const textareaProps = {
+					isDisabled: true,
+					placeholder: props.textareaPlaceholder
+				};
 
-				return toRawHtmlWidget( widgetView, writer, widgetLabel );
+				domElement.append( createDomTextarea( { domDocument, state, props: textareaProps } ) );
 			}
-		} );
 
-		editor.conversion.for( 'editingDowncast' ).add( downcastRawHtmlValueAttribute( htmlEmbedConfig ) );
-	}
-}
+			const buttonsWrapperProps = {
+				onEditClick: props.onEditClick,
+				onSaveClick: () => {
+					props.onSaveClick( domTextarea.value );
+				},
+				onCancelClick: props.onCancelClick
+			};
+			domElement.prepend( createDomButtonsWrapper( { editor, domDocument, state, props: buttonsWrapperProps } ) );
+		}
+
+		function createDomButtonsWrapper( { editor, domDocument, state, props } ) {
+			const domButtonsWrapper = createElement( domDocument, 'div', {
+				class: 'raw-html-embed__buttons-wrapper'
+			} );
+			// TODO these should be cached and we should only clone here these cached nodes!
+			const domEditButton = createDomButton( editor.locale, 'edit' );
+			const domSaveButton = createDomButton( editor.locale, 'save' );
+			const domCancelButton = createDomButton( editor.locale, 'cancel' );
+
+			if ( state.isEditable ) {
+				const clonedDomSaveButton = domSaveButton.cloneNode( true );
+				const clonedDomCancelButton = domCancelButton.cloneNode( true );
+
+				clonedDomSaveButton.addEventListener( 'click', evt => {
+					evt.preventDefault();
+					props.onSaveClick( );
+				} );
 
-// Returns a converter that handles the `value` attribute of the `rawHtml` element.
-//
-// It updates the source (`textarea`) value and passes an HTML to the preview element if `htmlEmbed.previewsInData=true`.
-//
-// @params {module:html-embed/htmlembed~HtmlEmbedConfig} htmlEmbedConfig
-// @returns {Function}
-function downcastRawHtmlValueAttribute( htmlEmbedConfig ) {
-	return dispatcher => {
-		dispatcher.on( 'attribute:value:rawHtml', ( evt, data, conversionApi ) => {
-			const widgetView = conversionApi.mapper.toViewElement( data.item );
-			const rawHtmlContainer = widgetView.getChild( 0 );
-
-			const textareaDomElement = rawHtmlContainer.getChild( 1 ).getCustomProperty( 'domElement' );
-
-			if ( textareaDomElement ) {
-				textareaDomElement.value = data.item.getAttribute( 'value' );
-			}
+				clonedDomCancelButton.addEventListener( 'click', evt => {
+					evt.preventDefault();
+					props.onCancelClick( );
+				} );
+
+				domButtonsWrapper.appendChild( clonedDomSaveButton );
+				domButtonsWrapper.appendChild( clonedDomCancelButton );
+			} else {
+				const clonedDomEditButton = domEditButton.cloneNode( true );
 
-			if ( htmlEmbedConfig.previewsInData ) {
-				const previewDomElement = rawHtmlContainer.getChild( 2 ).getCustomProperty( 'domElement' );
+				clonedDomEditButton.addEventListener( 'click', evt => {
+					evt.preventDefault();
+					props.onEditClick();
+				} );
 
-				if ( previewDomElement ) {
-					const sanitizeOutput = htmlEmbedConfig.sanitizeHtml( data.item.getAttribute( 'value' ) );
-					previewDomElement.innerHTML = sanitizeOutput.html;
-				}
+				domButtonsWrapper.appendChild( clonedDomEditButton );
 			}
-		} );
-	};
+
+			return domButtonsWrapper;
+		}
+
+		function createDomTextarea( { domDocument, state, props } ) {
+			const domTextarea = createElement( domDocument, 'textarea', {
+				placeholder: props.placeholder,
+				class: 'ck ck-reset ck-input ck-input-text raw-html-embed__source'
+			} );
+
+			domTextarea.disabled = props.isDisabled;
+			domTextarea.value = state.getRawHtmlValue();
+
+			return domTextarea;
+		}
+
+		function createPreviewContainer( { domDocument, state, props } ) {
+			const domPreviewContainer = createElement( domDocument, 'div', {
+				class: 'raw-html-embed__preview'
+			} );
+
+			const sanitizeOutput = props.sanitizeHtml( state.getRawHtmlValue() );
+			domPreviewContainer.innerHTML = sanitizeOutput.html;
+
+			return domPreviewContainer;
+		}
+	}
 }
 
-// Converts a given {@link module:engine/view/element~Element} to a raw html widget:
-// * Adds a {@link module:engine/view/element~Element#_setCustomProperty custom property} allowing to recognize the raw html widget element.
-// * Calls the {@link module:widget/utils~toWidget} function with the proper element's label creator.
+// Returns a toggle mode button DOM element that can be cloned and used in conversion.
 //
-//  @param {module:engine/view/element~Element} viewElement
-//  @param {module:engine/view/downcastwriter~DowncastWriter} writer An instance of the view writer.
-//  @param {String} label The element's label.
-//  @returns {module:engine/view/element~Element}
-function toRawHtmlWidget( viewElement, writer, label ) {
-	writer.setCustomProperty( 'rawHtml', true, viewElement );
-
-	return toWidget( viewElement, writer, { label } );
+//  @param {module:utils/locale~Locale} locale Editor locale.
+//  @param {'edit'|'save'|'cancel'} type Type of button to create.
+//  @returns {HTMLElement}
+function createDomButton( locale, type ) {
+	const t = locale.t;
+	const buttonView = new ButtonView( locale );
+
+	buttonView.set( {
+		tooltipPosition: 'sw',
+		icon: pencilIcon,
+		tooltip: true
+	} );
+
+	buttonView.render();
+
+	if ( type === 'edit' ) {
+		buttonView.set( {
+			icon: pencilIcon,
+			label: t( 'Edit source' ),
+			class: 'raw-html-embed__edit-button'
+		} );
+	} else if ( type === 'save' ) {
+		buttonView.set( {
+			icon: checkIcon,
+			label: t( 'Save changes' ),
+			class: 'raw-html-embed__save-button'
+		} );
+	} else {
+		buttonView.set( {
+			icon: cancelIcon,
+			label: t( 'Cancel' ),
+			class: 'raw-html-embed__cancel-button'
+		} );
+	}
+
+	buttonView.destroy();
+
+	return buttonView.element.cloneNode( true );
 }

+ 5 - 9
packages/ckeditor5-html-embed/src/htmlembedui.js

@@ -9,7 +9,8 @@
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-import htmlEmbedIcon from '../theme/icons/htmlembed.svg';
+
+import htmlEmbedIcon from '../theme/icons/html.svg';
 
 /**
  * The HTML embed UI plugin.
@@ -23,7 +24,7 @@ export default class HtmlEmbedUI extends Plugin {
 
 		// Add the `htmlEmbed` button to feature components.
 		editor.ui.componentFactory.add( 'htmlEmbed', locale => {
-			const command = editor.commands.get( 'htmlEmbedInsert' );
+			const command = editor.commands.get( 'insertHtmlEmbed' );
 			const view = new ButtonView( locale );
 
 			view.set( {
@@ -36,17 +37,12 @@ export default class HtmlEmbedUI extends Plugin {
 
 			// Execute the command.
 			this.listenTo( view, 'execute', () => {
-				editor.execute( 'htmlEmbedInsert' );
+				editor.execute( 'insertHtmlEmbed' );
 				editor.editing.view.focus();
 
 				const widgetWrapper = editor.editing.view.document.selection.getSelectedElement();
-				const rawHtmlContainer = widgetWrapper.getChild( 0 );
-
-				// After inserting a new element, switch to "Edit source" mode.
-				rawHtmlContainer.getChild( 0 ).getCustomProperty( 'domElement' ).click();
 
-				// And focus the edit source element (`textarea`).
-				rawHtmlContainer.getChild( 1 ).getCustomProperty( 'domElement' ).focus();
+				widgetWrapper.getCustomProperty( 'rawHtmlApi' ).makeEditable();
 			} );
 
 			return view;

+ 4 - 4
packages/ckeditor5-html-embed/src/htmlembedinsertcommand.js

@@ -4,7 +4,7 @@
  */
 
 /**
- * @module html-embed/htmlembedinsertcommand
+ * @module html-embed/inserthtmlembedcommand
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
@@ -13,15 +13,15 @@ import { findOptimalInsertionPosition } from '@ckeditor/ckeditor5-widget/src/uti
 /**
  * The insert raw HTML element command.
  *
- * The command is registered by {@link module:html-embed/htmlembedediting~HtmlEmbedEditing} as `'htmlEmbedInsert'`.
+ * The command is registered by {@link module:html-embed/htmlembedediting~HtmlEmbedEditing} as `'insertHtmlEmbed'`.
  *
  * To insert the raw HTML element at the current selection, execute the command:
  *
- *		editor.execute( 'htmlEmbedInsert' );
+ *		editor.execute( 'insertHtmlEmbed' );
  *
  * @extends module:core/command~Command
  */
-export default class HtmlEmbedInsertCommand extends Command {
+export default class InsertHtmlEmbedCommand extends Command {
 	/**
 	 * @inheritDoc
 	 */

+ 4 - 4
packages/ckeditor5-html-embed/src/htmlembedupdatecommand.js

@@ -4,7 +4,7 @@
  */
 
 /**
- * @module html-embed/htmlembedupdatecommand
+ * @module html-embed/updatehtmlembedcommand
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
@@ -12,15 +12,15 @@ import Command from '@ckeditor/ckeditor5-core/src/command';
 /**
  * The update raw HTML value command.
  *
- * The command is registered by {@link module:html-embed/htmlembedediting~HtmlEmbedEditing} as `'htmlEmbedUpdate'`.
+ * The command is registered by {@link module:html-embed/htmlembedediting~HtmlEmbedEditing} as `'updateHtmlEmbed'`.
  *
  * To update the value of the raw HTML element at the current selection, execute the command:
  *
- *		editor.execute( 'htmlEmbedUpdate', '<b>HTML.</b>' );
+ *		editor.execute( 'updateHtmlEmbed', '<b>HTML.</b>' );
  *
  * @extends module:core/command~Command
  */
-export default class HtmlEmbedUpdateCommand extends Command {
+export default class UpdateHtmlEmbedCommand extends Command {
 	/**
 	 * @inheritDoc
 	 */

+ 163 - 228
packages/ckeditor5-html-embed/tests/htmlembedediting.js

@@ -3,13 +3,13 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* global console, document, HTMLTextAreaElement, HTMLDivElement, Event */
+/* global console, document */
 
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import HtmlEmbedEditing from '../src/htmlembedediting';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
-import HtmlEmbedUpdateCommand from '../src/htmlembedupdatecommand';
-import HtmlEmbedInsertCommand from '../src/htmlembedinsertcommand';
+import UpdateHtmlEmbedCommand from '../src/updatehtmlembedcommand';
+import InsertHtmlEmbedCommand from '../src/inserthtmlembedcommand';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { isWidget } from '@ckeditor/ckeditor5-widget/src/utils';
 
@@ -60,12 +60,12 @@ describe( 'HtmlEmbedEditing', () => {
 	} );
 
 	describe( 'commands', () => {
-		it( 'should register htmlEmbedUpdate command', () => {
-			expect( editor.commands.get( 'htmlEmbedUpdate' ) ).to.be.instanceOf( HtmlEmbedUpdateCommand );
+		it( 'should register updateHtmlEmbed command', () => {
+			expect( editor.commands.get( 'updateHtmlEmbed' ) ).to.be.instanceOf( UpdateHtmlEmbedCommand );
 		} );
 
-		it( 'should register htmlEmbedInsert command', () => {
-			expect( editor.commands.get( 'htmlEmbedInsert' ) ).to.be.instanceOf( HtmlEmbedInsertCommand );
+		it( 'should register insertHtmlEmbed command', () => {
+			expect( editor.commands.get( 'insertHtmlEmbed' ) ).to.be.instanceOf( InsertHtmlEmbedCommand );
 		} );
 	} );
 
@@ -76,9 +76,9 @@ describe( 'HtmlEmbedEditing', () => {
 			htmlEmbed = editor.config.get( 'htmlEmbed' );
 		} );
 
-		describe( 'htmlEmbed.previewsInData', () => {
+		describe( 'htmlEmbed.showPreviews', () => {
 			it( 'should be set to `false` by default', () => {
-				expect( htmlEmbed.previewsInData ).to.equal( false );
+				expect( htmlEmbed.showPreviews ).to.equal( false );
 			} );
 		} );
 
@@ -90,7 +90,7 @@ describe( 'HtmlEmbedEditing', () => {
 			it( 'should return an object with cleaned html and a note whether something has changed', () => {
 				expect( htmlEmbed.sanitizeHtml( 'foo' ) ).to.deep.equal( {
 					html: 'foo',
-					hasModified: false
+					hasChanged: false
 				} );
 			} );
 
@@ -109,7 +109,7 @@ describe( 'HtmlEmbedEditing', () => {
 		} );
 	} );
 
-	describe( 'conversion in data pipeline', () => {
+	describe( 'conversion in the data pipeline', () => {
 		describe( 'model to view', () => {
 			it( 'should convert an empty `rawHtml` element', () => {
 				setModelData( model, '[<rawHtml></rawHtml>]' );
@@ -228,8 +228,8 @@ describe( 'HtmlEmbedEditing', () => {
 		} );
 	} );
 
-	describe( 'conversion in editing pipeline (model to view)', () => {
-		describe( 'without previews (htmlEmbed.dataInPreviews=false)', () => {
+	describe( 'conversion in the editing pipeline (model to view)', () => {
+		describe( 'without previews (htmlEmbed.showPreviews=false)', () => {
 			it( 'converted element should be widgetized', () => {
 				setModelData( model, '<rawHtml></rawHtml>' );
 				const widget = viewDocument.getRoot().getChild( 0 );
@@ -237,124 +237,196 @@ describe( 'HtmlEmbedEditing', () => {
 				expect( widget.name ).to.equal( 'div' );
 				expect( isRawHtmlWidget( widget ) ).to.be.true;
 
-				const rawHtmlContainer = widget.getChild( 0 );
+				const contentWrapper = widget.getChild( 1 );
 
-				expect( rawHtmlContainer ).to.not.be.undefined;
+				expect( contentWrapper.hasClass( 'raw-html-embed__content-wrapper' ) );
 			} );
 
-			it( 'widget should not contain a class that informs about available preview mode', () => {
+			it( 'the main element should expose rawHtmlApi custom property', () => {
 				setModelData( model, '<rawHtml></rawHtml>' );
-
 				const widget = viewDocument.getRoot().getChild( 0 );
 
-				expect( widget.hasClass( 'raw-html--preview-enabled' ) ).to.equal( false );
+				expect( widget.getCustomProperty( 'rawHtmlApi' ) ).has.keys( [ 'makeEditable', 'save', 'cancel' ] );
 			} );
 
-			it( 'should render the toggle mode icon and edit source elements', () => {
-				setModelData( model, '<rawHtml></rawHtml>' );
-
+			it( 'renders a disabled textarea as a preview', () => {
+				setModelData( model, '<rawHtml value="foo"></rawHtml>' );
 				const widget = viewDocument.getRoot().getChild( 0 );
-				const viewHtmlContainer = widget.getChild( 0 );
+				const contentWrapper = widget.getChild( 1 );
+				const domContentWrapper = editor.editing.view.domConverter.mapViewToDom( contentWrapper );
 
-				expect( viewHtmlContainer.childCount ).to.equal( 2 );
+				expect( domContentWrapper.querySelector( 'textarea.raw-html-embed__source' ).value ).to.equal( 'foo' );
+				expect( domContentWrapper.querySelector( 'textarea.raw-html-embed__source' ).disabled ).to.be.true;
+			} );
 
-				const toggleIconElement = viewHtmlContainer.getChild( 0 );
+			it( 'updates the textarea preview once the model changes', () => {
+				setModelData( model, '<rawHtml value="foo"></rawHtml>' );
 
-				expect( toggleIconElement.is( 'uiElement' ) ).to.equal( true );
-				expect( toggleIconElement.getCustomProperty( 'domElement' ) ).to.be.an.instanceOf( HTMLDivElement );
+				editor.model.change( writer => writer.setAttribute( 'value', 'bar', editor.model.document.getRoot().getChild( 0 ) ) );
 
-				const sourceElement = viewHtmlContainer.getChild( 1 );
+				const widget = viewDocument.getRoot().getChild( 0 );
+				const contentWrapper = widget.getChild( 1 );
+				const domContentWrapper = editor.editing.view.domConverter.mapViewToDom( contentWrapper );
 
-				expect( sourceElement.is( 'uiElement' ) ).to.equal( true );
-				expect( sourceElement.getCustomProperty( 'domElement' ) ).to.be.an.instanceOf( HTMLTextAreaElement );
+				expect( domContentWrapper.querySelector( 'textarea.raw-html-embed__source' ).value ).to.equal( 'bar' );
+				expect( domContentWrapper.querySelector( 'textarea.raw-html-embed__source' ).disabled ).to.be.true;
 			} );
 
-			it( 'source element should have a placeholder', () => {
-				setModelData( model, '<rawHtml></rawHtml>' );
-
+			it( 'renders the "edit" button', () => {
+				setModelData( model, '<rawHtml value="foo"></rawHtml>' );
 				const widget = viewDocument.getRoot().getChild( 0 );
-				const viewHtmlContainer = widget.getChild( 0 );
-				const sourceElement = viewHtmlContainer.getChild( 1 );
+				const contentWrapper = widget.getChild( 1 );
+				const domContentWrapper = editor.editing.view.domConverter.mapViewToDom( contentWrapper );
 
-				expect( sourceElement.getAttribute( 'placeholder' ) ).to.equal( 'Paste the raw code here.' );
+				// There's exactly this button, and nothing else.
+				expect( domContentWrapper.querySelectorAll( 'button' ) ).to.have.lengthOf( 1 );
+				expect( domContentWrapper.querySelectorAll( '.raw-html-embed__edit-button' ) ).to.have.lengthOf( 1 );
 			} );
 
-			it( 'should update the edit source element when the `value` attribute has changed', () => {
-				setModelData( model, '<rawHtml></rawHtml>' );
+			it( 'allows editing the source after clicking the "edit" button', () => {
+				setModelData( model, '<rawHtml value="foo"></rawHtml>' );
+				const widget = viewDocument.getRoot().getChild( 0 );
+				const contentWrapper = widget.getChild( 1 );
+				const domContentWrapper = editor.editing.view.domConverter.mapViewToDom( contentWrapper );
 
-				model.change( writer => {
-					writer.setAttribute( 'value', '<b>Foo.</b>', model.document.getRoot().getChild( 0 ) );
-				} );
+				const makeEditableStub = sinon.stub( widget.getCustomProperty( 'rawHtmlApi' ), 'makeEditable' );
 
-				const widget = viewDocument.getRoot().getChild( 0 );
-				const viewHtmlContainer = widget.getChild( 0 );
-				const sourceElement = viewHtmlContainer.getChild( 1 );
+				domContentWrapper.querySelector( '.raw-html-embed__edit-button' ).click();
 
-				expect( sourceElement.getCustomProperty( 'domElement' ).value ).to.equal( '<b>Foo.</b>' );
+				expect( makeEditableStub.callCount ).to.equal( 1 );
 			} );
 
-			it( 'should update the `value` attribute after applying changes in the edit source element', () => {
-				setModelData( model, '<rawHtml></rawHtml>' );
+			it( 'renders the "save changes" and "cancel" button in edit source mode', () => {
+				setModelData( model, '<rawHtml value="foo"></rawHtml>' );
+				const widget = viewDocument.getRoot().getChild( 0 );
+				const contentWrapper = widget.getChild( 1 );
+				const domContentWrapper = editor.editing.view.domConverter.mapViewToDom( contentWrapper );
 
+				widget.getCustomProperty( 'rawHtmlApi' ).makeEditable();
+
+				expect( domContentWrapper.querySelectorAll( 'button' ) ).to.have.lengthOf( 2 );
+				expect( domContentWrapper.querySelectorAll( '.raw-html-embed__save-button' ) ).to.have.lengthOf( 1 );
+				expect( domContentWrapper.querySelectorAll( '.raw-html-embed__cancel-button' ) ).to.have.lengthOf( 1 );
+			} );
+
+			it( 'updates the model state after clicking the "save changes" button', () => {
+				setModelData( model, '<rawHtml value="foo"></rawHtml>' );
 				const widget = viewDocument.getRoot().getChild( 0 );
-				const viewHtmlContainer = widget.getChild( 0 );
-				const sourceElement = viewHtmlContainer.getChild( 1 );
+				const contentWrapper = widget.getChild( 1 );
+				const domContentWrapper = editor.editing.view.domConverter.mapViewToDom( contentWrapper );
 
-				const textarea = sourceElement.getCustomProperty( 'domElement' );
+				widget.getCustomProperty( 'rawHtmlApi' ).makeEditable();
 
-				textarea.value = '<b>Foo.</b>';
-				textarea.dispatchEvent( new Event( 'input' ) );
+				domContentWrapper.querySelector( 'textarea' ).value = 'Foo Bar.';
+				domContentWrapper.querySelector( '.raw-html-embed__save-button' ).click();
 
-				expect( model.document.getRoot().getChild( 0 ).getAttribute( 'value' ) ).to.equal( '<b>Foo.</b>' );
+				expect( getModelData( model ) ).to.equal( '[<rawHtml value="Foo Bar."></rawHtml>]' );
 			} );
 
-			it( 'should show the preview element by default (source element should be disabled)', () => {
-				setModelData( model, '<rawHtml value="Foo"></rawHtml>' );
+			it( 'switches to "preview mode" after saving changes', () => {
+				setModelData( model, '<rawHtml value="foo"></rawHtml>' );
+
+				let widget = viewDocument.getRoot().getChild( 0 );
+				let contentWrapper = widget.getChild( 1 );
+				let domContentWrapper = editor.editing.view.domConverter.mapViewToDom( contentWrapper );
 
+				widget.getCustomProperty( 'rawHtmlApi' ).makeEditable();
+
+				domContentWrapper.querySelector( 'textarea' ).value = 'Foo Bar.';
+				domContentWrapper.querySelector( '.raw-html-embed__save-button' ).click();
+
+				// The entire DOM has rendered once again. The references were invalid.
+				widget = viewDocument.getRoot().getChild( 0 );
+				contentWrapper = widget.getChild( 1 );
+				domContentWrapper = editor.editing.view.domConverter.mapViewToDom( contentWrapper );
+
+				// There's exactly this button, and nothing else.
+				expect( domContentWrapper.querySelectorAll( 'button' ) ).to.have.lengthOf( 1 );
+				expect( domContentWrapper.querySelectorAll( '.raw-html-embed__edit-button' ) ).to.have.lengthOf( 1 );
+			} );
+
+			it( 'does not update the model state after saving the same changes', () => {
+				setModelData( model, '<rawHtml value="foo"></rawHtml>' );
 				const widget = viewDocument.getRoot().getChild( 0 );
-				const viewHtmlContainer = widget.getChild( 0 );
-				const sourceElement = viewHtmlContainer.getChild( 1 );
+				const contentWrapper = widget.getChild( 1 );
+				const domContentWrapper = editor.editing.view.domConverter.mapViewToDom( contentWrapper );
+
+				const executeStub = sinon.stub( editor.commands.get( 'updateHtmlEmbed' ), 'execute' );
 
-				const textarea = sourceElement.getCustomProperty( 'domElement' );
+				widget.getCustomProperty( 'rawHtmlApi' ).makeEditable();
+				domContentWrapper.querySelector( '.raw-html-embed__save-button' ).click();
 
-				expect( textarea.disabled ).to.equal( true );
-				expect( textarea.value ).to.equal( 'Foo' );
+				expect( executeStub.callCount ).to.equal( 0 );
 			} );
 
-			it( 'should allows modifying the source after clicking the toggle icon', () => {
-				setModelData( model, '<rawHtml></rawHtml>' );
+			it( 'does not update the model state after clicking the "cancel" button', () => {
+				setModelData( model, '<rawHtml value="foo"></rawHtml>' );
+				const widget = viewDocument.getRoot().getChild( 0 );
+				const contentWrapper = widget.getChild( 1 );
+				const domContentWrapper = editor.editing.view.domConverter.mapViewToDom( contentWrapper );
+
+				widget.getCustomProperty( 'rawHtmlApi' ).makeEditable();
+				domContentWrapper.querySelector( '.raw-html-embed__cancel-button' ).click();
+
+				expect( getModelData( model ) ).to.equal( '[<rawHtml value="foo"></rawHtml>]' );
+			} );
 
+			it( 'switches to "preview mode" after canceling editing', () => {
+				setModelData( model, '<rawHtml value="foo"></rawHtml>' );
 				const widget = viewDocument.getRoot().getChild( 0 );
-				const viewHtmlContainer = widget.getChild( 0 );
+				const contentWrapper = widget.getChild( 1 );
+				const domContentWrapper = editor.editing.view.domConverter.mapViewToDom( contentWrapper );
 
-				const toggleIconElement = viewHtmlContainer.getChild( 0 );
-				toggleIconElement.getCustomProperty( 'domElement' ).click();
+				widget.getCustomProperty( 'rawHtmlApi' ).makeEditable();
 
-				const sourceElement = viewHtmlContainer.getChild( 1 );
+				domContentWrapper.querySelector( '.raw-html-embed__cancel-button' ).click();
 
-				expect( sourceElement.is( 'uiElement' ) ).to.equal( true );
-				expect( sourceElement.getCustomProperty( 'domElement' ).disabled ).to.equal( false );
+				expect( domContentWrapper.querySelector( 'textarea.raw-html-embed__source' ).value ).to.equal( 'foo' );
+				expect( domContentWrapper.querySelector( 'textarea.raw-html-embed__source' ).disabled ).to.be.true;
 			} );
 
-			it( 'should disable the source element after clicking the toggle icon when edit source mode is enabled', () => {
-				setModelData( model, '<rawHtml></rawHtml>' );
+			describe( 'rawHtmlApi.makeEditable()', () => {
+				it( 'makes the textarea editable', () => {
+					setModelData( model, '<rawHtml value="foo"></rawHtml>' );
+					const widget = viewDocument.getRoot().getChild( 0 );
+					const contentWrapper = widget.getChild( 1 );
+					const domContentWrapper = editor.editing.view.domConverter.mapViewToDom( contentWrapper );
 
-				const widget = viewDocument.getRoot().getChild( 0 );
-				const viewHtmlContainer = widget.getChild( 0 );
+					widget.getCustomProperty( 'rawHtmlApi' ).makeEditable();
+
+					expect( domContentWrapper.querySelector( 'textarea.raw-html-embed__source' ).value ).to.equal( 'foo' );
+					expect( domContentWrapper.querySelector( 'textarea.raw-html-embed__source' ).disabled ).to.be.false;
+				} );
+			} );
+
+			describe( 'rawHtmlApi.save()', () => {
+				it( 'saves the new value to the model', () => {
+					setModelData( model, '<rawHtml value="foo"></rawHtml>' );
+					const widget = viewDocument.getRoot().getChild( 0 );
 
-				const toggleIconElement = viewHtmlContainer.getChild( 0 );
-				// Switch to edit source mode.
-				toggleIconElement.getCustomProperty( 'domElement' ).click();
-				// Switch to preview mode.
-				toggleIconElement.getCustomProperty( 'domElement' ).click();
+					widget.getCustomProperty( 'rawHtmlApi' ).makeEditable();
+					widget.getCustomProperty( 'rawHtmlApi' ).save( 'bar' );
+
+					expect( getModelData( model ) ).to.equal( '[<rawHtml value="bar"></rawHtml>]' );
+				} );
 
-				const sourceElement = viewHtmlContainer.getChild( 1 );
+				it( 'turns back to the non-editable mode and updates the textarea value', () => {
+					setModelData( model, '<rawHtml value="foo"></rawHtml>' );
+					const widget = viewDocument.getRoot().getChild( 0 );
 
-				expect( sourceElement.getCustomProperty( 'domElement' ).disabled ).to.equal( true );
+					widget.getCustomProperty( 'rawHtmlApi' ).makeEditable();
+					widget.getCustomProperty( 'rawHtmlApi' ).save( 'bar' );
+
+					const newWidget = viewDocument.getRoot().getChild( 0 );
+					const contentWrapper = newWidget.getChild( 1 );
+					const domContentWrapper = editor.editing.view.domConverter.mapViewToDom( contentWrapper );
+					expect( domContentWrapper.querySelector( 'textarea.raw-html-embed__source' ).value ).to.equal( 'bar' );
+					expect( domContentWrapper.querySelector( 'textarea.raw-html-embed__source' ).disabled ).to.be.true;
+				} );
 			} );
 		} );
 
-		describe( 'with previews (htmlEmbed.dataInPreviews=true)', () => {
+		describe( 'with previews (htmlEmbed.showPreviews=true)', () => {
 			let element, editor, model, view, viewDocument, sanitizeHtml;
 
 			testUtils.createSinonSandbox();
@@ -370,7 +442,7 @@ describe( 'HtmlEmbedEditing', () => {
 					.create( element, {
 						plugins: [ HtmlEmbedEditing ],
 						htmlEmbed: {
-							previewsInData: true,
+							showPreviews: true,
 							sanitizeHtml
 						}
 					} )
@@ -389,162 +461,25 @@ describe( 'HtmlEmbedEditing', () => {
 					} );
 			} );
 
-			it( 'converted element should be widgetized', () => {
-				setModelData( model, '<rawHtml></rawHtml>' );
-				const widget = viewDocument.getRoot().getChild( 0 );
-
-				expect( widget.name ).to.equal( 'div' );
-				expect( isRawHtmlWidget( widget ) ).to.be.true;
-
-				const rawHtmlContainer = widget.getChild( 0 );
-
-				expect( rawHtmlContainer ).to.not.be.undefined;
-			} );
-
-			it( 'widget should contain a class that informs about available preview mode', () => {
-				setModelData( model, '<rawHtml></rawHtml>' );
-
-				const widget = viewDocument.getRoot().getChild( 0 );
-
-				expect( widget.hasClass( 'raw-html--preview-enabled' ) ).to.equal( true );
-			} );
-
-			it( 'should render the toggle mode icon, edit source and preview container elements', () => {
-				setModelData( model, '<rawHtml></rawHtml>' );
-
-				const widget = viewDocument.getRoot().getChild( 0 );
-				const viewHtmlContainer = widget.getChild( 0 );
-
-				expect( viewHtmlContainer.childCount ).to.equal( 3 );
-
-				const toggleIconElement = viewHtmlContainer.getChild( 0 );
-
-				expect( toggleIconElement.is( 'uiElement' ) ).to.equal( true );
-				expect( toggleIconElement.getCustomProperty( 'domElement' ) ).to.be.an.instanceOf( HTMLDivElement );
-
-				const sourceElement = viewHtmlContainer.getChild( 1 );
-
-				expect( sourceElement.is( 'uiElement' ) ).to.equal( true );
-				expect( sourceElement.getCustomProperty( 'domElement' ) ).to.be.an.instanceOf( HTMLTextAreaElement );
-
-				const previewElement = viewHtmlContainer.getChild( 2 );
-
-				expect( previewElement.is( 'rawElement' ) ).to.equal( true );
-				expect( previewElement.getCustomProperty( 'domElement' ) ).to.be.an.instanceOf( HTMLDivElement );
-			} );
-
-			it( 'source element should have a placeholder', () => {
-				setModelData( model, '<rawHtml></rawHtml>' );
-
-				const widget = viewDocument.getRoot().getChild( 0 );
-				const viewHtmlContainer = widget.getChild( 0 );
-				const sourceElement = viewHtmlContainer.getChild( 1 );
-
-				expect( sourceElement.getAttribute( 'placeholder' ) ).to.equal( 'Paste the raw code here.' );
-			} );
-
-			it( 'should update the source and preview elements when the `value` attribute has changed', () => {
-				setModelData( model, '<rawHtml></rawHtml>' );
-
-				model.change( writer => {
-					writer.setAttribute( 'value', '<b>Foo.</b>', model.document.getRoot().getChild( 0 ) );
-				} );
-
+			it( 'renders a div with a preview', () => {
+				setModelData( model, '<rawHtml value="foo"></rawHtml>' );
 				const widget = viewDocument.getRoot().getChild( 0 );
-				const viewHtmlContainer = widget.getChild( 0 );
-				const sourceElement = viewHtmlContainer.getChild( 1 );
-				const previewElement = viewHtmlContainer.getChild( 2 );
+				const contentWrapper = widget.getChild( 1 );
+				const domContentWrapper = editor.editing.view.domConverter.mapViewToDom( contentWrapper );
 
-				expect( sourceElement.getCustomProperty( 'domElement' ).value ).to.equal( '<b>Foo.</b>' );
-				expect( previewElement.getCustomProperty( 'domElement' ).innerHTML ).to.equal( '<b>Foo.</b>' );
+				expect( domContentWrapper.querySelector( 'div.raw-html-embed__preview' ).innerHTML ).to.equal( 'foo' );
 			} );
 
-			it( 'should update the `value` attribute after applying changes in the edit source element', () => {
-				setModelData( model, '<rawHtml></rawHtml>' );
+			it( 'updates the preview once the model changes', () => {
+				setModelData( model, '<rawHtml value="foo"></rawHtml>' );
 
-				const event = new Event( 'input' );
+				editor.model.change( writer => writer.setAttribute( 'value', 'bar', editor.model.document.getRoot().getChild( 0 ) ) );
 
 				const widget = viewDocument.getRoot().getChild( 0 );
-				const viewHtmlContainer = widget.getChild( 0 );
-				const sourceElement = viewHtmlContainer.getChild( 1 );
-
-				const textarea = sourceElement.getCustomProperty( 'domElement' );
-
-				textarea.value = '<b>Foo.</b>';
-				textarea.dispatchEvent( event );
-
-				expect( model.document.getRoot().getChild( 0 ).getAttribute( 'value' ) ).to.equal( '<b>Foo.</b>' );
-			} );
-
-			it( 'should re-render the preview element after applying changes in the edit source element', () => {
-				setModelData( model, '<rawHtml></rawHtml>' );
-
-				const event = new Event( 'input' );
-
-				const widget = viewDocument.getRoot().getChild( 0 );
-				const viewHtmlContainer = widget.getChild( 0 );
-				const sourceElement = viewHtmlContainer.getChild( 1 );
-
-				const textarea = sourceElement.getCustomProperty( 'domElement' );
-
-				textarea.value = '<b>Foo.</b>';
-				textarea.dispatchEvent( event );
-
-				const previewElement = viewHtmlContainer.getChild( 2 );
-
-				expect( previewElement.getCustomProperty( 'domElement' ).innerHTML ).to.equal( '<b>Foo.</b>' );
-			} );
-
-			it( 'should show the preview element by default', () => {
-				setModelData( model, '<rawHtml value="Foo"></rawHtml>' );
-
-				const widget = viewDocument.getRoot().getChild( 0 );
-
-				expect( widget.hasClass( 'raw-html--display-preview' ) ).to.equal( true );
-
-				const viewHtmlContainer = widget.getChild( 0 );
-				const sourceElement = viewHtmlContainer.getChild( 1 );
-
-				const textarea = sourceElement.getCustomProperty( 'domElement' );
-
-				expect( textarea.disabled ).to.equal( true );
-				expect( textarea.value ).to.equal( 'Foo' );
-
-				const previewElement = viewHtmlContainer.getChild( 2 ).getCustomProperty( 'domElement' );
-				expect( previewElement.innerHTML ).to.equal( 'Foo' );
-			} );
-
-			it( 'should allows modifying the source after clicking the toggle icon', () => {
-				setModelData( model, '<rawHtml></rawHtml>' );
-
-				const widget = viewDocument.getRoot().getChild( 0 );
-				const viewHtmlContainer = widget.getChild( 0 );
-
-				const toggleIconElement = viewHtmlContainer.getChild( 0 );
-				toggleIconElement.getCustomProperty( 'domElement' ).click();
-
-				expect( widget.hasClass( 'raw-html--display-preview' ) ).to.equal( false );
-
-				const sourceElement = viewHtmlContainer.getChild( 1 );
-				expect( sourceElement.getCustomProperty( 'domElement' ).disabled ).to.equal( false );
-			} );
-
-			it( 'should display preview element after clicking the toggle icon when displaying edit source mode', () => {
-				setModelData( model, '<rawHtml></rawHtml>' );
-
-				const widget = viewDocument.getRoot().getChild( 0 );
-				const viewHtmlContainer = widget.getChild( 0 );
-
-				const toggleIconElement = viewHtmlContainer.getChild( 0 );
-				// Switch to edit source mode.
-				toggleIconElement.getCustomProperty( 'domElement' ).click();
-				// Switch to preview mode.
-				toggleIconElement.getCustomProperty( 'domElement' ).click();
-
-				expect( widget.hasClass( 'raw-html--display-preview' ) ).to.equal( true );
+				const contentWrapper = widget.getChild( 1 );
+				const domContentWrapper = editor.editing.view.domConverter.mapViewToDom( contentWrapper );
 
-				const sourceElement = viewHtmlContainer.getChild( 1 );
-				expect( sourceElement.getCustomProperty( 'domElement' ).disabled ).to.equal( true );
+				expect( domContentWrapper.querySelector( 'div.raw-html-embed__preview' ).innerHTML ).to.equal( 'bar' );
 			} );
 		} );
 	} );

+ 6 - 9
packages/ckeditor5-html-embed/tests/htmlembedui.js

@@ -12,7 +12,7 @@ import HtmlEmbedUI from '../src/htmlembedui';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 
 describe( 'HtmlEmbedEditing', () => {
-	let element, editor, view, viewDocument, htmlEmbedView;
+	let element, editor, htmlEmbedView;
 
 	testUtils.createSinonSandbox();
 
@@ -26,8 +26,6 @@ describe( 'HtmlEmbedEditing', () => {
 			} )
 			.then( newEditor => {
 				editor = newEditor;
-				view = editor.editing.view;
-				viewDocument = view.document;
 
 				htmlEmbedView = editor.ui.componentFactory.create( 'htmlEmbed' );
 			} );
@@ -45,17 +43,17 @@ describe( 'HtmlEmbedEditing', () => {
 		expect( htmlEmbedView.isToggleable ).to.be.false;
 	} );
 
-	it( 'should execute htmlEmbedInsert command on model execute event', () => {
+	it( 'should execute insertHtmlEmbed command on model execute event', () => {
 		const executeSpy = testUtils.sinon.spy( editor, 'execute' );
 
 		htmlEmbedView.fire( 'execute' );
 
 		sinon.assert.calledOnce( executeSpy );
-		sinon.assert.calledWithExactly( executeSpy, 'htmlEmbedInsert' );
+		sinon.assert.calledWithExactly( executeSpy, 'insertHtmlEmbed' );
 	} );
 
-	it( 'should bind model to htmlEmbedInsert command', () => {
-		const command = editor.commands.get( 'htmlEmbedInsert' );
+	it( 'should bind model to insertHtmlEmbed command', () => {
+		const command = editor.commands.get( 'insertHtmlEmbed' );
 
 		expect( htmlEmbedView.isEnabled ).to.be.true;
 
@@ -66,7 +64,6 @@ describe( 'HtmlEmbedEditing', () => {
 	it( 'should switch to edit source mode after inserting the element', () => {
 		htmlEmbedView.fire( 'execute' );
 
-		const editSourceView = viewDocument.getRoot().getChild( 0 ).getChild( 0 ).getChild( 1 );
-		expect( document.activeElement ).to.equal( editSourceView.getCustomProperty( 'domElement' ) );
+		expect( document.activeElement.tagName ).to.equal( 'TEXTAREA' );
 	} );
 } );

+ 2 - 2
packages/ckeditor5-html-embed/tests/htmlembedinsertcommand.js

@@ -11,7 +11,7 @@ import HtmlEmbedEditing from '../src/htmlembedediting';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
-describe( 'HtmlEmbedInsertCommand', () => {
+describe( 'InsertHtmlEmbedCommand', () => {
 	let editor, model, editorElement, command;
 
 	testUtils.createSinonSandbox();
@@ -27,7 +27,7 @@ describe( 'HtmlEmbedInsertCommand', () => {
 			.then( newEditor => {
 				editor = newEditor;
 				model = editor.model;
-				command = editor.commands.get( 'htmlEmbedInsert' );
+				command = editor.commands.get( 'insertHtmlEmbed' );
 			} );
 	} );
 

+ 138 - 138
packages/ckeditor5-html-embed/tests/manual/htmlembed.html

@@ -1,157 +1,157 @@
 <head>
-    <meta http-equiv="Content-Security-Policy" content="default-src 'none'; media-src http://techslides.com/; connect-src 'self' https://cksource.com http://*.cke-cs.com; script-src 'self' https://cksource.com; img-src * data:; style-src 'self' 'unsafe-inline'; frame-src *">
+	<meta http-equiv="Content-Security-Policy" content="default-src 'none'; media-src http://techslides.com/; connect-src 'self' https://cksource.com http://*.cke-cs.com; script-src 'self' https://cksource.com; img-src * data:; style-src 'self' 'unsafe-inline'; frame-src *">
 </head>
 
 <p>
-    <b>Mode of HTML previews</b>:
-    <input type="radio" id="mode-enabled" name="mode" value="enabled" checked><label for="mode-enabled">Enabled</label>
-    <input type="radio" id="mode-disabled" name="mode" value="disabled"><label for="mode-disabled">Disabled</label>
+	<b>Value of the <code>"htmlEmbed.showPreviews"</code></b>:
+	<input type="radio" id="raw-html-previews-enabled" name="mode" value="enabled" checked><label for="raw-html-previews-enabled"><code>true</code></label>
+	<input type="radio" id="raw-html-previews-disabled" name="mode" value="disabled"><label for="raw-html-previews-disabled"><code>false</code></label>
 </p>
 
 <div id="editor">
-    <h3><code>&lt;video&gt;</code> tag as HTML snippet</h3>
-        <div class="raw-html-embed">
-            <video width="320" height="240" controls>
-                <source src="http://techslides.com/demos/sample-videos/small.mp4" type="video/mp4">
-                <source src="http://techslides.com/demos/sample-videos/small.ogv" type="video/ogg">
-                Your browser does not support the video tag.
-            </video>
-        </div>
+	<h3><code>&lt;video&gt;</code> tag as HTML snippet</h3>
+		<div class="raw-html-embed">
+			<video width="320" height="240" controls>
+				<source src="http://techslides.com/demos/sample-videos/small.mp4" type="video/mp4">
+				<source src="http://techslides.com/demos/sample-videos/small.ogv" type="video/ogg">
+				Your browser does not support the video tag.
+			</video>
+		</div>
 
 
-    <h3><code>&lt;audio&gt;</code> tag as HTML snippet</h3>
-        <div class="raw-html-embed">
-            <audio controls>
-                <source src="http://techslides.com/demos/samples/sample.ogg" type="audio/ogg">
-                <source src="http://techslides.com/demos/samples/sample.mp3" type="audio/mpeg">
-                Your browser does not support the audio tag.
-            </audio>
-        </div>
+	<h3><code>&lt;audio&gt;</code> tag as HTML snippet</h3>
+		<div class="raw-html-embed">
+			<audio controls>
+				<source src="http://techslides.com/demos/samples/sample.ogg" type="audio/ogg">
+				<source src="http://techslides.com/demos/samples/sample.mp3" type="audio/mpeg">
+				Your browser does not support the audio tag.
+			</audio>
+		</div>
 
 
-    <h3><code>&lt;picture&gt;</code> tag as HTML snippet</h3>
-        <div class="raw-html-embed">
-            <picture>
-                <source media="(min-width: 1200px)" srcset="http://techslides.com/demos/samples/sample.jpg">
-                <source media="(min-width: 650px)" srcset="http://techslides.com/demos/samples/sample.png">
-                <img src="http://techslides.com/demos/samples/sample.gif">
-            </picture>
-        </div>
+	<h3><code>&lt;picture&gt;</code> tag as HTML snippet</h3>
+		<div class="raw-html-embed">
+			<picture>
+				<source media="(min-width: 1200px)" srcset="http://techslides.com/demos/samples/sample.jpg">
+				<source media="(min-width: 650px)" srcset="http://techslides.com/demos/samples/sample.png">
+				<img src="http://techslides.com/demos/samples/sample.gif">
+			</picture>
+		</div>
 
-    <h3><code>&lt;iframe&gt;</code> tag as HTML snippet</h3>
-        <div class="raw-html-embed">
-            <iframe src="http://techslides.com/demos/samples/sample.txt"></iframe>
-        </div>
+	<h3><code>&lt;iframe&gt;</code> tag as HTML snippet</h3>
+		<div class="raw-html-embed">
+			<iframe src="http://techslides.com/demos/samples/sample.txt"></iframe>
+		</div>
 
 
-    <h3><code>&lt;table&gt;</code> tag as HTML snippet (code coverage report)</h3>
-    <div class="raw-html-embed">
-        <table class="coverage-summary">
-            <thead>
-            <tr>
-                <th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
-                <th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
-                <th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
-                <th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
-                <th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
-                <th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
-                <th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
-                <th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
-                <th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
-                <th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
-            </tr>
-            </thead>
-            <tbody>
-            <tr>
-                <td class="file high" data-value="htmlembed.js">htmlembed.js</td>
-                <td data-value="100" class="pic high">
-                    <div class="chart">
-                        <div class="cover-fill cover-full" style="width: 100%"></div>
-                        <div class="cover-empty" style="width: 0%"></div>
-                    </div>
-                </td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="2" class="abs high">2/2</td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="0" class="abs high">0/0</td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="2" class="abs high">2/2</td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="2" class="abs high">2/2</td>
-            </tr>
+	<h3><code>&lt;table&gt;</code> tag as HTML snippet (code coverage report)</h3>
+	<div class="raw-html-embed">
+		<table class="coverage-summary">
+			<thead>
+			<tr>
+				<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
+				<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
+				<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
+				<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
+				<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
+				<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
+				<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
+				<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
+				<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
+				<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
+			</tr>
+			</thead>
+			<tbody>
+			<tr>
+				<td class="file high" data-value="htmlembed.js">htmlembed.js</td>
+				<td data-value="100" class="pic high">
+					<div class="chart">
+						<div class="cover-fill cover-full" style="width: 100%"></div>
+						<div class="cover-empty" style="width: 0%"></div>
+					</div>
+				</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="2" class="abs high">2/2</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="0" class="abs high">0/0</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="2" class="abs high">2/2</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="2" class="abs high">2/2</td>
+			</tr>
 
-            <tr>
-                <td class="file high" data-value="htmlembedediting.js">htmlembedediting.js</td>
-                <td data-value="100" class="pic high">
-                    <div class="chart">
-                        <div class="cover-fill cover-full" style="width: 100%"></div>
-                        <div class="cover-empty" style="width: 0%"></div>
-                    </div>
-                </td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="81" class="abs high">81/81</td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="18" class="abs high">18/18</td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="19" class="abs high">19/19</td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="81" class="abs high">81/81</td>
-            </tr>
+			<tr>
+				<td class="file high" data-value="htmlembedediting.js">htmlembedediting.js</td>
+				<td data-value="100" class="pic high">
+					<div class="chart">
+						<div class="cover-fill cover-full" style="width: 100%"></div>
+						<div class="cover-empty" style="width: 0%"></div>
+					</div>
+				</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="81" class="abs high">81/81</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="18" class="abs high">18/18</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="19" class="abs high">19/19</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="81" class="abs high">81/81</td>
+			</tr>
 
-            <tr>
-                <td class="file high" data-value="htmlembedinsertcommand.js">htmlembedinsertcommand.js</td>
-                <td data-value="100" class="pic high">
-                    <div class="chart">
-                        <div class="cover-fill cover-full" style="width: 100%"></div>
-                        <div class="cover-empty" style="width: 0%"></div>
-                    </div>
-                </td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="18" class="abs high">18/18</td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="8" class="abs high">8/8</td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="7" class="abs high">7/7</td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="18" class="abs high">18/18</td>
-            </tr>
+			<tr>
+				<td class="file high" data-value="inserthtmlembedcommand.js">inserthtmlembedcommand.js</td>
+				<td data-value="100" class="pic high">
+					<div class="chart">
+						<div class="cover-fill cover-full" style="width: 100%"></div>
+						<div class="cover-empty" style="width: 0%"></div>
+					</div>
+				</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="18" class="abs high">18/18</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="8" class="abs high">8/8</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="7" class="abs high">7/7</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="18" class="abs high">18/18</td>
+			</tr>
 
-            <tr>
-                <td class="file high" data-value="htmlembedui.js">htmlembedui.js</td>
-                <td data-value="100" class="pic high">
-                    <div class="chart">
-                        <div class="cover-fill cover-full" style="width: 100%"></div>
-                        <div class="cover-empty" style="width: 0%"></div>
-                    </div>
-                </td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="15" class="abs high">15/15</td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="0" class="abs high">0/0</td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="3" class="abs high">3/3</td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="15" class="abs high">15/15</td>
-            </tr>
+			<tr>
+				<td class="file high" data-value="htmlembedui.js">htmlembedui.js</td>
+				<td data-value="100" class="pic high">
+					<div class="chart">
+						<div class="cover-fill cover-full" style="width: 100%"></div>
+						<div class="cover-empty" style="width: 0%"></div>
+					</div>
+				</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="15" class="abs high">15/15</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="0" class="abs high">0/0</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="3" class="abs high">3/3</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="15" class="abs high">15/15</td>
+			</tr>
 
-            <tr>
-                <td class="file high" data-value="htmlembedupdatecommand.js">htmlembedupdatecommand.js</td>
-                <td data-value="100" class="pic high">
-                    <div class="chart">
-                        <div class="cover-fill cover-full" style="width: 100%"></div>
-                        <div class="cover-empty" style="width: 0%"></div>
-                    </div>
-                </td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="13" class="abs high">13/13</td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="4" class="abs high">4/4</td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="4" class="abs high">4/4</td>
-                <td data-value="100" class="pct high">100%</td>
-                <td data-value="13" class="abs high">13/13</td>
-            </tr>
-            </tbody>
-        </table>
-    </div>
+			<tr>
+				<td class="file high" data-value="updatehtmlembedcommand.js">updatehtmlembedcommand.js</td>
+				<td data-value="100" class="pic high">
+					<div class="chart">
+						<div class="cover-fill cover-full" style="width: 100%"></div>
+						<div class="cover-empty" style="width: 0%"></div>
+					</div>
+				</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="13" class="abs high">13/13</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="4" class="abs high">4/4</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="4" class="abs high">4/4</td>
+				<td data-value="100" class="pct high">100%</td>
+				<td data-value="13" class="abs high">13/13</td>
+			</tr>
+			</tbody>
+		</table>
+	</div>
 </div>

+ 6 - 6
packages/ckeditor5-html-embed/tests/manual/htmlembed.js

@@ -12,11 +12,11 @@ import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articleplugi
 import Code from '@ckeditor/ckeditor5-basic-styles/src/code';
 import HtmlEmbed from '../../src/htmlembed';
 
-const restrictedModeButton = document.getElementById( 'mode-enabled' );
-const standardModeButton = document.getElementById( 'mode-disabled' );
+const previewsModeButton = document.getElementById( 'raw-html-previews-enabled' );
+const noPreviewsModeButton = document.getElementById( 'raw-html-previews-disabled' );
 
-restrictedModeButton.addEventListener( 'change', handleModeChange );
-standardModeButton.addEventListener( 'change', handleModeChange );
+previewsModeButton.addEventListener( 'change', handleModeChange );
+noPreviewsModeButton.addEventListener( 'change', handleModeChange );
 
 startMode( document.querySelector( 'input[name="mode"]:checked' ).value );
 
@@ -35,14 +35,14 @@ async function startMode( selectedMode ) {
 async function startEnabledPreviewsMode() {
 	await reloadEditor( {
 		htmlEmbed: {
-			previewsInData: true,
+			showPreviews: true,
 			sanitizeHtml( rawHtml ) {
 				const config = getSanitizeHtmlConfig( sanitizeHtml.defaults );
 				const cleanHtml = sanitizeHtml( rawHtml, config );
 
 				return {
 					html: cleanHtml,
-					hasModified: rawHtml !== cleanHtml
+					hasChanged: rawHtml !== cleanHtml
 				};
 			}
 		}

+ 1 - 1
packages/ckeditor5-html-embed/tests/manual/htmlembed.md

@@ -28,7 +28,7 @@ After the editor initialization, it should contain 5 widgets with embedded HTML:
 
 All resources are provided by the ["Sample Files for Development"](http://techslides.com/sample-files-for-development) article. Thanks!
 
-By default, the previews in data mode is enabled. It means that previews should be visible. 
+By default, the "previews in view" mode is enabled. It means that previews should be visible. 
 
 We use the [`sanitize-html`](https://www.npmjs.com/package/sanitize-html) package to clean up the input HTML. It means that some of the 
 elements or attributes may be not rendered in the editing view. However, they still will be returned in the editor's data.

+ 2 - 2
packages/ckeditor5-html-embed/tests/htmlembedupdatecommand.js

@@ -11,7 +11,7 @@ import HtmlEmbedEditing from '../src/htmlembedediting';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
-describe( 'HtmlEmbedUpdateCommand', () => {
+describe( 'UpdateHtmlEmbedCommand', () => {
 	let editor, model, editorElement, command;
 
 	testUtils.createSinonSandbox();
@@ -27,7 +27,7 @@ describe( 'HtmlEmbedUpdateCommand', () => {
 			.then( newEditor => {
 				editor = newEditor;
 				model = editor.model;
-				command = editor.commands.get( 'htmlEmbedUpdate' );
+				command = editor.commands.get( 'updateHtmlEmbed' );
 			} );
 	} );
 

+ 19 - 0
packages/ckeditor5-html-embed/theme/htmlembed.css

@@ -2,3 +2,22 @@
  * Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
+
+/* The feature container. */
+.ck-widget.raw-html-embed {
+	/* Give the embed some air. */
+	margin: 1em auto;
+	position: relative;
+
+	/* The switch mode button wrapper. */
+	& .raw-html-embed__buttons-wrapper {
+		position: absolute;
+		display: flex;
+		flex-direction: column;
+	}
+}
+
+.ck-content .raw-html-embed {
+	/* Give the embed some air. */
+	margin: 1em auto;
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
packages/ckeditor5-html-embed/theme/icons/html.svg


+ 0 - 1
packages/ckeditor5-html-embed/theme/icons/htmlembed.svg

@@ -1 +0,0 @@
-<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M2 9h16v2H2z"/></svg>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 6
packages/ckeditor5-html-embed/theme/icons/htmlembedmode.svg


+ 1 - 0
packages/ckeditor5-theme-lark/package.json

@@ -22,6 +22,7 @@
     "@ckeditor/ckeditor5-font": "^23.0.0",
     "@ckeditor/ckeditor5-heading": "^23.0.0",
     "@ckeditor/ckeditor5-highlight": "^23.0.0",
+    "@ckeditor/ckeditor5-html-embed": "^0.0.1",
     "@ckeditor/ckeditor5-indent": "^23.0.0",
     "@ckeditor/ckeditor5-link": "^23.0.0",
     "@ckeditor/ckeditor5-list": "^23.0.0",

+ 5 - 0
packages/ckeditor5-theme-lark/tests/manual/iconset.js

@@ -64,6 +64,8 @@ import outdent from '@ckeditor/ckeditor5-indent/theme/icons/outdent.svg';
 import marker from '@ckeditor/ckeditor5-highlight/theme/icons/marker.svg';
 import pen from '@ckeditor/ckeditor5-highlight/theme/icons/pen.svg';
 
+import html from '@ckeditor/ckeditor5-html-embed/theme/icons/html.svg';
+
 import link from '@ckeditor/ckeditor5-link/theme/icons/link.svg';
 import unlink from '@ckeditor/ckeditor5-link/theme/icons/unlink.svg';
 
@@ -126,6 +128,9 @@ const icons = {
 	// highlight
 	marker, pen,
 
+	// html-embed,
+	html,
+
 	// indent
 	indent, outdent,
 

+ 60 - 36
packages/ckeditor5-theme-lark/theme/ckeditor5-html-embed/htmlembed.css

@@ -3,52 +3,76 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-.ck-editor__editable {
-	/* The feature container. */
-	& .raw-html {
-		position: relative;
-
-		/* The switch mode button. */
-		& .raw-html__switch-mode {
-			position: absolute;
-			top: 0;
-			right: 0;
-			width: 5em;
-			height: 5em;
-			cursor: pointer;
+:root {
+	--ck-html-embed-content-width: calc(100% - 1.5 * var(--ck-icon-size));
+	--ck-html-embed-source-height: 10em;
+	--ck-html-embed-content-min-height: calc(var(--ck-icon-size) + var(--ck-spacing-standard));
+}
+
+/* The feature container. */
+.ck-widget.raw-html-embed {
+	font-size: var(--ck-font-size-base);
+	background-color: var(--ck-color-base-foreground);
+
+	& .raw-html-embed__content-wrapper {
+		padding: var(--ck-spacing-standard);
+	}
+
+	/* The switch mode button wrapper. */
+	& .raw-html-embed__buttons-wrapper {
+		top: var(--ck-spacing-standard);
+		right: var(--ck-spacing-standard);
+
+		& .ck-button.raw-html-embed__save-button {
+			color: var(--ck-color-button-save);
 		}
 
-		/* The edit source element. */
-		& .raw-html__source {
-			width: 30em;
-			height: 5em;
-			resize: none;
+		& .ck-button.raw-html-embed__cancel-button {
+			color: var(--ck-color-button-cancel);
+		}
+
+		& .ck-button:not(:first-child) {
+			margin-top: var(--ck-spacing-small);
 		}
 	}
 
-	/* Using the feature with enabled previews in data. */
-	& .raw-html--preview-enabled {
-		/* The preview data container. */
-		& .raw-html__preview {
-			min-height: 5em;
-			min-width: 30em;
-			width: calc(100% - 5em);
+	/* The edit source element. */
+	& .raw-html-embed__source {
+		box-sizing: border-box;
+		height: var(--ck-html-embed-source-height);
+		width: var(--ck-html-embed-content-width);
+		resize: none;
+		min-width: 0;
+		padding: var(--ck-spacing-standard);
+
+		font-family: monospace;
+		tab-size: 4;
+		white-space: pre-wrap;
+		font-size: var(--ck-font-size-base); /* Safari needs this. */
+
+		&[disabled] {
+			background: hsl(0deg 0% 97%);
+			color: hsl(0deg 0% 56%);
 		}
+	}
 
-		/* Mode: preview the HTML. */
-		&.raw-html--display-preview {
-			& .raw-html__source {
-				display: none;
-			}
+	/* The preview data container. */
+	& .raw-html-embed__preview {
+		overflow: hidden;
+		box-sizing: border-box;
+		width: var(--ck-html-embed-content-width);
+		min-height: var(--ck-html-embed-content-min-height);
+		text-align: center;
 
-			& .raw-html__preview {
-				display: block;
-			}
+		& > table {
+			margin-left: auto;
+			margin-right: auto;
 		}
 
-		/* Mode: edit the HTML source. */
-		&:not(.raw-html--display-preview) .raw-html__preview {
-			display: none;
+		/* Disable all mouse interaction as long as the editor is not read–only. */
+		@nest .ck-editor__editable:not(.ck-read-only) & {
+			pointer-events: none;
 		}
 	}
 }
+

+ 31 - 4
packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/tooltip/tooltip.css

@@ -49,7 +49,7 @@
 	}
 
 	/**
-	 * A class once applied displays the tooltip south of the element.
+	 * A class that displays the tooltip south of the element.
 	 *
 	 *       [element]
 	 *           ^
@@ -57,12 +57,14 @@
 	 *     |  Tooltip  |
 	 *     +-----------+
 	 */
-	&.ck-tooltip_s {
+	&.ck-tooltip_s,
+	&.ck-tooltip_sw {
 		bottom: calc(-1 * var(--ck-tooltip-arrow-size));
 		transform: translateY( 100% );
 
 		& .ck-tooltip__text::after {
-			top: calc(-1 * var(--ck-tooltip-arrow-size));
+			/* 1px addresses gliches in rendering causing gap between the triangle and the text */
+			top: calc(-1 * var(--ck-tooltip-arrow-size) + 1px);
 			transform: translateX( -50% );
 			border-color: transparent transparent var(--ck-color-tooltip-background) transparent;
 			border-width: 0 var(--ck-tooltip-arrow-size) var(--ck-tooltip-arrow-size) var(--ck-tooltip-arrow-size);
@@ -70,7 +72,32 @@
 	}
 
 	/**
-	 * A class once applied displays the tooltip north of the element.
+	 * A class that displays the tooltip south-west of the element.
+	 *
+	 *        [element]
+	 *            ^
+	 *  +-----------+
+	 *  |  Tooltip  |
+	 *  +-----------+
+	 */
+
+	&.ck-tooltip_sw {
+		right: 50%;
+		left: auto;
+
+		& .ck-tooltip__text {
+			left: auto;
+			right: calc( -2 * var(--ck-tooltip-arrow-size));
+		}
+
+		& .ck-tooltip__text::after {
+			left: auto;
+			right: 0;
+		}
+	}
+
+	/**
+	 * A class that displays the tooltip north of the element.
 	 *
 	 *     +-----------+
 	 *     |  Tooltip  |

+ 8 - 2
packages/ckeditor5-ui/src/tooltip/tooltipview.js

@@ -32,7 +32,7 @@ export default class TooltipView extends View {
 		this.set( 'text', '' );
 
 		/**
-		 * The position of the tooltip (south or north).
+		 * The position of the tooltip (south, south-west, or north).
 		 *
 		 *		+-----------+
 		 *		|   north   |
@@ -46,9 +46,15 @@ export default class TooltipView extends View {
 		 *		|   south   |
 		 *		+-----------+
 		 *
+		 *		         [element]
+		 *		             ^
+		 *		+--------------+
+		 *		|  south west  |
+		 *		+--------------+
+		 *
 		 * @observable
 		 * @default 's'
-		 * @member {'s'|'n'} #position
+		 * @member {'s'|'sw'|'n'} #position
 		 */
 		this.set( 'position', 's' );