1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855 |
- /**
- * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or http://ckeditor.com/license
- */
- /**
- * @fileOverview The Magic Line 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,bg,ca,cs,cy,da,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,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.
- // (#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 (#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 ) {
- var pointedElement = doc.$.elementFromPoint( mouse.x, mouse.y );
- // IE9QM: from times to times it will return an empty object on scroll bar hover. (#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) (#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;
- 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 = 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(
- '<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; 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. (#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;
- // (#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).
- *
- * // 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}.
- *
- * // 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 access the closest unreachable focus space **before**
- * the caret (start of the selection). If there's no any focus space, selection remains.
- *
- * // 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 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 the magic line from being
- * used within these elements.
- *
- * // 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.
- *
- * // 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.
- *
- * // Enables the greedy "put everywhere" mode.
- * CKEDITOR.config.magicline_everywhere = true;
- *
- * @cfg {Boolean} [magicline_everywhere=false]
- * @member CKEDITOR.config
- */
|