8
0
Просмотр исходного кода

Basic code split between common widget feature and image feature.

Szymon Kupś 9 лет назад
Родитель
Сommit
b7443b1edd

+ 37 - 52
packages/ckeditor5-image/src/converters.js

@@ -8,45 +8,6 @@ import ViewEmptyElement from '../engine/view/emptyelement.js';
 import ModelElement from '../engine/model/element.js';
 import { isImageWidget } from './utils.js';
 
-const WIDGET_SELECTED_CLASS_NAME = 'ck-widget_selected';
-
-/**
- * Returns model to view selection converter. This converter is used after default selection conversion is made.
- * It creates fake view selection when {@link engine.view.Selection#getSelectedElement} returns instance of image widget.
- *
- * @param {Function} t {@link utils.Locale#t Locale#t function} used to translate default fake selection's label.
- * @returns {Function}
- */
-export function modelToViewSelection( t ) {
-	let previouslySelected;
-
-	return ( evt, data, consumable, conversionApi ) => {
-		const viewSelection = conversionApi.viewSelection;
-		const selectedElement = viewSelection.getSelectedElement();
-
-		// Remove selected class from previously selected widget.
-		if ( previouslySelected && previouslySelected.hasClass( WIDGET_SELECTED_CLASS_NAME ) ) {
-			previouslySelected.removeClass( WIDGET_SELECTED_CLASS_NAME );
-		}
-
-		if ( !selectedElement || !isImageWidget( selectedElement ) ) {
-			return;
-		}
-
-		let fakeSelectionLabel = t( 'image widget' );
-		const imgElement = selectedElement.getChild( 0 );
-		const altText = imgElement.getAttribute( 'alt' );
-
-		if ( altText ) {
-			fakeSelectionLabel = `${ altText } ${ fakeSelectionLabel }`;
-		}
-
-		viewSelection.setFake( true, { label: fakeSelectionLabel } );
-		selectedElement.addClass( WIDGET_SELECTED_CLASS_NAME );
-		previouslySelected = selectedElement;
-	};
-}
-
 /**
  * Returns function that converts image view representation:
  *
@@ -62,31 +23,27 @@ export function viewToModelImage() {
 	return ( evt, data, consumable, conversionApi ) => {
 		const viewFigureElement = data.input;
 
-		// Check if figure element from view can be consumed with `image` class.
-		if ( !consumable.consume( viewFigureElement, { name: true, class: 'image' } ) ) {
+		// *** Step 1: Validate conversion.
+		// Check if figure element can be consumed.
+		if ( !consumable.test( viewFigureElement, { name: true, class: 'image' } ) ) {
 			return;
 		}
 
-		// Check if image can be placed in current context.
+		// Check if image element can be converted in current context.
 		if ( !conversionApi.schema.check( { name: 'image', inside: data.context } ) ) {
 			return;
 		}
 
-		// Check if figure element have img inside.
-		if ( viewFigureElement.childCount != 1 ) {
-			return;
-		}
-
+		// Check if img element is placed inside figure element and can be consumed with `src` attribute.
 		const viewImg = viewFigureElement.getChild( 0 );
 
-		if ( viewImg.name != 'img' ) {
+		if ( !viewImg || viewImg.name != 'img' || !consumable.test( viewImg, { name: true, attribute: 'src' } ) ) {
 			return;
 		}
 
-		// Consume img element with src attribute.
-		if ( !consumable.consume( viewImg, { name: true, attribute: 'src' } ) ) {
-			return;
-		}
+		// *** Step2: Convert to model.
+		consumable.consume( viewFigureElement, { name: true, class: 'image' } );
+		consumable.consume( viewImg, { name: true, attribute: 'src' } );
 
 		// Create model element.
 		const modelImage = new ModelElement( 'image', {
@@ -102,6 +59,34 @@ export function viewToModelImage() {
 	};
 }
 
+/**
+ * Returns model to view selection converter. This converter is applied after default selection conversion is made.
+ * It creates fake view selection when {@link engine.view.Selection#getSelectedElement} returns instance of widget.
+ *
+ * @param {Function} t {@link utils.Locale#t Locale#t function} used to translate default fake selection's label.
+ * @returns {Function}
+ */
+export function modelToViewSelection( t ) {
+	return ( evt, data, consumable, conversionApi ) => {
+		const viewSelection = conversionApi.viewSelection;
+		const selectedElement = viewSelection.getSelectedElement();
+
+		if ( !selectedElement || !isImageWidget( selectedElement ) ) {
+			return;
+		}
+
+		let fakeSelectionLabel = t( 'image widget' );
+		const imgElement = selectedElement.getChild( 0 );
+		const altText = imgElement.getAttribute( 'alt' );
+
+		if ( altText ) {
+			fakeSelectionLabel = `${ altText } ${ fakeSelectionLabel }`;
+		}
+
+		viewSelection.setFake( true, { label: fakeSelectionLabel } );
+	};
+}
+
 /**
  * Converts model `image` element to view representation:
  *

+ 2 - 61
packages/ckeditor5-image/src/image.js

@@ -5,9 +5,7 @@
 
 import Feature from '../core/feature.js';
 import ImageEngine from './imageengine.js';
-import ModelRange from '../engine/model/range.js';
-import MouseObserver from '../engine/view/observer/mouseobserver.js';
-import { isImageWidget } from './utils.js';
+import Widget from './widget/widget.js';
 
 /**
  * The image feature.
@@ -22,63 +20,6 @@ export default class Image extends Feature {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ ImageEngine ];
+		return [ ImageEngine, Widget ];
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		const viewDocument = this.editor.editing.view;
-
-		// If mouse down is pressed create selection over whole view element.
-		viewDocument.addObserver( MouseObserver );
-		this.listenTo( viewDocument, 'mousedown', ( ...args ) => this._onMousedown( ...args ) );
-	}
-
-	/**
-	 * Handles `mousedown` event.
-	 *
-	 * @private
-	 * @param {utils.EventInfo} eventInfo
-	 * @param {envine.view.observer.DomEventData} domEventData
-	 */
-	_onMousedown( eventInfo, domEventData ) {
-		let widgetElement = domEventData.target;
-		const viewDocument = this.editor.editing.view;
-
-		// If target is not a widget element - check if one of the ancestors is.
-		if ( !isImageWidget( widgetElement ) ) {
-			widgetElement = widgetElement.findAncestor( element => isImageWidget( element ) );
-
-			if ( !widgetElement ) {
-				return;
-			}
-		}
-
-		domEventData.preventDefault();
-
-		// Focus editor if is not focused already.
-		if ( !viewDocument.isFocused ) {
-			viewDocument.focus();
-		}
-
-		createModelSelectionOverElement( this.editor, widgetElement );
-	}
-}
-
-// Creates model selection over provided {@link engine.view.Element view Element} and calls
-// {@link engine.model.document#engueueChanges} to set new selection in model.
-//
-// @private
-// @param {core.editor.Editor} editor
-// @param {core.view.Element} viewElement
-function createModelSelectionOverElement( editor, viewElement ) {
-	const document = editor.document;
-	const modelElement = editor.editing.mapper.toModelElement( viewElement );
-	const modelRange = ModelRange.createOn( modelElement );
-
-	document.enqueueChanges( ( ) => {
-		document.selection.setRanges( [ modelRange ] );
-	} );
 }

+ 14 - 28
packages/ckeditor5-image/src/imageengine.js

@@ -5,23 +5,26 @@
 
 import Feature from '../core/feature.js';
 import buildModelConverter from '../engine/conversion/buildmodelconverter.js';
-import {
-	modelToViewImage,
-	modelToViewSelection,
-	viewToModelImage
-} from './converters.js';
-
-const WIDGET_CLASS_NAME = 'ck-widget';
+import WidgetEngine from './widget/widgetengine.js';
+import { modelToViewImage, viewToModelImage, modelToViewSelection } from './converters.js';
+import { toImageWidget } from './utils.js';
 
 /**
  * The image engine feature.
- * Registers `image` as block element in document's schema and allows it to have two attributes: `src` and `alt`.
+ * Registers `image` as a block element in document's schema and allows it to have two attributes: `src` and `alt`.
  * Creates model converter for data and editing pipelines, and view converter for data pipeline.
  *
  * @memberof image
  * @extends core.Feature.
  */
 export default class ImageEngine extends Feature {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ WidgetEngine ];
+	}
+
 	/**
 	 * @inheritDoc
 	 */
@@ -43,29 +46,12 @@ export default class ImageEngine extends Feature {
 		// Build converter from model to view for editing pipeline.
 		buildModelConverter().for( editingPipeline.modelToView )
 			.fromElement( 'image' )
-			.toElement( ( data ) => widgetize( modelToViewImage( data.item ) ) );
+			.toElement( ( data ) => toImageWidget( modelToViewImage( data.item ) ) );
 
 		// Converter for figure element from view to model.
 		dataPipeline.viewToModel.on( 'element:figure', viewToModelImage() );
 
-		// Selection converter from view to model - applies fake selection if model selection is on widget.
-		editingPipeline.modelToView.on( 'selection', modelToViewSelection( editor.t ), { priority: 'low' } );
+		// Creates fake selection label if selection is placed around image widget.
+		editingPipeline.modelToView.on( 'selection', modelToViewSelection( editor.t ), { priority: 'lowest' } );
 	}
 }
-
-// "Widgetizes" provided {@link engie.view.ContainerElement} by:
-// - changing return value of {@link engine.view.ContainerElement#getFillerOffset} to `null`,
-// - adding `contenteditable="false"` attribute,
-// - adding `ck-widget` class,
-// - setting `element.isWidget` to true.
-//
-// @param {engine.view.ContainerElement} viewContainer
-// @returns {engine.view.ContainerElement}
-function widgetize( viewContainer ) {
-	viewContainer.getFillerOffset = () => null;
-	viewContainer.setAttribute( 'contenteditable', false );
-	viewContainer.addClass( WIDGET_CLASS_NAME );
-	viewContainer.isWidget = true;
-
-	return viewContainer;
-}

+ 19 - 2
packages/ckeditor5-image/src/utils.js

@@ -3,12 +3,29 @@
  * For licensing, see LICENSE.md.
  */
 
+import { widgetize, isWidget } from './widget/utils.js';
+
+const imageSymbol = Symbol( 'isImage' );
+
+/**
+ * Converts given {@link engine.view.Element} to image widget. Adds {@link engine.view.Element#addCustomProperty custom
+ * property} and calls {@link image.widget.utils.widgetize widgetize} method on given element.
+ *
+ * @param {engine.view.Element} viewElement
+ * @returns {engine.view.Element}
+ */
+export function toImageWidget( viewElement ) {
+	viewElement.setCustomProperty( imageSymbol, true );
+
+	return widgetize( viewElement );
+}
+
 /**
- * Checks if provided {@link engine.view.Element} is instance of image widget.
+ * Checks if given view element is image widget.
  *
  * @param {engine.view.Element} viewElement
  * @returns {Boolean}
  */
 export function isImageWidget( viewElement ) {
-	return viewElement.isWidget && viewElement.name == 'figure' && viewElement.hasClass( 'image' );
+	return viewElement.getCustomProperty( imageSymbol ) && isWidget( viewElement );
 }

+ 50 - 0
packages/ckeditor5-image/src/widget/utils.js

@@ -0,0 +1,50 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+const widgetSymbol = Symbol( 'isWidget' );
+
+/**
+ * CSS classes added to each widget element.
+ *
+ * @member {String} image.widget.utils.WIDGET_CLASS_NAME
+ */
+export const WIDGET_CLASS_NAME = 'ck-widget';
+
+/**
+ * CSS classes added to currently selected widget element.
+ *
+ * @member {String} image.widget.utils.WIDGET_SELECTED_CLASS_NAME
+ */
+export const WIDGET_SELECTED_CLASS_NAME = 'ck-widget_selected';
+
+/**
+ * Returns `true` if given {@link engine.view.Element} is a widget.
+ *
+ * @method image.widget.utils.isWidget
+ * @param {engine.view.Element} element
+ * @returns {Boolean}
+ */
+export function isWidget( element ) {
+	return !!element.getCustomProperty( widgetSymbol );
+}
+
+/**
+ * "Widgetizes" given {@link engine.view.Element}:
+ * * sets `contenteditable` attribue to `true`,
+ * * adds custom `getFillerOffset` method returning `null`,
+ * * adds `ck-widget` CSS class,
+ * * adds custom property allowing to recognize widget elements by using {@link image.widget.utils.isWidget}.
+ *
+ * @param {engine.view.Element} element
+ * @returns {engine.view.Element} Returns same element.
+ */
+export function widgetize( element ) {
+	element.setAttribute( 'contenteditable', false );
+	element.getFillerOffset = () => null;
+	element.addClass( WIDGET_CLASS_NAME );
+	element.setCustomProperty( widgetSymbol, true );
+
+	return element;
+}

+ 75 - 0
packages/ckeditor5-image/src/widget/widget.js

@@ -0,0 +1,75 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Feature from '../../core/feature.js';
+import WidgetEngine from './widgetengine.js';
+import MouseObserver from '../../engine/view/observer/mouseobserver.js';
+import ModelRange from '../../engine/model/range.js';
+import { isWidget } from './utils.js';
+
+/**
+ * The widget feature.
+ * Adds default {@link engine.view.Document#mousedown mousedown} handling on widget elements.
+ *
+ * @memberOf image.widget
+ * @extends core.Feature.
+ */
+export default class Widget extends Feature {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ WidgetEngine ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const viewDocument = this.editor.editing.view;
+
+		// If mouse down is pressed on widget - create selection over whole widget.
+		viewDocument.addObserver( MouseObserver );
+		this.listenTo( viewDocument, 'mousedown', ( ...args ) => this._onMousedown( ...args ) );
+	}
+
+	/**
+	 * Handles {@link engine.view.Document#mousedown mousedown} events on widget elements.
+	 *
+	 * @param {utils.EventInfo} eventInfo
+	 * @param {envine.view.observer.DomEventData} domEventData
+	 * @private
+	 */
+	_onMousedown( eventInfo, domEventData ) {
+		let widgetElement = domEventData.target;
+		const editor = this.editor;
+		const viewDocument = editor.editing.view;
+
+		// If target is not a widget element - check if one of the ancestors is.
+		if ( !isWidget( widgetElement ) ) {
+			widgetElement = widgetElement.findAncestor( element => isWidget( element ) );
+
+			if ( !widgetElement ) {
+				return;
+			}
+		}
+
+		domEventData.preventDefault();
+
+		// Focus editor if is not focused already.
+		if ( !viewDocument.isFocused ) {
+			viewDocument.focus();
+		}
+
+		// Create model selection over widget.
+		const document = editor.document;
+		const modelElement = editor.editing.mapper.toModelElement( widgetElement );
+		const modelRange = ModelRange.createOn( modelElement );
+
+		document.enqueueChanges( ( ) => {
+			document.selection.setRanges( [ modelRange ] );
+		} );
+	}
+}

+ 45 - 0
packages/ckeditor5-image/src/widget/widgetengine.js

@@ -0,0 +1,45 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Feature from '../../core/feature.js';
+import { WIDGET_SELECTED_CLASS_NAME, isWidget } from './utils.js';
+
+/**
+ * The widget engine feature.
+ * Registers model to view selection converter for editing pipeline. It is hooked after default selection conversion.
+ * If converted selection is placed around widget element, selection is marked as fake. Additionally, proper CSS class
+ * is added to indicate that widget has been selected.
+ *
+ * @memberOf image.widget
+ * @extends core.Feature.
+ */
+export default class WidgetEngine extends Feature {
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		let previouslySelected;
+
+		// Model to view selection converter.
+		// Converts selection placed over widget element to fake selection
+		this.editor.editing.modelToView.on( 'selection', ( evt, data, consumable, conversionApi ) => {
+			// Remove selected class from previously selected widget.
+			if ( previouslySelected && previouslySelected.hasClass( WIDGET_SELECTED_CLASS_NAME ) ) {
+				previouslySelected.removeClass( WIDGET_SELECTED_CLASS_NAME );
+			}
+
+			const viewSelection = conversionApi.viewSelection;
+			const selectedElement = viewSelection.getSelectedElement();
+
+			if ( !selectedElement || !isWidget( selectedElement ) ) {
+				return;
+			}
+
+			viewSelection.setFake( true );
+			selectedElement.addClass( WIDGET_SELECTED_CLASS_NAME );
+			previouslySelected = selectedElement;
+		}, { priority: 'low' } );
+	}
+}

+ 3 - 4
packages/ckeditor5-image/tests/imageengine.js

@@ -5,6 +5,7 @@
 
 import VirtualTestEditor from 'tests/core/_utils/virtualtesteditor.js';
 import ImageEngine from 'ckeditor5/image/imageengine.js';
+import WidgetElement from 'ckeditor5/image/widgetelement.js';
 import { getData as getModelData, setData as setModelData } from 'ckeditor5/engine/dev-utils/model.js';
 import { getData as getViewData } from 'ckeditor5/engine/dev-utils/view.js';
 import buildViewConverter from 'ckeditor5/engine/conversion/buildviewconverter.js';
@@ -141,14 +142,12 @@ describe( `ImageEngine`, () => {
 					.to.equal( '<figure class="image ck-widget" contenteditable="false"><img alt="alt text" src="foo.png"></img></figure>' );
 			} );
 
-			it( 'should widgetize element', () => {
+			it( 'converted element should be instance of WidgetElement', () => {
 				setModelData( document, '<image src="foo.png" alt="alt text"></image>' );
 				const figure = viewDocument.getRoot().getChild( 0 );
 
 				expect( figure.name ).to.equal( 'figure' );
-				expect( figure.isWidget ).to.be.true;
-				expect( figure.getFillerOffset() ).to.be.null;
-				expect( figure.hasClass( 'ck-widget' ) ).to.be.true;
+				expect( figure ).to.be.instanceof( WidgetElement );
 			} );
 		} );
 	} );