/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @fileOverview The [Magic Line](https://ckeditor.com/cke4/addon/magicline) plugin that makes it easier to access some document areas that
* are difficult to focus.
*/
'use strict';
( function() {
CKEDITOR.plugins.add( 'magicline', {
lang: 'af,ar,az,bg,ca,cs,cy,da,de,de-ch,el,en,en-au,en-gb,eo,es,es-mx,et,eu,fa,fi,fr,fr-ca,gl,he,hr,hu,id,it,ja,km,ko,ku,lv,nb,nl,no,oc,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sv,tr,tt,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, checkMouseTimer;
// %REMOVE_START%
// Internal DEBUG uses tools located in the topmost window.
// (https://dev.ckeditor.com/ticket/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;
// 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() {
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();
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() {
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() {
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() {
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() {
var elements, element, i;
for ( var t in { p: 1, br: 1, div: 1 } ) {
// document.find is not available in QM (https://dev.ckeditor.com/ticket/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 = '' + WHITE_SPACE + '';
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 ) {
var pointedElement = doc.$.elementFromPoint( mouse.x, mouse.y );
// IE9QM: from times to times it will return an empty object on scroll bar hover. (https://dev.ckeditor.com/ticket/12185)
return pointedElement && pointedElement.nodeType ?
new CKEDITOR.dom.element( pointedElement ) :
null;
}
return function( that, ignoreBox, forceMouse ) {
if ( !that.mouse )
return null;
var doc = that.doc,
lineWrap = that.line.wrap,
mouse = forceMouse || that.mouse,
// Note: element might be null.
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.
// Don't consider trigger as a limit as it may be nested editable (includeSelf=false) (https://dev.ckeditor.com/ticket/12009).
var limit = getClosestEditableLimit( trigger );
// 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;
} 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( '', doc ),
iconPath = CKEDITOR.getUrl( this.path + 'images/' + ( env.hidpi ? 'hidpi/' : '' ) + 'icon' + ( that.rtl ? '-rtl' : '' ) + '.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(
'↵', 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; border-radius: 2px;',
'top:-17px; border-radius: 2px 2px 0px 0px;',
'top:-1px; border-radius: 0px 0px 2px 2px;'
]
}
),
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. (https://dev.ckeditor.com/ticket/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.
// Command is executed in the same direction.
that.lastCmdDirection === !!insertAfter; // jshint ignore:line
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;
// (https://dev.ckeditor.com/ticket/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 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 getStyle( propertyName ) {
return element.getComputedStyle.call( element, propertyName );
}
}
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 the edge of the element and the mouse pointer that
* causes the magic line to appear. This option accepts a value in pixels, without the unit (for example:
* `15` for 15 pixels).
*
* Read more in the [documentation](#!/guide/dev_magicline)
* and see the [SDK sample](https://sdk.ckeditor.com/samples/magicline.html).
*
* // 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 the mouse pointer and the box within
* which the magic line stays revealed and no other focus space is offered to be accessed.
* This value is relative to {@link #magicline_triggerOffset}.
*
* Read more in the [documentation](#!/guide/dev_magicline)
* and see the [SDK sample](https://sdk.ckeditor.com/samples/magicline.html).
*
* // 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 the default keystroke that accesses the closest unreachable focus space **before**
* the caret (start of the selection). If there is no focus space available, the selection remains unchanged.
*
* Read more in the [documentation](#!/guide/dev_magicline)
* and see the [SDK sample](https://sdk.ckeditor.com/samples/magicline.html).
*
* // Changes the default 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 the default keystroke that accesses the closest unreachable focus space **after**
* the caret (start of the selection). If there is no focus space available, the selection remains unchanged.
*
* Read more in the [documentation](#!/guide/dev_magicline)
* and see the [SDK sample](https://sdk.ckeditor.com/samples/magicline.html).
*
* // 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 the magic line from being
* used within these elements.
*
* Read more in the [documentation](#!/guide/dev_magicline)
* and see the [SDK sample](https://sdk.ckeditor.com/samples/magicline.html).
*
* // Adds the "data-tabu" attribute to the magic line tabu list.
* CKEDITOR.config.magicline_tabuList = [ 'data-tabu' ];
*
* @cfg {Number} [magicline_tabuList=[ 'data-widget-wrapper' ]]
* @member CKEDITOR.config
*/
/**
* Defines the color of the magic line. The color may be adjusted to enhance readability.
*
* Read more in the [documentation](#!/guide/dev_magicline)
* and see the [SDK sample](https://sdk.ckeditor.com/samples/magicline.html).
*
* // Changes magic line color to blue.
* CKEDITOR.config.magicline_color = '#0000FF';
*
* @cfg {String} [magicline_color='#FF0000']
* @member CKEDITOR.config
*/
/**
* Activates the special all-encompassing mode that considers all focus spaces between
* {@link CKEDITOR.dtd#$block} elements as accessible by the magic line.
*
* Read more in the [documentation](#!/guide/dev_magicline)
* and see the [SDK sample](https://sdk.ckeditor.com/samples/magicline.html).
*
* // Enables the greedy "put everywhere" mode.
* CKEDITOR.config.magicline_everywhere = true;
*
* @cfg {Boolean} [magicline_everywhere=false]
* @member CKEDITOR.config
*/