plugin.js 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018
  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 A set of utilities to find and create horizontal spaces in edited content.
  7. */
  8. 'use strict';
  9. ( function() {
  10. CKEDITOR.plugins.add( 'lineutils' );
  11. /**
  12. * Determines a position relative to an element in DOM (before).
  13. *
  14. * @readonly
  15. * @property {Number} [=0]
  16. * @member CKEDITOR
  17. */
  18. CKEDITOR.LINEUTILS_BEFORE = 1;
  19. /**
  20. * Determines a position relative to an element in DOM (after).
  21. *
  22. * @readonly
  23. * @property {Number} [=2]
  24. * @member CKEDITOR
  25. */
  26. CKEDITOR.LINEUTILS_AFTER = 2;
  27. /**
  28. * Determines a position relative to an element in DOM (inside).
  29. *
  30. * @readonly
  31. * @property {Number} [=4]
  32. * @member CKEDITOR
  33. */
  34. CKEDITOR.LINEUTILS_INSIDE = 4;
  35. /**
  36. * A utility that traverses the DOM tree and discovers elements
  37. * (relations) matching user-defined lookups.
  38. *
  39. * @private
  40. * @class CKEDITOR.plugins.lineutils.finder
  41. * @constructor Creates a Finder class instance.
  42. * @param {CKEDITOR.editor} editor Editor instance that the Finder belongs to.
  43. * @param {Object} def Finder's definition.
  44. * @since 4.3
  45. */
  46. function Finder( editor, def ) {
  47. CKEDITOR.tools.extend( this, {
  48. editor: editor,
  49. editable: editor.editable(),
  50. doc: editor.document,
  51. win: editor.window
  52. }, def, true );
  53. this.inline = this.editable.isInline();
  54. if ( !this.inline ) {
  55. this.frame = this.win.getFrame();
  56. }
  57. this.target = this[ this.inline ? 'editable' : 'doc' ];
  58. }
  59. Finder.prototype = {
  60. /**
  61. * Initializes searching for elements with every mousemove event fired.
  62. * To stop searching use {@link #stop}.
  63. *
  64. * @param {Function} [callback] Function executed on every iteration.
  65. */
  66. start: function( callback ) {
  67. var that = this,
  68. editor = this.editor,
  69. doc = this.doc,
  70. el, elfp, x, y;
  71. var moveBuffer = CKEDITOR.tools.eventsBuffer( 50, function() {
  72. if ( editor.readOnly || editor.mode != 'wysiwyg' )
  73. return;
  74. that.relations = {};
  75. // Sometimes it happens that elementFromPoint returns null (especially on IE).
  76. // Any further traversal makes no sense if there's no start point. Abort.
  77. // Note: In IE8 elementFromPoint may return zombie nodes of undefined nodeType,
  78. // so rejecting those as well.
  79. if ( !( elfp = doc.$.elementFromPoint( x, y ) ) || !elfp.nodeType ) {
  80. return;
  81. }
  82. el = new CKEDITOR.dom.element( elfp );
  83. that.traverseSearch( el );
  84. if ( !isNaN( x + y ) ) {
  85. that.pixelSearch( el, x, y );
  86. }
  87. callback && callback( that.relations, x, y );
  88. } );
  89. // Searching starting from element from point on mousemove.
  90. this.listener = this.editable.attachListener( this.target, 'mousemove', function( evt ) {
  91. x = evt.data.$.clientX;
  92. y = evt.data.$.clientY;
  93. moveBuffer.input();
  94. } );
  95. this.editable.attachListener( this.inline ? this.editable : this.frame, 'mouseout', function() {
  96. moveBuffer.reset();
  97. } );
  98. },
  99. /**
  100. * Stops observing mouse events attached by {@link #start}.
  101. */
  102. stop: function() {
  103. if ( this.listener ) {
  104. this.listener.removeListener();
  105. }
  106. },
  107. /**
  108. * Returns a range representing the relation, according to its element
  109. * and type.
  110. *
  111. * @param {Object} location Location containing a unique identifier and type.
  112. * @returns {CKEDITOR.dom.range} Range representing the relation.
  113. */
  114. getRange: ( function() {
  115. var where = {};
  116. where[ CKEDITOR.LINEUTILS_BEFORE ] = CKEDITOR.POSITION_BEFORE_START;
  117. where[ CKEDITOR.LINEUTILS_AFTER ] = CKEDITOR.POSITION_AFTER_END;
  118. where[ CKEDITOR.LINEUTILS_INSIDE ] = CKEDITOR.POSITION_AFTER_START;
  119. return function( location ) {
  120. var range = this.editor.createRange();
  121. range.moveToPosition( this.relations[ location.uid ].element, where[ location.type ] );
  122. return range;
  123. };
  124. } )(),
  125. /**
  126. * Stores given relation in a {@link #relations} object. Processes the relation
  127. * to normalize and avoid duplicates.
  128. *
  129. * @param {CKEDITOR.dom.element} el Element of the relation.
  130. * @param {Number} type Relation, one of `CKEDITOR.LINEUTILS_AFTER`, `CKEDITOR.LINEUTILS_BEFORE`, `CKEDITOR.LINEUTILS_INSIDE`.
  131. */
  132. store: ( function() {
  133. function merge( el, type, relations ) {
  134. var uid = el.getUniqueId();
  135. if ( uid in relations ) {
  136. relations[ uid ].type |= type;
  137. } else {
  138. relations[ uid ] = { element: el, type: type };
  139. }
  140. }
  141. return function( el, type ) {
  142. var alt;
  143. // Normalization to avoid duplicates:
  144. // CKEDITOR.LINEUTILS_AFTER becomes CKEDITOR.LINEUTILS_BEFORE of el.getNext().
  145. if ( is( type, CKEDITOR.LINEUTILS_AFTER ) && isStatic( alt = el.getNext() ) && alt.isVisible() ) {
  146. merge( alt, CKEDITOR.LINEUTILS_BEFORE, this.relations );
  147. type ^= CKEDITOR.LINEUTILS_AFTER;
  148. }
  149. // Normalization to avoid duplicates:
  150. // CKEDITOR.LINEUTILS_INSIDE becomes CKEDITOR.LINEUTILS_BEFORE of el.getFirst().
  151. if ( is( type, CKEDITOR.LINEUTILS_INSIDE ) && isStatic( alt = el.getFirst() ) && alt.isVisible() ) {
  152. merge( alt, CKEDITOR.LINEUTILS_BEFORE, this.relations );
  153. type ^= CKEDITOR.LINEUTILS_INSIDE;
  154. }
  155. merge( el, type, this.relations );
  156. };
  157. } )(),
  158. /**
  159. * Traverses the DOM tree towards root, checking all ancestors
  160. * with lookup rules, avoiding duplicates. Stores positive relations
  161. * in the {@link #relations} object.
  162. *
  163. * @param {CKEDITOR.dom.element} el Element which is the starting point.
  164. */
  165. traverseSearch: function( el ) {
  166. var l, type, uid;
  167. // Go down DOM towards root (or limit).
  168. do {
  169. uid = el.$[ 'data-cke-expando' ];
  170. // This element was already visited and checked.
  171. if ( uid && uid in this.relations ) {
  172. continue;
  173. }
  174. if ( el.equals( this.editable ) ) {
  175. return;
  176. }
  177. if ( isStatic( el ) ) {
  178. // Collect all addresses yielded by lookups for that element.
  179. for ( l in this.lookups ) {
  180. if ( ( type = this.lookups[ l ]( el ) ) ) {
  181. this.store( el, type );
  182. }
  183. }
  184. }
  185. } while ( !isLimit( el ) && ( el = el.getParent() ) );
  186. },
  187. /**
  188. * Iterates vertically pixel-by-pixel within a given element starting
  189. * from given coordinates, searching for elements in the neighborhood.
  190. * Once an element is found it is processed by {@link #traverseSearch}.
  191. *
  192. * @param {CKEDITOR.dom.element} el Element which is the starting point.
  193. * @param {Number} [x] Horizontal mouse coordinate relative to the viewport.
  194. * @param {Number} [y] Vertical mouse coordinate relative to the viewport.
  195. */
  196. pixelSearch: ( function() {
  197. var contains = CKEDITOR.env.ie || CKEDITOR.env.webkit ?
  198. function( el, found ) {
  199. return el.contains( found );
  200. } : function( el, found ) {
  201. return !!( el.compareDocumentPosition( found ) & 16 );
  202. };
  203. // Iterates pixel-by-pixel from starting coordinates, moving by defined
  204. // step and getting elementFromPoint in every iteration. Iteration stops when:
  205. // * A valid element is found.
  206. // * Condition function returns `false` (i.e. reached boundaries of viewport).
  207. // * No element is found (i.e. coordinates out of viewport).
  208. // * Element found is ascendant of starting element.
  209. //
  210. // @param {Object} doc Native DOM document.
  211. // @param {Object} el Native DOM element.
  212. // @param {Number} xStart Horizontal starting coordinate to use.
  213. // @param {Number} yStart Vertical starting coordinate to use.
  214. // @param {Number} step Step of the algorithm.
  215. // @param {Function} condition A condition relative to current vertical coordinate.
  216. function iterate( el, xStart, yStart, step, condition ) {
  217. var y = yStart,
  218. tryouts = 0,
  219. found;
  220. while ( condition( y ) ) {
  221. y += step;
  222. // If we try and we try, and still nothing's found, let's end
  223. // that party.
  224. if ( ++tryouts == 25 ) {
  225. return;
  226. }
  227. found = this.doc.$.elementFromPoint( xStart, y );
  228. // Nothing found. This is crazy... but...
  229. // It might be that a line, which is in different document,
  230. // covers that pixel (elementFromPoint is doc-sensitive).
  231. // Better let's have another try.
  232. if ( !found ) {
  233. continue;
  234. }
  235. // Still in the same element.
  236. else if ( found == el ) {
  237. tryouts = 0;
  238. continue;
  239. }
  240. // Reached the edge of an element and found an ancestor or...
  241. // A line, that covers that pixel. Better let's have another try.
  242. else if ( !contains( el, found ) ) {
  243. continue;
  244. }
  245. tryouts = 0;
  246. // Found a valid element. Stop iterating.
  247. if ( isStatic( ( found = new CKEDITOR.dom.element( found ) ) ) ) {
  248. return found;
  249. }
  250. }
  251. }
  252. return function( el, x, y ) {
  253. var paneHeight = this.win.getViewPaneSize().height,
  254. // Try to find an element iterating *up* from the starting point.
  255. neg = iterate.call( this, el.$, x, y, -1, function( y ) {
  256. return y > 0;
  257. } ),
  258. // Try to find an element iterating *down* from the starting point.
  259. pos = iterate.call( this, el.$, x, y, 1, function( y ) {
  260. return y < paneHeight;
  261. } );
  262. if ( neg ) {
  263. this.traverseSearch( neg );
  264. // Iterate towards DOM root until neg is a direct child of el.
  265. while ( !neg.getParent().equals( el ) ) {
  266. neg = neg.getParent();
  267. }
  268. }
  269. if ( pos ) {
  270. this.traverseSearch( pos );
  271. // Iterate towards DOM root until pos is a direct child of el.
  272. while ( !pos.getParent().equals( el ) ) {
  273. pos = pos.getParent();
  274. }
  275. }
  276. // Iterate forwards starting from neg and backwards from
  277. // pos to harvest all children of el between those elements.
  278. // Stop when neg and pos meet each other or there's none of them.
  279. // TODO (?) reduce number of hops forwards/backwards.
  280. while ( neg || pos ) {
  281. if ( neg ) {
  282. neg = neg.getNext( isStatic );
  283. }
  284. if ( !neg || neg.equals( pos ) ) {
  285. break;
  286. }
  287. this.traverseSearch( neg );
  288. if ( pos ) {
  289. pos = pos.getPrevious( isStatic );
  290. }
  291. if ( !pos || pos.equals( neg ) ) {
  292. break;
  293. }
  294. this.traverseSearch( pos );
  295. }
  296. };
  297. } )(),
  298. /**
  299. * Unlike {@link #traverseSearch}, it collects **all** elements from editable's DOM tree
  300. * and runs lookups for every one of them, collecting relations.
  301. *
  302. * @returns {Object} {@link #relations}.
  303. */
  304. greedySearch: function() {
  305. this.relations = {};
  306. var all = this.editable.getElementsByTag( '*' ),
  307. i = 0,
  308. el, type, l;
  309. while ( ( el = all.getItem( i++ ) ) ) {
  310. // Don't consider editable, as it might be inline,
  311. // and i.e. checking it's siblings is pointless.
  312. if ( el.equals( this.editable ) ) {
  313. continue;
  314. }
  315. // On IE8 element.getElementsByTagName returns comments... sic! (#13176)
  316. if ( el.type != CKEDITOR.NODE_ELEMENT ) {
  317. continue;
  318. }
  319. // Don't visit non-editable internals, for example widget's
  320. // guts (above wrapper, below nested). Still check editable limits,
  321. // as they are siblings with editable contents.
  322. if ( !el.hasAttribute( 'contenteditable' ) && el.isReadOnly() ) {
  323. continue;
  324. }
  325. if ( isStatic( el ) && el.isVisible() ) {
  326. // Collect all addresses yielded by lookups for that element.
  327. for ( l in this.lookups ) {
  328. if ( ( type = this.lookups[ l ]( el ) ) ) {
  329. this.store( el, type );
  330. }
  331. }
  332. }
  333. }
  334. return this.relations;
  335. }
  336. /**
  337. * Relations express elements in DOM that match user-defined {@link #lookups}.
  338. * Every relation has its own `type` that determines whether
  339. * it refers to the space before, after or inside the `element`.
  340. * This object stores relations found by {@link #traverseSearch} or {@link #greedySearch}, structured
  341. * in the following way:
  342. *
  343. * relations: {
  344. * // Unique identifier of the element.
  345. * Number: {
  346. * // Element of this relation.
  347. * element: {@link CKEDITOR.dom.element}
  348. * // Conjunction of CKEDITOR.LINEUTILS_BEFORE, CKEDITOR.LINEUTILS_AFTER and CKEDITOR.LINEUTILS_INSIDE.
  349. * type: Number
  350. * },
  351. * ...
  352. * }
  353. *
  354. * @property {Object} relations
  355. * @readonly
  356. */
  357. /**
  358. * A set of user-defined functions used by Finder to check if an element
  359. * is a valid relation, belonging to {@link #relations}.
  360. * When the criterion is met, lookup returns a logical conjunction of `CKEDITOR.LINEUTILS_BEFORE`,
  361. * `CKEDITOR.LINEUTILS_AFTER` or `CKEDITOR.LINEUTILS_INSIDE`.
  362. *
  363. * Lookups are passed along with Finder's definition.
  364. *
  365. * lookups: {
  366. * 'some lookup': function( el ) {
  367. * if ( someCondition )
  368. * return CKEDITOR.LINEUTILS_BEFORE;
  369. * },
  370. * ...
  371. * }
  372. *
  373. * @property {Object} lookups
  374. */
  375. };
  376. /**
  377. * A utility that analyses relations found by
  378. * CKEDITOR.plugins.lineutils.finder and locates them
  379. * in the viewport as horizontal lines of specific coordinates.
  380. *
  381. * @private
  382. * @class CKEDITOR.plugins.lineutils.locator
  383. * @constructor Creates a Locator class instance.
  384. * @param {CKEDITOR.editor} editor Editor instance that Locator belongs to.
  385. * @since 4.3
  386. */
  387. function Locator( editor, def ) {
  388. CKEDITOR.tools.extend( this, def, {
  389. editor: editor
  390. }, true );
  391. }
  392. Locator.prototype = {
  393. /**
  394. * Locates the Y coordinate for all types of every single relation and stores
  395. * them in an object.
  396. *
  397. * @param {Object} relations {@link CKEDITOR.plugins.lineutils.finder#relations}.
  398. * @returns {Object} {@link #locations}.
  399. */
  400. locate: ( function() {
  401. function locateSibling( rel, type ) {
  402. var sib = rel.element[ type === CKEDITOR.LINEUTILS_BEFORE ? 'getPrevious' : 'getNext' ]();
  403. // Return the middle point between siblings.
  404. if ( sib && isStatic( sib ) ) {
  405. rel.siblingRect = sib.getClientRect();
  406. if ( type == CKEDITOR.LINEUTILS_BEFORE ) {
  407. return ( rel.siblingRect.bottom + rel.elementRect.top ) / 2;
  408. } else {
  409. return ( rel.elementRect.bottom + rel.siblingRect.top ) / 2;
  410. }
  411. }
  412. // If there's no sibling, use the edge of an element.
  413. else {
  414. if ( type == CKEDITOR.LINEUTILS_BEFORE ) {
  415. return rel.elementRect.top;
  416. } else {
  417. return rel.elementRect.bottom;
  418. }
  419. }
  420. }
  421. return function( relations ) {
  422. var rel;
  423. this.locations = {};
  424. for ( var uid in relations ) {
  425. rel = relations[ uid ];
  426. rel.elementRect = rel.element.getClientRect();
  427. if ( is( rel.type, CKEDITOR.LINEUTILS_BEFORE ) ) {
  428. this.store( uid, CKEDITOR.LINEUTILS_BEFORE, locateSibling( rel, CKEDITOR.LINEUTILS_BEFORE ) );
  429. }
  430. if ( is( rel.type, CKEDITOR.LINEUTILS_AFTER ) ) {
  431. this.store( uid, CKEDITOR.LINEUTILS_AFTER, locateSibling( rel, CKEDITOR.LINEUTILS_AFTER ) );
  432. }
  433. // The middle point of the element.
  434. if ( is( rel.type, CKEDITOR.LINEUTILS_INSIDE ) ) {
  435. this.store( uid, CKEDITOR.LINEUTILS_INSIDE, ( rel.elementRect.top + rel.elementRect.bottom ) / 2 );
  436. }
  437. }
  438. return this.locations;
  439. };
  440. } )(),
  441. /**
  442. * Calculates distances from every location to given vertical coordinate
  443. * and sorts locations according to that distance.
  444. *
  445. * @param {Number} y The vertical coordinate used for sorting, used as a reference.
  446. * @param {Number} [howMany] Determines the number of "closest locations" to be returned.
  447. * @returns {Array} Sorted, array representation of {@link #locations}.
  448. */
  449. sort: ( function() {
  450. var locations, sorted,
  451. dist, i;
  452. function distance( y, uid, type ) {
  453. return Math.abs( y - locations[ uid ][ type ] );
  454. }
  455. return function( y, howMany ) {
  456. locations = this.locations;
  457. sorted = [];
  458. for ( var uid in locations ) {
  459. for ( var type in locations[ uid ] ) {
  460. dist = distance( y, uid, type );
  461. // An array is empty.
  462. if ( !sorted.length ) {
  463. sorted.push( { uid: +uid, type: type, dist: dist } );
  464. } else {
  465. // Sort the array on fly when it's populated.
  466. for ( i = 0; i < sorted.length; i++ ) {
  467. if ( dist < sorted[ i ].dist ) {
  468. sorted.splice( i, 0, { uid: +uid, type: type, dist: dist } );
  469. break;
  470. }
  471. }
  472. // Nothing was inserted, so the distance is bigger than
  473. // any of already calculated: push to the end.
  474. if ( i == sorted.length ) {
  475. sorted.push( { uid: +uid, type: type, dist: dist } );
  476. }
  477. }
  478. }
  479. }
  480. if ( typeof howMany != 'undefined' ) {
  481. return sorted.slice( 0, howMany );
  482. } else {
  483. return sorted;
  484. }
  485. };
  486. } )(),
  487. /**
  488. * Stores the location in a collection.
  489. *
  490. * @param {Number} uid Unique identifier of the relation.
  491. * @param {Number} type One of `CKEDITOR.LINEUTILS_BEFORE`, `CKEDITOR.LINEUTILS_AFTER` and `CKEDITOR.LINEUTILS_INSIDE`.
  492. * @param {Number} y Vertical position of the relation.
  493. */
  494. store: function( uid, type, y ) {
  495. if ( !this.locations[ uid ] ) {
  496. this.locations[ uid ] = {};
  497. }
  498. this.locations[ uid ][ type ] = y;
  499. }
  500. /**
  501. * @readonly
  502. * @property {Object} locations
  503. */
  504. };
  505. var tipCss = {
  506. display: 'block',
  507. width: '0px',
  508. height: '0px',
  509. 'border-color': 'transparent',
  510. 'border-style': 'solid',
  511. position: 'absolute',
  512. top: '-6px'
  513. },
  514. lineStyle = {
  515. height: '0px',
  516. 'border-top': '1px dashed red',
  517. position: 'absolute',
  518. 'z-index': 9999
  519. },
  520. lineTpl =
  521. '<div data-cke-lineutils-line="1" class="cke_reset_all" style="{lineStyle}">' +
  522. '<span style="{tipLeftStyle}">&nbsp;</span>' +
  523. '<span style="{tipRightStyle}">&nbsp;</span>' +
  524. '</div>';
  525. /**
  526. * A utility that draws horizontal lines in DOM according to locations
  527. * returned by CKEDITOR.plugins.lineutils.locator.
  528. *
  529. * @private
  530. * @class CKEDITOR.plugins.lineutils.liner
  531. * @constructor Creates a Liner class instance.
  532. * @param {CKEDITOR.editor} editor Editor instance that Liner belongs to.
  533. * @param {Object} def Liner's definition.
  534. * @since 4.3
  535. */
  536. function Liner( editor, def ) {
  537. var editable = editor.editable();
  538. CKEDITOR.tools.extend( this, {
  539. editor: editor,
  540. editable: editable,
  541. inline: editable.isInline(),
  542. doc: editor.document,
  543. win: editor.window,
  544. container: CKEDITOR.document.getBody(),
  545. winTop: CKEDITOR.document.getWindow()
  546. }, def, true );
  547. this.hidden = {};
  548. this.visible = {};
  549. if ( !this.inline ) {
  550. this.frame = this.win.getFrame();
  551. }
  552. this.queryViewport();
  553. // Callbacks must be wrapped. Otherwise they're not attached
  554. // to global DOM objects (i.e. topmost window) for every editor
  555. // because they're treated as duplicates. They belong to the
  556. // same prototype shared among Liner instances.
  557. var queryViewport = CKEDITOR.tools.bind( this.queryViewport, this ),
  558. hideVisible = CKEDITOR.tools.bind( this.hideVisible, this ),
  559. removeAll = CKEDITOR.tools.bind( this.removeAll, this );
  560. editable.attachListener( this.winTop, 'resize', queryViewport );
  561. editable.attachListener( this.winTop, 'scroll', queryViewport );
  562. editable.attachListener( this.winTop, 'resize', hideVisible );
  563. editable.attachListener( this.win, 'scroll', hideVisible );
  564. editable.attachListener( this.inline ? editable : this.frame, 'mouseout', function( evt ) {
  565. var x = evt.data.$.clientX,
  566. y = evt.data.$.clientY;
  567. this.queryViewport();
  568. // Check if mouse is out of the element (iframe/editable).
  569. if ( x <= this.rect.left || x >= this.rect.right || y <= this.rect.top || y >= this.rect.bottom ) {
  570. this.hideVisible();
  571. }
  572. // Check if mouse is out of the top-window vieport.
  573. if ( x <= 0 || x >= this.winTopPane.width || y <= 0 || y >= this.winTopPane.height ) {
  574. this.hideVisible();
  575. }
  576. }, this );
  577. editable.attachListener( editor, 'resize', queryViewport );
  578. editable.attachListener( editor, 'mode', removeAll );
  579. editor.on( 'destroy', removeAll );
  580. this.lineTpl = new CKEDITOR.template( lineTpl ).output( {
  581. lineStyle: CKEDITOR.tools.writeCssText(
  582. CKEDITOR.tools.extend( {}, lineStyle, this.lineStyle, true )
  583. ),
  584. tipLeftStyle: CKEDITOR.tools.writeCssText(
  585. CKEDITOR.tools.extend( {}, tipCss, {
  586. left: '0px',
  587. 'border-left-color': 'red',
  588. 'border-width': '6px 0 6px 6px'
  589. }, this.tipCss, this.tipLeftStyle, true )
  590. ),
  591. tipRightStyle: CKEDITOR.tools.writeCssText(
  592. CKEDITOR.tools.extend( {}, tipCss, {
  593. right: '0px',
  594. 'border-right-color': 'red',
  595. 'border-width': '6px 6px 6px 0'
  596. }, this.tipCss, this.tipRightStyle, true )
  597. )
  598. } );
  599. }
  600. Liner.prototype = {
  601. /**
  602. * Permanently removes all lines (both hidden and visible) from DOM.
  603. */
  604. removeAll: function() {
  605. var l;
  606. for ( l in this.hidden ) {
  607. this.hidden[ l ].remove();
  608. delete this.hidden[ l ];
  609. }
  610. for ( l in this.visible ) {
  611. this.visible[ l ].remove();
  612. delete this.visible[ l ];
  613. }
  614. },
  615. /**
  616. * Hides a given line.
  617. *
  618. * @param {CKEDITOR.dom.element} line The line to be hidden.
  619. */
  620. hideLine: function( line ) {
  621. var uid = line.getUniqueId();
  622. line.hide();
  623. this.hidden[ uid ] = line;
  624. delete this.visible[ uid ];
  625. },
  626. /**
  627. * Shows a given line.
  628. *
  629. * @param {CKEDITOR.dom.element} line The line to be shown.
  630. */
  631. showLine: function( line ) {
  632. var uid = line.getUniqueId();
  633. line.show();
  634. this.visible[ uid ] = line;
  635. delete this.hidden[ uid ];
  636. },
  637. /**
  638. * Hides all visible lines.
  639. */
  640. hideVisible: function() {
  641. for ( var l in this.visible ) {
  642. this.hideLine( this.visible[ l ] );
  643. }
  644. },
  645. /**
  646. * Shows a line at given location.
  647. *
  648. * @param {Object} location Location object containing the unique identifier of the relation
  649. * and its type. Usually returned by {@link CKEDITOR.plugins.lineutils.locator#sort}.
  650. * @param {Function} [callback] A callback to be called once the line is shown.
  651. */
  652. placeLine: function( location, callback ) {
  653. var styles, line, l;
  654. // No style means that line would be out of viewport.
  655. if ( !( styles = this.getStyle( location.uid, location.type ) ) ) {
  656. return;
  657. }
  658. // Search for any visible line of a different hash first.
  659. // It's faster to re-position visible line than to show it.
  660. for ( l in this.visible ) {
  661. if ( this.visible[ l ].getCustomData( 'hash' ) !== this.hash ) {
  662. line = this.visible[ l ];
  663. break;
  664. }
  665. }
  666. // Search for any hidden line of a different hash.
  667. if ( !line ) {
  668. for ( l in this.hidden ) {
  669. if ( this.hidden[ l ].getCustomData( 'hash' ) !== this.hash ) {
  670. this.showLine( ( line = this.hidden[ l ] ) );
  671. break;
  672. }
  673. }
  674. }
  675. // If no line available, add the new one.
  676. if ( !line ) {
  677. this.showLine( ( line = this.addLine() ) );
  678. }
  679. // Mark the line with current hash.
  680. line.setCustomData( 'hash', this.hash );
  681. // Mark the line as visible.
  682. this.visible[ line.getUniqueId() ] = line;
  683. line.setStyles( styles );
  684. callback && callback( line );
  685. },
  686. /**
  687. * Creates a style set to be used by the line, representing a particular
  688. * relation (location).
  689. *
  690. * @param {Number} uid Unique identifier of the relation.
  691. * @param {Number} type Type of the relation.
  692. * @returns {Object} An object containing styles.
  693. */
  694. getStyle: function( uid, type ) {
  695. var rel = this.relations[ uid ],
  696. loc = this.locations[ uid ][ type ],
  697. styles = {},
  698. hdiff;
  699. // Line should be between two elements.
  700. if ( rel.siblingRect ) {
  701. styles.width = Math.max( rel.siblingRect.width, rel.elementRect.width );
  702. }
  703. // Line is relative to a single element.
  704. else {
  705. styles.width = rel.elementRect.width;
  706. }
  707. // Let's calculate the vertical position of the line.
  708. if ( this.inline ) {
  709. // (#13155)
  710. styles.top = loc + this.winTopScroll.y - this.rect.relativeY;
  711. } else {
  712. styles.top = this.rect.top + this.winTopScroll.y + loc;
  713. }
  714. // Check if line would be vertically out of the viewport.
  715. if ( styles.top - this.winTopScroll.y < this.rect.top || styles.top - this.winTopScroll.y > this.rect.bottom ) {
  716. return false;
  717. }
  718. // Now let's calculate the horizontal alignment (left and width).
  719. if ( this.inline ) {
  720. // (#13155)
  721. styles.left = rel.elementRect.left - this.rect.relativeX;
  722. } else {
  723. if ( rel.elementRect.left > 0 )
  724. styles.left = this.rect.left + rel.elementRect.left;
  725. // H-scroll case. Left edge of element may be out of viewport.
  726. else {
  727. styles.width += rel.elementRect.left;
  728. styles.left = this.rect.left;
  729. }
  730. // H-scroll case. Right edge of element may be out of viewport.
  731. if ( ( hdiff = styles.left + styles.width - ( this.rect.left + this.winPane.width ) ) > 0 ) {
  732. styles.width -= hdiff;
  733. }
  734. }
  735. // Finally include horizontal scroll of the global window.
  736. styles.left += this.winTopScroll.x;
  737. // Append 'px' to style values.
  738. for ( var style in styles ) {
  739. styles[ style ] = CKEDITOR.tools.cssLength( styles[ style ] );
  740. }
  741. return styles;
  742. },
  743. /**
  744. * Adds a new line to DOM.
  745. *
  746. * @returns {CKEDITOR.dom.element} A brand-new line.
  747. */
  748. addLine: function() {
  749. var line = CKEDITOR.dom.element.createFromHtml( this.lineTpl );
  750. line.appendTo( this.container );
  751. return line;
  752. },
  753. /**
  754. * Assigns a unique hash to the instance that is later used
  755. * to tell unwanted lines from new ones. This method **must** be called
  756. * before a new set of relations is to be visualized so {@link #cleanup}
  757. * eventually hides obsolete lines. This is because lines
  758. * are re-used between {@link #placeLine} calls and the number of
  759. * necessary ones may vary depending on the number of relations.
  760. *
  761. * @param {Object} relations {@link CKEDITOR.plugins.lineutils.finder#relations}.
  762. * @param {Object} locations {@link CKEDITOR.plugins.lineutils.locator#locations}.
  763. */
  764. prepare: function( relations, locations ) {
  765. this.relations = relations;
  766. this.locations = locations;
  767. this.hash = Math.random();
  768. },
  769. /**
  770. * Hides all visible lines that do not belong to current hash
  771. * and no longer represent relations (locations).
  772. *
  773. * See also: {@link #prepare}.
  774. */
  775. cleanup: function() {
  776. var line;
  777. for ( var l in this.visible ) {
  778. line = this.visible[ l ];
  779. if ( line.getCustomData( 'hash' ) !== this.hash ) {
  780. this.hideLine( line );
  781. }
  782. }
  783. },
  784. /**
  785. * Queries dimensions of the viewport, editable, frame etc.
  786. * that are used for correct positioning of the line.
  787. */
  788. queryViewport: function() {
  789. this.winPane = this.win.getViewPaneSize();
  790. this.winTopScroll = this.winTop.getScrollPosition();
  791. this.winTopPane = this.winTop.getViewPaneSize();
  792. // (#13155)
  793. this.rect = this.getClientRect( this.inline ? this.editable : this.frame );
  794. },
  795. /**
  796. * Returns `boundingClientRect` of an element, shifted by the position
  797. * of `container` when the container is not `static` (#13155).
  798. *
  799. * See also: {@link CKEDITOR.dom.element#getClientRect}.
  800. *
  801. * @param {CKEDITOR.dom.element} el A DOM element.
  802. * @returns {Object} A shifted rect, extended by `relativeY` and `relativeX` properties.
  803. */
  804. getClientRect: function( el ) {
  805. var rect = el.getClientRect(),
  806. relativeContainerDocPosition = this.container.getDocumentPosition(),
  807. relativeContainerComputedPosition = this.container.getComputedStyle( 'position' );
  808. // Static or not, those values are used to offset the position of the line so they cannot be undefined.
  809. rect.relativeX = rect.relativeY = 0;
  810. if ( relativeContainerComputedPosition != 'static' ) {
  811. // Remember the offset used to shift the clientRect.
  812. rect.relativeY = relativeContainerDocPosition.y;
  813. rect.relativeX = relativeContainerDocPosition.x;
  814. rect.top -= rect.relativeY;
  815. rect.bottom -= rect.relativeY;
  816. rect.left -= rect.relativeX;
  817. rect.right -= rect.relativeX;
  818. }
  819. return rect;
  820. }
  821. };
  822. function is( type, flag ) {
  823. return type & flag;
  824. }
  825. var floats = { left: 1, right: 1, center: 1 },
  826. positions = { absolute: 1, fixed: 1 };
  827. function isElement( node ) {
  828. return node && node.type == CKEDITOR.NODE_ELEMENT;
  829. }
  830. function isFloated( el ) {
  831. return !!( floats[ el.getComputedStyle( 'float' ) ] || floats[ el.getAttribute( 'align' ) ] );
  832. }
  833. function isPositioned( el ) {
  834. return !!positions[ el.getComputedStyle( 'position' ) ];
  835. }
  836. function isLimit( node ) {
  837. return isElement( node ) && node.getAttribute( 'contenteditable' ) == 'true';
  838. }
  839. function isStatic( node ) {
  840. return isElement( node ) && !isFloated( node ) && !isPositioned( node );
  841. }
  842. /**
  843. * Global namespace storing definitions and global helpers for the Line Utilities plugin.
  844. *
  845. * @private
  846. * @class
  847. * @singleton
  848. * @since 4.3
  849. */
  850. CKEDITOR.plugins.lineutils = {
  851. finder: Finder,
  852. locator: Locator,
  853. liner: Liner
  854. };
  855. } )();