Kamil Piechaczek пре 5 година
родитељ
комит
07283ce788

+ 38 - 2
packages/ckeditor5-html-embed/src/htmlembedcommand.js

@@ -8,6 +8,8 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
+import { findOptimalInsertionPosition } from '@ckeditor/ckeditor5-widget/src/utils';
+import { getSelectedRawHtmlModelWidget, insertRawHtml } from './utils';
 
 /**
  * The HTML embed command.
@@ -25,13 +27,47 @@ export default class HTMLEmbedCommand extends Command {
 	 * @inheritDoc
 	 */
 	refresh() {
+		const model = this.editor.model;
+		const selection = model.document.selection;
+		const schema = model.schema;
+		const insertPosition = findOptimalInsertionPosition( selection, model );
+		const selectedRawHtml = getSelectedRawHtmlModelWidget( selection );
+
+		let parent = insertPosition.parent;
+
+		// The model.insertContent() will remove empty parent (unless it is a $root or a limit).
+		if ( parent.isEmpty && !model.schema.isLimit( parent ) ) {
+			parent = parent.parent;
+		}
+
+		this.value = selectedRawHtml ? selectedRawHtml.getAttribute( 'value' ) : null;
+		this.isEnabled = schema.checkChild( parent, 'rawHtml' );
 	}
 
 	/**
-	 * Executes the command.
+	 * Executes the command, which either:
+	 *
+	 * * updates the URL of the selected media,
+	 * * inserts the new media into the editor and puts the selection around it.
 	 *
 	 * @fires execute
+	 * @param {Object} [options={}] The command options.
+	 * @param {String} [options.rawHtml] A HTML string that will be inserted into the editor.
+	 * @param {module:engine/model/element~Element|null} [options.element] If present, the `value` attribute will be updated
+	 * with the specified `options.rawHtml` value. Otherwise, a new element will be inserted into the editor.
 	 */
-	execute() {
+	execute( options = {} ) {
+		const model = this.editor.model;
+
+		const rawHtml = options.rawHtml;
+		const element = options.element;
+
+		if ( element ) {
+			model.change( writer => {
+				writer.setAttribute( 'value', rawHtml, element );
+			} );
+		} else {
+			insertRawHtml( model, rawHtml );
+		}
 	}
 }

+ 62 - 23
packages/ckeditor5-html-embed/src/htmlembedediting.js

@@ -12,8 +12,8 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
 import UpcastWriter from '@ckeditor/ckeditor5-engine/src/view/upcastwriter';
 import HTMLEmbedCommand from './htmlembedcommand';
-import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
 import { clone } from 'lodash-es';
+import { toRawHtmlWidget } from './utils';
 
 import '../theme/htmlembed.css';
 
@@ -40,10 +40,11 @@ export default class HTMLEmbedEditing extends Plugin {
 		const conversion = editor.conversion;
 		const viewDocument = editor.editing.view.document;
 
+		const htmlEmbedCommand = new HTMLEmbedCommand( editor );
 		const upcastWriter = new UpcastWriter( viewDocument );
 		const htmlProcessor = new HtmlDataProcessor( viewDocument );
 
-		const saniteHtmlConfig = getSaniteHtmlConfig( sanitizeHtml.defaults );
+		const sanitizeHtmlConfig = getSanitizeHtmlConfig( sanitizeHtml.defaults );
 
 		schema.register( 'rawHtml', {
 			isObject: true,
@@ -51,6 +52,8 @@ export default class HTMLEmbedEditing extends Plugin {
 			allowAttributes: [ 'value' ]
 		} );
 
+		editor.commands.add( 'htmlEmbed', htmlEmbedCommand );
+
 		conversion.for( 'upcast' ).elementToElement( {
 			view: {
 				name: 'div',
@@ -70,7 +73,7 @@ export default class HTMLEmbedEditing extends Plugin {
 			model: 'rawHtml',
 			view: ( modelElement, { writer } ) => {
 				return writer.createRawElement( 'div', { class: 'raw-html-embed' }, function( domElement ) {
-					domElement.innerHTML = modelElement.getAttribute( 'value' );
+					domElement.innerHTML = modelElement.getAttribute( 'value' ) || '';
 				} );
 			}
 		} );
@@ -79,42 +82,78 @@ export default class HTMLEmbedEditing extends Plugin {
 			model: 'rawHtml',
 			view: ( modelElement, { writer } ) => {
 				const label = t( 'HTML snippet' );
-				const viewWrapper = writer.createContainerElement( 'div' );
+				const viewWrapper = writer.createContainerElement( 'div', { 'data-cke-ignore-events': true } );
+
+				// Whether to show a preview mode or editing area.
+				let isPreviewActive = false;
+
+				// The editing raw HTML field.
+				const textarea = writer.createUIElement( 'textarea', { rows: 5 }, function( domDocument ) {
+					const root = this.toDomElement( domDocument );
+
+					root.value = modelElement.getAttribute( 'value' ) || '';
+
+					this.listenTo( root, 'input', () => {
+						htmlEmbedCommand.execute( {
+							rawHtml: root.value,
+							element: modelElement
+						} );
+					} );
+
+					return root;
+				} );
+
+				// The switch button between preview and editing HTML.
+				const toggleButton = writer.createUIElement( 'div', { class: 'raw-html__edit-preview' }, function( domDocument ) {
+					const root = this.toDomElement( domDocument );
 
+					// TODO: This event does not work.
+					this.listenTo( root, 'click', () => {
+						if ( isPreviewActive ) {
+							writer.removeClass( 'raw-html--active-preview', viewWrapper );
+						} else {
+							writer.addClass( 'raw-html--active-preview', viewWrapper );
+						}
+
+						isPreviewActive = !isPreviewActive;
+					} );
+
+					// The icon is used a temporary placeholder. Thanks to https://www.freepik.com/free-icon/eye_775336.htm.
+					// eslint-disable-next-line max-len
+					root.innerHTML = '<?xml version="1.0" encoding="iso-8859-1"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 456.795 456.795" style="enable-background:new 0 0 456.795 456.795;"xml:space="preserve"> <g> <g> <path d="M448.947,218.475c-0.922-1.168-23.055-28.933-61-56.81c-50.705-37.253-105.877-56.944-159.551-56.944 c-53.672,0-108.844,19.691-159.551,56.944c-37.944,27.876-60.077,55.642-61,56.81L0,228.397l7.846,9.923 c0.923,1.168,23.056,28.934,61,56.811c50.707,37.252,105.879,56.943,159.551,56.943c53.673,0,108.845-19.691,159.55-56.943 c37.945-27.877,60.078-55.643,61-56.811l7.848-9.923L448.947,218.475z M228.396,315.039c-47.774,0-86.642-38.867-86.642-86.642 c0-7.485,0.954-14.751,2.747-21.684l-19.781-3.329c-1.938,8.025-2.966,16.401-2.966,25.013c0,30.86,13.182,58.696,34.204,78.187 c-27.061-9.996-50.072-24.023-67.439-36.709c-21.516-15.715-37.641-31.609-46.834-41.478c9.197-9.872,25.32-25.764,46.834-41.478 c17.367-12.686,40.379-26.713,67.439-36.71l13.27,14.958c15.498-14.512,36.312-23.412,59.168-23.412 c47.774,0,86.641,38.867,86.641,86.642C315.037,276.172,276.17,315.039,228.396,315.039z M368.273,269.875 c-17.369,12.686-40.379,26.713-67.439,36.709c21.021-19.49,34.203-47.326,34.203-78.188s-13.182-58.697-34.203-78.188 c27.061,9.997,50.07,24.024,67.439,36.71c21.516,15.715,37.641,31.609,46.834,41.477 C405.91,238.269,389.787,254.162,368.273,269.875z"/> <path d="M173.261,211.555c-1.626,5.329-2.507,10.982-2.507,16.843c0,31.834,25.807,57.642,57.642,57.642 c31.834,0,57.641-25.807,57.641-57.642s-25.807-57.642-57.641-57.642c-15.506,0-29.571,6.134-39.932,16.094l28.432,32.048 L173.261,211.555z"/> </g> </g></svg>';
+
+					return root;
+				} );
+
+				// The container that renders the HTML.
 				const rawElement = writer.createRawElement( 'div', { class: 'raw-html-embed' }, function( domElement ) {
-					domElement.innerHTML = sanitizeHtml( modelElement.getAttribute( 'value' ), saniteHtmlConfig );
+					domElement.innerHTML = sanitizeHtml( modelElement.getAttribute( 'value' ) || '', sanitizeHtmlConfig );
 				} );
 
-				writer.insert( writer.createPositionAt( viewWrapper, 0 ), rawElement );
+				writer.insert( writer.createPositionAt( viewWrapper, 0 ), toggleButton );
+				writer.insert( writer.createPositionAt( viewWrapper, 1 ), textarea );
+				writer.insert( writer.createPositionAt( viewWrapper, 2 ), rawElement );
 
 				return toRawHtmlWidget( viewWrapper, writer, label );
 			}
 		} );
 
-		editor.commands.add( 'htmlEmbed', new HTMLEmbedCommand( editor ) );
+		// TODO: How to re-render the `rawElement`?
+		// conversion.for( 'editingDowncast' ).add( dispatcher => {
+		// 	dispatcher.on( 'attribute:value:rawHtml', ( evt, data, conversionApi ) => {
+		// 		const viewWrapper = conversionApi.mapper.toViewElement( data.item );
+		//
+		// 		console.log( viewWrapper );
+		// 	} );
+		// } );
 	}
 }
 
-// Converts a given {@link module:engine/view/element~Element} to a html widget:
-// * Adds a {@link module:engine/view/element~Element#_setCustomProperty custom property} allowing to
-//   recognize the html widget element.
-// * Calls the {@link module:widget/utils~toWidget} function with the proper element's label creator.
-//
-//  @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 } );
-}
-
 // Modifies the `defaultConfig` configuration and returns a new object that matches our needs. See #8204.
 //
 // @params {String} defaultConfig The default configuration that will be extended.
 // @returns {Object}
-function getSaniteHtmlConfig( defaultConfig ) {
+function getSanitizeHtmlConfig( defaultConfig ) {
 	const config = clone( defaultConfig );
 
 	config.allowedTags.push(

+ 1 - 1
packages/ckeditor5-html-embed/src/htmlembedui.js

@@ -36,7 +36,7 @@ export default class HTMLEmbedUI extends Plugin {
 
 			// Execute the command.
 			this.listenTo( view, 'execute', () => {
-				editor.execute( 'htmlEmbed', { html: '' } );
+				editor.execute( 'htmlEmbed' );
 				editor.editing.view.focus();
 			} );
 

+ 88 - 0
packages/ckeditor5-html-embed/src/utils.js

@@ -0,0 +1,88 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module html-embed/utils
+ */
+
+import { isWidget, toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
+
+/**
+ * Converts a given {@link module:engine/view/element~Element} to a html widget:
+ * * Adds a {@link module:engine/view/element~Element#_setCustomProperty custom property} allowing to recognize the html widget element.
+ * * Calls the {@link module:widget/utils~toWidget} function with the proper element's label creator.
+ *
+ *  @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}
+ */
+export function toRawHtmlWidget( viewElement, writer, label ) {
+	writer.setCustomProperty( 'rawHtml', true, viewElement );
+
+	return toWidget( viewElement, writer, { label } );
+}
+
+/**
+ * Returns a raw html widget editing view element if one is selected.
+ *
+ * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} selection
+ * @returns {module:engine/view/element~Element|null}
+ */
+export function getSelectedRawHtmlViewWidget( selection ) {
+	const viewElement = selection.getSelectedElement();
+
+	if ( viewElement && isRawHtmlWidget( viewElement ) ) {
+		return viewElement;
+	}
+
+	return null;
+}
+
+/**
+ * Checks if a given view element is a raw html widget.
+ *
+ * @param {module:engine/view/element~Element} viewElement
+ * @returns {Boolean}
+ */
+export function isRawHtmlWidget( viewElement ) {
+	return !!viewElement.getCustomProperty( 'rawHtml' ) && isWidget( viewElement );
+}
+
+/**
+ * Returns a selected raw html element in the model, if any.
+ *
+ * @param {module:engine/model/selection~Selection} selection
+ * @returns {module:engine/model/element~Element|null}
+ */
+export function getSelectedRawHtmlModelWidget( selection ) {
+	const selectedElement = selection.getSelectedElement();
+
+	if ( selectedElement && selectedElement.is( 'element', 'rawHtml' ) ) {
+		return selectedElement;
+	}
+
+	return null;
+}
+
+/**
+ * Creates a raw html element and inserts it into the model.
+ *
+ * **Note**: This method will use {@link module:engine/model/model~Model#insertContent `model.insertContent()`} logic of inserting content
+ * if no `insertPosition` is passed.
+ *
+ * @param {module:engine/model/model~Model} model
+ * @param {String} [rawHtml='']
+ */
+export function insertRawHtml( model, rawHtml = null ) {
+	model.change( writer => {
+		const rawHtmlElement = writer.createElement( 'rawHtml' );
+
+		model.insertContent( rawHtmlElement );
+
+		writer.setSelection( rawHtmlElement, 'on' );
+		writer.setAttribute( 'value', rawHtml, rawHtmlElement );
+	} );
+}

+ 52 - 0
packages/ckeditor5-html-embed/tests/manual/htmlembed.html

@@ -1,4 +1,56 @@
 <div id="editor">
+<!--    <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>-->
+
+
+<!--    <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>-->
+
+
+<!--    <div class="raw-html-embed">-->
+<!--        <picture>-->
+<!--            <source media="(min-width: 650px)" srcset="http://techslides.com/demos/samples/sample.jpg">-->
+<!--            <source media="(min-width: 465px)" srcset="http://techslides.com/demos/samples/sample.png">-->
+<!--            <img src="http://techslides.com/demos/samples/sample.gif">-->
+<!--        </picture>-->
+<!--    </div>-->
+
+
+<!--    <div class="raw-html-embed">-->
+<!--        <form action="/my-handling-form-page" method="post">-->
+<!--            <ul>-->
+<!--                <li>-->
+<!--                    <label for="name">Name:</label>-->
+<!--                    <input type="text" id="name" name="user_name">-->
+<!--                </li>-->
+<!--                <li>-->
+<!--                    <label for="mail">E-mail:</label>-->
+<!--                    <input type="email" id="mail" name="user_email">-->
+<!--                </li>-->
+<!--                <li>-->
+<!--                    <label for="msg">Message:</label>-->
+<!--                    <textarea id="msg" name="user_message"></textarea>-->
+<!--                </li>-->
+<!--            </ul>-->
+<!--        </form>-->
+<!--    </div>-->
+
+
+<!--    <div class="raw-html-embed">-->
+<!--        <iframe src="http://techslides.com/demos/samples/sample.txt"></iframe>-->
+<!--    </div>-->
+
+
     <div class="raw-html-embed">
         <h2>Ingredients</h2>
         <p>Ingredients required for making a coffee:</p>

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

@@ -2,3 +2,8 @@
  * Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
+
+.raw-html__edit-preview {
+	width: 100px;
+	height: 100px;
+}