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

Created separate file for converters. Moved selection conversion form Image to ImageEngine.

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

+ 161 - 0
packages/ckeditor5-image/src/converters.js

@@ -0,0 +1,161 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ViewContainerElement from '../engine/view/containerelement.js';
+import ViewEmptyElement from '../engine/view/emptyelement.js';
+import ModelElement from '../engine/model/element.js';
+
+const WIDGET_CLASS_NAME = 'ck-widget';
+const WIDGET_SELECTED_CLASS_NAME = 'ck-widget_selected';
+
+/**
+ * Returns model to view selection converter. If model selection is placed around image widget:
+ *
+ *		[<image src="..." alt="..."></image>]
+ *
+ * then it will be converted into fake selection.
+ *
+ * @returns {Function}
+ */
+export function modelToViewSelection() {
+	let selected;
+
+	return ( evt, data, consumable, conversionApi ) => {
+		const viewSelection = conversionApi.viewSelection;
+		const modelSelection = data.selection;
+
+		const nodeAfter = modelSelection.anchor.nodeAfter;
+		const nodeBefore = modelSelection.focus.nodeBefore;
+
+		if ( selected && selected.hasClass( WIDGET_SELECTED_CLASS_NAME ) ) {
+			selected.removeClass( WIDGET_SELECTED_CLASS_NAME );
+		}
+
+		// Check if model selection is over image and create fake selection in the view.
+		if ( !modelSelection.isCollapsed && nodeAfter && nodeAfter.name == 'image' && nodeAfter == nodeBefore ) {
+			let fakeSelectionLabel = 'image widget';
+			const altText = nodeAfter.getAttribute( 'alt' );
+
+			if ( altText ) {
+				fakeSelectionLabel = `${ altText } ${ fakeSelectionLabel }`;
+			}
+
+			viewSelection.setFake( true, { label: fakeSelectionLabel } );
+			const viewElement = conversionApi.mapper.toViewElement( nodeAfter );
+			viewElement.addClass( WIDGET_SELECTED_CLASS_NAME );
+			selected = viewElement;
+		}
+	};
+}
+
+/**
+ * Returns function that converts image view representation:
+ *
+ *		<figure class="image"><img src="..." alt="..."></img></figure>
+ *
+ * to model representation:
+ *
+ *		<image src="..." alt="..."></image>
+ *
+ * @returns {Function}
+ */
+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' } ) ) {
+			return;
+		}
+
+		// Check if image can be placed 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;
+		}
+
+		const viewImg = viewFigureElement.getChild( 0 );
+
+		if ( viewImg.name != 'img' ) {
+			return;
+		}
+
+		// Consume img element.
+		if ( !consumable.consume( viewImg, { name: true } ) ) {
+			return;
+		}
+
+		// Create model element.
+		const modelImage = new ModelElement( 'image' );
+
+		// Add src if one is present.
+		if ( consumable.consume( viewImg, { attributes: [ 'src' ] } ) ) {
+			modelImage.setAttribute( 'src', viewImg.getAttribute( 'src' ) );
+		}
+
+		// Add alt if one is present.
+		if ( consumable.consume( viewImg, { attributes: [ 'alt' ] } ) ) {
+			modelImage.setAttribute( 'alt', viewImg.getAttribute( 'alt' ) );
+		}
+
+		data.output = modelImage;
+	};
+}
+
+/**
+ * Returns function that converts model `image` element to view representation.
+ * For data pipeline model image will be converted to:
+ *
+ *		<figure class="image"><img src="..." alt="..."></img></figure>
+ *
+ * For editing pipeline image will be "widgetized" and it will be converted to:
+ *
+ *		<figure class="image ck-widget" contenteditable="false"><img src="..." alt="..."></img></figure>
+ *
+ * @param {Boolean} [isDataPipeline=false] If true, figure element will not be "widgetized" as conversion is taking place
+ * in data pipeline.
+ * @returns {Function}
+ */
+export function modelToViewImage( isDataPipeline = false ) {
+	return ( data ) => {
+		const modelElement = data.item;
+		const viewImg = new ViewEmptyElement( 'img' );
+
+		if ( modelElement.hasAttribute( 'src' ) ) {
+			viewImg.setAttribute( 'src', modelElement.getAttribute( 'src' ) );
+		}
+
+		if ( modelElement.hasAttribute( 'alt' ) ) {
+			viewImg.setAttribute( 'alt', modelElement.getAttribute( 'alt' ) );
+		}
+
+		return isDataPipeline ?
+			new ViewContainerElement( 'figure', { class: 'image' }, viewImg ) :
+			widgetize( new ViewContainerElement( 'figure', { class: 'image' }, viewImg ) );
+	};
+}
+
+/**
+ * "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;
+}

+ 43 - 123
packages/ckeditor5-image/src/image.js

@@ -5,11 +5,7 @@
 
 import Feature from '../core/feature.js';
 import ImageEngine from './imageengine.js';
-import ViewRange from '../engine/view/range.js';
-import ModelLiveRange from '../engine/model/liverange.js';
 import ModelRange from '../engine/model/range.js';
-import ViewPosition from '../engine/view/position.js';
-import ViewSelection from '../engine/view/selection.js';
 import MouseDownObserver from './mousedownobserver.js';
 
 export default class Image extends Feature {
@@ -21,135 +17,59 @@ export default class Image extends Feature {
 	}
 
 	init() {
-		const editor = this.editor;
-		const modelDocument = editor.document;
-		const viewDocument = editor.editing.view;
-		const editingPipeline = editor.editing;
-		const selected = new Set();
+		const viewDocument = this.editor.editing.view;
 
-		// Prevent selection change on mousedown.
+		// If mouse down is pressed create selection over whole view element.
 		viewDocument.addObserver( MouseDownObserver );
-		viewDocument.on( 'mousedown', ( eventInfo, domEventData ) => {
-			const target = domEventData.target;
-
-			if ( isImageWidget( target ) ) {
-				domEventData.preventDefault();
-
-				if ( !viewDocument.isFocused ) {
-					viewDocument.focus();
-				}
-
-				const viewRange = ViewRange.createOn( target );
-				const modelRange = ModelLiveRange.createFromRange( editingPipeline.mapper.toModelRange( viewRange ) );
-
-				modelDocument.enqueueChanges( ( ) => {
-					modelDocument.selection.setRanges( [ modelRange ] );
-				} );
-			}
-		} );
-
-		// Handle selection placing inside figure - convert it to selecting whole image widget.
-		viewDocument.on( 'selectionChange', ( eventInfo, data ) => {
-			const newSelection = data.newSelection;
-
-			if ( newSelection.rangeCount && newSelection.getFirstPosition().parent.name == 'figure' ) {
-				eventInfo.stop();
-
-				const viewRange = ViewRange.createOn( newSelection.getFirstPosition().parent );
-				const modelRange = ModelLiveRange.createFromRange( editingPipeline.mapper.toModelRange( viewRange ) );
-
-				modelDocument.enqueueChanges( ( ) => {
-					modelDocument.selection.setRanges( [ modelRange ] );
-				} );
-			}
-		}, { priority: 'high' } );
-
-		viewDocument.on( 'keydown', ( eventInfo, data ) => {
-			const viewSelection = viewDocument.selection;
-
-			if ( viewSelection.isFake && !viewSelection.isCollapsed && viewSelection.rangeCount == 1 ) {
-				const nodeAfter = viewSelection.anchor.nodeAfter;
-				const nodeBefore = viewSelection.focus.nodeBefore;
-
-				if ( nodeAfter === nodeBefore && isImageWidget( nodeAfter ) ) {
-					// Stop to prevent default fake selection handling.
-					eventInfo.stop();
-
-					// Prevent selection change.
-					data.preventDefault();
-
-					if ( data.keyCode == 39 || data.keyCode == 40 ) {
-						// TODO: Check if sibling exists.
-						// const modelElement = editingPipeline.mapper.toModelElement( nodeAfter.nextSibling );
-						// const modelPosition = ModelPosition.createAt( modelElement );
-						// const modelRange = new ModelLiveRange( modelPosition );
-						//
-						// modelDocument.enqueueChanges( () => {
-						// 	modelDocument.selection.setRanges( [ modelRange ] );
-						// } );
-						const viewPosition = ViewPosition.createAt( nodeAfter.nextSibling );
-						const viewRange = new ViewRange( viewPosition );
-						const newSelection = new ViewSelection();
-
-						newSelection.addRange( viewRange );
-
-						viewDocument.fire( 'selectionChange', {
-							oldSelection: viewDocument.selection,
-							newSelection: newSelection,
-							domSelection: null
-						} );
-					}
-
-					if ( data.keyCode == 37 || data.keyCode == 38 ) {
-						// const modelElement = editingPipeline.mapper.toModelElement( nodeAfter.previousSibling );
-						// const modelPosition = ModelPosition.createAt( modelElement, 'end' );
-						// const modelRange = new ModelLiveRange( modelPosition );
-						//
-						// modelDocument.enqueueChanges( () => {
-						// 	modelDocument.selection.setRanges( [ modelRange ] );
-						// } );
-
-						const viewPosition = ViewPosition.createAt( nodeAfter.previousSibling, 'end' );
-						const viewRange = new ViewRange( viewPosition );
-						const newSelection = new ViewSelection();
-
-						newSelection.addRange( viewRange );
+		this.listenTo( viewDocument, 'mousedown', this._onMouseDown, { context: this } );
+	}
 
-						viewDocument.fire( 'selectionChange', {
-							oldSelection: viewDocument.selection,
-							newSelection: newSelection,
-							domSelection: null
-						} );
-					}
-				}
-			}
-		} );
+	/**
+	 * Handles `mousedown` event. If image widget is event's target then new selection in model is created around that
+	 * element.
+	 *
+	 * @private
+	 * @param {utils.EventInfo} eventInfo
+	 * @param {envine.view.observer.DomEventData} domEventData
+	 */
+	_onMouseDown( eventInfo, domEventData ) {
+		const target = domEventData.target;
+		const viewDocument = this.editor.editing.view;
 
-		// Handle selection conversion.
-		editingPipeline.modelToView.on( 'selection', ( evt, data, consumable, conversionApi ) => {
-			const viewSelection = conversionApi.viewSelection;
-			const range = data.selection.getFirstRange();
-			const nodeAfterStart = range.start.nodeAfter;
+		if ( isImageWidget( target ) ) {
+			domEventData.preventDefault();
 
-			// remove selection from all selected widgets
-			for ( let viewElement of selected ) {
-				viewElement.removeClass( 'selected' );
+			// Focus editor if is not focused already.
+			if ( !viewDocument.isFocused ) {
+				viewDocument.focus();
 			}
 
-			// This could be just one element instead of set.
-			selected.clear();
-
-			if ( !data.selection.isCollapsed && nodeAfterStart && nodeAfterStart.name == 'image' && ModelRange.createOn( nodeAfterStart ).isEqual( range ) ) {
-				viewSelection.setFake( true, { label: 'image fake selection' } );
-				const viewElement = conversionApi.mapper.toViewElement( nodeAfterStart );
-				viewElement.addClass( 'selected' );
-				selected.add( viewElement );
-			}
-		}, { priority: 'low' } );
+			createModelSelectionOverElement( this.editor, target );
+		}
 	}
 }
 
+// Checks if provided {@link engine.view.Element} is instance of image widget.
+//
+// @private
+// @param {engine.view.Element} viewElement
+// @returns {Boolean}
 function isImageWidget( viewElement ) {
-	// TODO: check CSS class.
 	return viewElement.isWidget && viewElement.name == 'figure' && viewElement.hasClass( 'image' );
 }
+
+// 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 ] );
+	} );
+}

+ 17 - 88
packages/ckeditor5-image/src/imageengine.js

@@ -5,26 +5,19 @@
 
 import Feature from '../core/feature.js';
 import buildModelConverter from '../engine/conversion/buildmodelconverter.js';
-import ViewContainerElement from '../engine/view/containerelement.js';
-import ViewWidgetElement from '../engine/view/widgetelement.js';
-import ViewEmptyElement from '../engine/view/emptyelement.js';
-import ModelElement from '../engine/model/element.js';
+import {
+	modelToViewImage,
+	modelToViewSelection,
+	viewToModelImage
+} from './converters.js';
 
 /**
  * The image engine feature.
  * Registers `image` as 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.
  *
- * Creates model to view converters for data and editing pipelines. These converts turns image model representation
- * to view representation:
- *
- *		<figure class="image" contenteditable="false">
- *			<img src="<source>" alt="<alternative>" />
- *		</figure>
- *
- * Creates also a view to model converter for data pipeline.
- *
- * memberof image
- * extends core.Feature.
+ * @memberof image
+ * @extends core.Feature.
  */
 export default class ImageEngine extends Feature {
 	/**
@@ -32,92 +25,28 @@ export default class ImageEngine extends Feature {
 	 */
 	init() {
 		const editor = this.editor;
-		const modelDocument = editor.document;
+		const document = editor.document;
 		const dataPipeline = editor.data;
 		const editingPipeline = editor.editing;
 
 		// Configure schema.
-		modelDocument.schema.registerItem( 'image', '$block' );
-		modelDocument.schema.allow( { name: 'image', attributes: [ 'src', 'alt' ] } );
+		document.schema.registerItem( 'image', '$block' );
+		document.schema.allow( { name: 'image', attributes: [ 'src', 'alt' ] } );
 
 		// Build converter from model to view for data pipeline.
 		buildModelConverter().for( dataPipeline.modelToView )
 			.fromElement( 'image' )
-			.toElement( ( data ) => {
-				const modelElement = data.item;
-				const viewImg = new ViewEmptyElement( 'img' );
-
-				if ( modelElement.hasAttribute( 'src' ) ) {
-					viewImg.setAttribute( 'src', modelElement.getAttribute( 'src' ) );
-				}
-
-				if ( modelElement.hasAttribute( 'alt' ) ) {
-					viewImg.setAttribute( 'alt', modelElement.getAttribute( 'alt' ) );
-				}
-
-				return new ViewContainerElement( 'figure', { class: 'image' }, viewImg );
-			} );
+			.toElement( modelToViewImage( true ) );
 
 		// Build converter from model to view for editing pipeline.
 		buildModelConverter().for( editingPipeline.modelToView )
 			.fromElement( 'image' )
-			.toElement( ( data ) => {
-				const modelElement = data.item;
-				const viewImg = new ViewEmptyElement( 'img' );
-
-				if ( modelElement.hasAttribute( 'src' ) ) {
-					viewImg.setAttribute( 'src', modelElement.getAttribute( 'src' ) );
-				}
-
-				if ( modelElement.hasAttribute( 'alt' ) ) {
-					viewImg.setAttribute( 'alt', modelElement.getAttribute( 'alt' ) );
-				}
-
-				return new ViewWidgetElement( 'figure', { class: 'image' }, viewImg );
-			} );
-
-		dataPipeline.viewToModel.on( 'element:figure', ( evt, data, consumable, conversionApi ) => {
-			const viewElement = data.input;
-
-			// Check if image can be converted in current context.
-			if ( !conversionApi.schema.check( { name: 'image', inside: data.context } ) ) {
-				return;
-			}
-
-			// Check if figure element have img inside.
-			if ( viewElement.childCount != 1 ) {
-				return;
-			}
-
-			const viewImg = viewElement.getChild( 0 );
-
-			if ( viewImg.name != 'img' ) {
-				return;
-			}
-
-			// Consume img name and figure element.
-			if ( !consumable.consume( viewImg, { name: true } ) ) {
-				return;
-			}
-
-			if ( !consumable.consume( viewElement, { name: true, class: 'image' } ) ) {
-				return;
-			}
-
-			// Create model element.
-			const modelImage = new ModelElement( 'image' );
-
-			// Add src if one is present.
-			if ( viewImg.hasAttribute( 'src' ) && consumable.consume( viewImg, { attributes: [ 'src' ] } ) ) {
-				modelImage.setAttribute( 'src', viewImg.getAttribute( 'src' ) );
-			}
+			.toElement( modelToViewImage() );
 
-			// Add alt if one is present.
-			if ( viewImg.hasAttribute( 'alt' ) && consumable.consume( viewImg, { attributes: [ 'alt' ] } ) ) {
-				modelImage.setAttribute( 'alt', viewImg.getAttribute( 'alt' ) );
-			}
+		// Converter for figure element from view to model.
+		dataPipeline.viewToModel.on( 'element:figure', viewToModelImage() );
 
-			data.output = modelImage;
-		} );
+		// Selection converter from view to model - applies fake selection if model selection is on widget.
+		editingPipeline.modelToView.on( 'selection', modelToViewSelection(), { priority: 'low' } );
 	}
 }

+ 0 - 121
packages/ckeditor5-image/src/tmp.js

@@ -1,121 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* globals console:false, document, window */
-
-import ClassicEditor from '/ckeditor5/editor-classic/classic.js';
-import testUtils from '/tests/utils/_utils/utils.js';
-import Feature from '/ckeditor5/core/feature.js';
-import buildModelConverter from '/ckeditor5/engine/conversion/buildmodelconverter.js';
-import buildViewConverter from '/ckeditor5/engine/conversion/buildviewconverter.js';
-import ViewContainerElement from '/ckeditor5/engine/view/containerelement.js';
-import ViewRange from '/ckeditor5/engine/view/range.js';
-import ModelElement from '/ckeditor5/engine/model/element.js';
-import ModelRange from '/ckeditor5/engine/model/range.js';
-import ModelLiveRange from '/ckeditor5/engine/model/liverange.js';
-import ClickObserver from '/ckeditor5/engine/view/observer/clickobserver.js';
-
-let editor, editable, observer;
-
-class BasicImage extends Feature {
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		const editor = this.editor;
-		const data = editor.data;
-		const editing = editor.editing;
-		const view  = editing.view;
-		const document = editor.document;
-		const selected = new Set();
-
-		// Handle clicks on view elements.
-		view.addObserver( ClickObserver );
-		editor.editing.view.on( 'click', ( eventInfo, domEventData ) => {
-			const target = domEventData.target;
-
-			if ( target.name == 'img' ) {
-				const viewRange = ViewRange.createOn( target );
-				const modelRange = ModelLiveRange.createFromRange( editing.mapper.toModelRange( viewRange ) );
-
-				document.enqueueChanges( ( ) => {
-					document.selection.setRanges( [ modelRange ] );
-				} );
-			}
-		} );
-
-		// Handle selection conversion.
-		editing.modelToView.on( 'selection', ( evt, data, consumable, conversionApi ) => {
-			const viewSelection = conversionApi.viewSelection;
-			const range = data.selection.getFirstRange();
-			const nodeAfterStart = range.start.nodeAfter;
-
-			// remove selection from all selected widgets
-			for ( let viewElement of selected ) {
-				viewElement.setStyle( 'border', '2px solid yellow' );
-			}
-
-			// This could be just one element instead of set.
-			selected.clear();
-
-			if ( !data.selection.isCollapsed && nodeAfterStart && nodeAfterStart.name == 'image' && ModelRange.createOn( nodeAfterStart ).isEqual( range ) ) {
-				viewSelection.setFake( true, { label: 'image fake selection' } );
-				const viewElement = conversionApi.mapper.toViewElement( nodeAfterStart );
-				viewElement.setStyle( 'border', '2px solid red' );
-				selected.add( viewElement );
-			}
-		}, { priority: 'low' } );
-
-		// Allow bold attribute on all inline nodes.
-		// document.schema.allow( { name: '$inline', attributes: [ 'span' ] } );
-		document.schema.registerItem( 'image', '$block' );
-		document.schema.allow( { name: 'image', attributes: [ 'src', 'alt' ] } );
-
-		// Build converter from model to view for data and editing pipelines.
-		buildModelConverter().for( data.modelToView, editing.modelToView )
-			.fromElement( 'image' )
-			.toElement( ( data ) => {
-				return widgetize( new ViewContainerElement( 'img', {
-					src: data.item.getAttribute( 'src' ),
-					alt: data.item.getAttribute( 'alt' ),
-					style: 'border: 2px solid yellow',
-					class: 'ck-image'
-				} ) );
-			} );
-
-		// Build converter from view to model for data pipeline.
-		buildViewConverter().for( data.viewToModel )
-			.fromElement( 'img' ).consuming( { name: true, attributes: [ 'src', 'alt' ] } )
-			.toElement( ( viewElement ) => new ModelElement( 'image', {
-				src: viewElement.getAttribute( 'src' ),
-				alt: viewElement.getAttribute( 'alt' )
-			} ) );
-	}
-}
-
-function initEditor() {
-	ClassicEditor.create( document.querySelector( '#editor' ), {
-		features: [ 'enter', 'typing', 'paragraph', 'undo', 'heading', Test, BasicImage ],
-		toolbar: [ 'headings' ]
-	} )
-		.then( newEditor => {
-			window.editor = editor = newEditor;
-			window.editable = editable = editor.editing.view.getRoot();
-
-			observer = testUtils.createObserver();
-			observer.observe( 'Editable', editable, [ 'isFocused' ] );
-		} )
-		.catch( err => {
-			console.error( err.stack );
-		} );
-}
-
-initEditor();
-
-function widgetize( element ) {
-	element.isWidget = true;
-
-	return element;
-}