1839 lines
		
	
	
		
			66 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
		
		
			
		
	
	
			1839 lines
		
	
	
		
			66 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
|  | /** | |||
|  |  * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved. | |||
|  |  * For licensing, see LICENSE.md or http://ckeditor.com/license
 | |||
|  |  */ | |||
|  | 
 | |||
|  | /** | |||
|  |  * @fileOverview Allows accessing difficult focus spaces. | |||
|  |  */ | |||
|  | 
 | |||
|  | 'use strict'; | |||
|  | 
 | |||
|  | ( function() { | |||
|  | 	CKEDITOR.plugins.add( 'magicline', { | |||
|  | 		lang: 'ar,bg,ca,cs,cy,de,el,en,en-gb,eo,es,et,eu,fa,fi,fr,fr-ca,gl,he,hr,hu,id,it,ja,km,ko,ku,lv,nb,nl,no,pl,pt,pt-br,ru,si,sk,sl,sq,sv,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
 | |||
|  | 		init: initPlugin | |||
|  | 	} ); | |||
|  | 
 | |||
|  | 	// Activates the box inside of an editor.
 | |||
|  | 	function initPlugin( editor ) { | |||
|  | 		// Configurables
 | |||
|  | 		var config = editor.config, | |||
|  | 			triggerOffset = config.magicline_triggerOffset || 30, | |||
|  | 			enterMode = config.enterMode, | |||
|  | 			that = { | |||
|  | 				// Global stuff is being initialized here.
 | |||
|  | 				editor: editor, | |||
|  | 				enterMode: enterMode, | |||
|  | 				triggerOffset: triggerOffset, | |||
|  | 				holdDistance: 0 | triggerOffset * ( config.magicline_holdDistance || 0.5 ), | |||
|  | 				boxColor: config.magicline_color || '#ff0000', | |||
|  | 				rtl: config.contentsLangDirection == 'rtl', | |||
|  | 				tabuList: [ 'data-cke-hidden-sel' ].concat( config.magicline_tabuList || [] ), | |||
|  | 				triggers: config.magicline_everywhere ? DTD_BLOCK : { table: 1, hr: 1, div: 1, ul: 1, ol: 1, dl: 1, form: 1, blockquote: 1 } | |||
|  | 			}, | |||
|  | 			scrollTimeout, checkMouseTimeoutPending, checkMouseTimeout, checkMouseTimer; | |||
|  | 
 | |||
|  | 		// %REMOVE_START%
 | |||
|  | 		// Internal DEBUG uses tools located in the topmost window.
 | |||
|  | 
 | |||
|  | 		// (#9701) Due to security limitations some browsers may throw
 | |||
|  | 		// errors when accessing window.top object. Do it safely first then.
 | |||
|  | 		try { | |||
|  | 			that.debug = window.top.DEBUG; | |||
|  | 		} | |||
|  | 		catch ( e ) {} | |||
|  | 
 | |||
|  | 		that.debug = that.debug || { | |||
|  | 			groupEnd: function() {}, | |||
|  | 			groupStart: function() {}, | |||
|  | 			log: function() {}, | |||
|  | 			logElements: function() {}, | |||
|  | 			logElementsEnd: function() {}, | |||
|  | 			logEnd: function() {}, | |||
|  | 			mousePos: function() {}, | |||
|  | 			showHidden: function() {}, | |||
|  | 			showTrigger: function() {}, | |||
|  | 			startTimer: function() {}, | |||
|  | 			stopTimer: function() {} | |||
|  | 		}; | |||
|  | 		// %REMOVE_END%
 | |||
|  | 
 | |||
|  | 		// Simple irrelevant elements filter.
 | |||
|  | 		that.isRelevant = function( node ) { | |||
|  | 			return isHtml( node ) 			// 	-> Node must be an existing HTML element.
 | |||
|  | 				&& !isLine( that, node ) 	// 	-> Node can be neither the box nor its child.
 | |||
|  | 				&& !isFlowBreaker( node ); 	// 	-> Node can be neither floated nor positioned nor aligned.
 | |||
|  | 		}; | |||
|  | 
 | |||
|  | 		editor.on( 'contentDom', addListeners, this ); | |||
|  | 
 | |||
|  | 		function addListeners() { | |||
|  | 			var editable = editor.editable(), | |||
|  | 				doc = editor.document, | |||
|  | 				win = editor.window, | |||
|  | 				listener; | |||
|  | 
 | |||
|  | 			// Global stuff is being initialized here.
 | |||
|  | 			extend( that, { | |||
|  | 				editable: editable, | |||
|  | 				inInlineMode: editable.isInline(), | |||
|  | 				doc: doc, | |||
|  | 				win: win, | |||
|  | 				hotNode: null | |||
|  | 			}, true ); | |||
|  | 
 | |||
|  | 			// This is the boundary of the editor. For inline the boundary is editable itself.
 | |||
|  | 			// For classic (`iframe`-based) editor, the HTML element is a real boundary.
 | |||
|  | 			that.boundary = that.inInlineMode ? that.editable : that.doc.getDocumentElement(); | |||
|  | 
 | |||
|  | 			// Enabling the box inside of inline editable is pointless.
 | |||
|  | 			// There's no need to access spaces inside paragraphs, links, spans, etc.
 | |||
|  | 			if ( editable.is( dtd.$inline ) ) | |||
|  | 				return; | |||
|  | 
 | |||
|  | 			// Handle in-line editing by setting appropriate position.
 | |||
|  | 			// If current position is static, make it relative and clear top/left coordinates.
 | |||
|  | 			if ( that.inInlineMode && !isPositioned( editable ) ) { | |||
|  | 				editable.setStyles( { | |||
|  | 					position: 'relative', | |||
|  | 					top: null, | |||
|  | 					left: null | |||
|  | 				} ); | |||
|  | 			} | |||
|  | 			// Enable the box. Let it produce children elements, initialize
 | |||
|  | 			// event handlers and own methods.
 | |||
|  | 			initLine.call( this, that ); | |||
|  | 
 | |||
|  | 			// Get view dimensions and scroll positions.
 | |||
|  | 			// At this stage (before any checkMouse call) it is used mostly
 | |||
|  | 			// by tests. Nevertheless it a crucial thing.
 | |||
|  | 			updateWindowSize( that ); | |||
|  | 
 | |||
|  | 			// Remove the box before an undo image is created.
 | |||
|  | 			// This is important. If we didn't do that, the *undo thing* would revert the box into an editor.
 | |||
|  | 			// Thanks to that, undo doesn't even know about the existence of the box.
 | |||
|  | 			editable.attachListener( editor, 'beforeUndoImage', function() { | |||
|  | 				that.line.detach(); | |||
|  | 			} ); | |||
|  | 
 | |||
|  | 			// Removes the box HTML from editor data string if getData is called.
 | |||
|  | 			// Thanks to that, an editor never yields data polluted by the box.
 | |||
|  | 			// Listen with very high priority, so line will be removed before other
 | |||
|  | 			// listeners will see it.
 | |||
|  | 			editable.attachListener( editor, 'beforeGetData', function() { | |||
|  | 				// If the box is in editable, remove it.
 | |||
|  | 				if ( that.line.wrap.getParent() ) { | |||
|  | 					that.line.detach(); | |||
|  | 
 | |||
|  | 					// Restore line in the last listener for 'getData'.
 | |||
|  | 					editor.once( 'getData', function() { | |||
|  | 						that.line.attach(); | |||
|  | 					}, null, null, 1000 ); | |||
|  | 				} | |||
|  | 			}, null, null, 0 ); | |||
|  | 
 | |||
|  | 			// Hide the box on mouseout if mouse leaves document.
 | |||
|  | 			editable.attachListener( that.inInlineMode ? doc : doc.getWindow().getFrame(), 'mouseout', function( event ) { | |||
|  | 				if ( editor.mode != 'wysiwyg' ) | |||
|  | 					return; | |||
|  | 
 | |||
|  | 				// Check for inline-mode editor. If so, check mouse position
 | |||
|  | 				// and remove the box if mouse outside of an editor.
 | |||
|  | 				if ( that.inInlineMode ) { | |||
|  | 					var mouse = { | |||
|  | 						x: event.data.$.clientX, | |||
|  | 						y: event.data.$.clientY | |||
|  | 					}; | |||
|  | 
 | |||
|  | 					updateWindowSize( that ); | |||
|  | 					updateEditableSize( that, true ); | |||
|  | 
 | |||
|  | 					var size = that.view.editable, | |||
|  | 						scroll = that.view.scroll; | |||
|  | 
 | |||
|  | 					// If outside of an editor...
 | |||
|  | 					if ( !inBetween( mouse.x, size.left - scroll.x, size.right - scroll.x ) || !inBetween( mouse.y, size.top - scroll.y, size.bottom - scroll.y ) ) { | |||
|  | 						clearTimeout( checkMouseTimer ); | |||
|  | 						checkMouseTimer = null; | |||
|  | 						that.line.detach(); | |||
|  | 					} | |||
|  | 				} | |||
|  | 
 | |||
|  | 				else { | |||
|  | 					clearTimeout( checkMouseTimer ); | |||
|  | 					checkMouseTimer = null; | |||
|  | 					that.line.detach(); | |||
|  | 				} | |||
|  | 			} ); | |||
|  | 
 | |||
|  | 			// This one deactivates hidden mode of an editor which
 | |||
|  | 			// prevents the box from being shown.
 | |||
|  | 			editable.attachListener( editable, 'keyup', function( event ) { | |||
|  | 				that.hiddenMode = 0; | |||
|  | 				that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
 | |||
|  | 			} ); | |||
|  | 
 | |||
|  | 			editable.attachListener( editable, 'keydown', function( event ) { | |||
|  | 				if ( editor.mode != 'wysiwyg' ) | |||
|  | 					return; | |||
|  | 
 | |||
|  | 				var keyStroke = event.data.getKeystroke(), | |||
|  | 					selection = editor.getSelection(), | |||
|  | 					selected = selection.getStartElement(); | |||
|  | 
 | |||
|  | 				switch ( keyStroke ) { | |||
|  | 					// Shift pressed
 | |||
|  | 					case 2228240: // IE
 | |||
|  | 					case 16: | |||
|  | 						that.hiddenMode = 1; | |||
|  | 						that.line.detach(); | |||
|  | 				} | |||
|  | 
 | |||
|  | 				that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
 | |||
|  | 			} ); | |||
|  | 
 | |||
|  | 			// This method ensures that checkMouse aren't executed
 | |||
|  | 			// in parallel and no more frequently than specified in timeout function.
 | |||
|  | 			// In classic (`iframe`-based) editor, document is used as a trigger, to provide magicline
 | |||
|  | 			// functionality when mouse is below the body (short content, short body).
 | |||
|  | 			editable.attachListener( that.inInlineMode ? editable : doc, 'mousemove', function( event ) { | |||
|  | 				checkMouseTimeoutPending = true; | |||
|  | 
 | |||
|  | 				if ( editor.mode != 'wysiwyg' || editor.readOnly || checkMouseTimer ) | |||
|  | 					return; | |||
|  | 
 | |||
|  | 				// IE<9 requires this event-driven object to be created
 | |||
|  | 				// outside of the setTimeout statement.
 | |||
|  | 				// Otherwise it loses the event object with its properties.
 | |||
|  | 				var mouse = { | |||
|  | 					x: event.data.$.clientX, | |||
|  | 					y: event.data.$.clientY | |||
|  | 				}; | |||
|  | 
 | |||
|  | 				checkMouseTimer = setTimeout( function() { | |||
|  | 					checkMouse( mouse ); | |||
|  | 				}, 30 ); // balances performance and accessibility
 | |||
|  | 			} ); | |||
|  | 
 | |||
|  | 			// This one removes box on scroll event.
 | |||
|  | 			// It is to avoid box displacement.
 | |||
|  | 			editable.attachListener( win, 'scroll', function( event ) { | |||
|  | 				if ( editor.mode != 'wysiwyg' ) | |||
|  | 					return; | |||
|  | 
 | |||
|  | 				that.line.detach(); | |||
|  | 
 | |||
|  | 				// To figure this out just look at the mouseup
 | |||
|  | 				// event handler below.
 | |||
|  | 				if ( env.webkit ) { | |||
|  | 					that.hiddenMode = 1; | |||
|  | 
 | |||
|  | 					clearTimeout( scrollTimeout ); | |||
|  | 					scrollTimeout = setTimeout( function() { | |||
|  | 						// Don't leave hidden mode until mouse remains pressed and
 | |||
|  | 						// scroll is being used, i.e. when dragging something.
 | |||
|  | 						if ( !that.mouseDown ) | |||
|  | 							that.hiddenMode = 0; | |||
|  | 						that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
 | |||
|  | 					}, 50 ); | |||
|  | 
 | |||
|  | 					that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
 | |||
|  | 				} | |||
|  | 			} ); | |||
|  | 
 | |||
|  | 			// Those event handlers remove the box on mousedown
 | |||
|  | 			// and don't reveal it until the mouse is released.
 | |||
|  | 			// It is to prevent box insertion e.g. while scrolling
 | |||
|  | 			// (w/ scrollbar), selecting and so on.
 | |||
|  | 			editable.attachListener( env_ie8 ? doc : win, 'mousedown', function( event ) { | |||
|  | 				if ( editor.mode != 'wysiwyg' ) | |||
|  | 					return; | |||
|  | 
 | |||
|  | 				that.line.detach(); | |||
|  | 				that.hiddenMode = 1; | |||
|  | 				that.mouseDown = 1; | |||
|  | 
 | |||
|  | 				that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
 | |||
|  | 			} ); | |||
|  | 
 | |||
|  | 			// Google Chrome doesn't trigger this on the scrollbar (since 2009...)
 | |||
|  | 			// so it is totally useless to check for scroll finish
 | |||
|  | 			// see: http://code.google.com/p/chromium/issues/detail?id=14204
 | |||
|  | 			editable.attachListener( env_ie8 ? doc : win, 'mouseup', function( event ) { | |||
|  | 				that.hiddenMode = 0; | |||
|  | 				that.mouseDown = 0; | |||
|  | 				that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
 | |||
|  | 			} ); | |||
|  | 
 | |||
|  | 			// Editor commands for accessing difficult focus spaces.
 | |||
|  | 			editor.addCommand( 'accessPreviousSpace', accessFocusSpaceCmd( that ) ); | |||
|  | 			editor.addCommand( 'accessNextSpace', accessFocusSpaceCmd( that, true ) ); | |||
|  | 
 | |||
|  | 			editor.setKeystroke( [ | |||
|  | 				[ config.magicline_keystrokePrevious, 'accessPreviousSpace' ], | |||
|  | 				[ config.magicline_keystrokeNext, 'accessNextSpace' ] | |||
|  | 			] ); | |||
|  | 
 | |||
|  | 			// Revert magicline hot node on undo/redo.
 | |||
|  | 			editor.on( 'loadSnapshot', function( event ) { | |||
|  | 				var elements, element, i; | |||
|  | 
 | |||
|  | 				for ( var t in { p: 1, br: 1, div: 1 } ) { | |||
|  | 					// document.find is not available in QM (#11149).
 | |||
|  | 					elements = editor.document.getElementsByTag( t ); | |||
|  | 
 | |||
|  | 					for ( i = elements.count(); i--; ) { | |||
|  | 						if ( ( element = elements.getItem( i ) ).data( 'cke-magicline-hot' ) ) { | |||
|  | 							// Restore hotNode
 | |||
|  | 							that.hotNode = element; | |||
|  | 							// Restore last access direction
 | |||
|  | 							that.lastCmdDirection = element.data( 'cke-magicline-dir' ) === 'true' ? true : false; | |||
|  | 
 | |||
|  | 							return; | |||
|  | 						} | |||
|  | 					} | |||
|  | 				} | |||
|  | 			} ); | |||
|  | 
 | |||
|  | 			// This method handles mousemove mouse for box toggling.
 | |||
|  | 			// It uses mouse position to determine underlying element, then
 | |||
|  | 			// it tries to use different trigger type in order to place the box
 | |||
|  | 			// in correct place. The following procedure is executed periodically.
 | |||
|  | 			function checkMouse( mouse ) { | |||
|  | 				that.debug.groupStart( 'CheckMouse' ); // %REMOVE_LINE%
 | |||
|  | 				that.debug.startTimer(); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 				that.mouse = mouse; | |||
|  | 				that.trigger = null; | |||
|  | 
 | |||
|  | 				checkMouseTimer = null; | |||
|  | 				updateWindowSize( that ); | |||
|  | 
 | |||
|  | 				if ( checkMouseTimeoutPending 								//	-> There must be an event pending.
 | |||
|  | 					&& !that.hiddenMode 									// 	-> Can't be in hidden mode.
 | |||
|  | 					&& editor.focusManager.hasFocus 						// 	-> Editor must have focus.
 | |||
|  | 					&& !that.line.mouseNear() 								// 	-> Mouse pointer can't be close to the box.
 | |||
|  | 					&& ( that.element = elementFromMouse( that, true ) ) ) 	// 	-> There must be valid element.
 | |||
|  | 				{ | |||
|  | 					// If trigger exists, and trigger is correct -> show the box.
 | |||
|  | 					// Don't show the line if trigger is a descendant of some tabu-list element.
 | |||
|  | 					if ( ( that.trigger = triggerEditable( that ) || triggerEdge( that ) || triggerExpand( that ) ) && | |||
|  | 						!isInTabu( that, that.trigger.upper || that.trigger.lower ) ) { | |||
|  | 						that.line.attach().place(); | |||
|  | 					} | |||
|  | 
 | |||
|  | 					// Otherwise remove the box
 | |||
|  | 					else { | |||
|  | 						that.trigger = null; | |||
|  | 						that.line.detach(); | |||
|  | 					} | |||
|  | 
 | |||
|  | 					that.debug.showTrigger( that.trigger ); // %REMOVE_LINE%
 | |||
|  | 					that.debug.mousePos( mouse.y, that.element ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 					checkMouseTimeoutPending = false; | |||
|  | 				} | |||
|  | 
 | |||
|  | 				that.debug.stopTimer(); // %REMOVE_LINE%
 | |||
|  | 				that.debug.groupEnd(); // %REMOVE_LINE%
 | |||
|  | 			} | |||
|  | 
 | |||
|  | 			// This one allows testing and debugging. It reveals some
 | |||
|  | 			// inner methods to the world.
 | |||
|  | 			this.backdoor = { | |||
|  | 				accessFocusSpace: accessFocusSpace, | |||
|  | 				boxTrigger: boxTrigger, | |||
|  | 				isLine: isLine, | |||
|  | 				getAscendantTrigger: getAscendantTrigger, | |||
|  | 				getNonEmptyNeighbour: getNonEmptyNeighbour, | |||
|  | 				getSize: getSize, | |||
|  | 				that: that, | |||
|  | 				triggerEdge: triggerEdge, | |||
|  | 				triggerEditable: triggerEditable, | |||
|  | 				triggerExpand: triggerExpand | |||
|  | 			}; | |||
|  | 		} | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// Some shorthands for common methods to save bytes
 | |||
|  | 	var extend = CKEDITOR.tools.extend, | |||
|  | 		newElement = CKEDITOR.dom.element, | |||
|  | 		newElementFromHtml = newElement.createFromHtml, | |||
|  | 		env = CKEDITOR.env, | |||
|  | 		env_ie8 = CKEDITOR.env.ie && CKEDITOR.env.version < 9, | |||
|  | 		dtd = CKEDITOR.dtd, | |||
|  | 
 | |||
|  | 		// Global object associating enter modes with elements.
 | |||
|  | 		enterElements = {}, | |||
|  | 
 | |||
|  | 		// Constant values, types and so on.
 | |||
|  | 		EDGE_TOP = 128, | |||
|  | 		EDGE_BOTTOM = 64, | |||
|  | 		EDGE_MIDDLE = 32, | |||
|  | 		TYPE_EDGE = 16, | |||
|  | 		TYPE_EXPAND = 8, | |||
|  | 		LOOK_TOP = 4, | |||
|  | 		LOOK_BOTTOM = 2, | |||
|  | 		LOOK_NORMAL = 1, | |||
|  | 		WHITE_SPACE = '\u00A0', | |||
|  | 		DTD_LISTITEM = dtd.$listItem, | |||
|  | 		DTD_TABLECONTENT = dtd.$tableContent, | |||
|  | 		DTD_NONACCESSIBLE = extend( {}, dtd.$nonEditable, dtd.$empty ), | |||
|  | 		DTD_BLOCK = dtd.$block, | |||
|  | 
 | |||
|  | 		// Minimum time that must elapse between two update*Size calls.
 | |||
|  | 		// It prevents constant getComuptedStyle calls and improves performance.
 | |||
|  | 		CACHE_TIME = 100, | |||
|  | 
 | |||
|  | 		// Shared CSS stuff for box elements
 | |||
|  | 		CSS_COMMON = 'width:0px;height:0px;padding:0px;margin:0px;display:block;' + 'z-index:9999;color:#fff;position:absolute;font-size: 0px;line-height:0px;', | |||
|  | 		CSS_TRIANGLE = CSS_COMMON + 'border-color:transparent;display:block;border-style:solid;', | |||
|  | 		TRIANGLE_HTML = '<span>' + WHITE_SPACE + '</span>'; | |||
|  | 
 | |||
|  | 	enterElements[ CKEDITOR.ENTER_BR ] = 'br'; | |||
|  | 	enterElements[ CKEDITOR.ENTER_P ] = 'p'; | |||
|  | 	enterElements[ CKEDITOR.ENTER_DIV ] = 'div'; | |||
|  | 
 | |||
|  | 	function areSiblings( that, upper, lower ) { | |||
|  | 		return isHtml( upper ) && isHtml( lower ) && lower.equals( upper.getNext( function( node ) { | |||
|  | 			return !( isEmptyTextNode( node ) || isComment( node ) || isFlowBreaker( node ) ); | |||
|  | 		} ) ); | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// boxTrigger is an abstract type which describes
 | |||
|  | 	// the relationship between elements that may result
 | |||
|  | 	// in showing the box.
 | |||
|  | 	//
 | |||
|  | 	// The following type is used by numerous methods
 | |||
|  | 	// to share information about the hypothetical box placement
 | |||
|  | 	// and look by referring to boxTrigger properties.
 | |||
|  | 	function boxTrigger( triggerSetup ) { | |||
|  | 		this.upper = triggerSetup[ 0 ]; | |||
|  | 		this.lower = triggerSetup[ 1 ]; | |||
|  | 		this.set.apply( this, triggerSetup.slice( 2 ) ); | |||
|  | 	} | |||
|  | 
 | |||
|  | 	boxTrigger.prototype = { | |||
|  | 		set: function( edge, type, look ) { | |||
|  | 			this.properties = edge + type + ( look || LOOK_NORMAL ); | |||
|  | 			return this; | |||
|  | 		}, | |||
|  | 
 | |||
|  | 		is: function( property ) { | |||
|  | 			return ( this.properties & property ) == property; | |||
|  | 		} | |||
|  | 	}; | |||
|  | 
 | |||
|  | 	var elementFromMouse = ( function() { | |||
|  | 		function elementFromPoint( doc, mouse ) { | |||
|  | 			return new CKEDITOR.dom.element( doc.$.elementFromPoint( mouse.x, mouse.y ) ); | |||
|  | 		} | |||
|  | 
 | |||
|  | 		return function( that, ignoreBox, forceMouse ) { | |||
|  | 			if ( !that.mouse ) | |||
|  | 				return null; | |||
|  | 
 | |||
|  | 			var doc = that.doc, | |||
|  | 				lineWrap = that.line.wrap, | |||
|  | 				mouse = forceMouse || that.mouse, | |||
|  | 				element = elementFromPoint( doc, mouse ); | |||
|  | 
 | |||
|  | 			// If ignoreBox is set and element is the box, it means that we
 | |||
|  | 			// need to hide the box for a while, repeat elementFromPoint
 | |||
|  | 			// and show it again.
 | |||
|  | 			if ( ignoreBox && isLine( that, element ) ) { | |||
|  | 				lineWrap.hide(); | |||
|  | 				element = elementFromPoint( doc, mouse ); | |||
|  | 				lineWrap.show(); | |||
|  | 			} | |||
|  | 
 | |||
|  | 			// Return nothing if:
 | |||
|  | 			//	\-> Element is not HTML.
 | |||
|  | 			if ( !( element && element.type == CKEDITOR.NODE_ELEMENT && element.$ ) ) | |||
|  | 				return null; | |||
|  | 
 | |||
|  | 			// Also return nothing if:
 | |||
|  | 			//	\-> We're IE<9 and element is out of the top-level element (editable for inline and HTML for classic (`iframe`-based)).
 | |||
|  | 			//		This is due to the bug which allows IE<9 firing mouse events on element
 | |||
|  | 			//		with contenteditable=true while doing selection out (far, away) of the element.
 | |||
|  | 			//		Thus we must always be sure that we stay in editable or HTML.
 | |||
|  | 			if ( env.ie && env.version < 9 ) { | |||
|  | 				if ( !( that.boundary.equals( element ) || that.boundary.contains( element ) ) ) | |||
|  | 					return null; | |||
|  | 			} | |||
|  | 
 | |||
|  | 			return element; | |||
|  | 		}; | |||
|  | 	} )(); | |||
|  | 
 | |||
|  | 	// Gets the closest parent node that belongs to triggers group.
 | |||
|  | 	function getAscendantTrigger( that ) { | |||
|  | 		var node = that.element, | |||
|  | 			trigger; | |||
|  | 
 | |||
|  | 		if ( node && isHtml( node ) ) { | |||
|  | 			trigger = node.getAscendant( that.triggers, true ); | |||
|  | 
 | |||
|  | 			// If trigger is an element, neither editable nor editable's ascendant.
 | |||
|  | 			if ( trigger && that.editable.contains( trigger ) ) { | |||
|  | 				// Check for closest editable limit.
 | |||
|  | 				var limit = getClosestEditableLimit( trigger, true ); | |||
|  | 
 | |||
|  | 				// Trigger in nested editable area.
 | |||
|  | 				if ( limit.getAttribute( 'contenteditable' ) == 'true' ) | |||
|  | 					return trigger; | |||
|  | 				// Trigger in non-editable area.
 | |||
|  | 				else if ( limit.is( that.triggers ) ) | |||
|  | 					return limit; | |||
|  | 				else | |||
|  | 					return null; | |||
|  | 
 | |||
|  | 				return trigger; | |||
|  | 			} else | |||
|  | 				return null; | |||
|  | 		} | |||
|  | 
 | |||
|  | 		return null; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	function getMidpoint( that, upper, lower ) { | |||
|  | 		updateSize( that, upper ); | |||
|  | 		updateSize( that, lower ); | |||
|  | 
 | |||
|  | 		var upperSizeBottom = upper.size.bottom, | |||
|  | 			lowerSizeTop = lower.size.top; | |||
|  | 
 | |||
|  | 		return upperSizeBottom && lowerSizeTop ? 0 | ( upperSizeBottom + lowerSizeTop ) / 2 : upperSizeBottom || lowerSizeTop; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// Get nearest node (either text or HTML), but:
 | |||
|  | 	//	\->	Omit all empty text nodes (containing white characters only).
 | |||
|  | 	//	\-> Omit BR elements
 | |||
|  | 	//	\-> Omit flow breakers.
 | |||
|  | 	function getNonEmptyNeighbour( that, node, goBack ) { | |||
|  | 		node = node[ goBack ? 'getPrevious' : 'getNext' ]( function( node ) { | |||
|  | 			return ( isTextNode( node ) && !isEmptyTextNode( node ) ) || | |||
|  | 				( isHtml( node ) && !isFlowBreaker( node ) && !isLine( that, node ) ); | |||
|  | 		} ); | |||
|  | 
 | |||
|  | 		return node; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	function inBetween( val, lower, upper ) { | |||
|  | 		return val > lower && val < upper; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// Returns the closest ancestor that has contenteditable attribute.
 | |||
|  | 	// Such ancestor is the limit of (non-)editable DOM branch that element
 | |||
|  | 	// belongs to. This method omits editor editable.
 | |||
|  | 	function getClosestEditableLimit( element, includeSelf ) { | |||
|  | 		if ( element.data( 'cke-editable' ) ) | |||
|  | 			return null; | |||
|  | 
 | |||
|  | 		if ( !includeSelf ) | |||
|  | 			element = element.getParent(); | |||
|  | 
 | |||
|  | 		while ( element ) { | |||
|  | 			if ( element.data( 'cke-editable' ) ) | |||
|  | 				return null; | |||
|  | 
 | |||
|  | 			if ( element.hasAttribute( 'contenteditable' ) ) | |||
|  | 				return element; | |||
|  | 
 | |||
|  | 			element = element.getParent(); | |||
|  | 		} | |||
|  | 
 | |||
|  | 		return null; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// Access space line consists of a few elements (spans):
 | |||
|  | 	// 	\-> Line wrapper.
 | |||
|  | 	// 	\-> Line.
 | |||
|  | 	// 	\-> Line triangles: left triangle (LT), right triangle (RT).
 | |||
|  | 	// 	\-> Button handler (BTN).
 | |||
|  | 	//
 | |||
|  | 	//	+--------------------------------------------------- line.wrap (span) -----+
 | |||
|  | 	//	| +---------------------------------------------------- line (span) -----+ |
 | |||
|  | 	//	| | +- LT \                                           +- BTN -+  / RT -+ | |
 | |||
|  | 	//	| | |      \                                          |     | | /      | | |
 | |||
|  | 	//	| | |      /                                          |  <__| | \      | | |
 | |||
|  | 	//	| | +-----/                                           +-------+  \-----+ | |
 | |||
|  | 	//	| +----------------------------------------------------------------------+ |
 | |||
|  | 	//  +--------------------------------------------------------------------------+
 | |||
|  | 	//
 | |||
|  | 	function initLine( that ) { | |||
|  | 		var doc = that.doc, | |||
|  | 			// This the main box element that holds triangles and the insertion button
 | |||
|  | 			line = newElementFromHtml( '<span contenteditable="false" style="' + CSS_COMMON + 'position:absolute;border-top:1px dashed ' + that.boxColor + '"></span>', doc ), | |||
|  | 			iconPath = this.path + 'images/' + ( env.hidpi ? 'hidpi/' : '' ) + 'icon.png'; | |||
|  | 
 | |||
|  | 		extend( line, { | |||
|  | 
 | |||
|  | 			attach: function() { | |||
|  | 				// Only if not already attached
 | |||
|  | 				if ( !this.wrap.getParent() ) | |||
|  | 					this.wrap.appendTo( that.editable, true ); | |||
|  | 
 | |||
|  | 				return this; | |||
|  | 			}, | |||
|  | 
 | |||
|  | 			// Looks are as follows: [ LOOK_TOP, LOOK_BOTTOM, LOOK_NORMAL ].
 | |||
|  | 			lineChildren: [ | |||
|  | 				extend( | |||
|  | 					newElementFromHtml( '<span title="' + that.editor.lang.magicline.title + | |||
|  | 						'" contenteditable="false">↵</span>', doc ), { | |||
|  | 					base: CSS_COMMON + 'height:17px;width:17px;' + ( that.rtl ? 'left' : 'right' ) + ':17px;' | |||
|  | 						+ 'background:url(' + iconPath + ') center no-repeat ' + that.boxColor + ';cursor:pointer;' | |||
|  | 						+ ( env.hc ? 'font-size: 15px;line-height:14px;border:1px solid #fff;text-align:center;' : '' ) | |||
|  | 						+ ( env.hidpi ? 'background-size: 9px 10px;' : '' ), | |||
|  | 					looks: [ | |||
|  | 						'top:-8px;' + CKEDITOR.tools.cssVendorPrefix( 'border-radius', '2px', 1 ), | |||
|  | 						'top:-17px;' + CKEDITOR.tools.cssVendorPrefix( 'border-radius', '2px 2px 0px 0px', 1 ), | |||
|  | 						'top:-1px;' + CKEDITOR.tools.cssVendorPrefix( 'border-radius', '0px 0px 2px 2px', 1 ) | |||
|  | 					] | |||
|  | 				} ), | |||
|  | 				extend( newElementFromHtml( TRIANGLE_HTML, doc ), { | |||
|  | 					base: CSS_TRIANGLE + 'left:0px;border-left-color:' + that.boxColor + ';', | |||
|  | 					looks: [ | |||
|  | 						'border-width:8px 0 8px 8px;top:-8px', | |||
|  | 						'border-width:8px 0 0 8px;top:-8px', | |||
|  | 						'border-width:0 0 8px 8px;top:0px' | |||
|  | 					] | |||
|  | 				} ), | |||
|  | 				extend( newElementFromHtml( TRIANGLE_HTML, doc ), { | |||
|  | 					base: CSS_TRIANGLE + 'right:0px;border-right-color:' + that.boxColor + ';', | |||
|  | 					looks: [ | |||
|  | 						'border-width:8px 8px 8px 0;top:-8px', | |||
|  | 						'border-width:8px 8px 0 0;top:-8px', | |||
|  | 						'border-width:0 8px 8px 0;top:0px' | |||
|  | 					] | |||
|  | 				} ) | |||
|  | 			], | |||
|  | 
 | |||
|  | 			detach: function() { | |||
|  | 				// Detach only if already attached.
 | |||
|  | 				if ( this.wrap.getParent() ) | |||
|  | 					this.wrap.remove(); | |||
|  | 
 | |||
|  | 				return this; | |||
|  | 			}, | |||
|  | 
 | |||
|  | 			// Checks whether mouseY is around an element by comparing boundaries and considering
 | |||
|  | 			// an offset distance.
 | |||
|  | 			mouseNear: function() { | |||
|  | 				that.debug.groupStart( 'mouseNear' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 				updateSize( that, this ); | |||
|  | 				var offset = that.holdDistance, | |||
|  | 					size = this.size; | |||
|  | 
 | |||
|  | 				// Determine neighborhood by element dimensions and offsets.
 | |||
|  | 				if ( size && inBetween( that.mouse.y, size.top - offset, size.bottom + offset ) && inBetween( that.mouse.x, size.left - offset, size.right + offset ) ) { | |||
|  | 					that.debug.logEnd( 'Mouse is near.' ); // %REMOVE_LINE%
 | |||
|  | 					return true; | |||
|  | 				} | |||
|  | 
 | |||
|  | 				that.debug.logEnd( 'Mouse isn\'t near.' ); // %REMOVE_LINE%
 | |||
|  | 				return false; | |||
|  | 			}, | |||
|  | 
 | |||
|  | 			// Adjusts position of the box according to the trigger properties.
 | |||
|  | 			// If also affects look of the box depending on the type of the trigger.
 | |||
|  | 			place: function() { | |||
|  | 				var view = that.view, | |||
|  | 					editable = that.editable, | |||
|  | 					trigger = that.trigger, | |||
|  | 					upper = trigger.upper, | |||
|  | 					lower = trigger.lower, | |||
|  | 					any = upper || lower, | |||
|  | 					parent = any.getParent(), | |||
|  | 					styleSet = {}; | |||
|  | 
 | |||
|  | 				// Save recent trigger for further insertion.
 | |||
|  | 				// It is necessary due to the fact, that that.trigger may
 | |||
|  | 				// contain different boxTrigger at the moment of insertion
 | |||
|  | 				// or may be even null.
 | |||
|  | 				this.trigger = trigger; | |||
|  | 
 | |||
|  | 				upper && updateSize( that, upper, true ); | |||
|  | 				lower && updateSize( that, lower, true ); | |||
|  | 				updateSize( that, parent, true ); | |||
|  | 
 | |||
|  | 				// Yeah, that's gonna be useful in inline-mode case.
 | |||
|  | 				if ( that.inInlineMode ) | |||
|  | 					updateEditableSize( that, true ); | |||
|  | 
 | |||
|  | 				// Set X coordinate (left, right, width).
 | |||
|  | 				if ( parent.equals( editable ) ) { | |||
|  | 					styleSet.left = view.scroll.x; | |||
|  | 					styleSet.right = -view.scroll.x; | |||
|  | 					styleSet.width = ''; | |||
|  | 				} else { | |||
|  | 					styleSet.left = any.size.left - any.size.margin.left + view.scroll.x - ( that.inInlineMode ? view.editable.left + view.editable.border.left : 0 ); | |||
|  | 					styleSet.width = any.size.outerWidth + any.size.margin.left + any.size.margin.right + view.scroll.x; | |||
|  | 					styleSet.right = ''; | |||
|  | 				} | |||
|  | 
 | |||
|  | 				// Set Y coordinate (top) for trigger consisting of two elements.
 | |||
|  | 				if ( upper && lower ) { | |||
|  | 					// No margins at all or they're equal. Place box right between.
 | |||
|  | 					if ( upper.size.margin.bottom === lower.size.margin.top ) | |||
|  | 						styleSet.top = 0 | ( upper.size.bottom + upper.size.margin.bottom / 2 ); | |||
|  | 					else { | |||
|  | 						// Upper margin < lower margin. Place at lower margin.
 | |||
|  | 						if ( upper.size.margin.bottom < lower.size.margin.top ) | |||
|  | 							styleSet.top = upper.size.bottom + upper.size.margin.bottom; | |||
|  | 						// Upper margin > lower margin. Place at upper margin - lower margin.
 | |||
|  | 						else | |||
|  | 							styleSet.top = upper.size.bottom + upper.size.margin.bottom - lower.size.margin.top; | |||
|  | 					} | |||
|  | 				} | |||
|  | 				// Set Y coordinate (top) for single-edge trigger.
 | |||
|  | 				else if ( !upper ) | |||
|  | 					styleSet.top = lower.size.top - lower.size.margin.top; | |||
|  | 				else if ( !lower ) | |||
|  | 					styleSet.top = upper.size.bottom + upper.size.margin.bottom; | |||
|  | 
 | |||
|  | 				// Set box button modes if close to the viewport horizontal edge
 | |||
|  | 				// or look forced by the trigger.
 | |||
|  | 				if ( trigger.is( LOOK_TOP ) || inBetween( styleSet.top, view.scroll.y - 15, view.scroll.y + 5 ) ) { | |||
|  | 					styleSet.top = that.inInlineMode ? 0 : view.scroll.y; | |||
|  | 					this.look( LOOK_TOP ); | |||
|  | 				} else if ( trigger.is( LOOK_BOTTOM ) || inBetween( styleSet.top, view.pane.bottom - 5, view.pane.bottom + 15 ) ) { | |||
|  | 					styleSet.top = that.inInlineMode ? | |||
|  | 							view.editable.height + view.editable.padding.top + view.editable.padding.bottom | |||
|  | 						: | |||
|  | 							view.pane.bottom - 1; | |||
|  | 
 | |||
|  | 					this.look( LOOK_BOTTOM ); | |||
|  | 				} else { | |||
|  | 					if ( that.inInlineMode ) | |||
|  | 						styleSet.top -= view.editable.top + view.editable.border.top; | |||
|  | 
 | |||
|  | 					this.look( LOOK_NORMAL ); | |||
|  | 				} | |||
|  | 
 | |||
|  | 				if ( that.inInlineMode ) { | |||
|  | 					// 1px bug here...
 | |||
|  | 					styleSet.top--; | |||
|  | 
 | |||
|  | 					// Consider the editable to be an element with overflow:scroll
 | |||
|  | 					// and non-zero scrollTop/scrollLeft value.
 | |||
|  | 					// For example: divarea editable. (#9383)
 | |||
|  | 					styleSet.top += view.editable.scroll.top; | |||
|  | 					styleSet.left += view.editable.scroll.left; | |||
|  | 				} | |||
|  | 
 | |||
|  | 				// Append `px` prefixes.
 | |||
|  | 				for ( var style in styleSet ) | |||
|  | 					styleSet[ style ] = CKEDITOR.tools.cssLength( styleSet[ style ] ); | |||
|  | 
 | |||
|  | 				this.setStyles( styleSet ); | |||
|  | 			}, | |||
|  | 
 | |||
|  | 			// Changes look of the box according to current needs.
 | |||
|  | 			// Three different styles are available: [ LOOK_TOP, LOOK_BOTTOM, LOOK_NORMAL ].
 | |||
|  | 			look: function( look ) { | |||
|  | 				if ( this.oldLook == look ) | |||
|  | 					return; | |||
|  | 
 | |||
|  | 				for ( var i = this.lineChildren.length, child; i--; ) | |||
|  | 					( child = this.lineChildren[ i ] ).setAttribute( 'style', child.base + child.looks[ 0 | look / 2 ] ); | |||
|  | 
 | |||
|  | 				this.oldLook = look; | |||
|  | 			}, | |||
|  | 
 | |||
|  | 			wrap: new newElement( 'span', that.doc ) | |||
|  | 
 | |||
|  | 		} ); | |||
|  | 
 | |||
|  | 		// Insert children into the box.
 | |||
|  | 		for ( var i = line.lineChildren.length; i--; ) | |||
|  | 			line.lineChildren[ i ].appendTo( line ); | |||
|  | 
 | |||
|  | 		// Set default look of the box.
 | |||
|  | 		line.look( LOOK_NORMAL ); | |||
|  | 
 | |||
|  | 		// Using that wrapper prevents IE (8,9) from resizing editable area at the moment
 | |||
|  | 		// of box insertion. This works thanks to the fact, that positioned box is wrapped by
 | |||
|  | 		// an inline element. So much tricky.
 | |||
|  | 		line.appendTo( line.wrap ); | |||
|  | 
 | |||
|  | 		// Make the box unselectable.
 | |||
|  | 		line.unselectable(); | |||
|  | 
 | |||
|  | 		// Handle accessSpace node insertion.
 | |||
|  | 		line.lineChildren[ 0 ].on( 'mouseup', function( event ) { | |||
|  | 			line.detach(); | |||
|  | 
 | |||
|  | 			accessFocusSpace( that, function( accessNode ) { | |||
|  | 				// Use old trigger that was saved by 'place' method. Look: line.place
 | |||
|  | 				var trigger = that.line.trigger; | |||
|  | 
 | |||
|  | 				accessNode[ trigger.is( EDGE_TOP ) ? 'insertBefore' : 'insertAfter' ] | |||
|  | 					( trigger.is( EDGE_TOP ) ? trigger.lower : trigger.upper ); | |||
|  | 			}, true ); | |||
|  | 
 | |||
|  | 			that.editor.focus(); | |||
|  | 
 | |||
|  | 			if ( !env.ie && that.enterMode != CKEDITOR.ENTER_BR ) | |||
|  | 				that.hotNode.scrollIntoView(); | |||
|  | 
 | |||
|  | 			event.data.preventDefault( true ); | |||
|  | 		} ); | |||
|  | 
 | |||
|  | 		// Prevents IE9 from displaying the resize box and disables drag'n'drop functionality.
 | |||
|  | 		line.on( 'mousedown', function( event ) { | |||
|  | 			event.data.preventDefault( true ); | |||
|  | 		} ); | |||
|  | 
 | |||
|  | 		that.line = line; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// This function allows accessing any focus space according to the insert function:
 | |||
|  | 	// 	* For enterMode ENTER_P it creates P element filled with dummy white-space.
 | |||
|  | 	// 	* For enterMode ENTER_DIV it creates DIV element filled with dummy white-space.
 | |||
|  | 	// 	* For enterMode ENTER_BR it creates BR element or   in IE.
 | |||
|  | 	//
 | |||
|  | 	// The node is being inserted according to insertFunction. Finally the method
 | |||
|  | 	// selects the non-breaking space making the node ready for typing.
 | |||
|  | 	function accessFocusSpace( that, insertFunction, doSave ) { | |||
|  | 		var range = new CKEDITOR.dom.range( that.doc ), | |||
|  | 			editor = that.editor, | |||
|  | 			accessNode; | |||
|  | 
 | |||
|  | 		// IE requires text node of   in ENTER_BR mode.
 | |||
|  | 		if ( env.ie && that.enterMode == CKEDITOR.ENTER_BR ) | |||
|  | 			accessNode = that.doc.createText( WHITE_SPACE ); | |||
|  | 
 | |||
|  | 		// In other cases a regular element is used.
 | |||
|  | 		else { | |||
|  | 			// Use the enterMode of editable's limit or editor's
 | |||
|  | 			// enter mode if not in nested editable.
 | |||
|  | 			var limit = getClosestEditableLimit( that.element, true ), | |||
|  | 
 | |||
|  | 				// This is an enter mode for the context. We cannot use
 | |||
|  | 				// editor.activeEnterMode because the focused nested editable will
 | |||
|  | 				// have a different enterMode as editor but magicline will be inserted
 | |||
|  | 				// directly into editor's editable.
 | |||
|  | 				enterMode = limit && limit.data( 'cke-enter-mode' ) || that.enterMode; | |||
|  | 
 | |||
|  | 			accessNode = new newElement( enterElements[ enterMode ], that.doc ); | |||
|  | 
 | |||
|  | 			if ( !accessNode.is( 'br' ) ) { | |||
|  | 				var dummy = that.doc.createText( WHITE_SPACE ); | |||
|  | 				dummy.appendTo( accessNode ); | |||
|  | 			} | |||
|  | 		} | |||
|  | 
 | |||
|  | 		doSave && editor.fire( 'saveSnapshot' ); | |||
|  | 
 | |||
|  | 		insertFunction( accessNode ); | |||
|  | 		//dummy.appendTo( accessNode );
 | |||
|  | 		range.moveToPosition( accessNode, CKEDITOR.POSITION_AFTER_START ); | |||
|  | 		editor.getSelection().selectRanges( [ range ] ); | |||
|  | 		that.hotNode = accessNode; | |||
|  | 
 | |||
|  | 		doSave && editor.fire( 'saveSnapshot' ); | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// Access focus space on demand by taking an element under the caret as a reference.
 | |||
|  | 	// The space is accessed provided the element under the caret is trigger AND:
 | |||
|  | 	//
 | |||
|  | 	//  1. First/last-child of its parent:
 | |||
|  | 	//		+----------------------- Parent element -+
 | |||
|  | 	//		| +------------------------------ DIV -+ |	<-- Access before
 | |||
|  | 	//		| | Foo^                               | |
 | |||
|  | 	//		| |                                    | |
 | |||
|  | 	//		| +------------------------------------+ |	<-- Access after
 | |||
|  | 	//		+----------------------------------------+
 | |||
|  | 	//
 | |||
|  | 	//                       OR
 | |||
|  | 	//
 | |||
|  | 	//  2. It has a direct sibling element, which is also a trigger:
 | |||
|  | 	//		+-------------------------------- DIV#1 -+
 | |||
|  | 	//		| Foo^                                   |
 | |||
|  | 	//		|                                        |
 | |||
|  | 	//		+----------------------------------------+
 | |||
|  | 	//                                                	<-- Access here
 | |||
|  | 	//		+-------------------------------- DIV#2 -+
 | |||
|  | 	//		| Bar                                    |
 | |||
|  | 	//		|                                        |
 | |||
|  | 	//		+----------------------------------------+
 | |||
|  | 	//
 | |||
|  | 	//                       OR
 | |||
|  | 	//
 | |||
|  | 	//  3. It has a direct sibling, which is a trigger and has a valid neighbour trigger,
 | |||
|  | 	//     but belongs to dtd.$.empty/nonEditable:
 | |||
|  | 	//		+------------------------------------ P -+
 | |||
|  | 	//		| Foo^                                   |
 | |||
|  | 	//		|                                        |
 | |||
|  | 	//		+----------------------------------------+
 | |||
|  | 	//		+----------------------------------- HR -+
 | |||
|  | 	//                                                	<-- Access here
 | |||
|  | 	//		+-------------------------------- DIV#2 -+
 | |||
|  | 	//		| Bar                                    |
 | |||
|  | 	//		|                                        |
 | |||
|  | 	//		+----------------------------------------+
 | |||
|  | 	//
 | |||
|  | 	function accessFocusSpaceCmd( that, insertAfter ) { | |||
|  | 		return { | |||
|  | 			canUndo: true, | |||
|  | 			modes: { wysiwyg: 1 }, | |||
|  | 			exec: ( function() { | |||
|  | 
 | |||
|  | 				// Inserts line (accessNode) at the position by taking target node as a reference.
 | |||
|  | 				function doAccess( target ) { | |||
|  | 					// Remove old hotNode under certain circumstances.
 | |||
|  | 					var hotNodeChar = ( env.ie && env.version < 9 ? ' ' : WHITE_SPACE ), | |||
|  | 						removeOld = that.hotNode &&							// Old hotNode must exist.
 | |||
|  | 							that.hotNode.getText() == hotNodeChar &&		// Old hotNode hasn't been changed.
 | |||
|  | 							that.element.equals( that.hotNode ) &&			// Caret is inside old hotNode.
 | |||
|  | 							that.lastCmdDirection === !!insertAfter;		// Command is executed in the same direction.
 | |||
|  | 
 | |||
|  | 					accessFocusSpace( that, function( accessNode ) { | |||
|  | 						if ( removeOld && that.hotNode ) | |||
|  | 							that.hotNode.remove(); | |||
|  | 
 | |||
|  | 						accessNode[ insertAfter ? 'insertAfter' : 'insertBefore' ]( target ); | |||
|  | 
 | |||
|  | 						// Make this element distinguishable. Also remember the direction
 | |||
|  | 						// it's been inserted into document.
 | |||
|  | 						accessNode.setAttributes( { | |||
|  | 							'data-cke-magicline-hot': 1, | |||
|  | 							'data-cke-magicline-dir': !!insertAfter | |||
|  | 						} ); | |||
|  | 
 | |||
|  | 						// Save last direction of the command (is insertAfter?).
 | |||
|  | 						that.lastCmdDirection = !!insertAfter; | |||
|  | 					} ); | |||
|  | 
 | |||
|  | 					if ( !env.ie && that.enterMode != CKEDITOR.ENTER_BR ) | |||
|  | 						that.hotNode.scrollIntoView(); | |||
|  | 
 | |||
|  | 					// Detach the line if was visible (previously triggered by mouse).
 | |||
|  | 					that.line.detach(); | |||
|  | 				} | |||
|  | 
 | |||
|  | 				return function( editor ) { | |||
|  | 					var selected = editor.getSelection().getStartElement(), | |||
|  | 						limit; | |||
|  | 
 | |||
|  | 					// (#9833) Go down to the closest non-inline element in DOM structure
 | |||
|  | 					// since inline elements don't participate in in magicline.
 | |||
|  | 					selected = selected.getAscendant( DTD_BLOCK, 1 ); | |||
|  | 
 | |||
|  | 					// Stop if selected is a child of a tabu-list element.
 | |||
|  | 					if ( isInTabu( that, selected ) ) | |||
|  | 						return; | |||
|  | 
 | |||
|  | 					// Sometimes it may happen that there's no parent block below selected element
 | |||
|  | 					// or, for example, getAscendant reaches editable or editable parent.
 | |||
|  | 					// We must avoid such pathological cases.
 | |||
|  | 					if ( !selected || selected.equals( that.editable ) || selected.contains( that.editable ) ) | |||
|  | 						return; | |||
|  | 
 | |||
|  | 					// Executing the command directly in nested editable should
 | |||
|  | 					// access space before/after it.
 | |||
|  | 					if ( ( limit = getClosestEditableLimit( selected ) ) && limit.getAttribute( 'contenteditable' ) == 'false' ) | |||
|  | 						selected = limit; | |||
|  | 
 | |||
|  | 					// That holds element from mouse. Replace it with the
 | |||
|  | 					// element under the caret.
 | |||
|  | 					that.element = selected; | |||
|  | 
 | |||
|  | 					// (3.) Handle the following cases where selected neighbour
 | |||
|  | 					// is a trigger inaccessible for the caret AND:
 | |||
|  | 					//	- Is first/last-child
 | |||
|  | 					//	OR
 | |||
|  | 					//	- Has a sibling, which is also a trigger.
 | |||
|  | 					var neighbor = getNonEmptyNeighbour( that, selected, !insertAfter ), | |||
|  | 						neighborSibling; | |||
|  | 
 | |||
|  | 					// Check for a neighbour that belongs to triggers.
 | |||
|  | 					// Consider only non-accessible elements (they cannot have any children)
 | |||
|  | 					// since they cannot be given a caret inside, to run the command
 | |||
|  | 					// the regular way (1. & 2.).
 | |||
|  | 					if ( isHtml( neighbor ) && neighbor.is( that.triggers ) && neighbor.is( DTD_NONACCESSIBLE ) && | |||
|  | 							( | |||
|  | 									// Check whether neighbor is first/last-child.
 | |||
|  | 									!getNonEmptyNeighbour( that, neighbor, !insertAfter ) | |||
|  | 								|| | |||
|  | 									// Check for a sibling of a neighbour that also is a trigger.
 | |||
|  | 									( | |||
|  | 										( neighborSibling = getNonEmptyNeighbour( that, neighbor, !insertAfter ) ) && | |||
|  | 										isHtml( neighborSibling ) && | |||
|  | 										neighborSibling.is( that.triggers ) | |||
|  | 									) | |||
|  | 							) | |||
|  | 						) { | |||
|  | 						doAccess( neighbor ); | |||
|  | 						return; | |||
|  | 					} | |||
|  | 
 | |||
|  | 					// Look for possible target element DOWN "selected" DOM branch (towards editable)
 | |||
|  | 					// that belong to that.triggers
 | |||
|  | 					var target = getAscendantTrigger( that, selected ); | |||
|  | 
 | |||
|  | 					// No HTML target -> no access.
 | |||
|  | 					if ( !isHtml( target ) ) | |||
|  | 						return; | |||
|  | 
 | |||
|  | 					// (1.) Target is first/last child -> access.
 | |||
|  | 					if ( !getNonEmptyNeighbour( that, target, !insertAfter ) ) { | |||
|  | 						doAccess( target ); | |||
|  | 						return; | |||
|  | 					} | |||
|  | 
 | |||
|  | 					var sibling = getNonEmptyNeighbour( that, target, !insertAfter ); | |||
|  | 
 | |||
|  | 					// (2.) Target has a sibling that belongs to that.triggers -> access.
 | |||
|  | 					if ( sibling && isHtml( sibling ) && sibling.is( that.triggers ) ) { | |||
|  | 						doAccess( target ); | |||
|  | 						return; | |||
|  | 					} | |||
|  | 				}; | |||
|  | 			} )() | |||
|  | 		}; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	function isLine( that, node ) { | |||
|  | 		if ( !( node && node.type == CKEDITOR.NODE_ELEMENT && node.$ ) ) | |||
|  | 			return false; | |||
|  | 
 | |||
|  | 		var line = that.line; | |||
|  | 
 | |||
|  | 		return line.wrap.equals( node ) || line.wrap.contains( node ); | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// Is text node containing white-spaces only?
 | |||
|  | 	var isEmptyTextNode = CKEDITOR.dom.walker.whitespaces(); | |||
|  | 
 | |||
|  | 	// Is fully visible HTML node?
 | |||
|  | 	function isHtml( node ) { | |||
|  | 		return node && node.type == CKEDITOR.NODE_ELEMENT && node.$;	// IE requires that
 | |||
|  | 	} | |||
|  | 
 | |||
|  | 	function isFloated( element ) { | |||
|  | 		if ( !isHtml( element ) ) | |||
|  | 			return false; | |||
|  | 
 | |||
|  | 		var options = { left: 1, right: 1, center: 1 }; | |||
|  | 
 | |||
|  | 		return !!( options[ element.getComputedStyle( 'float' ) ] || options[ element.getAttribute( 'align' ) ] ); | |||
|  | 	} | |||
|  | 
 | |||
|  | 	function isFlowBreaker( element ) { | |||
|  | 		if ( !isHtml( element ) ) | |||
|  | 			return false; | |||
|  | 
 | |||
|  | 		return isPositioned( element ) || isFloated( element ); | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// Isn't node of NODE_COMMENT type?
 | |||
|  | 	var isComment = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_COMMENT ); | |||
|  | 
 | |||
|  | 	function isPositioned( element ) { | |||
|  | 		return !!{ absolute: 1, fixed: 1 }[ element.getComputedStyle( 'position' ) ]; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// Is text node?
 | |||
|  | 	function isTextNode( node ) { | |||
|  | 		return node && node.type == CKEDITOR.NODE_TEXT; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	function isTrigger( that, element ) { | |||
|  | 		return isHtml( element ) ? element.is( that.triggers ) : null; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	function isInTabu( that, element ) { | |||
|  | 		if ( !element ) | |||
|  | 			return false; | |||
|  | 
 | |||
|  | 		var parents = element.getParents( 1 ); | |||
|  | 
 | |||
|  | 		for ( var i = parents.length ; i-- ; ) { | |||
|  | 			for ( var j = that.tabuList.length ; j-- ; ) { | |||
|  | 				if ( parents[ i ].hasAttribute( that.tabuList[ j ] ) ) | |||
|  | 					return true; | |||
|  | 			} | |||
|  | 		} | |||
|  | 
 | |||
|  | 		return false; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// This function checks vertically is there's a relevant child between element's edge
 | |||
|  | 	// and the pointer.
 | |||
|  | 	//	\-> Table contents are omitted.
 | |||
|  | 	function isChildBetweenPointerAndEdge( that, parent, edgeBottom ) { | |||
|  | 		var edgeChild = parent[ edgeBottom ? 'getLast' : 'getFirst' ]( function( node ) { | |||
|  | 			return that.isRelevant( node ) && !node.is( DTD_TABLECONTENT ); | |||
|  | 		} ); | |||
|  | 
 | |||
|  | 		if ( !edgeChild ) | |||
|  | 			return false; | |||
|  | 
 | |||
|  | 		updateSize( that, edgeChild ); | |||
|  | 
 | |||
|  | 		return edgeBottom ? edgeChild.size.top > that.mouse.y : edgeChild.size.bottom < that.mouse.y; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// This method handles edge cases:
 | |||
|  | 	// 	\-> Mouse is around upper or lower edge of view pane.
 | |||
|  | 	// 	\-> Also scroll position is either minimal or maximal.
 | |||
|  | 	// 	\-> It's OK to show LOOK_TOP(BOTTOM) type line.
 | |||
|  | 	//
 | |||
|  | 	// This trigger doesn't need additional post-filtering.
 | |||
|  | 	//
 | |||
|  | 	//	+----------------------------- Editable -+  /--
 | |||
|  | 	//	| +---------------------- First child -+ |  | <-- Top edge (first child)
 | |||
|  | 	//	| |                                    | |  |
 | |||
|  | 	//	| |                                    | |  |	 * Mouse activation area *
 | |||
|  | 	//	| |                                    | |  |
 | |||
|  | 	//	| |                 ...                | |	\-- Top edge + trigger offset
 | |||
|  | 	//	| .                                    . |
 | |||
|  | 	//	|                                        |
 | |||
|  | 	//	| .                                    . |
 | |||
|  | 	//	| |                 ...                | |  /-- Bottom edge - trigger offset
 | |||
|  | 	//	| |                                    | |  |
 | |||
|  | 	//	| |                                    | |  |	 * Mouse activation area *
 | |||
|  | 	//	| |                                    | |  |
 | |||
|  | 	//	| +----------------------- Last child -+ |  | <-- Bottom edge (last child)
 | |||
|  | 	//	+----------------------------------------+  \--
 | |||
|  | 	//
 | |||
|  | 	function triggerEditable( that ) { | |||
|  | 		that.debug.groupStart( 'triggerEditable' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 		var editable = that.editable, | |||
|  | 			mouse = that.mouse, | |||
|  | 			view = that.view, | |||
|  | 			triggerOffset = that.triggerOffset, | |||
|  | 			triggerLook; | |||
|  | 
 | |||
|  | 		// Update editable dimensions.
 | |||
|  | 		updateEditableSize( that ); | |||
|  | 
 | |||
|  | 		// This flag determines whether checking bottom trigger.
 | |||
|  | 		var bottomTrigger = mouse.y > ( that.inInlineMode ? | |||
|  | 				view.editable.top + view.editable.height / 2 | |||
|  | 			: | |||
|  | 				// This is to handle case when editable.height / 2 <<< pane.height.
 | |||
|  | 				Math.min( view.editable.height, view.pane.height ) / 2 ), | |||
|  | 
 | |||
|  | 		// Edge node according to bottomTrigger.
 | |||
|  | 		edgeNode = editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( function( node ) { | |||
|  | 			return !( isEmptyTextNode( node ) || isComment( node ) ); | |||
|  | 		} ); | |||
|  | 
 | |||
|  | 		// There's no edge node. Abort.
 | |||
|  | 		if ( !edgeNode ) { | |||
|  | 			that.debug.logEnd( 'ABORT. No edge node found.' ); // %REMOVE_LINE%
 | |||
|  | 			return null; | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// If the edgeNode in editable is ML, get the next one.
 | |||
|  | 		if ( isLine( that, edgeNode ) ) { | |||
|  | 			edgeNode = that.line.wrap[ bottomTrigger ? 'getPrevious' : 'getNext' ]( function( node ) { | |||
|  | 				return !( isEmptyTextNode( node ) || isComment( node ) ); | |||
|  | 			} ); | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// Exclude bad nodes (no ML needed then):
 | |||
|  | 		//	\-> Edge node is text.
 | |||
|  | 		//	\-> Edge node is floated, etc.
 | |||
|  | 		//
 | |||
|  | 		// Edge node *must be* a valid trigger at this stage as well.
 | |||
|  | 		if ( !isHtml( edgeNode ) || isFlowBreaker( edgeNode ) || !isTrigger( that, edgeNode ) ) { | |||
|  | 			that.debug.logEnd( 'ABORT. Invalid edge node.' ); // %REMOVE_LINE%
 | |||
|  | 			return null; | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// Update size of edge node. Dimensions will be necessary.
 | |||
|  | 		updateSize( that, edgeNode ); | |||
|  | 
 | |||
|  | 		// Return appropriate trigger according to bottomTrigger.
 | |||
|  | 		// \->	Top edge trigger case first.
 | |||
|  | 		if ( !bottomTrigger &&													// Top trigger case.
 | |||
|  | 			edgeNode.size.top >= 0 &&											// Check if the first element is fully visible.
 | |||
|  | 			inBetween( mouse.y, 0, edgeNode.size.top + triggerOffset ) ) {		// Check if mouse in [0, edgeNode.top + triggerOffset].
 | |||
|  | 
 | |||
|  | 			// Determine trigger look.
 | |||
|  | 			triggerLook = that.inInlineMode || view.scroll.y === 0 ? | |||
|  | 				LOOK_TOP : LOOK_NORMAL; | |||
|  | 
 | |||
|  | 			that.debug.logEnd( 'SUCCESS. Created box trigger. EDGE_TOP.' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 			return new boxTrigger( [ null, edgeNode, | |||
|  | 				EDGE_TOP, | |||
|  | 				TYPE_EDGE, | |||
|  | 				triggerLook | |||
|  | 			] ); | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// \->	Bottom case.
 | |||
|  | 		else if ( bottomTrigger && | |||
|  | 			edgeNode.size.bottom <= view.pane.height &&							// Check if the last element is fully visible
 | |||
|  | 			inBetween( mouse.y,													// Check if mouse in...
 | |||
|  | 				edgeNode.size.bottom - triggerOffset, view.pane.height ) ) {	// [ edgeNode.bottom - triggerOffset, paneHeight ]
 | |||
|  | 
 | |||
|  | 			// Determine trigger look.
 | |||
|  | 			triggerLook = that.inInlineMode || | |||
|  | 				inBetween( edgeNode.size.bottom, view.pane.height - triggerOffset, view.pane.height ) ? | |||
|  | 					LOOK_BOTTOM : LOOK_NORMAL; | |||
|  | 
 | |||
|  | 			that.debug.logEnd( 'SUCCESS. Created box trigger. EDGE_BOTTOM.' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 			return new boxTrigger( [ edgeNode, null, | |||
|  | 				EDGE_BOTTOM, | |||
|  | 				TYPE_EDGE, | |||
|  | 				triggerLook | |||
|  | 			] ); | |||
|  | 		} | |||
|  | 
 | |||
|  | 		that.debug.logEnd( 'ABORT. No trigger created.' ); // %REMOVE_LINE%
 | |||
|  | 		return null; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// This method covers cases *inside* of an element:
 | |||
|  | 	// 	\->	The pointer is in the top (bottom) area of an element and there's
 | |||
|  | 	//		HTML node before (after) this element.
 | |||
|  | 	// 	\-> An element being the first or last child of its parent.
 | |||
|  | 	//
 | |||
|  | 	//	+----------------------- Parent element -+
 | |||
|  | 	//	| +----------------------- Element #1 -+ |  /--
 | |||
|  | 	//	| |                                    | |  |	 * Mouse activation area (as first child) *
 | |||
|  | 	//	| |                                    | |  \--
 | |||
|  | 	//	| |                                    | |  /--
 | |||
|  | 	//	| |                                    | |  |	 * Mouse activation area (Element #2) *
 | |||
|  | 	//	| +------------------------------------+ |  \--
 | |||
|  | 	//	|                                        |
 | |||
|  | 	//	| +----------------------- Element #2 -+ |  /--
 | |||
|  | 	//	| |                                    | |  |	 * Mouse activation area (Element #1) *
 | |||
|  | 	//	| |                                    | |  \--
 | |||
|  | 	//	| |                                    | |
 | |||
|  | 	//	| +------------------------------------+ |
 | |||
|  | 	//	|                                        |
 | |||
|  | 	//	|            Text node is here.          |
 | |||
|  | 	//	|                                        |
 | |||
|  | 	//	| +----------------------- Element #3 -+ |
 | |||
|  | 	//	| |                                    | |
 | |||
|  | 	//	| |                                    | |
 | |||
|  | 	//	| |                                    | |  /--
 | |||
|  | 	//	| |                                    | |  |	 * Mouse activation area (as last child) *
 | |||
|  | 	//	| +------------------------------------+ |  \--
 | |||
|  | 	//	+----------------------------------------+
 | |||
|  | 	//
 | |||
|  | 	function triggerEdge( that ) { | |||
|  | 		that.debug.groupStart( 'triggerEdge' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 		var mouse = that.mouse, | |||
|  | 			view = that.view, | |||
|  | 			triggerOffset = that.triggerOffset; | |||
|  | 
 | |||
|  | 		// Get the ascendant trigger basing on elementFromMouse.
 | |||
|  | 		var element = getAscendantTrigger( that ); | |||
|  | 
 | |||
|  | 		that.debug.logElements( [ element ], [ 'Ascendant trigger' ], 'First stage' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 		// Abort if there's no appropriate element.
 | |||
|  | 		if ( !element ) { | |||
|  | 			that.debug.logEnd( 'ABORT. No element, element is editable or element contains editable.' ); // %REMOVE_LINE%
 | |||
|  | 			return null; | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// Dimensions will be necessary.
 | |||
|  | 		updateSize( that, element ); | |||
|  | 
 | |||
|  | 		// If triggerOffset is larger than a half of element's height,
 | |||
|  | 		// use an offset of 1/2 of element's height. If the offset wasn't reduced,
 | |||
|  | 		// top area would cover most (all) cases.
 | |||
|  | 		var fixedOffset = Math.min( triggerOffset, | |||
|  | 				0 | ( element.size.outerHeight / 2 ) ), | |||
|  | 
 | |||
|  | 		// This variable will hold the trigger to be returned.
 | |||
|  | 			triggerSetup = [], | |||
|  | 			triggerLook, | |||
|  | 
 | |||
|  | 		// This flag determines whether dealing with a bottom trigger.
 | |||
|  | 			bottomTrigger; | |||
|  | 
 | |||
|  | 		//	\-> Top trigger.
 | |||
|  | 		if ( inBetween( mouse.y, element.size.top - 1, element.size.top + fixedOffset ) ) | |||
|  | 			bottomTrigger = false; | |||
|  | 		//	\-> Bottom trigger.
 | |||
|  | 		else if ( inBetween( mouse.y, element.size.bottom - fixedOffset, element.size.bottom + 1 ) ) | |||
|  | 			bottomTrigger = true; | |||
|  | 		//	\-> Abort. Not in a valid trigger space.
 | |||
|  | 		else { | |||
|  | 			that.debug.logEnd( 'ABORT. Not around of any edge.' ); // %REMOVE_LINE%
 | |||
|  | 			return null; | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// Reject wrong elements.
 | |||
|  | 		// 	\-> Reject an element which is a flow breaker.
 | |||
|  | 		// 	\-> Reject an element which has a child above/below the mouse pointer.
 | |||
|  | 		//	\-> Reject an element which belongs to list items.
 | |||
|  | 		if ( isFlowBreaker( element ) || | |||
|  | 			isChildBetweenPointerAndEdge( that, element, bottomTrigger ) || | |||
|  | 			element.getParent().is( DTD_LISTITEM ) ) { | |||
|  | 				that.debug.logEnd( 'ABORT. element is wrong', element ); // %REMOVE_LINE%
 | |||
|  | 				return null; | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// Get sibling according to bottomTrigger.
 | |||
|  | 		var elementSibling = getNonEmptyNeighbour( that, element, !bottomTrigger ); | |||
|  | 
 | |||
|  | 		// No sibling element.
 | |||
|  | 		// This is a first or last child case.
 | |||
|  | 		if ( !elementSibling ) { | |||
|  | 			// No need to reject the element as it has already been done before.
 | |||
|  | 			// Prepare a trigger.
 | |||
|  | 
 | |||
|  | 			// Determine trigger look.
 | |||
|  | 			if ( element.equals( that.editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( that.isRelevant ) ) ) { | |||
|  | 				updateEditableSize( that ); | |||
|  | 
 | |||
|  | 				if ( bottomTrigger && inBetween( mouse.y, | |||
|  | 					element.size.bottom - fixedOffset, view.pane.height ) && | |||
|  | 					inBetween( element.size.bottom, view.pane.height - fixedOffset, view.pane.height ) ) { | |||
|  | 						triggerLook = LOOK_BOTTOM; | |||
|  | 				} | |||
|  | 				else if ( inBetween( mouse.y, 0, element.size.top + fixedOffset ) ) | |||
|  | 					triggerLook = LOOK_TOP; | |||
|  | 
 | |||
|  | 			} | |||
|  | 			else | |||
|  | 				triggerLook = LOOK_NORMAL; | |||
|  | 
 | |||
|  | 			triggerSetup = [ null, element ][ bottomTrigger ? 'reverse' : 'concat' ]().concat( [ | |||
|  | 					bottomTrigger ? EDGE_BOTTOM : EDGE_TOP, | |||
|  | 					TYPE_EDGE, | |||
|  | 					triggerLook, | |||
|  | 					element.equals( that.editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( that.isRelevant ) ) ? | |||
|  | 						( bottomTrigger ? LOOK_BOTTOM : LOOK_TOP ) : LOOK_NORMAL | |||
|  | 				] ); | |||
|  | 
 | |||
|  | 			that.debug.log( 'Configured edge trigger of ' + ( bottomTrigger ? 'EDGE_BOTTOM' : 'EDGE_TOP' ) ); // %REMOVE_LINE%
 | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// Abort. Sibling is a text element.
 | |||
|  | 		else if ( isTextNode( elementSibling ) ) { | |||
|  | 			that.debug.logEnd( 'ABORT. Sibling is non-empty text element' ); // %REMOVE_LINE%
 | |||
|  | 			return null; | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// Check if the sibling is a HTML element.
 | |||
|  | 		// If so, create an TYPE_EDGE, EDGE_MIDDLE trigger.
 | |||
|  | 		else if ( isHtml( elementSibling ) ) { | |||
|  | 			// Reject wrong elementSiblings.
 | |||
|  | 			// 	\-> Reject an elementSibling which is a flow breaker.
 | |||
|  | 			//	\-> Reject an elementSibling which isn't a trigger.
 | |||
|  | 			//	\-> Reject an elementSibling which belongs to list items.
 | |||
|  | 			if ( isFlowBreaker( elementSibling ) || | |||
|  | 				!isTrigger( that, elementSibling ) || | |||
|  | 				elementSibling.getParent().is( DTD_LISTITEM ) ) { | |||
|  | 					that.debug.logEnd( 'ABORT. elementSibling is wrong', elementSibling ); // %REMOVE_LINE%
 | |||
|  | 					return null; | |||
|  | 			} | |||
|  | 
 | |||
|  | 			// Prepare a trigger.
 | |||
|  | 			triggerSetup = [ elementSibling, element ][ bottomTrigger ? 'reverse' : 'concat' ]().concat( [ | |||
|  | 					EDGE_MIDDLE, | |||
|  | 					TYPE_EDGE | |||
|  | 				] ); | |||
|  | 
 | |||
|  | 			that.debug.log( 'Configured edge trigger of EDGE_MIDDLE' ); // %REMOVE_LINE%
 | |||
|  | 		} | |||
|  | 
 | |||
|  | 		if ( 0 in triggerSetup ) { | |||
|  | 			that.debug.logEnd( 'SUCCESS. Returning a trigger.' ); // %REMOVE_LINE%
 | |||
|  | 			return new boxTrigger( triggerSetup ); | |||
|  | 		} | |||
|  | 
 | |||
|  | 		that.debug.logEnd( 'ABORT. No trigger generated.' ); // %REMOVE_LINE%
 | |||
|  | 		return null; | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// Checks iteratively up and down in search for elements using elementFromMouse method.
 | |||
|  | 	// Useful if between two triggers.
 | |||
|  | 	//
 | |||
|  | 	//	+----------------------- Parent element -+
 | |||
|  | 	//	| +----------------------- Element #1 -+ |
 | |||
|  | 	//	| |                                    | |
 | |||
|  | 	//	| |                                    | |
 | |||
|  | 	//	| |                                    | |
 | |||
|  | 	//	| +------------------------------------+ |
 | |||
|  | 	//	|                                        |  /--
 | |||
|  | 	//	|                  .                     |  |
 | |||
|  | 	//	|                  .      +-- Floated -+ |  |
 | |||
|  | 	//	|                  |      |            | |  |	* Mouse activation area *
 | |||
|  | 	//	|                  |      |   IGNORE   | |  |
 | |||
|  | 	//	|                  X      |            | |  |	Method searches vertically for sibling elements.
 | |||
|  | 	//	|                  |      +------------+ |  |	Start point is X (mouse-y coordinate).
 | |||
|  | 	//	|                  |                     |  |	Floated elements, comments and empty text nodes are omitted.
 | |||
|  | 	//	|                  .                     |  |
 | |||
|  | 	//	|                  .                     |  |
 | |||
|  | 	//	|                                        |  \--
 | |||
|  | 	//	| +----------------------- Element #2 -+ |
 | |||
|  | 	//	| |                                    | |
 | |||
|  | 	//	| |                                    | |
 | |||
|  | 	//	| |                                    | |
 | |||
|  | 	//	| |                                    | |
 | |||
|  | 	//	| +------------------------------------+ |
 | |||
|  | 	//	+----------------------------------------+
 | |||
|  | 	//
 | |||
|  | 	var triggerExpand = ( function() { | |||
|  | 		// The heart of the procedure. This method creates triggers that are
 | |||
|  | 		// filtered by expandFilter method.
 | |||
|  | 		function expandEngine( that ) { | |||
|  | 			that.debug.groupStart( 'expandEngine' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 			var startElement = that.element, | |||
|  | 				upper, lower, trigger; | |||
|  | 
 | |||
|  | 			if ( !isHtml( startElement ) || startElement.contains( that.editable ) ) { | |||
|  | 				that.debug.logEnd( 'ABORT. No start element, or start element contains editable.' ); // %REMOVE_LINE%
 | |||
|  | 				return null; | |||
|  | 			} | |||
|  | 
 | |||
|  | 			// Stop searching if element is in non-editable branch of DOM.
 | |||
|  | 			if ( startElement.isReadOnly() ) | |||
|  | 				return null; | |||
|  | 
 | |||
|  | 			trigger = verticalSearch( that, | |||
|  | 				function( current, startElement ) { | |||
|  | 					return !startElement.equals( current );	// stop when start element and the current one differ
 | |||
|  | 				}, function( that, mouse ) { | |||
|  | 					return elementFromMouse( that, true, mouse ); | |||
|  | 				}, startElement ), | |||
|  | 
 | |||
|  | 			upper = trigger.upper, | |||
|  | 			lower = trigger.lower; | |||
|  | 
 | |||
|  | 			that.debug.logElements( [ upper, lower ], [ 'Upper', 'Lower' ], 'Pair found' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 			// Success: two siblings have been found
 | |||
|  | 			if ( areSiblings( that, upper, lower ) ) { | |||
|  | 				that.debug.logEnd( 'SUCCESS. Expand trigger created.' ); // %REMOVE_LINE%
 | |||
|  | 				return trigger.set( EDGE_MIDDLE, TYPE_EXPAND ); | |||
|  | 			} | |||
|  | 
 | |||
|  | 			that.debug.logElements( [ startElement, upper, lower ], // %REMOVE_LINE%
 | |||
|  | 				[ 'Start', 'Upper', 'Lower' ], 'Post-processing' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 			// Danger. Dragons ahead.
 | |||
|  | 			// No siblings have been found during previous phase, post-processing may be necessary.
 | |||
|  | 			// We can traverse DOM until a valid pair of elements around the pointer is found.
 | |||
|  | 
 | |||
|  | 			// Prepare for post-processing:
 | |||
|  | 			// 	1. Determine if upper and lower are children of startElement.
 | |||
|  | 			// 		1.1. If so, find their ascendants that are closest to startElement (one level deeper than startElement).
 | |||
|  | 			// 		1.2. Otherwise use first/last-child of the startElement as upper/lower. Why?:
 | |||
|  | 			// 			a) 	upper/lower belongs to another branch of the DOM tree.
 | |||
|  | 			// 			b) 	verticalSearch encountered an edge of the viewport and failed.
 | |||
|  | 			// 		1.3. Make sure upper and lower still exist. Why?:
 | |||
|  | 			// 			a) 	Upper and lower may be not belong to the branch of the startElement (may not exist at all) and
 | |||
|  | 			// 				startElement has no children.
 | |||
|  | 			// 	2. Perform the post-processing.
 | |||
|  | 			// 		2.1. Gather dimensions of an upper element.
 | |||
|  | 			// 		2.2. Abort if lower edge of upper is already under the mouse pointer. Why?:
 | |||
|  | 			// 			a) 	We expect upper to be above and lower below the mouse pointer.
 | |||
|  | 			// 	3. Perform iterative search while upper != lower.
 | |||
|  | 			// 		3.1. Find the upper-next element. If there's no such element, break current search. Why?:
 | |||
|  | 			// 			a)	There's no point in further search if there are only text nodes ahead.
 | |||
|  | 			// 		3.2. Calculate the distance between the middle point of ( upper, upperNext ) and mouse-y.
 | |||
|  | 			// 		3.3. If the distance is shorter than the previous best, save it (save upper, upperNext as well).
 | |||
|  | 			// 		3.4. If the optimal pair is found, assign it back to the trigger.
 | |||
|  | 
 | |||
|  | 			// 1.1., 1.2.
 | |||
|  | 			if ( upper && startElement.contains( upper ) ) { | |||
|  | 				while ( !upper.getParent().equals( startElement ) ) | |||
|  | 					upper = upper.getParent(); | |||
|  | 			} else { | |||
|  | 				upper = startElement.getFirst( function( node ) { | |||
|  | 					return expandSelector( that, node ); | |||
|  | 				} ); | |||
|  | 			} | |||
|  | 
 | |||
|  | 			if ( lower && startElement.contains( lower ) ) { | |||
|  | 				while ( !lower.getParent().equals( startElement ) ) | |||
|  | 					lower = lower.getParent(); | |||
|  | 			} else { | |||
|  | 				lower = startElement.getLast( function( node ) { | |||
|  | 					return expandSelector( that, node ); | |||
|  | 				} ); | |||
|  | 			} | |||
|  | 
 | |||
|  | 			// 1.3.
 | |||
|  | 			if ( !upper || !lower ) { | |||
|  | 				that.debug.logEnd( 'ABORT. There is no upper or no lower element.' ); // %REMOVE_LINE%
 | |||
|  | 				return null; | |||
|  | 			} | |||
|  | 
 | |||
|  | 			// 2.1.
 | |||
|  | 			updateSize( that, upper ); | |||
|  | 			updateSize( that, lower ); | |||
|  | 
 | |||
|  | 			if ( !checkMouseBetweenElements( that, upper, lower ) ) { | |||
|  | 				that.debug.logEnd( 'ABORT. Mouse is already above upper or below lower.' ); // %REMOVE_LINE%
 | |||
|  | 				return null; | |||
|  | 			} | |||
|  | 
 | |||
|  | 			var minDistance = Number.MAX_VALUE, | |||
|  | 				currentDistance, upperNext, minElement, minElementNext; | |||
|  | 
 | |||
|  | 			while ( lower && !lower.equals( upper ) ) { | |||
|  | 				// 3.1.
 | |||
|  | 				if ( !( upperNext = upper.getNext( that.isRelevant ) ) ) | |||
|  | 					break; | |||
|  | 
 | |||
|  | 				// 3.2.
 | |||
|  | 				currentDistance = Math.abs( getMidpoint( that, upper, upperNext ) - that.mouse.y ); | |||
|  | 
 | |||
|  | 				// 3.3.
 | |||
|  | 				if ( currentDistance < minDistance ) { | |||
|  | 					minDistance = currentDistance; | |||
|  | 					minElement = upper; | |||
|  | 					minElementNext = upperNext; | |||
|  | 				} | |||
|  | 
 | |||
|  | 				upper = upperNext; | |||
|  | 				updateSize( that, upper ); | |||
|  | 			} | |||
|  | 
 | |||
|  | 			that.debug.logElements( [ minElement, minElementNext ], // %REMOVE_LINE%
 | |||
|  | 				[ 'Min', 'MinNext' ], 'Post-processing results' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 			// 3.4.
 | |||
|  | 			if ( !minElement || !minElementNext ) { | |||
|  | 				that.debug.logEnd( 'ABORT. No Min or MinNext' ); // %REMOVE_LINE%
 | |||
|  | 				return null; | |||
|  | 			} | |||
|  | 
 | |||
|  | 			if ( !checkMouseBetweenElements( that, minElement, minElementNext ) ) { | |||
|  | 				that.debug.logEnd( 'ABORT. Mouse is already above minElement or below minElementNext.' ); // %REMOVE_LINE%
 | |||
|  | 				return null; | |||
|  | 			} | |||
|  | 
 | |||
|  | 			// An element of minimal distance has been found. Assign it to the trigger.
 | |||
|  | 			trigger.upper = minElement; | |||
|  | 			trigger.lower = minElementNext; | |||
|  | 
 | |||
|  | 			// Success: post-processing revealed a pair of elements.
 | |||
|  | 			that.debug.logEnd( 'SUCCESSFUL post-processing. Trigger created.' ); // %REMOVE_LINE%
 | |||
|  | 			return trigger.set( EDGE_MIDDLE, TYPE_EXPAND ); | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// This is default element selector used by the engine.
 | |||
|  | 		function expandSelector( that, node ) { | |||
|  | 			return !( isTextNode( node ) | |||
|  | 				|| isComment( node ) | |||
|  | 				|| isFlowBreaker( node ) | |||
|  | 				|| isLine( that, node ) | |||
|  | 				|| ( node.type == CKEDITOR.NODE_ELEMENT && node.$ && node.is( 'br' ) ) ); | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// This method checks whether mouse-y is between the top edge of upper
 | |||
|  | 		// and bottom edge of lower.
 | |||
|  | 		//
 | |||
|  | 		// NOTE: This method assumes that updateSize has already been called
 | |||
|  | 		// for the elements and is up-to-date.
 | |||
|  | 		//
 | |||
|  | 		//	+---------------------------- Upper -+  /--
 | |||
|  | 		//	|                                    |  |
 | |||
|  | 		//	+------------------------------------+  |
 | |||
|  | 		//                                          |
 | |||
|  | 		//                     ...                  |
 | |||
|  | 		//                                          |
 | |||
|  | 		//						X                   |	* Return true for mouse-y in this range *
 | |||
|  | 		//                                          |
 | |||
|  | 		//                     ...                  |
 | |||
|  | 		//                                          |
 | |||
|  | 		//	+---------------------------- Lower -+  |
 | |||
|  | 		//	|                                    |  |
 | |||
|  | 		//	+------------------------------------+  \--
 | |||
|  | 		//
 | |||
|  | 		function checkMouseBetweenElements( that, upper, lower ) { | |||
|  | 			return inBetween( that.mouse.y, upper.size.top, lower.size.bottom ); | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// A method for trigger filtering. Accepts or rejects trigger pairs
 | |||
|  | 		// by their location in DOM etc.
 | |||
|  | 		function expandFilter( that, trigger ) { | |||
|  | 			that.debug.groupStart( 'expandFilter' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 			var upper = trigger.upper, | |||
|  | 				lower = trigger.lower; | |||
|  | 
 | |||
|  | 			if ( !upper || !lower 											// NOT: EDGE_MIDDLE trigger ALWAYS has two elements.
 | |||
|  | 				|| isFlowBreaker( lower ) || isFlowBreaker( upper )			// NOT: one of the elements is floated or positioned
 | |||
|  | 				|| lower.equals( upper ) || upper.equals( lower ) 			// NOT: two trigger elements, one equals another.
 | |||
|  | 				|| lower.contains( upper ) || upper.contains( lower ) ) { 	// NOT: two trigger elements, one contains another.
 | |||
|  | 				that.debug.logEnd( 'REJECTED. No upper or no lower or they contain each other.' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 				return false; | |||
|  | 			} | |||
|  | 
 | |||
|  | 			// YES: two trigger elements, pure siblings.
 | |||
|  | 			else if ( isTrigger( that, upper ) && isTrigger( that, lower ) && areSiblings( that, upper, lower ) ) { | |||
|  | 				that.debug.logElementsEnd( [ upper, lower ], // %REMOVE_LINE%
 | |||
|  | 					[ 'upper', 'lower' ], 'APPROVED EDGE_MIDDLE' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 				return true; | |||
|  | 			} | |||
|  | 
 | |||
|  | 			that.debug.logElementsEnd( [ upper, lower ], // %REMOVE_LINE%
 | |||
|  | 				[ 'upper', 'lower' ], 'Rejected unknown pair' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 			return false; | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// Simple wrapper for expandEngine and expandFilter.
 | |||
|  | 		return function( that ) { | |||
|  | 			that.debug.groupStart( 'triggerExpand' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 			var trigger = expandEngine( that ); | |||
|  | 
 | |||
|  | 			that.debug.groupEnd(); // %REMOVE_LINE%
 | |||
|  | 			return trigger && expandFilter( that, trigger ) ? trigger : null; | |||
|  | 		}; | |||
|  | 	} )(); | |||
|  | 
 | |||
|  | 	// Collects dimensions of an element.
 | |||
|  | 	var sizePrefixes = [ 'top', 'left', 'right', 'bottom' ]; | |||
|  | 
 | |||
|  | 	function getSize( that, element, ignoreScroll, force ) { | |||
|  | 		var getStyle = ( function() { | |||
|  | 			// Better "cache and reuse" than "call again and again".
 | |||
|  | 			var computed = env.ie ? element.$.currentStyle : that.win.$.getComputedStyle( element.$, '' ); | |||
|  | 
 | |||
|  | 				return env.ie ? | |||
|  | 					function( propertyName ) { | |||
|  | 						return computed[ CKEDITOR.tools.cssStyleToDomStyle( propertyName ) ]; | |||
|  | 					} : function( propertyName ) { | |||
|  | 						return computed.getPropertyValue( propertyName ); | |||
|  | 					}; | |||
|  | 			} )(), | |||
|  | 			docPosition = element.getDocumentPosition(), | |||
|  | 			border = {}, | |||
|  | 			margin = {}, | |||
|  | 			padding = {}, | |||
|  | 			box = {}; | |||
|  | 
 | |||
|  | 		for ( var i = sizePrefixes.length; i--; ) { | |||
|  | 			border[ sizePrefixes[ i ] ] = parseInt( getStyle( 'border-' + sizePrefixes[ i ] + '-width' ), 10 ) || 0; | |||
|  | 			padding[ sizePrefixes[ i ] ] = parseInt( getStyle( 'padding-' + sizePrefixes[ i ] ), 10 ) || 0; | |||
|  | 			margin[ sizePrefixes[ i ] ] = parseInt( getStyle( 'margin-' + sizePrefixes[ i ] ), 10 ) || 0; | |||
|  | 		} | |||
|  | 
 | |||
|  | 		// updateWindowSize if forced to do so OR NOT ignoring scroll.
 | |||
|  | 		if ( !ignoreScroll || force ) | |||
|  | 			updateWindowSize( that, force ); | |||
|  | 
 | |||
|  | 		box.top = docPosition.y - ( ignoreScroll ? 0 : that.view.scroll.y ), box.left = docPosition.x - ( ignoreScroll ? 0 : that.view.scroll.x ), | |||
|  | 
 | |||
|  | 		// w/ borders and paddings.
 | |||
|  | 		box.outerWidth = element.$.offsetWidth, box.outerHeight = element.$.offsetHeight, | |||
|  | 
 | |||
|  | 		// w/o borders and paddings.
 | |||
|  | 		box.height = box.outerHeight - ( padding.top + padding.bottom + border.top + border.bottom ), box.width = box.outerWidth - ( padding.left + padding.right + border.left + border.right ), | |||
|  | 
 | |||
|  | 		box.bottom = box.top + box.outerHeight, box.right = box.left + box.outerWidth; | |||
|  | 
 | |||
|  | 		if ( that.inInlineMode ) { | |||
|  | 			box.scroll = { | |||
|  | 				top: element.$.scrollTop, | |||
|  | 				left: element.$.scrollLeft | |||
|  | 			}; | |||
|  | 		} | |||
|  | 
 | |||
|  | 		return extend( { | |||
|  | 			border: border, | |||
|  | 			padding: padding, | |||
|  | 			margin: margin, | |||
|  | 			ignoreScroll: ignoreScroll | |||
|  | 		}, box, true ); | |||
|  | 	} | |||
|  | 
 | |||
|  | 	function updateSize( that, element, ignoreScroll ) { | |||
|  | 		if ( !isHtml( element ) ) // i.e. an element is hidden
 | |||
|  | 			return ( element.size = null ); //	-> reset size to make it useless for other methods
 | |||
|  | 
 | |||
|  | 		if ( !element.size ) | |||
|  | 			element.size = {}; | |||
|  | 
 | |||
|  | 		// Abort if there was a similar query performed recently.
 | |||
|  | 		// This kind of caching provides great performance improvement.
 | |||
|  | 		else if ( element.size.ignoreScroll == ignoreScroll && element.size.date > new Date() - CACHE_TIME ) { | |||
|  | 			that.debug.log( 'element.size: get from cache' ); // %REMOVE_LINE%
 | |||
|  | 			return null; | |||
|  | 		} | |||
|  | 
 | |||
|  | 		that.debug.log( 'element.size: capture' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 		return extend( element.size, getSize( that, element, ignoreScroll ), { | |||
|  | 			date: +new Date() | |||
|  | 		}, true ); | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// Updates that.view.editable object.
 | |||
|  | 	// This one must be called separately outside of updateWindowSize
 | |||
|  | 	// to prevent cyclic dependency getSize<->updateWindowSize.
 | |||
|  | 	// It calls getSize with force flag to avoid getWindowSize cache (look: getSize).
 | |||
|  | 	function updateEditableSize( that, ignoreScroll ) { | |||
|  | 		that.view.editable = getSize( that, that.editable, ignoreScroll, true ); | |||
|  | 	} | |||
|  | 
 | |||
|  | 	function updateWindowSize( that, force ) { | |||
|  | 		if ( !that.view ) | |||
|  | 			that.view = {}; | |||
|  | 
 | |||
|  | 		var view = that.view; | |||
|  | 
 | |||
|  | 		if ( !force && view && view.date > new Date() - CACHE_TIME ) { | |||
|  | 			that.debug.log( 'win.size: get from cache' ); // %REMOVE_LINE%
 | |||
|  | 			return; | |||
|  | 		} | |||
|  | 
 | |||
|  | 		that.debug.log( 'win.size: capturing' ); // %REMOVE_LINE%
 | |||
|  | 
 | |||
|  | 		var win = that.win, | |||
|  | 			scroll = win.getScrollPosition(), | |||
|  | 			paneSize = win.getViewPaneSize(); | |||
|  | 
 | |||
|  | 		extend( that.view, { | |||
|  | 			scroll: { | |||
|  | 				x: scroll.x, | |||
|  | 				y: scroll.y, | |||
|  | 				width: that.doc.$.documentElement.scrollWidth - paneSize.width, | |||
|  | 				height: that.doc.$.documentElement.scrollHeight - paneSize.height | |||
|  | 			}, | |||
|  | 			pane: { | |||
|  | 				width: paneSize.width, | |||
|  | 				height: paneSize.height, | |||
|  | 				bottom: paneSize.height + scroll.y | |||
|  | 			}, | |||
|  | 			date: +new Date() | |||
|  | 		}, true ); | |||
|  | 	} | |||
|  | 
 | |||
|  | 	// This method searches document vertically using given
 | |||
|  | 	// select criterion until stop criterion is fulfilled.
 | |||
|  | 	function verticalSearch( that, stopCondition, selectCriterion, startElement ) { | |||
|  | 		var upper = startElement, | |||
|  | 			lower = startElement, | |||
|  | 			mouseStep = 0, | |||
|  | 			upperFound = false, | |||
|  | 			lowerFound = false, | |||
|  | 			viewPaneHeight = that.view.pane.height, | |||
|  | 			mouse = that.mouse; | |||
|  | 
 | |||
|  | 		while ( mouse.y + mouseStep < viewPaneHeight && mouse.y - mouseStep > 0 ) { | |||
|  | 			if ( !upperFound ) | |||
|  | 				upperFound = stopCondition( upper, startElement ); | |||
|  | 
 | |||
|  | 			if ( !lowerFound ) | |||
|  | 				lowerFound = stopCondition( lower, startElement ); | |||
|  | 
 | |||
|  | 			// Still not found...
 | |||
|  | 			if ( !upperFound && mouse.y - mouseStep > 0 ) | |||
|  | 				upper = selectCriterion( that, { x: mouse.x, y: mouse.y - mouseStep } ); | |||
|  | 
 | |||
|  | 			if ( !lowerFound && mouse.y + mouseStep < viewPaneHeight ) | |||
|  | 				lower = selectCriterion( that, { x: mouse.x, y: mouse.y + mouseStep } ); | |||
|  | 
 | |||
|  | 			if ( upperFound && lowerFound ) | |||
|  | 				break; | |||
|  | 
 | |||
|  | 			// Instead of ++ to reduce the number of invocations by half.
 | |||
|  | 			// It's trades off accuracy in some edge cases for improved performance.
 | |||
|  | 			mouseStep += 2; | |||
|  | 		} | |||
|  | 
 | |||
|  | 		return new boxTrigger( [ upper, lower, null, null ] ); | |||
|  | 	} | |||
|  | 
 | |||
|  | } )(); | |||
|  | 
 | |||
|  | /** | |||
|  |  * Sets the default vertical distance between element edge and mouse pointer that | |||
|  |  * causes the box to appear. The distance is expressed in pixels (px). | |||
|  |  * | |||
|  |  *		// Changes the offset to 15px.
 | |||
|  |  *		CKEDITOR.config.magicline_triggerOffset = 15; | |||
|  |  * | |||
|  |  * @cfg {Number} [magicline_triggerOffset=30] | |||
|  |  * @member CKEDITOR.config | |||
|  |  * @see CKEDITOR.config#magicline_holdDistance | |||
|  |  */ | |||
|  | 
 | |||
|  | /** | |||
|  |  * Defines the distance between mouse pointer and the box, within | |||
|  |  * which the box stays revealed and no other focus space is offered to be accessed. | |||
|  |  * The value is relative to {@link #magicline_triggerOffset}. | |||
|  |  * | |||
|  |  *		// Increases the distance to 80% of CKEDITOR.config.magicline_triggerOffset.
 | |||
|  |  *		CKEDITOR.config.magicline_holdDistance = .8; | |||
|  |  * | |||
|  |  * @cfg {Number} [magicline_holdDistance=0.5] | |||
|  |  * @member CKEDITOR.config | |||
|  |  * @see CKEDITOR.config#magicline_triggerOffset | |||
|  |  */ | |||
|  | 
 | |||
|  | /** | |||
|  |  * Defines default keystroke that access the closest unreachable focus space **before** | |||
|  |  * the caret (start of the selection). If there's no any focus space, selection remains. | |||
|  |  * | |||
|  |  *		// Changes keystroke to CTRL + ,
 | |||
|  |  *		CKEDITOR.config.magicline_keystrokePrevious = CKEDITOR.CTRL + 188; | |||
|  |  * | |||
|  |  * @cfg {Number} [magicline_keystrokePrevious=CKEDITOR.CTRL + CKEDITOR.SHIFT + 51 (CTRL + SHIFT + 3)] | |||
|  |  * @member CKEDITOR.config | |||
|  |  */ | |||
|  | CKEDITOR.config.magicline_keystrokePrevious = CKEDITOR.CTRL + CKEDITOR.SHIFT + 51; // CTRL + SHIFT + 3
 | |||
|  | 
 | |||
|  | /** | |||
|  |  * Defines default keystroke that access the closest unreachable focus space **after** | |||
|  |  * the caret (start of the selection). If there's no any focus space, selection remains. | |||
|  |  * | |||
|  |  *		// Changes keystroke to CTRL + .
 | |||
|  |  *		CKEDITOR.config.magicline_keystrokeNext = CKEDITOR.CTRL + 190; | |||
|  |  * | |||
|  |  * @cfg {Number} [magicline_keystrokeNext=CKEDITOR.CTRL + CKEDITOR.SHIFT + 52 (CTRL + SHIFT + 4)] | |||
|  |  * @member CKEDITOR.config | |||
|  |  */ | |||
|  | CKEDITOR.config.magicline_keystrokeNext = CKEDITOR.CTRL + CKEDITOR.SHIFT + 52; // CTRL + SHIFT + 4
 | |||
|  | 
 | |||
|  | /** | |||
|  |  * Defines a list of attributes that, if assigned to some elements, prevent magicline from being | |||
|  |  * used within these elements. | |||
|  |  * | |||
|  |  *		// Adds "data-tabu" attribute to magicline tabu list.
 | |||
|  |  *		CKEDITOR.config.magicline_tabuList = [ 'data-tabu' ]; | |||
|  |  * | |||
|  |  * @cfg {Number} [magicline_tabuList=[ 'data-widget-wrapper' ]] | |||
|  |  * @member CKEDITOR.config | |||
|  |  */ | |||
|  | 
 | |||
|  | /** | |||
|  |  * Defines box color. The color may be adjusted to enhance readability. | |||
|  |  * | |||
|  |  *		// Changes color to blue.
 | |||
|  |  *		CKEDITOR.config.magicline_color = '#0000FF'; | |||
|  |  * | |||
|  |  * @cfg {String} [magicline_color='#FF0000'] | |||
|  |  * @member CKEDITOR.config | |||
|  |  */ | |||
|  | 
 | |||
|  | /** | |||
|  |  * Activates plugin mode that considers all focus spaces between | |||
|  |  * {@link CKEDITOR.dtd#$block} elements as accessible by the box. | |||
|  |  * | |||
|  |  *		// Enables "put everywhere" mode.
 | |||
|  |  *		CKEDITOR.config.magicline_putEverywhere = true; | |||
|  |  * | |||
|  |  * @cfg {Boolean} [magicline_putEverywhere=false] | |||
|  |  * @member CKEDITOR.config | |||
|  |  */ |