Explorar o código

Merge branch 'master' into docs-fixes

Maciek %!s(int64=9) %!d(string=hai) anos
pai
achega
c918101c9f

+ 24 - 0
packages/ckeditor5-engine/src/view/element.js

@@ -12,6 +12,7 @@ import Text from './text.js';
 import objectToMap from '../../utils/objecttomap.js';
 import objectToMap from '../../utils/objecttomap.js';
 import isIterable from '../../utils/isiterable.js';
 import isIterable from '../../utils/isiterable.js';
 import isPlainObject from '../../utils/lib/lodash/isPlainObject.js';
 import isPlainObject from '../../utils/lib/lodash/isPlainObject.js';
+import Matcher from './matcher.js';
 
 
 /**
 /**
  * View element.
  * View element.
@@ -572,6 +573,29 @@ export default class Element extends Node {
 		this._fireChange( 'attributes', this );
 		this._fireChange( 'attributes', this );
 		property.forEach( name => this._styles.delete( name ) );
 		property.forEach( name => this._styles.delete( name ) );
 	}
 	}
+
+	/**
+	 * Returns ancestor element that match specified pattern.
+	 * Provided patterns should be compatible with {@link engine.view.Matcher Matcher} as it is used internally.
+	 *
+	 * @see engine.view.Matcher
+	 * @param {Object|String|RegExp|Function} patterns Patterns used to match correct ancestor. See {@link engine.view.Matcher}.
+	 * @returns {engine.view.Element|null} Found element or `null` if no matching ancestor was found.
+	 */
+	findAncestor( ...patterns ) {
+		const matcher = new Matcher( ...patterns );
+		let parent = this.parent;
+
+		while ( parent ) {
+			if ( matcher.match( parent ) ) {
+				return parent;
+			}
+
+			parent = parent.parent;
+		}
+
+		return null;
+	}
 }
 }
 
 
 // Parses inline styles and puts property - value pairs into styles map.
 // Parses inline styles and puts property - value pairs into styles map.

+ 3 - 3
packages/ckeditor5-engine/src/view/matcher.js

@@ -90,7 +90,7 @@ export default class Matcher {
 	 *
 	 *
 	 * 		matcher.add( 'div', { class: 'foobar' } );
 	 * 		matcher.add( 'div', { class: 'foobar' } );
 	 *
 	 *
-	 * @param {Object|String|RegExp|function} pattern Object describing pattern details. If string or regular expression
+	 * @param {Object|String|RegExp|Function} pattern Object describing pattern details. If string or regular expression
 	 * is provided it will be used to match element's name. Pattern can be also provided in a form
 	 * is provided it will be used to match element's name. Pattern can be also provided in a form
 	 * of a function - then this function will be called with each {@link engine.view.Element element} as a parameter.
 	 * of a function - then this function will be called with each {@link engine.view.Element element} as a parameter.
 	 * Function's return value will be stored under `match` key of the object returned from
 	 * Function's return value will be stored under `match` key of the object returned from
@@ -143,7 +143,7 @@ export default class Matcher {
 	 * @param {...core.view.Element} element View element to match against stored patterns.
 	 * @param {...core.view.Element} element View element to match against stored patterns.
 	 * @returns {Object|null} result
 	 * @returns {Object|null} result
 	 * @returns {core.view.Element} result.element Matched view element.
 	 * @returns {core.view.Element} result.element Matched view element.
-	 * @returns {Object|String|RegExp|function} result.pattern Pattern that was used to find matched element.
+	 * @returns {Object|String|RegExp|Function} result.pattern Pattern that was used to find matched element.
 	 * @returns {Object} result.match Object representing matched element parts.
 	 * @returns {Object} result.match Object representing matched element parts.
 	 * @returns {Boolean} [result.match.name] True if name of the element was matched.
 	 * @returns {Boolean} [result.match.name] True if name of the element was matched.
 	 * @returns {Array} [result.match.attribute] Array with matched attribute names.
 	 * @returns {Array} [result.match.attribute] Array with matched attribute names.
@@ -216,7 +216,7 @@ export default class Matcher {
 // If element cannot be matched to provided pattern - returns `null`.
 // If element cannot be matched to provided pattern - returns `null`.
 //
 //
 // @param {engine.view.Element} element
 // @param {engine.view.Element} element
-// @param {Object|String|RegExp|function} pattern
+// @param {Object|String|RegExp|Function} pattern
 // @returns {Object|null} Returns object with match information or null if element is not matching.
 // @returns {Object|null} Returns object with match information or null if element is not matching.
 function isElementMatching( element, pattern ) {
 function isElementMatching( element, pattern ) {
 	// If pattern is provided as function - return result of that function;
 	// If pattern is provided as function - return result of that function;

+ 40 - 0
packages/ckeditor5-engine/src/view/observer/mouseobserver.js

@@ -0,0 +1,40 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import DomEventObserver from './domeventobserver.js';
+
+/**
+ * Mouse events observer.
+ *
+ * Note that this observer is not available by default. To make it available it needs to be added to {@link engine.view.Document}
+ * by {@link engine.view.Document#addObserver} method.
+ *
+ * @memberOf engine.view.observer
+ * @extends engine.view.observer.DomEventObserver
+ */
+export default class MouseObserver extends DomEventObserver {
+	constructor( document ) {
+		super( document );
+
+		this.domEventType = 'mousedown';
+	}
+
+	onDomEvent( domEvent ) {
+		this.fire( domEvent.type, domEvent );
+	}
+}
+
+/**
+ * Fired when mouse button is pressed down on one of the editables.
+ *
+ * Introduced by {@link engine.view.observer.MouseObserver}.
+ *
+ * Note that this event is not available by default. To make it available {@link engine.view.observer.MouseObserver}
+ * needs to be added to {@link engine.view.Document} by a {@link engine.view.Document#addObserver} method.
+ *
+ * @see engine.view.observer.MouseObserver
+ * @event engine.view.Document#mousedown
+ * @param {engine.view.observer.DomEventData} data Event data.
+ */

+ 20 - 0
packages/ckeditor5-engine/src/view/selection.js

@@ -12,6 +12,7 @@ import Range from './range.js';
 import Position from './position.js';
 import Position from './position.js';
 import mix from '../../utils/mix.js';
 import mix from '../../utils/mix.js';
 import EmitterMixin from '../../utils/emittermixin.js';
 import EmitterMixin from '../../utils/emittermixin.js';
+import Element from './element.js';
 
 
 /**
 /**
  * Class representing selection in tree view.
  * Class representing selection in tree view.
@@ -464,6 +465,25 @@ export default class Selection {
 		}
 		}
 	}
 	}
 
 
+	/**
+	 * Returns the selected element. {@link engine.view.Element Element} is considered as selected if there is only
+	 * one range in the selection, and that range contains exactly one element.
+	 * Returns `null` if there is no selected element.
+	 *
+	 * @returns {engine.view.Element|null}
+	 */
+	getSelectedElement() {
+		if ( this.rangeCount !== 1 ) {
+			return null;
+		}
+
+		const range = this.getFirstRange();
+		const nodeAfterStart = range.start.nodeAfter;
+		const nodeBeforeEnd = range.end.nodeBefore;
+
+		return ( nodeAfterStart instanceof Element && nodeAfterStart == nodeBeforeEnd ) ? nodeAfterStart : null;
+	}
+
 	/**
 	/**
 	 * Creates and returns an instance of `Selection` that is a clone of given selection, meaning that it has same
 	 * Creates and returns an instance of `Selection` that is a clone of given selection, meaning that it has same
 	 * ranges and same direction as this selection.
 	 * ranges and same direction as this selection.

+ 33 - 0
packages/ckeditor5-engine/tests/view/element.js

@@ -784,4 +784,37 @@ describe( 'Element', () => {
 			} );
 			} );
 		} );
 		} );
 	} );
 	} );
+
+	describe( 'findAncestor', () => {
+		it( 'should return null if element have no ancestor', () => {
+			const el = new Element( 'p' );
+
+			expect( el.findAncestor( 'div' ) ).to.be.null;
+		} );
+
+		it( 'should return ancestor if matching', () => {
+			const el1 = new Element( 'p' );
+			const el2 = new Element( 'div', null, el1 );
+
+			expect( el1.findAncestor( 'div' ) ).to.equal( el2 );
+		} );
+
+		it( 'should return parent\'s ancestor if matching', () => {
+			const el1 = new Element( 'p' );
+			const el2 = new Element( 'div', null, el1 );
+			const el3 = new Element( 'div', { class: 'foo bar' }, el2 );
+
+			expect( el1.findAncestor( { class: 'foo' } ) ).to.equal( el3 );
+		} );
+
+		it( 'should return null if no matches found', () => {
+			const el1 = new Element( 'p' );
+			new Element( 'div', null, el1 );
+
+			expect( el1.findAncestor( {
+				name: 'div',
+				class: 'container'
+			} ) ).to.be.null;
+		} );
+	} );
 } );
 } );

+ 42 - 0
packages/ckeditor5-engine/tests/view/observer/mouseobserver.js

@@ -0,0 +1,42 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+/* bender-tags: view, browser-only */
+
+import MouseObserver from 'ckeditor5/engine/view/observer/mouseobserver.js';
+import ViewDocument from 'ckeditor5/engine/view/document.js';
+
+describe( 'MouseObserver', () => {
+	let viewDocument, observer;
+
+	beforeEach( () => {
+		viewDocument = new ViewDocument();
+		observer = viewDocument.addObserver( MouseObserver );
+	} );
+
+	afterEach( () => {
+		viewDocument.destroy();
+	} );
+
+	it( 'should define domEventType', () => {
+		expect( observer.domEventType ).to.equal( 'mousedown' );
+	} );
+
+	describe( 'onDomEvent', () => {
+		it( 'should fire mousedown with the right event data', () => {
+			const spy = sinon.spy();
+
+			viewDocument.on( 'mousedown', spy );
+
+			observer.onDomEvent( { type: 'mousedown', target: document.body } );
+
+			expect( spy.calledOnce ).to.be.true;
+
+			const data = spy.args[ 0 ][ 1 ];
+			expect( data.domTarget ).to.equal( document.body );
+		} );
+	} );
+} );

+ 38 - 0
packages/ckeditor5-engine/tests/view/selection.js

@@ -13,6 +13,7 @@ import Text from 'ckeditor5/engine/view/text.js';
 import Position from 'ckeditor5/engine/view/position.js';
 import Position from 'ckeditor5/engine/view/position.js';
 import CKEditorError from 'ckeditor5/utils/ckeditorerror.js';
 import CKEditorError from 'ckeditor5/utils/ckeditorerror.js';
 import count from 'ckeditor5/utils/count.js';
 import count from 'ckeditor5/utils/count.js';
+import { parse } from 'ckeditor5/engine/dev-utils/view.js';
 
 
 describe( 'Selection', () => {
 describe( 'Selection', () => {
 	let selection;
 	let selection;
@@ -489,6 +490,12 @@ describe( 'Selection', () => {
 
 
 			expect( selection.isEqual( otherSelection ) ).to.be.false;
 			expect( selection.isEqual( otherSelection ) ).to.be.false;
 		} );
 		} );
+
+		it( 'should return true if both selections are empty', () => {
+			const otherSelection = new Selection();
+
+			expect( selection.isEqual( otherSelection ) ).to.be.true;
+		} );
 	} );
 	} );
 
 
 	describe( 'removeAllRanges', () => {
 	describe( 'removeAllRanges', () => {
@@ -778,4 +785,35 @@ describe( 'Selection', () => {
 			selection.setFake( true, { label: 'foo bar baz' } );
 			selection.setFake( true, { label: 'foo bar baz' } );
 		} );
 		} );
 	} );
 	} );
+
+	describe( 'getSelectedElement', () => {
+		it( 'should return selected element', () => {
+			const { selection, view } = parse( 'foo [<b>bar</b>] baz' );
+			const b = view.getChild( 1 );
+
+			expect( selection.getSelectedElement() ).to.equal( b );
+		} );
+
+		it( 'should return null if there is more than one range', () => {
+			const { selection } = parse( 'foo [<b>bar</b>] [<i>baz</i>]' );
+
+			expect( selection.getSelectedElement() ).to.be.null;
+		} );
+
+		it( 'should return null if there is no selection', () => {
+			expect( selection.getSelectedElement() ).to.be.null;
+		} );
+
+		it( 'should return null if selection is not over single element #1', () => {
+			const { selection } = parse( 'foo [<b>bar</b> ba}z' );
+
+			expect( selection.getSelectedElement() ).to.be.null;
+		} );
+
+		it( 'should return null if selection is not over single element #2', () => {
+			const { selection } = parse( 'foo <b>{bar}</b> baz' );
+
+			expect( selection.getSelectedElement() ).to.be.null;
+		} );
+	} );
 } );
 } );