plugin.js 66 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or http://ckeditor.com/license
  4. */
  5. /**
  6. * @fileOverview The Magic Line plugin that makes it easier to access some document areas that
  7. * are difficult to focus.
  8. */
  9. 'use strict';
  10. ( function() {
  11. CKEDITOR.plugins.add( 'magicline', {
  12. 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%
  13. init: initPlugin
  14. } );
  15. // Activates the box inside of an editor.
  16. function initPlugin( editor ) {
  17. // Configurables
  18. var config = editor.config,
  19. triggerOffset = config.magicline_triggerOffset || 30,
  20. enterMode = config.enterMode,
  21. that = {
  22. // Global stuff is being initialized here.
  23. editor: editor,
  24. enterMode: enterMode,
  25. triggerOffset: triggerOffset,
  26. holdDistance: 0 | triggerOffset * ( config.magicline_holdDistance || 0.5 ),
  27. boxColor: config.magicline_color || '#ff0000',
  28. rtl: config.contentsLangDirection == 'rtl',
  29. tabuList: [ 'data-cke-hidden-sel' ].concat( config.magicline_tabuList || [] ),
  30. triggers: config.magicline_everywhere ? DTD_BLOCK : { table: 1, hr: 1, div: 1, ul: 1, ol: 1, dl: 1, form: 1, blockquote: 1 }
  31. },
  32. scrollTimeout, checkMouseTimeoutPending, checkMouseTimer;
  33. // %REMOVE_START%
  34. // Internal DEBUG uses tools located in the topmost window.
  35. // (#9701) Due to security limitations some browsers may throw
  36. // errors when accessing window.top object. Do it safely first then.
  37. try {
  38. that.debug = window.top.DEBUG;
  39. }
  40. catch ( e ) {}
  41. that.debug = that.debug || {
  42. groupEnd: function() {},
  43. groupStart: function() {},
  44. log: function() {},
  45. logElements: function() {},
  46. logElementsEnd: function() {},
  47. logEnd: function() {},
  48. mousePos: function() {},
  49. showHidden: function() {},
  50. showTrigger: function() {},
  51. startTimer: function() {},
  52. stopTimer: function() {}
  53. };
  54. // %REMOVE_END%
  55. // Simple irrelevant elements filter.
  56. that.isRelevant = function( node ) {
  57. return isHtml( node ) && // -> Node must be an existing HTML element.
  58. !isLine( that, node ) && // -> Node can be neither the box nor its child.
  59. !isFlowBreaker( node ); // -> Node can be neither floated nor positioned nor aligned.
  60. };
  61. editor.on( 'contentDom', addListeners, this );
  62. function addListeners() {
  63. var editable = editor.editable(),
  64. doc = editor.document,
  65. win = editor.window;
  66. // Global stuff is being initialized here.
  67. extend( that, {
  68. editable: editable,
  69. inInlineMode: editable.isInline(),
  70. doc: doc,
  71. win: win,
  72. hotNode: null
  73. }, true );
  74. // This is the boundary of the editor. For inline the boundary is editable itself.
  75. // For classic (`iframe`-based) editor, the HTML element is a real boundary.
  76. that.boundary = that.inInlineMode ? that.editable : that.doc.getDocumentElement();
  77. // Enabling the box inside of inline editable is pointless.
  78. // There's no need to access spaces inside paragraphs, links, spans, etc.
  79. if ( editable.is( dtd.$inline ) )
  80. return;
  81. // Handle in-line editing by setting appropriate position.
  82. // If current position is static, make it relative and clear top/left coordinates.
  83. if ( that.inInlineMode && !isPositioned( editable ) ) {
  84. editable.setStyles( {
  85. position: 'relative',
  86. top: null,
  87. left: null
  88. } );
  89. }
  90. // Enable the box. Let it produce children elements, initialize
  91. // event handlers and own methods.
  92. initLine.call( this, that );
  93. // Get view dimensions and scroll positions.
  94. // At this stage (before any checkMouse call) it is used mostly
  95. // by tests. Nevertheless it a crucial thing.
  96. updateWindowSize( that );
  97. // Remove the box before an undo image is created.
  98. // This is important. If we didn't do that, the *undo thing* would revert the box into an editor.
  99. // Thanks to that, undo doesn't even know about the existence of the box.
  100. editable.attachListener( editor, 'beforeUndoImage', function() {
  101. that.line.detach();
  102. } );
  103. // Removes the box HTML from editor data string if getData is called.
  104. // Thanks to that, an editor never yields data polluted by the box.
  105. // Listen with very high priority, so line will be removed before other
  106. // listeners will see it.
  107. editable.attachListener( editor, 'beforeGetData', function() {
  108. // If the box is in editable, remove it.
  109. if ( that.line.wrap.getParent() ) {
  110. that.line.detach();
  111. // Restore line in the last listener for 'getData'.
  112. editor.once( 'getData', function() {
  113. that.line.attach();
  114. }, null, null, 1000 );
  115. }
  116. }, null, null, 0 );
  117. // Hide the box on mouseout if mouse leaves document.
  118. editable.attachListener( that.inInlineMode ? doc : doc.getWindow().getFrame(), 'mouseout', function( event ) {
  119. if ( editor.mode != 'wysiwyg' )
  120. return;
  121. // Check for inline-mode editor. If so, check mouse position
  122. // and remove the box if mouse outside of an editor.
  123. if ( that.inInlineMode ) {
  124. var mouse = {
  125. x: event.data.$.clientX,
  126. y: event.data.$.clientY
  127. };
  128. updateWindowSize( that );
  129. updateEditableSize( that, true );
  130. var size = that.view.editable,
  131. scroll = that.view.scroll;
  132. // If outside of an editor...
  133. if ( !inBetween( mouse.x, size.left - scroll.x, size.right - scroll.x ) || !inBetween( mouse.y, size.top - scroll.y, size.bottom - scroll.y ) ) {
  134. clearTimeout( checkMouseTimer );
  135. checkMouseTimer = null;
  136. that.line.detach();
  137. }
  138. }
  139. else {
  140. clearTimeout( checkMouseTimer );
  141. checkMouseTimer = null;
  142. that.line.detach();
  143. }
  144. } );
  145. // This one deactivates hidden mode of an editor which
  146. // prevents the box from being shown.
  147. editable.attachListener( editable, 'keyup', function() {
  148. that.hiddenMode = 0;
  149. that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
  150. } );
  151. editable.attachListener( editable, 'keydown', function( event ) {
  152. if ( editor.mode != 'wysiwyg' )
  153. return;
  154. var keyStroke = event.data.getKeystroke();
  155. switch ( keyStroke ) {
  156. // Shift pressed
  157. case 2228240: // IE
  158. case 16:
  159. that.hiddenMode = 1;
  160. that.line.detach();
  161. }
  162. that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
  163. } );
  164. // This method ensures that checkMouse aren't executed
  165. // in parallel and no more frequently than specified in timeout function.
  166. // In classic (`iframe`-based) editor, document is used as a trigger, to provide magicline
  167. // functionality when mouse is below the body (short content, short body).
  168. editable.attachListener( that.inInlineMode ? editable : doc, 'mousemove', function( event ) {
  169. checkMouseTimeoutPending = true;
  170. if ( editor.mode != 'wysiwyg' || editor.readOnly || checkMouseTimer )
  171. return;
  172. // IE<9 requires this event-driven object to be created
  173. // outside of the setTimeout statement.
  174. // Otherwise it loses the event object with its properties.
  175. var mouse = {
  176. x: event.data.$.clientX,
  177. y: event.data.$.clientY
  178. };
  179. checkMouseTimer = setTimeout( function() {
  180. checkMouse( mouse );
  181. }, 30 ); // balances performance and accessibility
  182. } );
  183. // This one removes box on scroll event.
  184. // It is to avoid box displacement.
  185. editable.attachListener( win, 'scroll', function() {
  186. if ( editor.mode != 'wysiwyg' )
  187. return;
  188. that.line.detach();
  189. // To figure this out just look at the mouseup
  190. // event handler below.
  191. if ( env.webkit ) {
  192. that.hiddenMode = 1;
  193. clearTimeout( scrollTimeout );
  194. scrollTimeout = setTimeout( function() {
  195. // Don't leave hidden mode until mouse remains pressed and
  196. // scroll is being used, i.e. when dragging something.
  197. if ( !that.mouseDown )
  198. that.hiddenMode = 0;
  199. that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
  200. }, 50 );
  201. that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
  202. }
  203. } );
  204. // Those event handlers remove the box on mousedown
  205. // and don't reveal it until the mouse is released.
  206. // It is to prevent box insertion e.g. while scrolling
  207. // (w/ scrollbar), selecting and so on.
  208. editable.attachListener( env_ie8 ? doc : win, 'mousedown', function() {
  209. if ( editor.mode != 'wysiwyg' )
  210. return;
  211. that.line.detach();
  212. that.hiddenMode = 1;
  213. that.mouseDown = 1;
  214. that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
  215. } );
  216. // Google Chrome doesn't trigger this on the scrollbar (since 2009...)
  217. // so it is totally useless to check for scroll finish
  218. // see: http://code.google.com/p/chromium/issues/detail?id=14204
  219. editable.attachListener( env_ie8 ? doc : win, 'mouseup', function() {
  220. that.hiddenMode = 0;
  221. that.mouseDown = 0;
  222. that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
  223. } );
  224. // Editor commands for accessing difficult focus spaces.
  225. editor.addCommand( 'accessPreviousSpace', accessFocusSpaceCmd( that ) );
  226. editor.addCommand( 'accessNextSpace', accessFocusSpaceCmd( that, true ) );
  227. editor.setKeystroke( [
  228. [ config.magicline_keystrokePrevious, 'accessPreviousSpace' ],
  229. [ config.magicline_keystrokeNext, 'accessNextSpace' ]
  230. ] );
  231. // Revert magicline hot node on undo/redo.
  232. editor.on( 'loadSnapshot', function() {
  233. var elements, element, i;
  234. for ( var t in { p: 1, br: 1, div: 1 } ) {
  235. // document.find is not available in QM (#11149).
  236. elements = editor.document.getElementsByTag( t );
  237. for ( i = elements.count(); i--; ) {
  238. if ( ( element = elements.getItem( i ) ).data( 'cke-magicline-hot' ) ) {
  239. // Restore hotNode
  240. that.hotNode = element;
  241. // Restore last access direction
  242. that.lastCmdDirection = element.data( 'cke-magicline-dir' ) === 'true' ? true : false;
  243. return;
  244. }
  245. }
  246. }
  247. } );
  248. // This method handles mousemove mouse for box toggling.
  249. // It uses mouse position to determine underlying element, then
  250. // it tries to use different trigger type in order to place the box
  251. // in correct place. The following procedure is executed periodically.
  252. function checkMouse( mouse ) {
  253. that.debug.groupStart( 'CheckMouse' ); // %REMOVE_LINE%
  254. that.debug.startTimer(); // %REMOVE_LINE%
  255. that.mouse = mouse;
  256. that.trigger = null;
  257. checkMouseTimer = null;
  258. updateWindowSize( that );
  259. if (
  260. checkMouseTimeoutPending && // There must be an event pending.
  261. !that.hiddenMode && // Can't be in hidden mode.
  262. editor.focusManager.hasFocus && // Editor must have focus.
  263. !that.line.mouseNear() && // Mouse pointer can't be close to the box.
  264. ( that.element = elementFromMouse( that, true ) ) // There must be valid element.
  265. ) {
  266. // If trigger exists, and trigger is correct -> show the box.
  267. // Don't show the line if trigger is a descendant of some tabu-list element.
  268. if ( ( that.trigger = triggerEditable( that ) || triggerEdge( that ) || triggerExpand( that ) ) &&
  269. !isInTabu( that, that.trigger.upper || that.trigger.lower ) ) {
  270. that.line.attach().place();
  271. }
  272. // Otherwise remove the box
  273. else {
  274. that.trigger = null;
  275. that.line.detach();
  276. }
  277. that.debug.showTrigger( that.trigger ); // %REMOVE_LINE%
  278. that.debug.mousePos( mouse.y, that.element ); // %REMOVE_LINE%
  279. checkMouseTimeoutPending = false;
  280. }
  281. that.debug.stopTimer(); // %REMOVE_LINE%
  282. that.debug.groupEnd(); // %REMOVE_LINE%
  283. }
  284. // This one allows testing and debugging. It reveals some
  285. // inner methods to the world.
  286. this.backdoor = {
  287. accessFocusSpace: accessFocusSpace,
  288. boxTrigger: boxTrigger,
  289. isLine: isLine,
  290. getAscendantTrigger: getAscendantTrigger,
  291. getNonEmptyNeighbour: getNonEmptyNeighbour,
  292. getSize: getSize,
  293. that: that,
  294. triggerEdge: triggerEdge,
  295. triggerEditable: triggerEditable,
  296. triggerExpand: triggerExpand
  297. };
  298. }
  299. }
  300. // Some shorthands for common methods to save bytes
  301. var extend = CKEDITOR.tools.extend,
  302. newElement = CKEDITOR.dom.element,
  303. newElementFromHtml = newElement.createFromHtml,
  304. env = CKEDITOR.env,
  305. env_ie8 = CKEDITOR.env.ie && CKEDITOR.env.version < 9,
  306. dtd = CKEDITOR.dtd,
  307. // Global object associating enter modes with elements.
  308. enterElements = {},
  309. // Constant values, types and so on.
  310. EDGE_TOP = 128,
  311. EDGE_BOTTOM = 64,
  312. EDGE_MIDDLE = 32,
  313. TYPE_EDGE = 16,
  314. TYPE_EXPAND = 8,
  315. LOOK_TOP = 4,
  316. LOOK_BOTTOM = 2,
  317. LOOK_NORMAL = 1,
  318. WHITE_SPACE = '\u00A0',
  319. DTD_LISTITEM = dtd.$listItem,
  320. DTD_TABLECONTENT = dtd.$tableContent,
  321. DTD_NONACCESSIBLE = extend( {}, dtd.$nonEditable, dtd.$empty ),
  322. DTD_BLOCK = dtd.$block,
  323. // Minimum time that must elapse between two update*Size calls.
  324. // It prevents constant getComuptedStyle calls and improves performance.
  325. CACHE_TIME = 100,
  326. // Shared CSS stuff for box elements
  327. 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;',
  328. CSS_TRIANGLE = CSS_COMMON + 'border-color:transparent;display:block;border-style:solid;',
  329. TRIANGLE_HTML = '<span>' + WHITE_SPACE + '</span>';
  330. enterElements[ CKEDITOR.ENTER_BR ] = 'br';
  331. enterElements[ CKEDITOR.ENTER_P ] = 'p';
  332. enterElements[ CKEDITOR.ENTER_DIV ] = 'div';
  333. function areSiblings( that, upper, lower ) {
  334. return isHtml( upper ) && isHtml( lower ) && lower.equals( upper.getNext( function( node ) {
  335. return !( isEmptyTextNode( node ) || isComment( node ) || isFlowBreaker( node ) );
  336. } ) );
  337. }
  338. // boxTrigger is an abstract type which describes
  339. // the relationship between elements that may result
  340. // in showing the box.
  341. //
  342. // The following type is used by numerous methods
  343. // to share information about the hypothetical box placement
  344. // and look by referring to boxTrigger properties.
  345. function boxTrigger( triggerSetup ) {
  346. this.upper = triggerSetup[ 0 ];
  347. this.lower = triggerSetup[ 1 ];
  348. this.set.apply( this, triggerSetup.slice( 2 ) );
  349. }
  350. boxTrigger.prototype = {
  351. set: function( edge, type, look ) {
  352. this.properties = edge + type + ( look || LOOK_NORMAL );
  353. return this;
  354. },
  355. is: function( property ) {
  356. return ( this.properties & property ) == property;
  357. }
  358. };
  359. var elementFromMouse = ( function() {
  360. function elementFromPoint( doc, mouse ) {
  361. var pointedElement = doc.$.elementFromPoint( mouse.x, mouse.y );
  362. // IE9QM: from times to times it will return an empty object on scroll bar hover. (#12185)
  363. return pointedElement && pointedElement.nodeType ?
  364. new CKEDITOR.dom.element( pointedElement ) :
  365. null;
  366. }
  367. return function( that, ignoreBox, forceMouse ) {
  368. if ( !that.mouse )
  369. return null;
  370. var doc = that.doc,
  371. lineWrap = that.line.wrap,
  372. mouse = forceMouse || that.mouse,
  373. // Note: element might be null.
  374. element = elementFromPoint( doc, mouse );
  375. // If ignoreBox is set and element is the box, it means that we
  376. // need to hide the box for a while, repeat elementFromPoint
  377. // and show it again.
  378. if ( ignoreBox && isLine( that, element ) ) {
  379. lineWrap.hide();
  380. element = elementFromPoint( doc, mouse );
  381. lineWrap.show();
  382. }
  383. // Return nothing if:
  384. // \-> Element is not HTML.
  385. if ( !( element && element.type == CKEDITOR.NODE_ELEMENT && element.$ ) )
  386. return null;
  387. // Also return nothing if:
  388. // \-> We're IE<9 and element is out of the top-level element (editable for inline and HTML for classic (`iframe`-based)).
  389. // This is due to the bug which allows IE<9 firing mouse events on element
  390. // with contenteditable=true while doing selection out (far, away) of the element.
  391. // Thus we must always be sure that we stay in editable or HTML.
  392. if ( env.ie && env.version < 9 ) {
  393. if ( !( that.boundary.equals( element ) || that.boundary.contains( element ) ) )
  394. return null;
  395. }
  396. return element;
  397. };
  398. } )();
  399. // Gets the closest parent node that belongs to triggers group.
  400. function getAscendantTrigger( that ) {
  401. var node = that.element,
  402. trigger;
  403. if ( node && isHtml( node ) ) {
  404. trigger = node.getAscendant( that.triggers, true );
  405. // If trigger is an element, neither editable nor editable's ascendant.
  406. if ( trigger && that.editable.contains( trigger ) ) {
  407. // Check for closest editable limit.
  408. // Don't consider trigger as a limit as it may be nested editable (includeSelf=false) (#12009).
  409. var limit = getClosestEditableLimit( trigger );
  410. // Trigger in nested editable area.
  411. if ( limit.getAttribute( 'contenteditable' ) == 'true' )
  412. return trigger;
  413. // Trigger in non-editable area.
  414. else if ( limit.is( that.triggers ) )
  415. return limit;
  416. else
  417. return null;
  418. return trigger;
  419. } else {
  420. return null;
  421. }
  422. }
  423. return null;
  424. }
  425. function getMidpoint( that, upper, lower ) {
  426. updateSize( that, upper );
  427. updateSize( that, lower );
  428. var upperSizeBottom = upper.size.bottom,
  429. lowerSizeTop = lower.size.top;
  430. return upperSizeBottom && lowerSizeTop ? 0 | ( upperSizeBottom + lowerSizeTop ) / 2 : upperSizeBottom || lowerSizeTop;
  431. }
  432. // Get nearest node (either text or HTML), but:
  433. // \-> Omit all empty text nodes (containing white characters only).
  434. // \-> Omit BR elements
  435. // \-> Omit flow breakers.
  436. function getNonEmptyNeighbour( that, node, goBack ) {
  437. node = node[ goBack ? 'getPrevious' : 'getNext' ]( function( node ) {
  438. return ( isTextNode( node ) && !isEmptyTextNode( node ) ) ||
  439. ( isHtml( node ) && !isFlowBreaker( node ) && !isLine( that, node ) );
  440. } );
  441. return node;
  442. }
  443. function inBetween( val, lower, upper ) {
  444. return val > lower && val < upper;
  445. }
  446. // Returns the closest ancestor that has contenteditable attribute.
  447. // Such ancestor is the limit of (non-)editable DOM branch that element
  448. // belongs to. This method omits editor editable.
  449. function getClosestEditableLimit( element, includeSelf ) {
  450. if ( element.data( 'cke-editable' ) )
  451. return null;
  452. if ( !includeSelf )
  453. element = element.getParent();
  454. while ( element ) {
  455. if ( element.data( 'cke-editable' ) )
  456. return null;
  457. if ( element.hasAttribute( 'contenteditable' ) )
  458. return element;
  459. element = element.getParent();
  460. }
  461. return null;
  462. }
  463. // Access space line consists of a few elements (spans):
  464. // \-> Line wrapper.
  465. // \-> Line.
  466. // \-> Line triangles: left triangle (LT), right triangle (RT).
  467. // \-> Button handler (BTN).
  468. //
  469. // +--------------------------------------------------- line.wrap (span) -----+
  470. // | +---------------------------------------------------- line (span) -----+ |
  471. // | | +- LT \ +- BTN -+ / RT -+ | |
  472. // | | | \ | | | / | | |
  473. // | | | / | <__| | \ | | |
  474. // | | +-----/ +-------+ \-----+ | |
  475. // | +----------------------------------------------------------------------+ |
  476. // +--------------------------------------------------------------------------+
  477. //
  478. function initLine( that ) {
  479. var doc = that.doc,
  480. // This the main box element that holds triangles and the insertion button
  481. line = newElementFromHtml( '<span contenteditable="false" style="' + CSS_COMMON + 'position:absolute;border-top:1px dashed ' + that.boxColor + '"></span>', doc ),
  482. iconPath = CKEDITOR.getUrl( this.path + 'images/' + ( env.hidpi ? 'hidpi/' : '' ) + 'icon' + ( that.rtl ? '-rtl' : '' ) + '.png' );
  483. extend( line, {
  484. attach: function() {
  485. // Only if not already attached
  486. if ( !this.wrap.getParent() )
  487. this.wrap.appendTo( that.editable, true );
  488. return this;
  489. },
  490. // Looks are as follows: [ LOOK_TOP, LOOK_BOTTOM, LOOK_NORMAL ].
  491. lineChildren: [
  492. extend(
  493. newElementFromHtml(
  494. '<span title="' + that.editor.lang.magicline.title +
  495. '" contenteditable="false">&#8629;</span>', doc
  496. ), {
  497. base: CSS_COMMON + 'height:17px;width:17px;' + ( that.rtl ? 'left' : 'right' ) + ':17px;' +
  498. 'background:url(' + iconPath + ') center no-repeat ' + that.boxColor + ';cursor:pointer;' +
  499. ( env.hc ? 'font-size: 15px;line-height:14px;border:1px solid #fff;text-align:center;' : '' ) +
  500. ( env.hidpi ? 'background-size: 9px 10px;' : '' ),
  501. looks: [
  502. 'top:-8px; border-radius: 2px;',
  503. 'top:-17px; border-radius: 2px 2px 0px 0px;',
  504. 'top:-1px; border-radius: 0px 0px 2px 2px;'
  505. ]
  506. }
  507. ),
  508. extend( newElementFromHtml( TRIANGLE_HTML, doc ), {
  509. base: CSS_TRIANGLE + 'left:0px;border-left-color:' + that.boxColor + ';',
  510. looks: [
  511. 'border-width:8px 0 8px 8px;top:-8px',
  512. 'border-width:8px 0 0 8px;top:-8px',
  513. 'border-width:0 0 8px 8px;top:0px'
  514. ]
  515. } ),
  516. extend( newElementFromHtml( TRIANGLE_HTML, doc ), {
  517. base: CSS_TRIANGLE + 'right:0px;border-right-color:' + that.boxColor + ';',
  518. looks: [
  519. 'border-width:8px 8px 8px 0;top:-8px',
  520. 'border-width:8px 8px 0 0;top:-8px',
  521. 'border-width:0 8px 8px 0;top:0px'
  522. ]
  523. } )
  524. ],
  525. detach: function() {
  526. // Detach only if already attached.
  527. if ( this.wrap.getParent() )
  528. this.wrap.remove();
  529. return this;
  530. },
  531. // Checks whether mouseY is around an element by comparing boundaries and considering
  532. // an offset distance.
  533. mouseNear: function() {
  534. that.debug.groupStart( 'mouseNear' ); // %REMOVE_LINE%
  535. updateSize( that, this );
  536. var offset = that.holdDistance,
  537. size = this.size;
  538. // Determine neighborhood by element dimensions and offsets.
  539. if ( size && inBetween( that.mouse.y, size.top - offset, size.bottom + offset ) && inBetween( that.mouse.x, size.left - offset, size.right + offset ) ) {
  540. that.debug.logEnd( 'Mouse is near.' ); // %REMOVE_LINE%
  541. return true;
  542. }
  543. that.debug.logEnd( 'Mouse isn\'t near.' ); // %REMOVE_LINE%
  544. return false;
  545. },
  546. // Adjusts position of the box according to the trigger properties.
  547. // If also affects look of the box depending on the type of the trigger.
  548. place: function() {
  549. var view = that.view,
  550. editable = that.editable,
  551. trigger = that.trigger,
  552. upper = trigger.upper,
  553. lower = trigger.lower,
  554. any = upper || lower,
  555. parent = any.getParent(),
  556. styleSet = {};
  557. // Save recent trigger for further insertion.
  558. // It is necessary due to the fact, that that.trigger may
  559. // contain different boxTrigger at the moment of insertion
  560. // or may be even null.
  561. this.trigger = trigger;
  562. upper && updateSize( that, upper, true );
  563. lower && updateSize( that, lower, true );
  564. updateSize( that, parent, true );
  565. // Yeah, that's gonna be useful in inline-mode case.
  566. if ( that.inInlineMode )
  567. updateEditableSize( that, true );
  568. // Set X coordinate (left, right, width).
  569. if ( parent.equals( editable ) ) {
  570. styleSet.left = view.scroll.x;
  571. styleSet.right = -view.scroll.x;
  572. styleSet.width = '';
  573. } else {
  574. styleSet.left = any.size.left - any.size.margin.left + view.scroll.x - ( that.inInlineMode ? view.editable.left + view.editable.border.left : 0 );
  575. styleSet.width = any.size.outerWidth + any.size.margin.left + any.size.margin.right + view.scroll.x;
  576. styleSet.right = '';
  577. }
  578. // Set Y coordinate (top) for trigger consisting of two elements.
  579. if ( upper && lower ) {
  580. // No margins at all or they're equal. Place box right between.
  581. if ( upper.size.margin.bottom === lower.size.margin.top )
  582. styleSet.top = 0 | ( upper.size.bottom + upper.size.margin.bottom / 2 );
  583. else {
  584. // Upper margin < lower margin. Place at lower margin.
  585. if ( upper.size.margin.bottom < lower.size.margin.top )
  586. styleSet.top = upper.size.bottom + upper.size.margin.bottom;
  587. // Upper margin > lower margin. Place at upper margin - lower margin.
  588. else
  589. styleSet.top = upper.size.bottom + upper.size.margin.bottom - lower.size.margin.top;
  590. }
  591. }
  592. // Set Y coordinate (top) for single-edge trigger.
  593. else if ( !upper )
  594. styleSet.top = lower.size.top - lower.size.margin.top;
  595. else if ( !lower ) {
  596. styleSet.top = upper.size.bottom + upper.size.margin.bottom;
  597. }
  598. // Set box button modes if close to the viewport horizontal edge
  599. // or look forced by the trigger.
  600. if ( trigger.is( LOOK_TOP ) || inBetween( styleSet.top, view.scroll.y - 15, view.scroll.y + 5 ) ) {
  601. styleSet.top = that.inInlineMode ? 0 : view.scroll.y;
  602. this.look( LOOK_TOP );
  603. } else if ( trigger.is( LOOK_BOTTOM ) || inBetween( styleSet.top, view.pane.bottom - 5, view.pane.bottom + 15 ) ) {
  604. styleSet.top = that.inInlineMode ? (
  605. view.editable.height + view.editable.padding.top + view.editable.padding.bottom
  606. ) : (
  607. view.pane.bottom - 1
  608. );
  609. this.look( LOOK_BOTTOM );
  610. } else {
  611. if ( that.inInlineMode )
  612. styleSet.top -= view.editable.top + view.editable.border.top;
  613. this.look( LOOK_NORMAL );
  614. }
  615. if ( that.inInlineMode ) {
  616. // 1px bug here...
  617. styleSet.top--;
  618. // Consider the editable to be an element with overflow:scroll
  619. // and non-zero scrollTop/scrollLeft value.
  620. // For example: divarea editable. (#9383)
  621. styleSet.top += view.editable.scroll.top;
  622. styleSet.left += view.editable.scroll.left;
  623. }
  624. // Append `px` prefixes.
  625. for ( var style in styleSet )
  626. styleSet[ style ] = CKEDITOR.tools.cssLength( styleSet[ style ] );
  627. this.setStyles( styleSet );
  628. },
  629. // Changes look of the box according to current needs.
  630. // Three different styles are available: [ LOOK_TOP, LOOK_BOTTOM, LOOK_NORMAL ].
  631. look: function( look ) {
  632. if ( this.oldLook == look )
  633. return;
  634. for ( var i = this.lineChildren.length, child; i--; )
  635. ( child = this.lineChildren[ i ] ).setAttribute( 'style', child.base + child.looks[ 0 | look / 2 ] );
  636. this.oldLook = look;
  637. },
  638. wrap: new newElement( 'span', that.doc )
  639. } );
  640. // Insert children into the box.
  641. for ( var i = line.lineChildren.length; i--; )
  642. line.lineChildren[ i ].appendTo( line );
  643. // Set default look of the box.
  644. line.look( LOOK_NORMAL );
  645. // Using that wrapper prevents IE (8,9) from resizing editable area at the moment
  646. // of box insertion. This works thanks to the fact, that positioned box is wrapped by
  647. // an inline element. So much tricky.
  648. line.appendTo( line.wrap );
  649. // Make the box unselectable.
  650. line.unselectable();
  651. // Handle accessSpace node insertion.
  652. line.lineChildren[ 0 ].on( 'mouseup', function( event ) {
  653. line.detach();
  654. accessFocusSpace( that, function( accessNode ) {
  655. // Use old trigger that was saved by 'place' method. Look: line.place
  656. var trigger = that.line.trigger;
  657. accessNode[ trigger.is( EDGE_TOP ) ? 'insertBefore' : 'insertAfter' ](
  658. trigger.is( EDGE_TOP ) ? trigger.lower : trigger.upper );
  659. }, true );
  660. that.editor.focus();
  661. if ( !env.ie && that.enterMode != CKEDITOR.ENTER_BR )
  662. that.hotNode.scrollIntoView();
  663. event.data.preventDefault( true );
  664. } );
  665. // Prevents IE9 from displaying the resize box and disables drag'n'drop functionality.
  666. line.on( 'mousedown', function( event ) {
  667. event.data.preventDefault( true );
  668. } );
  669. that.line = line;
  670. }
  671. // This function allows accessing any focus space according to the insert function:
  672. // * For enterMode ENTER_P it creates P element filled with dummy white-space.
  673. // * For enterMode ENTER_DIV it creates DIV element filled with dummy white-space.
  674. // * For enterMode ENTER_BR it creates BR element or &nbsp; in IE.
  675. //
  676. // The node is being inserted according to insertFunction. Finally the method
  677. // selects the non-breaking space making the node ready for typing.
  678. function accessFocusSpace( that, insertFunction, doSave ) {
  679. var range = new CKEDITOR.dom.range( that.doc ),
  680. editor = that.editor,
  681. accessNode;
  682. // IE requires text node of &nbsp; in ENTER_BR mode.
  683. if ( env.ie && that.enterMode == CKEDITOR.ENTER_BR )
  684. accessNode = that.doc.createText( WHITE_SPACE );
  685. // In other cases a regular element is used.
  686. else {
  687. // Use the enterMode of editable's limit or editor's
  688. // enter mode if not in nested editable.
  689. var limit = getClosestEditableLimit( that.element, true ),
  690. // This is an enter mode for the context. We cannot use
  691. // editor.activeEnterMode because the focused nested editable will
  692. // have a different enterMode as editor but magicline will be inserted
  693. // directly into editor's editable.
  694. enterMode = limit && limit.data( 'cke-enter-mode' ) || that.enterMode;
  695. accessNode = new newElement( enterElements[ enterMode ], that.doc );
  696. if ( !accessNode.is( 'br' ) ) {
  697. var dummy = that.doc.createText( WHITE_SPACE );
  698. dummy.appendTo( accessNode );
  699. }
  700. }
  701. doSave && editor.fire( 'saveSnapshot' );
  702. insertFunction( accessNode );
  703. //dummy.appendTo( accessNode );
  704. range.moveToPosition( accessNode, CKEDITOR.POSITION_AFTER_START );
  705. editor.getSelection().selectRanges( [ range ] );
  706. that.hotNode = accessNode;
  707. doSave && editor.fire( 'saveSnapshot' );
  708. }
  709. // Access focus space on demand by taking an element under the caret as a reference.
  710. // The space is accessed provided the element under the caret is trigger AND:
  711. //
  712. // 1. First/last-child of its parent:
  713. // +----------------------- Parent element -+
  714. // | +------------------------------ DIV -+ | <-- Access before
  715. // | | Foo^ | |
  716. // | | | |
  717. // | +------------------------------------+ | <-- Access after
  718. // +----------------------------------------+
  719. //
  720. // OR
  721. //
  722. // 2. It has a direct sibling element, which is also a trigger:
  723. // +-------------------------------- DIV#1 -+
  724. // | Foo^ |
  725. // | |
  726. // +----------------------------------------+
  727. // <-- Access here
  728. // +-------------------------------- DIV#2 -+
  729. // | Bar |
  730. // | |
  731. // +----------------------------------------+
  732. //
  733. // OR
  734. //
  735. // 3. It has a direct sibling, which is a trigger and has a valid neighbour trigger,
  736. // but belongs to dtd.$.empty/nonEditable:
  737. // +------------------------------------ P -+
  738. // | Foo^ |
  739. // | |
  740. // +----------------------------------------+
  741. // +----------------------------------- HR -+
  742. // <-- Access here
  743. // +-------------------------------- DIV#2 -+
  744. // | Bar |
  745. // | |
  746. // +----------------------------------------+
  747. //
  748. function accessFocusSpaceCmd( that, insertAfter ) {
  749. return {
  750. canUndo: true,
  751. modes: { wysiwyg: 1 },
  752. exec: ( function() {
  753. // Inserts line (accessNode) at the position by taking target node as a reference.
  754. function doAccess( target ) {
  755. // Remove old hotNode under certain circumstances.
  756. var hotNodeChar = ( env.ie && env.version < 9 ? ' ' : WHITE_SPACE ),
  757. removeOld = that.hotNode && // Old hotNode must exist.
  758. that.hotNode.getText() == hotNodeChar && // Old hotNode hasn't been changed.
  759. that.element.equals( that.hotNode ) && // Caret is inside old hotNode.
  760. // Command is executed in the same direction.
  761. that.lastCmdDirection === !!insertAfter; // jshint ignore:line
  762. accessFocusSpace( that, function( accessNode ) {
  763. if ( removeOld && that.hotNode )
  764. that.hotNode.remove();
  765. accessNode[ insertAfter ? 'insertAfter' : 'insertBefore' ]( target );
  766. // Make this element distinguishable. Also remember the direction
  767. // it's been inserted into document.
  768. accessNode.setAttributes( {
  769. 'data-cke-magicline-hot': 1,
  770. 'data-cke-magicline-dir': !!insertAfter
  771. } );
  772. // Save last direction of the command (is insertAfter?).
  773. that.lastCmdDirection = !!insertAfter;
  774. } );
  775. if ( !env.ie && that.enterMode != CKEDITOR.ENTER_BR )
  776. that.hotNode.scrollIntoView();
  777. // Detach the line if was visible (previously triggered by mouse).
  778. that.line.detach();
  779. }
  780. return function( editor ) {
  781. var selected = editor.getSelection().getStartElement(),
  782. limit;
  783. // (#9833) Go down to the closest non-inline element in DOM structure
  784. // since inline elements don't participate in in magicline.
  785. selected = selected.getAscendant( DTD_BLOCK, 1 );
  786. // Stop if selected is a child of a tabu-list element.
  787. if ( isInTabu( that, selected ) )
  788. return;
  789. // Sometimes it may happen that there's no parent block below selected element
  790. // or, for example, getAscendant reaches editable or editable parent.
  791. // We must avoid such pathological cases.
  792. if ( !selected || selected.equals( that.editable ) || selected.contains( that.editable ) )
  793. return;
  794. // Executing the command directly in nested editable should
  795. // access space before/after it.
  796. if ( ( limit = getClosestEditableLimit( selected ) ) && limit.getAttribute( 'contenteditable' ) == 'false' )
  797. selected = limit;
  798. // That holds element from mouse. Replace it with the
  799. // element under the caret.
  800. that.element = selected;
  801. // (3.) Handle the following cases where selected neighbour
  802. // is a trigger inaccessible for the caret AND:
  803. // - Is first/last-child
  804. // OR
  805. // - Has a sibling, which is also a trigger.
  806. var neighbor = getNonEmptyNeighbour( that, selected, !insertAfter ),
  807. neighborSibling;
  808. // Check for a neighbour that belongs to triggers.
  809. // Consider only non-accessible elements (they cannot have any children)
  810. // since they cannot be given a caret inside, to run the command
  811. // the regular way (1. & 2.).
  812. if (
  813. isHtml( neighbor ) && neighbor.is( that.triggers ) && neighbor.is( DTD_NONACCESSIBLE ) &&
  814. (
  815. // Check whether neighbor is first/last-child.
  816. !getNonEmptyNeighbour( that, neighbor, !insertAfter ) ||
  817. // Check for a sibling of a neighbour that also is a trigger.
  818. (
  819. ( neighborSibling = getNonEmptyNeighbour( that, neighbor, !insertAfter ) ) &&
  820. isHtml( neighborSibling ) &&
  821. neighborSibling.is( that.triggers )
  822. )
  823. )
  824. ) {
  825. doAccess( neighbor );
  826. return;
  827. }
  828. // Look for possible target element DOWN "selected" DOM branch (towards editable)
  829. // that belong to that.triggers
  830. var target = getAscendantTrigger( that, selected );
  831. // No HTML target -> no access.
  832. if ( !isHtml( target ) )
  833. return;
  834. // (1.) Target is first/last child -> access.
  835. if ( !getNonEmptyNeighbour( that, target, !insertAfter ) ) {
  836. doAccess( target );
  837. return;
  838. }
  839. var sibling = getNonEmptyNeighbour( that, target, !insertAfter );
  840. // (2.) Target has a sibling that belongs to that.triggers -> access.
  841. if ( sibling && isHtml( sibling ) && sibling.is( that.triggers ) ) {
  842. doAccess( target );
  843. return;
  844. }
  845. };
  846. } )()
  847. };
  848. }
  849. function isLine( that, node ) {
  850. if ( !( node && node.type == CKEDITOR.NODE_ELEMENT && node.$ ) )
  851. return false;
  852. var line = that.line;
  853. return line.wrap.equals( node ) || line.wrap.contains( node );
  854. }
  855. // Is text node containing white-spaces only?
  856. var isEmptyTextNode = CKEDITOR.dom.walker.whitespaces();
  857. // Is fully visible HTML node?
  858. function isHtml( node ) {
  859. return node && node.type == CKEDITOR.NODE_ELEMENT && node.$; // IE requires that
  860. }
  861. function isFloated( element ) {
  862. if ( !isHtml( element ) )
  863. return false;
  864. var options = { left: 1, right: 1, center: 1 };
  865. return !!( options[ element.getComputedStyle( 'float' ) ] || options[ element.getAttribute( 'align' ) ] );
  866. }
  867. function isFlowBreaker( element ) {
  868. if ( !isHtml( element ) )
  869. return false;
  870. return isPositioned( element ) || isFloated( element );
  871. }
  872. // Isn't node of NODE_COMMENT type?
  873. var isComment = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_COMMENT );
  874. function isPositioned( element ) {
  875. return !!{ absolute: 1, fixed: 1 }[ element.getComputedStyle( 'position' ) ];
  876. }
  877. // Is text node?
  878. function isTextNode( node ) {
  879. return node && node.type == CKEDITOR.NODE_TEXT;
  880. }
  881. function isTrigger( that, element ) {
  882. return isHtml( element ) ? element.is( that.triggers ) : null;
  883. }
  884. function isInTabu( that, element ) {
  885. if ( !element )
  886. return false;
  887. var parents = element.getParents( 1 );
  888. for ( var i = parents.length ; i-- ; ) {
  889. for ( var j = that.tabuList.length ; j-- ; ) {
  890. if ( parents[ i ].hasAttribute( that.tabuList[ j ] ) )
  891. return true;
  892. }
  893. }
  894. return false;
  895. }
  896. // This function checks vertically is there's a relevant child between element's edge
  897. // and the pointer.
  898. // \-> Table contents are omitted.
  899. function isChildBetweenPointerAndEdge( that, parent, edgeBottom ) {
  900. var edgeChild = parent[ edgeBottom ? 'getLast' : 'getFirst' ]( function( node ) {
  901. return that.isRelevant( node ) && !node.is( DTD_TABLECONTENT );
  902. } );
  903. if ( !edgeChild )
  904. return false;
  905. updateSize( that, edgeChild );
  906. return edgeBottom ? edgeChild.size.top > that.mouse.y : edgeChild.size.bottom < that.mouse.y;
  907. }
  908. // This method handles edge cases:
  909. // \-> Mouse is around upper or lower edge of view pane.
  910. // \-> Also scroll position is either minimal or maximal.
  911. // \-> It's OK to show LOOK_TOP(BOTTOM) type line.
  912. //
  913. // This trigger doesn't need additional post-filtering.
  914. //
  915. // +----------------------------- Editable -+ /--
  916. // | +---------------------- First child -+ | | <-- Top edge (first child)
  917. // | | | | |
  918. // | | | | | * Mouse activation area *
  919. // | | | | |
  920. // | | ... | | \-- Top edge + trigger offset
  921. // | . . |
  922. // | |
  923. // | . . |
  924. // | | ... | | /-- Bottom edge - trigger offset
  925. // | | | | |
  926. // | | | | | * Mouse activation area *
  927. // | | | | |
  928. // | +----------------------- Last child -+ | | <-- Bottom edge (last child)
  929. // +----------------------------------------+ \--
  930. //
  931. function triggerEditable( that ) {
  932. that.debug.groupStart( 'triggerEditable' ); // %REMOVE_LINE%
  933. var editable = that.editable,
  934. mouse = that.mouse,
  935. view = that.view,
  936. triggerOffset = that.triggerOffset,
  937. triggerLook;
  938. // Update editable dimensions.
  939. updateEditableSize( that );
  940. // This flag determines whether checking bottom trigger.
  941. var bottomTrigger = mouse.y > (
  942. that.inInlineMode ? (
  943. view.editable.top + view.editable.height / 2
  944. ) : (
  945. // This is to handle case when editable.height / 2 <<< pane.height.
  946. Math.min( view.editable.height, view.pane.height ) / 2
  947. )
  948. ),
  949. // Edge node according to bottomTrigger.
  950. edgeNode = editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( function( node ) {
  951. return !( isEmptyTextNode( node ) || isComment( node ) );
  952. } );
  953. // There's no edge node. Abort.
  954. if ( !edgeNode ) {
  955. that.debug.logEnd( 'ABORT. No edge node found.' ); // %REMOVE_LINE%
  956. return null;
  957. }
  958. // If the edgeNode in editable is ML, get the next one.
  959. if ( isLine( that, edgeNode ) ) {
  960. edgeNode = that.line.wrap[ bottomTrigger ? 'getPrevious' : 'getNext' ]( function( node ) {
  961. return !( isEmptyTextNode( node ) || isComment( node ) );
  962. } );
  963. }
  964. // Exclude bad nodes (no ML needed then):
  965. // \-> Edge node is text.
  966. // \-> Edge node is floated, etc.
  967. //
  968. // Edge node *must be* a valid trigger at this stage as well.
  969. if ( !isHtml( edgeNode ) || isFlowBreaker( edgeNode ) || !isTrigger( that, edgeNode ) ) {
  970. that.debug.logEnd( 'ABORT. Invalid edge node.' ); // %REMOVE_LINE%
  971. return null;
  972. }
  973. // Update size of edge node. Dimensions will be necessary.
  974. updateSize( that, edgeNode );
  975. // Return appropriate trigger according to bottomTrigger.
  976. // \-> Top edge trigger case first.
  977. if ( !bottomTrigger && // Top trigger case.
  978. edgeNode.size.top >= 0 && // Check if the first element is fully visible.
  979. inBetween( mouse.y, 0, edgeNode.size.top + triggerOffset ) ) { // Check if mouse in [0, edgeNode.top + triggerOffset].
  980. // Determine trigger look.
  981. triggerLook = that.inInlineMode || view.scroll.y === 0 ?
  982. LOOK_TOP : LOOK_NORMAL;
  983. that.debug.logEnd( 'SUCCESS. Created box trigger. EDGE_TOP.' ); // %REMOVE_LINE%
  984. return new boxTrigger( [ null, edgeNode,
  985. EDGE_TOP,
  986. TYPE_EDGE,
  987. triggerLook
  988. ] );
  989. }
  990. // \-> Bottom case.
  991. else if ( bottomTrigger &&
  992. edgeNode.size.bottom <= view.pane.height && // Check if the last element is fully visible
  993. inBetween( mouse.y, // Check if mouse in...
  994. edgeNode.size.bottom - triggerOffset, view.pane.height ) ) { // [ edgeNode.bottom - triggerOffset, paneHeight ]
  995. // Determine trigger look.
  996. triggerLook = that.inInlineMode ||
  997. inBetween( edgeNode.size.bottom, view.pane.height - triggerOffset, view.pane.height ) ?
  998. LOOK_BOTTOM : LOOK_NORMAL;
  999. that.debug.logEnd( 'SUCCESS. Created box trigger. EDGE_BOTTOM.' ); // %REMOVE_LINE%
  1000. return new boxTrigger( [ edgeNode, null,
  1001. EDGE_BOTTOM,
  1002. TYPE_EDGE,
  1003. triggerLook
  1004. ] );
  1005. }
  1006. that.debug.logEnd( 'ABORT. No trigger created.' ); // %REMOVE_LINE%
  1007. return null;
  1008. }
  1009. // This method covers cases *inside* of an element:
  1010. // \-> The pointer is in the top (bottom) area of an element and there's
  1011. // HTML node before (after) this element.
  1012. // \-> An element being the first or last child of its parent.
  1013. //
  1014. // +----------------------- Parent element -+
  1015. // | +----------------------- Element #1 -+ | /--
  1016. // | | | | | * Mouse activation area (as first child) *
  1017. // | | | | \--
  1018. // | | | | /--
  1019. // | | | | | * Mouse activation area (Element #2) *
  1020. // | +------------------------------------+ | \--
  1021. // | |
  1022. // | +----------------------- Element #2 -+ | /--
  1023. // | | | | | * Mouse activation area (Element #1) *
  1024. // | | | | \--
  1025. // | | | |
  1026. // | +------------------------------------+ |
  1027. // | |
  1028. // | Text node is here. |
  1029. // | |
  1030. // | +----------------------- Element #3 -+ |
  1031. // | | | |
  1032. // | | | |
  1033. // | | | | /--
  1034. // | | | | | * Mouse activation area (as last child) *
  1035. // | +------------------------------------+ | \--
  1036. // +----------------------------------------+
  1037. //
  1038. function triggerEdge( that ) {
  1039. that.debug.groupStart( 'triggerEdge' ); // %REMOVE_LINE%
  1040. var mouse = that.mouse,
  1041. view = that.view,
  1042. triggerOffset = that.triggerOffset;
  1043. // Get the ascendant trigger basing on elementFromMouse.
  1044. var element = getAscendantTrigger( that );
  1045. that.debug.logElements( [ element ], [ 'Ascendant trigger' ], 'First stage' ); // %REMOVE_LINE%
  1046. // Abort if there's no appropriate element.
  1047. if ( !element ) {
  1048. that.debug.logEnd( 'ABORT. No element, element is editable or element contains editable.' ); // %REMOVE_LINE%
  1049. return null;
  1050. }
  1051. // Dimensions will be necessary.
  1052. updateSize( that, element );
  1053. // If triggerOffset is larger than a half of element's height,
  1054. // use an offset of 1/2 of element's height. If the offset wasn't reduced,
  1055. // top area would cover most (all) cases.
  1056. var fixedOffset = Math.min( triggerOffset,
  1057. 0 | ( element.size.outerHeight / 2 ) ),
  1058. // This variable will hold the trigger to be returned.
  1059. triggerSetup = [],
  1060. triggerLook,
  1061. // This flag determines whether dealing with a bottom trigger.
  1062. bottomTrigger;
  1063. // \-> Top trigger.
  1064. if ( inBetween( mouse.y, element.size.top - 1, element.size.top + fixedOffset ) )
  1065. bottomTrigger = false;
  1066. // \-> Bottom trigger.
  1067. else if ( inBetween( mouse.y, element.size.bottom - fixedOffset, element.size.bottom + 1 ) )
  1068. bottomTrigger = true;
  1069. // \-> Abort. Not in a valid trigger space.
  1070. else {
  1071. that.debug.logEnd( 'ABORT. Not around of any edge.' ); // %REMOVE_LINE%
  1072. return null;
  1073. }
  1074. // Reject wrong elements.
  1075. // \-> Reject an element which is a flow breaker.
  1076. // \-> Reject an element which has a child above/below the mouse pointer.
  1077. // \-> Reject an element which belongs to list items.
  1078. if (
  1079. isFlowBreaker( element ) ||
  1080. isChildBetweenPointerAndEdge( that, element, bottomTrigger ) ||
  1081. element.getParent().is( DTD_LISTITEM )
  1082. ) {
  1083. that.debug.logEnd( 'ABORT. element is wrong', element ); // %REMOVE_LINE%
  1084. return null;
  1085. }
  1086. // Get sibling according to bottomTrigger.
  1087. var elementSibling = getNonEmptyNeighbour( that, element, !bottomTrigger );
  1088. // No sibling element.
  1089. // This is a first or last child case.
  1090. if ( !elementSibling ) {
  1091. // No need to reject the element as it has already been done before.
  1092. // Prepare a trigger.
  1093. // Determine trigger look.
  1094. if ( element.equals( that.editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( that.isRelevant ) ) ) {
  1095. updateEditableSize( that );
  1096. if (
  1097. bottomTrigger && inBetween( mouse.y,
  1098. element.size.bottom - fixedOffset, view.pane.height ) &&
  1099. inBetween( element.size.bottom, view.pane.height - fixedOffset, view.pane.height )
  1100. ) {
  1101. triggerLook = LOOK_BOTTOM;
  1102. } else if ( inBetween( mouse.y, 0, element.size.top + fixedOffset ) ) {
  1103. triggerLook = LOOK_TOP;
  1104. }
  1105. } else {
  1106. triggerLook = LOOK_NORMAL;
  1107. }
  1108. triggerSetup = [ null, element ][ bottomTrigger ? 'reverse' : 'concat' ]().concat( [
  1109. bottomTrigger ? EDGE_BOTTOM : EDGE_TOP,
  1110. TYPE_EDGE,
  1111. triggerLook,
  1112. element.equals( that.editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( that.isRelevant ) ) ?
  1113. ( bottomTrigger ? LOOK_BOTTOM : LOOK_TOP ) : LOOK_NORMAL
  1114. ] );
  1115. that.debug.log( 'Configured edge trigger of ' + ( bottomTrigger ? 'EDGE_BOTTOM' : 'EDGE_TOP' ) ); // %REMOVE_LINE%
  1116. }
  1117. // Abort. Sibling is a text element.
  1118. else if ( isTextNode( elementSibling ) ) {
  1119. that.debug.logEnd( 'ABORT. Sibling is non-empty text element' ); // %REMOVE_LINE%
  1120. return null;
  1121. }
  1122. // Check if the sibling is a HTML element.
  1123. // If so, create an TYPE_EDGE, EDGE_MIDDLE trigger.
  1124. else if ( isHtml( elementSibling ) ) {
  1125. // Reject wrong elementSiblings.
  1126. // \-> Reject an elementSibling which is a flow breaker.
  1127. // \-> Reject an elementSibling which isn't a trigger.
  1128. // \-> Reject an elementSibling which belongs to list items.
  1129. if (
  1130. isFlowBreaker( elementSibling ) ||
  1131. !isTrigger( that, elementSibling ) ||
  1132. elementSibling.getParent().is( DTD_LISTITEM )
  1133. ) {
  1134. that.debug.logEnd( 'ABORT. elementSibling is wrong', elementSibling ); // %REMOVE_LINE%
  1135. return null;
  1136. }
  1137. // Prepare a trigger.
  1138. triggerSetup = [ elementSibling, element ][ bottomTrigger ? 'reverse' : 'concat' ]().concat( [
  1139. EDGE_MIDDLE,
  1140. TYPE_EDGE
  1141. ] );
  1142. that.debug.log( 'Configured edge trigger of EDGE_MIDDLE' ); // %REMOVE_LINE%
  1143. }
  1144. if ( 0 in triggerSetup ) {
  1145. that.debug.logEnd( 'SUCCESS. Returning a trigger.' ); // %REMOVE_LINE%
  1146. return new boxTrigger( triggerSetup );
  1147. }
  1148. that.debug.logEnd( 'ABORT. No trigger generated.' ); // %REMOVE_LINE%
  1149. return null;
  1150. }
  1151. // Checks iteratively up and down in search for elements using elementFromMouse method.
  1152. // Useful if between two triggers.
  1153. //
  1154. // +----------------------- Parent element -+
  1155. // | +----------------------- Element #1 -+ |
  1156. // | | | |
  1157. // | | | |
  1158. // | | | |
  1159. // | +------------------------------------+ |
  1160. // | | /--
  1161. // | . | |
  1162. // | . +-- Floated -+ | |
  1163. // | | | | | | * Mouse activation area *
  1164. // | | | IGNORE | | |
  1165. // | X | | | | Method searches vertically for sibling elements.
  1166. // | | +------------+ | | Start point is X (mouse-y coordinate).
  1167. // | | | | Floated elements, comments and empty text nodes are omitted.
  1168. // | . | |
  1169. // | . | |
  1170. // | | \--
  1171. // | +----------------------- Element #2 -+ |
  1172. // | | | |
  1173. // | | | |
  1174. // | | | |
  1175. // | | | |
  1176. // | +------------------------------------+ |
  1177. // +----------------------------------------+
  1178. //
  1179. var triggerExpand = ( function() {
  1180. // The heart of the procedure. This method creates triggers that are
  1181. // filtered by expandFilter method.
  1182. function expandEngine( that ) {
  1183. that.debug.groupStart( 'expandEngine' ); // %REMOVE_LINE%
  1184. var startElement = that.element,
  1185. upper, lower, trigger;
  1186. if ( !isHtml( startElement ) || startElement.contains( that.editable ) ) {
  1187. that.debug.logEnd( 'ABORT. No start element, or start element contains editable.' ); // %REMOVE_LINE%
  1188. return null;
  1189. }
  1190. // Stop searching if element is in non-editable branch of DOM.
  1191. if ( startElement.isReadOnly() )
  1192. return null;
  1193. trigger = verticalSearch( that,
  1194. function( current, startElement ) {
  1195. return !startElement.equals( current ); // stop when start element and the current one differ
  1196. }, function( that, mouse ) {
  1197. return elementFromMouse( that, true, mouse );
  1198. }, startElement ),
  1199. upper = trigger.upper,
  1200. lower = trigger.lower;
  1201. that.debug.logElements( [ upper, lower ], [ 'Upper', 'Lower' ], 'Pair found' ); // %REMOVE_LINE%
  1202. // Success: two siblings have been found
  1203. if ( areSiblings( that, upper, lower ) ) {
  1204. that.debug.logEnd( 'SUCCESS. Expand trigger created.' ); // %REMOVE_LINE%
  1205. return trigger.set( EDGE_MIDDLE, TYPE_EXPAND );
  1206. }
  1207. that.debug.logElements( [ startElement, upper, lower ], // %REMOVE_LINE%
  1208. [ 'Start', 'Upper', 'Lower' ], 'Post-processing' ); // %REMOVE_LINE%
  1209. // Danger. Dragons ahead.
  1210. // No siblings have been found during previous phase, post-processing may be necessary.
  1211. // We can traverse DOM until a valid pair of elements around the pointer is found.
  1212. // Prepare for post-processing:
  1213. // 1. Determine if upper and lower are children of startElement.
  1214. // 1.1. If so, find their ascendants that are closest to startElement (one level deeper than startElement).
  1215. // 1.2. Otherwise use first/last-child of the startElement as upper/lower. Why?:
  1216. // a) upper/lower belongs to another branch of the DOM tree.
  1217. // b) verticalSearch encountered an edge of the viewport and failed.
  1218. // 1.3. Make sure upper and lower still exist. Why?:
  1219. // a) Upper and lower may be not belong to the branch of the startElement (may not exist at all) and
  1220. // startElement has no children.
  1221. // 2. Perform the post-processing.
  1222. // 2.1. Gather dimensions of an upper element.
  1223. // 2.2. Abort if lower edge of upper is already under the mouse pointer. Why?:
  1224. // a) We expect upper to be above and lower below the mouse pointer.
  1225. // 3. Perform iterative search while upper != lower.
  1226. // 3.1. Find the upper-next element. If there's no such element, break current search. Why?:
  1227. // a) There's no point in further search if there are only text nodes ahead.
  1228. // 3.2. Calculate the distance between the middle point of ( upper, upperNext ) and mouse-y.
  1229. // 3.3. If the distance is shorter than the previous best, save it (save upper, upperNext as well).
  1230. // 3.4. If the optimal pair is found, assign it back to the trigger.
  1231. // 1.1., 1.2.
  1232. if ( upper && startElement.contains( upper ) ) {
  1233. while ( !upper.getParent().equals( startElement ) )
  1234. upper = upper.getParent();
  1235. } else {
  1236. upper = startElement.getFirst( function( node ) {
  1237. return expandSelector( that, node );
  1238. } );
  1239. }
  1240. if ( lower && startElement.contains( lower ) ) {
  1241. while ( !lower.getParent().equals( startElement ) )
  1242. lower = lower.getParent();
  1243. } else {
  1244. lower = startElement.getLast( function( node ) {
  1245. return expandSelector( that, node );
  1246. } );
  1247. }
  1248. // 1.3.
  1249. if ( !upper || !lower ) {
  1250. that.debug.logEnd( 'ABORT. There is no upper or no lower element.' ); // %REMOVE_LINE%
  1251. return null;
  1252. }
  1253. // 2.1.
  1254. updateSize( that, upper );
  1255. updateSize( that, lower );
  1256. if ( !checkMouseBetweenElements( that, upper, lower ) ) {
  1257. that.debug.logEnd( 'ABORT. Mouse is already above upper or below lower.' ); // %REMOVE_LINE%
  1258. return null;
  1259. }
  1260. var minDistance = Number.MAX_VALUE,
  1261. currentDistance, upperNext, minElement, minElementNext;
  1262. while ( lower && !lower.equals( upper ) ) {
  1263. // 3.1.
  1264. if ( !( upperNext = upper.getNext( that.isRelevant ) ) )
  1265. break;
  1266. // 3.2.
  1267. currentDistance = Math.abs( getMidpoint( that, upper, upperNext ) - that.mouse.y );
  1268. // 3.3.
  1269. if ( currentDistance < minDistance ) {
  1270. minDistance = currentDistance;
  1271. minElement = upper;
  1272. minElementNext = upperNext;
  1273. }
  1274. upper = upperNext;
  1275. updateSize( that, upper );
  1276. }
  1277. that.debug.logElements( [ minElement, minElementNext ], // %REMOVE_LINE%
  1278. [ 'Min', 'MinNext' ], 'Post-processing results' ); // %REMOVE_LINE%
  1279. // 3.4.
  1280. if ( !minElement || !minElementNext ) {
  1281. that.debug.logEnd( 'ABORT. No Min or MinNext' ); // %REMOVE_LINE%
  1282. return null;
  1283. }
  1284. if ( !checkMouseBetweenElements( that, minElement, minElementNext ) ) {
  1285. that.debug.logEnd( 'ABORT. Mouse is already above minElement or below minElementNext.' ); // %REMOVE_LINE%
  1286. return null;
  1287. }
  1288. // An element of minimal distance has been found. Assign it to the trigger.
  1289. trigger.upper = minElement;
  1290. trigger.lower = minElementNext;
  1291. // Success: post-processing revealed a pair of elements.
  1292. that.debug.logEnd( 'SUCCESSFUL post-processing. Trigger created.' ); // %REMOVE_LINE%
  1293. return trigger.set( EDGE_MIDDLE, TYPE_EXPAND );
  1294. }
  1295. // This is default element selector used by the engine.
  1296. function expandSelector( that, node ) {
  1297. return !( isTextNode( node ) ||
  1298. isComment( node ) ||
  1299. isFlowBreaker( node ) ||
  1300. isLine( that, node ) ||
  1301. ( node.type == CKEDITOR.NODE_ELEMENT && node.$ && node.is( 'br' ) ) );
  1302. }
  1303. // This method checks whether mouse-y is between the top edge of upper
  1304. // and bottom edge of lower.
  1305. //
  1306. // NOTE: This method assumes that updateSize has already been called
  1307. // for the elements and is up-to-date.
  1308. //
  1309. // +---------------------------- Upper -+ /--
  1310. // | | |
  1311. // +------------------------------------+ |
  1312. // |
  1313. // ... |
  1314. // |
  1315. // X | * Return true for mouse-y in this range *
  1316. // |
  1317. // ... |
  1318. // |
  1319. // +---------------------------- Lower -+ |
  1320. // | | |
  1321. // +------------------------------------+ \--
  1322. //
  1323. function checkMouseBetweenElements( that, upper, lower ) {
  1324. return inBetween( that.mouse.y, upper.size.top, lower.size.bottom );
  1325. }
  1326. // A method for trigger filtering. Accepts or rejects trigger pairs
  1327. // by their location in DOM etc.
  1328. function expandFilter( that, trigger ) {
  1329. that.debug.groupStart( 'expandFilter' ); // %REMOVE_LINE%
  1330. var upper = trigger.upper,
  1331. lower = trigger.lower;
  1332. if (
  1333. !upper || !lower || // NOT: EDGE_MIDDLE trigger ALWAYS has two elements.
  1334. isFlowBreaker( lower ) || isFlowBreaker( upper ) || // NOT: one of the elements is floated or positioned
  1335. lower.equals( upper ) || upper.equals( lower ) || // NOT: two trigger elements, one equals another.
  1336. lower.contains( upper ) || upper.contains( lower )
  1337. ) { // NOT: two trigger elements, one contains another.
  1338. that.debug.logEnd( 'REJECTED. No upper or no lower or they contain each other.' ); // %REMOVE_LINE%
  1339. return false;
  1340. }
  1341. // YES: two trigger elements, pure siblings.
  1342. else if ( isTrigger( that, upper ) && isTrigger( that, lower ) && areSiblings( that, upper, lower ) ) {
  1343. that.debug.logElementsEnd( [ upper, lower ], // %REMOVE_LINE%
  1344. [ 'upper', 'lower' ], 'APPROVED EDGE_MIDDLE' ); // %REMOVE_LINE%
  1345. return true;
  1346. }
  1347. that.debug.logElementsEnd( [ upper, lower ], // %REMOVE_LINE%
  1348. [ 'upper', 'lower' ], 'Rejected unknown pair' ); // %REMOVE_LINE%
  1349. return false;
  1350. }
  1351. // Simple wrapper for expandEngine and expandFilter.
  1352. return function( that ) {
  1353. that.debug.groupStart( 'triggerExpand' ); // %REMOVE_LINE%
  1354. var trigger = expandEngine( that );
  1355. that.debug.groupEnd(); // %REMOVE_LINE%
  1356. return trigger && expandFilter( that, trigger ) ? trigger : null;
  1357. };
  1358. } )();
  1359. // Collects dimensions of an element.
  1360. var sizePrefixes = [ 'top', 'left', 'right', 'bottom' ];
  1361. function getSize( that, element, ignoreScroll, force ) {
  1362. var docPosition = element.getDocumentPosition(),
  1363. border = {},
  1364. margin = {},
  1365. padding = {},
  1366. box = {};
  1367. for ( var i = sizePrefixes.length; i--; ) {
  1368. border[ sizePrefixes[ i ] ] = parseInt( getStyle( 'border-' + sizePrefixes[ i ] + '-width' ), 10 ) || 0;
  1369. padding[ sizePrefixes[ i ] ] = parseInt( getStyle( 'padding-' + sizePrefixes[ i ] ), 10 ) || 0;
  1370. margin[ sizePrefixes[ i ] ] = parseInt( getStyle( 'margin-' + sizePrefixes[ i ] ), 10 ) || 0;
  1371. }
  1372. // updateWindowSize if forced to do so OR NOT ignoring scroll.
  1373. if ( !ignoreScroll || force )
  1374. updateWindowSize( that, force );
  1375. box.top = docPosition.y - ( ignoreScroll ? 0 : that.view.scroll.y ), box.left = docPosition.x - ( ignoreScroll ? 0 : that.view.scroll.x ),
  1376. // w/ borders and paddings.
  1377. box.outerWidth = element.$.offsetWidth, box.outerHeight = element.$.offsetHeight,
  1378. // w/o borders and paddings.
  1379. box.height = box.outerHeight - ( padding.top + padding.bottom + border.top + border.bottom ), box.width = box.outerWidth - ( padding.left + padding.right + border.left + border.right ),
  1380. box.bottom = box.top + box.outerHeight, box.right = box.left + box.outerWidth;
  1381. if ( that.inInlineMode ) {
  1382. box.scroll = {
  1383. top: element.$.scrollTop,
  1384. left: element.$.scrollLeft
  1385. };
  1386. }
  1387. return extend( {
  1388. border: border,
  1389. padding: padding,
  1390. margin: margin,
  1391. ignoreScroll: ignoreScroll
  1392. }, box, true );
  1393. function getStyle( propertyName ) {
  1394. return element.getComputedStyle.call( element, propertyName );
  1395. }
  1396. }
  1397. function updateSize( that, element, ignoreScroll ) {
  1398. if ( !isHtml( element ) ) // i.e. an element is hidden
  1399. return ( element.size = null ); // -> reset size to make it useless for other methods
  1400. if ( !element.size )
  1401. element.size = {};
  1402. // Abort if there was a similar query performed recently.
  1403. // This kind of caching provides great performance improvement.
  1404. else if ( element.size.ignoreScroll == ignoreScroll && element.size.date > new Date() - CACHE_TIME ) {
  1405. that.debug.log( 'element.size: get from cache' ); // %REMOVE_LINE%
  1406. return null;
  1407. }
  1408. that.debug.log( 'element.size: capture' ); // %REMOVE_LINE%
  1409. return extend( element.size, getSize( that, element, ignoreScroll ), {
  1410. date: +new Date()
  1411. }, true );
  1412. }
  1413. // Updates that.view.editable object.
  1414. // This one must be called separately outside of updateWindowSize
  1415. // to prevent cyclic dependency getSize<->updateWindowSize.
  1416. // It calls getSize with force flag to avoid getWindowSize cache (look: getSize).
  1417. function updateEditableSize( that, ignoreScroll ) {
  1418. that.view.editable = getSize( that, that.editable, ignoreScroll, true );
  1419. }
  1420. function updateWindowSize( that, force ) {
  1421. if ( !that.view )
  1422. that.view = {};
  1423. var view = that.view;
  1424. if ( !force && view && view.date > new Date() - CACHE_TIME ) {
  1425. that.debug.log( 'win.size: get from cache' ); // %REMOVE_LINE%
  1426. return;
  1427. }
  1428. that.debug.log( 'win.size: capturing' ); // %REMOVE_LINE%
  1429. var win = that.win,
  1430. scroll = win.getScrollPosition(),
  1431. paneSize = win.getViewPaneSize();
  1432. extend( that.view, {
  1433. scroll: {
  1434. x: scroll.x,
  1435. y: scroll.y,
  1436. width: that.doc.$.documentElement.scrollWidth - paneSize.width,
  1437. height: that.doc.$.documentElement.scrollHeight - paneSize.height
  1438. },
  1439. pane: {
  1440. width: paneSize.width,
  1441. height: paneSize.height,
  1442. bottom: paneSize.height + scroll.y
  1443. },
  1444. date: +new Date()
  1445. }, true );
  1446. }
  1447. // This method searches document vertically using given
  1448. // select criterion until stop criterion is fulfilled.
  1449. function verticalSearch( that, stopCondition, selectCriterion, startElement ) {
  1450. var upper = startElement,
  1451. lower = startElement,
  1452. mouseStep = 0,
  1453. upperFound = false,
  1454. lowerFound = false,
  1455. viewPaneHeight = that.view.pane.height,
  1456. mouse = that.mouse;
  1457. while ( mouse.y + mouseStep < viewPaneHeight && mouse.y - mouseStep > 0 ) {
  1458. if ( !upperFound )
  1459. upperFound = stopCondition( upper, startElement );
  1460. if ( !lowerFound )
  1461. lowerFound = stopCondition( lower, startElement );
  1462. // Still not found...
  1463. if ( !upperFound && mouse.y - mouseStep > 0 )
  1464. upper = selectCriterion( that, { x: mouse.x, y: mouse.y - mouseStep } );
  1465. if ( !lowerFound && mouse.y + mouseStep < viewPaneHeight )
  1466. lower = selectCriterion( that, { x: mouse.x, y: mouse.y + mouseStep } );
  1467. if ( upperFound && lowerFound )
  1468. break;
  1469. // Instead of ++ to reduce the number of invocations by half.
  1470. // It's trades off accuracy in some edge cases for improved performance.
  1471. mouseStep += 2;
  1472. }
  1473. return new boxTrigger( [ upper, lower, null, null ] );
  1474. }
  1475. } )();
  1476. /**
  1477. * Sets the default vertical distance between the edge of the element and the mouse pointer that
  1478. * causes the magic line to appear. This option accepts a value in pixels, without the unit (for example:
  1479. * `15` for 15 pixels).
  1480. *
  1481. * // Changes the offset to 15px.
  1482. * CKEDITOR.config.magicline_triggerOffset = 15;
  1483. *
  1484. * @cfg {Number} [magicline_triggerOffset=30]
  1485. * @member CKEDITOR.config
  1486. * @see CKEDITOR.config#magicline_holdDistance
  1487. */
  1488. /**
  1489. * Defines the distance between the mouse pointer and the box, within
  1490. * which the magic line stays revealed and no other focus space is offered to be accessed.
  1491. * This value is relative to {@link #magicline_triggerOffset}.
  1492. *
  1493. * // Increases the distance to 80% of CKEDITOR.config.magicline_triggerOffset.
  1494. * CKEDITOR.config.magicline_holdDistance = .8;
  1495. *
  1496. * @cfg {Number} [magicline_holdDistance=0.5]
  1497. * @member CKEDITOR.config
  1498. * @see CKEDITOR.config#magicline_triggerOffset
  1499. */
  1500. /**
  1501. * Defines the default keystroke that access the closest unreachable focus space **before**
  1502. * the caret (start of the selection). If there's no any focus space, selection remains.
  1503. *
  1504. * // Changes the default keystroke to "Ctrl + ,".
  1505. * CKEDITOR.config.magicline_keystrokePrevious = CKEDITOR.CTRL + 188;
  1506. *
  1507. * @cfg {Number} [magicline_keystrokePrevious=CKEDITOR.CTRL + CKEDITOR.SHIFT + 51 (CTRL + SHIFT + 3)]
  1508. * @member CKEDITOR.config
  1509. */
  1510. CKEDITOR.config.magicline_keystrokePrevious = CKEDITOR.CTRL + CKEDITOR.SHIFT + 51; // CTRL + SHIFT + 3
  1511. /**
  1512. * Defines the default keystroke that access the closest unreachable focus space **after**
  1513. * the caret (start of the selection). If there's no any focus space, selection remains.
  1514. *
  1515. * // Changes keystroke to "Ctrl + .".
  1516. * CKEDITOR.config.magicline_keystrokeNext = CKEDITOR.CTRL + 190;
  1517. *
  1518. * @cfg {Number} [magicline_keystrokeNext=CKEDITOR.CTRL + CKEDITOR.SHIFT + 52 (CTRL + SHIFT + 4)]
  1519. * @member CKEDITOR.config
  1520. */
  1521. CKEDITOR.config.magicline_keystrokeNext = CKEDITOR.CTRL + CKEDITOR.SHIFT + 52; // CTRL + SHIFT + 4
  1522. /**
  1523. * Defines a list of attributes that, if assigned to some elements, prevent the magic line from being
  1524. * used within these elements.
  1525. *
  1526. * // Adds the "data-tabu" attribute to the magic line tabu list.
  1527. * CKEDITOR.config.magicline_tabuList = [ 'data-tabu' ];
  1528. *
  1529. * @cfg {Number} [magicline_tabuList=[ 'data-widget-wrapper' ]]
  1530. * @member CKEDITOR.config
  1531. */
  1532. /**
  1533. * Defines the color of the magic line. The color may be adjusted to enhance readability.
  1534. *
  1535. * // Changes magic line color to blue.
  1536. * CKEDITOR.config.magicline_color = '#0000FF';
  1537. *
  1538. * @cfg {String} [magicline_color='#FF0000']
  1539. * @member CKEDITOR.config
  1540. */
  1541. /**
  1542. * Activates the special all-encompassing mode that considers all focus spaces between
  1543. * {@link CKEDITOR.dtd#$block} elements as accessible by the magic line.
  1544. *
  1545. * // Enables the greedy "put everywhere" mode.
  1546. * CKEDITOR.config.magicline_everywhere = true;
  1547. *
  1548. * @cfg {Boolean} [magicline_everywhere=false]
  1549. * @member CKEDITOR.config
  1550. */