editable.js 106 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158
  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. ( function() {
  6. /**
  7. * Editable class which provides all editing related activities by
  8. * the `contenteditable` element, dynamically get attached to editor instance.
  9. *
  10. * @class CKEDITOR.editable
  11. * @extends CKEDITOR.dom.element
  12. */
  13. CKEDITOR.editable = CKEDITOR.tools.createClass( {
  14. base: CKEDITOR.dom.element,
  15. /**
  16. * The constructor only stores generic editable creation logic that is commonly shared among
  17. * all different editable elements.
  18. *
  19. * @constructor Creates an editable class instance.
  20. * @param {CKEDITOR.editor} editor The editor instance on which the editable operates.
  21. * @param {HTMLElement/CKEDITOR.dom.element} element Any DOM element that was as the editor's
  22. * editing container, e.g. it could be either an HTML element with the `contenteditable` attribute
  23. * set to the `true` that handles WYSIWYG editing or a `<textarea>` element that handles source editing.
  24. */
  25. $: function( editor, element ) {
  26. // Transform the element into a CKEDITOR.dom.element instance.
  27. this.base( element.$ || element );
  28. this.editor = editor;
  29. /**
  30. * Indicates the initialization status of the editable element. The following statuses are available:
  31. *
  32. * * **unloaded** &ndash; the initial state. The editable's instance was created but
  33. * is not fully loaded (in particular it has no data).
  34. * * **ready** &ndash; the editable is fully initialized. The `ready` status is set after
  35. * the first {@link CKEDITOR.editor#method-setData} is called.
  36. * * **detached** &ndash; the editable was detached.
  37. *
  38. * @since 4.3.3
  39. * @readonly
  40. * @property {String}
  41. */
  42. this.status = 'unloaded';
  43. /**
  44. * Indicates whether the editable element gained focus.
  45. *
  46. * @property {Boolean} hasFocus
  47. */
  48. this.hasFocus = false;
  49. // The bootstrapping logic.
  50. this.setup();
  51. },
  52. proto: {
  53. focus: function() {
  54. var active;
  55. // [Webkit] When DOM focus is inside of nested contenteditable elements,
  56. // apply focus on the main editable will compromise it's text selection.
  57. if ( CKEDITOR.env.webkit && !this.hasFocus ) {
  58. // Restore focus on element which we cached (on selectionCheck) as previously active.
  59. active = this.editor._.previousActive || this.getDocument().getActive();
  60. if ( this.contains( active ) ) {
  61. active.focus();
  62. return;
  63. }
  64. }
  65. // [IE] Use instead "setActive" method to focus the editable if it belongs to
  66. // the host page document, to avoid bringing an unexpected scroll.
  67. try {
  68. this.$[ CKEDITOR.env.ie && this.getDocument().equals( CKEDITOR.document ) ? 'setActive' : 'focus' ]();
  69. } catch ( e ) {
  70. // IE throws unspecified error when focusing editable after closing dialog opened on nested editable.
  71. if ( !CKEDITOR.env.ie )
  72. throw e;
  73. }
  74. // Remedy if Safari doens't applies focus properly. (#279)
  75. if ( CKEDITOR.env.safari && !this.isInline() ) {
  76. active = CKEDITOR.document.getActive();
  77. if ( !active.equals( this.getWindow().getFrame() ) )
  78. this.getWindow().focus();
  79. }
  80. },
  81. /**
  82. * Overrides {@link CKEDITOR.dom.element#on} to have special `focus/blur` handling.
  83. * The `focusin/focusout` events are used in IE to replace regular `focus/blur` events
  84. * because we want to avoid the asynchronous nature of later ones.
  85. */
  86. on: function( name, fn ) {
  87. var args = Array.prototype.slice.call( arguments, 0 );
  88. if ( CKEDITOR.env.ie && ( /^focus|blur$/ ).exec( name ) ) {
  89. name = name == 'focus' ? 'focusin' : 'focusout';
  90. // The "focusin/focusout" events bubbled, e.g. If there are elements with layout
  91. // they fire this event when clicking in to edit them but it must be ignored
  92. // to allow edit their contents. (#4682)
  93. fn = isNotBubbling( fn, this );
  94. args[ 0 ] = name;
  95. args[ 1 ] = fn;
  96. }
  97. return CKEDITOR.dom.element.prototype.on.apply( this, args );
  98. },
  99. /**
  100. * Registers an event listener that needs to be removed when detaching this editable.
  101. * This means that it will be automatically removed when {@link #detach} is executed,
  102. * for example on {@link CKEDITOR.editor#setMode changing editor mode} or destroying editor.
  103. *
  104. * Except for `obj` all other arguments have the same meaning as in {@link CKEDITOR.event#on}.
  105. *
  106. * This method is strongly related to the {@link CKEDITOR.editor#contentDom} and
  107. * {@link CKEDITOR.editor#contentDomUnload} events, because they are fired
  108. * when an editable is being attached and detached. Therefore, this method is usually used
  109. * in the following way:
  110. *
  111. * editor.on( 'contentDom', function() {
  112. * var editable = editor.editable();
  113. * editable.attachListener( editable, 'mousedown', function() {
  114. * // ...
  115. * } );
  116. * } );
  117. *
  118. * This code will attach the `mousedown` listener every time a new editable is attached
  119. * to the editor, which in classic (`iframe`-based) editor happens every time the
  120. * data or the mode is set. This listener will also be removed when that editable is detached.
  121. *
  122. * It is also possible to attach a listener to another object (e.g. to a document).
  123. *
  124. * editor.on( 'contentDom', function() {
  125. * editor.editable().attachListener( editor.document, 'mousedown', function() {
  126. * // ...
  127. * } );
  128. * } );
  129. *
  130. * @param {CKEDITOR.event} obj The element/object to which the listener will be attached. Every object
  131. * which inherits from {@link CKEDITOR.event} may be used including {@link CKEDITOR.dom.element},
  132. * {@link CKEDITOR.dom.document}, and {@link CKEDITOR.editable}.
  133. * @param {String} eventName The name of the event that will be listened to.
  134. * @param {Function} listenerFunction The function listening to the
  135. * event. A single {@link CKEDITOR.eventInfo} object instance
  136. * containing all the event data is passed to this function.
  137. * @param {Object} [scopeObj] The object used to scope the listener
  138. * call (the `this` object). If omitted, the current object is used.
  139. * @param {Object} [listenerData] Data to be sent as the
  140. * {@link CKEDITOR.eventInfo#listenerData} when calling the listener.
  141. * @param {Number} [priority=10] The listener priority. Lower priority
  142. * listeners are called first. Listeners with the same priority
  143. * value are called in the registration order.
  144. * @returns {Object} An object containing the `removeListener`
  145. * function that can be used to remove the listener at any time.
  146. */
  147. attachListener: function( obj /*, event, fn, scope, listenerData, priority*/ ) {
  148. !this._.listeners && ( this._.listeners = [] );
  149. // Register the listener.
  150. var args = Array.prototype.slice.call( arguments, 1 ),
  151. listener = obj.on.apply( obj, args );
  152. this._.listeners.push( listener );
  153. return listener;
  154. },
  155. /**
  156. * Remove all event listeners registered from {@link #attachListener}.
  157. */
  158. clearListeners: function() {
  159. var listeners = this._.listeners;
  160. // Don't get broken by this.
  161. try {
  162. while ( listeners.length )
  163. listeners.pop().removeListener();
  164. } catch ( e ) {}
  165. },
  166. /**
  167. * Restore all attribution changes made by {@link #changeAttr }.
  168. */
  169. restoreAttrs: function() {
  170. var changes = this._.attrChanges, orgVal;
  171. for ( var attr in changes ) {
  172. if ( changes.hasOwnProperty( attr ) ) {
  173. orgVal = changes[ attr ];
  174. // Restore original attribute.
  175. orgVal !== null ? this.setAttribute( attr, orgVal ) : this.removeAttribute( attr );
  176. }
  177. }
  178. },
  179. /**
  180. * Adds a CSS class name to this editable that needs to be removed on detaching.
  181. *
  182. * @param {String} className The class name to be added.
  183. * @see CKEDITOR.dom.element#addClass
  184. */
  185. attachClass: function( cls ) {
  186. var classes = this.getCustomData( 'classes' );
  187. if ( !this.hasClass( cls ) ) {
  188. !classes && ( classes = [] ), classes.push( cls );
  189. this.setCustomData( 'classes', classes );
  190. this.addClass( cls );
  191. }
  192. },
  193. /**
  194. * Make an attribution change that would be reverted on editable detaching.
  195. * @param {String} attr The attribute name to be changed.
  196. * @param {String} val The value of specified attribute.
  197. */
  198. changeAttr: function( attr, val ) {
  199. var orgVal = this.getAttribute( attr );
  200. if ( val !== orgVal ) {
  201. !this._.attrChanges && ( this._.attrChanges = {} );
  202. // Saved the original attribute val.
  203. if ( !( attr in this._.attrChanges ) )
  204. this._.attrChanges[ attr ] = orgVal;
  205. this.setAttribute( attr, val );
  206. }
  207. },
  208. /**
  209. * Low-level method for inserting text into the editable.
  210. * See the {@link CKEDITOR.editor#method-insertText} method which is the editor-level API
  211. * for this purpose.
  212. *
  213. * @param {String} text
  214. */
  215. insertText: function( text ) {
  216. // Focus the editor before calling transformPlainTextToHtml. (#12726)
  217. this.editor.focus();
  218. this.insertHtml( this.transformPlainTextToHtml( text ), 'text' );
  219. },
  220. /**
  221. * Transforms plain text to HTML based on current selection and {@link CKEDITOR.editor#activeEnterMode}.
  222. *
  223. * @since 4.5
  224. * @param {String} text Text to transform.
  225. * @returns {String} HTML generated from the text.
  226. */
  227. transformPlainTextToHtml: function( text ) {
  228. var enterMode = this.editor.getSelection().getStartElement().hasAscendant( 'pre', true ) ?
  229. CKEDITOR.ENTER_BR :
  230. this.editor.activeEnterMode;
  231. return CKEDITOR.tools.transformPlainTextToHtml( text, enterMode );
  232. },
  233. /**
  234. * Low-level method for inserting HTML into the editable.
  235. * See the {@link CKEDITOR.editor#method-insertHtml} method which is the editor-level API
  236. * for this purpose.
  237. *
  238. * This method will insert HTML into the current selection or a given range. It also creates an undo snapshot,
  239. * scrolls the viewport to the insertion and selects the range next to the inserted content.
  240. * If you want to insert HTML without additional operations use {@link #method-insertHtmlIntoRange}.
  241. *
  242. * Fires the {@link CKEDITOR.editor#event-afterInsertHtml} event.
  243. *
  244. * @param {String} data The HTML to be inserted.
  245. * @param {String} [mode='html'] See {@link CKEDITOR.editor#method-insertHtml}'s param.
  246. * @param {CKEDITOR.dom.range} [range] If specified, the HTML will be inserted into the range
  247. * instead of into the selection. The selection will be placed at the end of the insertion (like in the normal case).
  248. * Introduced in CKEditor 4.5.
  249. */
  250. insertHtml: function( data, mode, range ) {
  251. var editor = this.editor;
  252. editor.focus();
  253. editor.fire( 'saveSnapshot' );
  254. if ( !range ) {
  255. // HTML insertion only considers the first range.
  256. // Note: getRanges will be overwritten for tests since we want to test
  257. // custom ranges and bypass native selections.
  258. range = editor.getSelection().getRanges()[ 0 ];
  259. }
  260. // Default mode is 'html'.
  261. insert( this, mode || 'html', data, range );
  262. // Make the final range selection.
  263. range.select();
  264. afterInsert( this );
  265. this.editor.fire( 'afterInsertHtml', {} );
  266. },
  267. /**
  268. * Inserts HTML into the position in the editor determined by the range.
  269. *
  270. * **Note:** This method does not {@link CKEDITOR.editor#saveSnapshot save undo snapshots} nor selects inserted
  271. * HTML. If you want to do it, use {@link #method-insertHtml}.
  272. *
  273. * Fires the {@link CKEDITOR.editor#event-afterInsertHtml} event.
  274. *
  275. * @since 4.5
  276. * @param {String} data HTML code to be inserted into the editor.
  277. * @param {CKEDITOR.dom.range} range The range as a place of insertion.
  278. * @param {String} [mode='html'] Mode in which HTML will be inserted.
  279. * See {@link CKEDITOR.editor#method-insertHtml}.
  280. */
  281. insertHtmlIntoRange: function( data, range, mode ) {
  282. // Default mode is 'html'
  283. insert( this, mode || 'html', data, range );
  284. this.editor.fire( 'afterInsertHtml', { intoRange: range } );
  285. },
  286. /**
  287. * Low-level method for inserting an element into the editable.
  288. * See the {@link CKEDITOR.editor#method-insertElement} method which is the editor-level API
  289. * for this purpose.
  290. *
  291. * This method will insert the element into the current selection or a given range. It also creates an undo
  292. * snapshot, scrolls the viewport to the insertion and selects the range next to the inserted content.
  293. * If you want to insert an element without additional operations use {@link #method-insertElementIntoRange}.
  294. *
  295. * @param {CKEDITOR.dom.element} element The element to insert.
  296. * @param {CKEDITOR.dom.range} [range] If specified, the element will be inserted into the range
  297. * instead of into the selection.
  298. */
  299. insertElement: function( element, range ) {
  300. var editor = this.editor;
  301. // Prepare for the insertion. For example - focus editor (#11848).
  302. editor.focus();
  303. editor.fire( 'saveSnapshot' );
  304. var enterMode = editor.activeEnterMode,
  305. selection = editor.getSelection(),
  306. elementName = element.getName(),
  307. isBlock = CKEDITOR.dtd.$block[ elementName ];
  308. if ( !range ) {
  309. range = selection.getRanges()[ 0 ];
  310. }
  311. // Insert element into first range only and ignore the rest (#11183).
  312. if ( this.insertElementIntoRange( element, range ) ) {
  313. range.moveToPosition( element, CKEDITOR.POSITION_AFTER_END );
  314. // If we're inserting a block element, the new cursor position must be
  315. // optimized. (#3100,#5436,#8950)
  316. if ( isBlock ) {
  317. // Find next, meaningful element.
  318. var next = element.getNext( function( node ) {
  319. return isNotEmpty( node ) && !isBogus( node );
  320. } );
  321. if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.is( CKEDITOR.dtd.$block ) ) {
  322. // If the next one is a text block, move cursor to the start of it's content.
  323. if ( next.getDtd()[ '#' ] )
  324. range.moveToElementEditStart( next );
  325. // Otherwise move cursor to the before end of the last element.
  326. else
  327. range.moveToElementEditEnd( element );
  328. }
  329. // Open a new line if the block is inserted at the end of parent.
  330. else if ( !next && enterMode != CKEDITOR.ENTER_BR ) {
  331. next = range.fixBlock( true, enterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' );
  332. range.moveToElementEditStart( next );
  333. }
  334. }
  335. }
  336. // Set up the correct selection.
  337. selection.selectRanges( [ range ] );
  338. afterInsert( this );
  339. },
  340. /**
  341. * Alias for {@link #insertElement}.
  342. *
  343. * @deprecated
  344. * @param {CKEDITOR.dom.element} element The element to be inserted.
  345. */
  346. insertElementIntoSelection: function( element ) {
  347. this.insertElement( element );
  348. },
  349. /**
  350. * Inserts an element into the position in the editor determined by the range.
  351. *
  352. * **Note:** This method does not {@link CKEDITOR.editor#saveSnapshot save undo snapshots} nor selects the inserted
  353. * element. If you want to do it, use the {@link #method-insertElement} method.
  354. *
  355. * @param {CKEDITOR.dom.element} element The element to be inserted.
  356. * @param {CKEDITOR.dom.range} range The range as a place of insertion.
  357. * @returns {Boolean} Informs whether the insertion was successful.
  358. */
  359. insertElementIntoRange: function( element, range ) {
  360. var editor = this.editor,
  361. enterMode = editor.config.enterMode,
  362. elementName = element.getName(),
  363. isBlock = CKEDITOR.dtd.$block[ elementName ];
  364. if ( range.checkReadOnly() )
  365. return false;
  366. // Remove the original contents, merge split nodes.
  367. range.deleteContents( 1 );
  368. // If range is placed in inermediate element (not td or th), we need to do three things:
  369. // * fill emptied <td/th>s with if browser needs them,
  370. // * remove empty text nodes so IE8 won't crash (http://dev.ckeditor.com/ticket/11183#comment:8),
  371. // * fix structure and move range into the <td/th> element.
  372. if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.is( { tr: 1, table: 1, tbody: 1, thead: 1, tfoot: 1 } ) )
  373. fixTableAfterContentsDeletion( range );
  374. // If we're inserting a block at dtd-violated position, split
  375. // the parent blocks until we reach blockLimit.
  376. var current, dtd;
  377. if ( isBlock ) {
  378. while ( ( current = range.getCommonAncestor( 0, 1 ) ) &&
  379. ( dtd = CKEDITOR.dtd[ current.getName() ] ) &&
  380. !( dtd && dtd[ elementName ] ) ) {
  381. // Split up inline elements.
  382. if ( current.getName() in CKEDITOR.dtd.span )
  383. range.splitElement( current );
  384. // If we're in an empty block which indicate a new paragraph,
  385. // simply replace it with the inserting block.(#3664)
  386. else if ( range.checkStartOfBlock() && range.checkEndOfBlock() ) {
  387. range.setStartBefore( current );
  388. range.collapse( true );
  389. current.remove();
  390. } else {
  391. range.splitBlock( enterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p', editor.editable() );
  392. }
  393. }
  394. }
  395. // Insert the new node.
  396. range.insertNode( element );
  397. // Return true if insertion was successful.
  398. return true;
  399. },
  400. /**
  401. * @see CKEDITOR.editor#setData
  402. */
  403. setData: function( data, isSnapshot ) {
  404. if ( !isSnapshot )
  405. data = this.editor.dataProcessor.toHtml( data );
  406. this.setHtml( data );
  407. this.fixInitialSelection();
  408. // Editable is ready after first setData.
  409. if ( this.status == 'unloaded' )
  410. this.status = 'ready';
  411. this.editor.fire( 'dataReady' );
  412. },
  413. /**
  414. * @see CKEDITOR.editor#getData
  415. */
  416. getData: function( isSnapshot ) {
  417. var data = this.getHtml();
  418. if ( !isSnapshot )
  419. data = this.editor.dataProcessor.toDataFormat( data );
  420. return data;
  421. },
  422. /**
  423. * Changes the read-only state of this editable.
  424. *
  425. * @param {Boolean} isReadOnly
  426. */
  427. setReadOnly: function( isReadOnly ) {
  428. this.setAttribute( 'contenteditable', !isReadOnly );
  429. },
  430. /**
  431. * Detaches this editable object from the DOM (removes classes, listeners, etc.)
  432. */
  433. detach: function() {
  434. // Cleanup the element.
  435. this.removeClass( 'cke_editable' );
  436. this.status = 'detached';
  437. // Save the editor reference which will be lost after
  438. // calling detach from super class.
  439. var editor = this.editor;
  440. this._.detach();
  441. delete editor.document;
  442. delete editor.window;
  443. },
  444. /**
  445. * Checks if the editable is one of the host page elements, indicates
  446. * an inline editing environment.
  447. *
  448. * @returns {Boolean}
  449. */
  450. isInline: function() {
  451. return this.getDocument().equals( CKEDITOR.document );
  452. },
  453. /**
  454. * Fixes the selection and focus which may be in incorrect state after
  455. * editable's inner HTML was overwritten.
  456. *
  457. * If the editable did not have focus, then the selection will be fixed when the editable
  458. * is focused for the first time. If the editable already had focus, then the selection will
  459. * be fixed immediately.
  460. *
  461. * To understand the problem see:
  462. *
  463. * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusaftersettingdata
  464. * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusafterundoing
  465. * * http://tests.ckeditor.dev:1030/tests/core/selection/manual/selectionafterfocusing
  466. * * http://tests.ckeditor.dev:1030/tests/plugins/newpage/manual/selectionafternewpage
  467. *
  468. * @since 4.4.6
  469. * @private
  470. */
  471. fixInitialSelection: function() {
  472. var that = this;
  473. // Deal with IE8- IEQM (the old MS selection) first.
  474. if ( CKEDITOR.env.ie && ( CKEDITOR.env.version < 9 || CKEDITOR.env.quirks ) ) {
  475. if ( this.hasFocus ) {
  476. this.focus();
  477. fixMSSelection();
  478. }
  479. return;
  480. }
  481. // If editable did not have focus, fix the selection when it is first focused.
  482. if ( !this.hasFocus ) {
  483. this.once( 'focus', function() {
  484. fixSelection();
  485. }, null, null, -999 );
  486. // If editable had focus, fix the selection immediately.
  487. } else {
  488. this.focus();
  489. fixSelection();
  490. }
  491. function fixSelection() {
  492. var $doc = that.getDocument().$,
  493. $sel = $doc.getSelection();
  494. if ( requiresFix( $sel ) ) {
  495. var range = new CKEDITOR.dom.range( that );
  496. range.moveToElementEditStart( that );
  497. var $range = $doc.createRange();
  498. $range.setStart( range.startContainer.$, range.startOffset );
  499. $range.collapse( true );
  500. $sel.removeAllRanges();
  501. $sel.addRange( $range );
  502. }
  503. }
  504. function requiresFix( $sel ) {
  505. // This condition covers most broken cases after setting data.
  506. if ( $sel.anchorNode && $sel.anchorNode == that.$ ) {
  507. return true;
  508. }
  509. // Fix for:
  510. // http://tests.ckeditor.dev:1030/tests/core/selection/manual/focusaftersettingdata
  511. // (the inline editor TC)
  512. if ( CKEDITOR.env.webkit ) {
  513. var active = that.getDocument().getActive();
  514. if ( active && active.equals( that ) && !$sel.anchorNode ) {
  515. return true;
  516. }
  517. }
  518. }
  519. function fixMSSelection() {
  520. var $doc = that.getDocument().$,
  521. $sel = $doc.selection,
  522. active = that.getDocument().getActive();
  523. if ( $sel.type == 'None' && active.equals( that ) ) {
  524. var range = new CKEDITOR.dom.range( that ),
  525. parentElement,
  526. $range = $doc.body.createTextRange();
  527. range.moveToElementEditStart( that );
  528. parentElement = range.startContainer;
  529. if ( parentElement.type != CKEDITOR.NODE_ELEMENT ) {
  530. parentElement = parentElement.getParent();
  531. }
  532. $range.moveToElementText( parentElement.$ );
  533. $range.collapse( true );
  534. $range.select();
  535. }
  536. }
  537. },
  538. /**
  539. * The base of the {@link CKEDITOR.editor#getSelectedHtml} method.
  540. *
  541. * @since 4.5
  542. * @method getHtmlFromRange
  543. * @param {CKEDITOR.dom.range} range
  544. * @returns {CKEDITOR.dom.documentFragment}
  545. */
  546. getHtmlFromRange: function( range ) {
  547. // There's nothing to return if range is collapsed.
  548. if ( range.collapsed )
  549. return new CKEDITOR.dom.documentFragment( range.document );
  550. // Info object passed between methods.
  551. var that = {
  552. doc: this.getDocument(),
  553. // Leave original range object untouched.
  554. range: range.clone()
  555. };
  556. getHtmlFromRangeHelpers.eol.detect( that, this );
  557. getHtmlFromRangeHelpers.bogus.exclude( that );
  558. getHtmlFromRangeHelpers.cell.shrink( that );
  559. that.fragment = that.range.cloneContents();
  560. getHtmlFromRangeHelpers.tree.rebuild( that, this );
  561. getHtmlFromRangeHelpers.eol.fix( that, this );
  562. return new CKEDITOR.dom.documentFragment( that.fragment.$ );
  563. },
  564. /**
  565. * The base of the {@link CKEDITOR.editor#extractSelectedHtml} method.
  566. *
  567. * **Note:** The range is modified so it matches the desired selection after extraction
  568. * even though the selection is not made.
  569. *
  570. * @since 4.5
  571. * @param {CKEDITOR.dom.range} range
  572. * @param {Boolean} [removeEmptyBlock=false] See {@link CKEDITOR.editor#extractSelectedHtml}'s parameter.
  573. * Note that the range will not be modified if this parameter is set to `true`.
  574. * @returns {CKEDITOR.dom.documentFragment} The extracted fragment of the editable content.
  575. */
  576. extractHtmlFromRange: function( range, removeEmptyBlock ) {
  577. var helpers = extractHtmlFromRangeHelpers,
  578. that = {
  579. range: range,
  580. doc: range.document
  581. },
  582. // Since it is quite hard to build a valid documentFragment
  583. // out of extracted contents because DOM changes, let's mimic
  584. // extracted HTML with #getHtmlFromRange. Yep. It's a hack.
  585. extractedFragment = this.getHtmlFromRange( range );
  586. // Collapsed range means that there's nothing to extract.
  587. if ( range.collapsed ) {
  588. range.optimize();
  589. return extractedFragment;
  590. }
  591. // Include inline element if possible.
  592. range.enlarge( CKEDITOR.ENLARGE_INLINE, 1 );
  593. // This got to be done before bookmarks are created because purging
  594. // depends on the position of the range at the boundaries of the table,
  595. // usually distorted by bookmark spans.
  596. helpers.table.detectPurge( that );
  597. // We'll play with DOM, let's hold the position of the range.
  598. that.bookmark = range.createBookmark();
  599. // While bookmarked, make unaccessible, to make sure that none of the methods
  600. // will try to use it (they should use that.bookmark).
  601. // This is done because ranges get desynchronized with the DOM when more bookmarks
  602. // is created (as for instance that.targetBookmark).
  603. delete that.range;
  604. // The range to be restored after extraction should be kept
  605. // outside of the range, so it's not removed by range.extractContents.
  606. var targetRange = this.editor.createRange();
  607. targetRange.moveToPosition( that.bookmark.startNode, CKEDITOR.POSITION_BEFORE_START );
  608. that.targetBookmark = targetRange.createBookmark();
  609. // Execute content-specific detections.
  610. helpers.list.detectMerge( that, this );
  611. helpers.table.detectRanges( that, this );
  612. helpers.block.detectMerge( that, this );
  613. // Simply, do the job.
  614. if ( that.tableContentsRanges ) {
  615. helpers.table.deleteRanges( that );
  616. // Done here only to remove bookmark's spans.
  617. range.moveToBookmark( that.bookmark );
  618. that.range = range;
  619. } else {
  620. // To use the range we need to restore the bookmark and make
  621. // the range accessible again.
  622. range.moveToBookmark( that.bookmark );
  623. that.range = range;
  624. range.extractContents( helpers.detectExtractMerge( that ) );
  625. }
  626. // Move working range to desired, pre-computed position.
  627. range.moveToBookmark( that.targetBookmark );
  628. // Make sure range is always anchored in an element. For consistency.
  629. range.optimize();
  630. // It my happen that the uncollapsed range which referred to a valid selection,
  631. // will be placed in an uneditable location after being collapsed:
  632. // <tr>[<td>x</td>]</tr> -> <tr>[]<td>x</td></tr> -> <tr><td>[]x</td></tr>
  633. helpers.fixUneditableRangePosition( range );
  634. // Execute content-specific post-extract routines.
  635. helpers.list.merge( that, this );
  636. helpers.table.purge( that, this );
  637. helpers.block.merge( that, this );
  638. // Remove empty block, duh!
  639. if ( removeEmptyBlock ) {
  640. var path = range.startPath();
  641. // <p><b>^</b></p> is empty block.
  642. if (
  643. range.checkStartOfBlock() &&
  644. range.checkEndOfBlock() &&
  645. path.block &&
  646. !range.root.equals( path.block ) &&
  647. // Do not remove a block with bookmarks. (#13465)
  648. !hasBookmarks( path.block ) ) {
  649. range.moveToPosition( path.block, CKEDITOR.POSITION_BEFORE_START );
  650. path.block.remove();
  651. }
  652. } else {
  653. // Auto paragraph, if needed.
  654. helpers.autoParagraph( this.editor, range );
  655. // Let's have a bogus next to the caret, if needed.
  656. if ( isEmpty( range.startContainer ) )
  657. range.startContainer.appendBogus();
  658. }
  659. // Merge inline siblings if any around the caret.
  660. range.startContainer.mergeSiblings();
  661. return extractedFragment;
  662. },
  663. /**
  664. * Editable element bootstrapping.
  665. *
  666. * @private
  667. */
  668. setup: function() {
  669. var editor = this.editor;
  670. // Handle the load/read of editor data/snapshot.
  671. this.attachListener( editor, 'beforeGetData', function() {
  672. var data = this.getData();
  673. // Post processing html output of wysiwyg editable.
  674. if ( !this.is( 'textarea' ) ) {
  675. // Reset empty if the document contains only one empty paragraph.
  676. if ( editor.config.ignoreEmptyParagraph !== false )
  677. data = data.replace( emptyParagraphRegexp, function( match, lookback ) {
  678. return lookback;
  679. } );
  680. }
  681. editor.setData( data, null, 1 );
  682. }, this );
  683. this.attachListener( editor, 'getSnapshot', function( evt ) {
  684. evt.data = this.getData( 1 );
  685. }, this );
  686. this.attachListener( editor, 'afterSetData', function() {
  687. this.setData( editor.getData( 1 ) );
  688. }, this );
  689. this.attachListener( editor, 'loadSnapshot', function( evt ) {
  690. this.setData( evt.data, 1 );
  691. }, this );
  692. // Delegate editor focus/blur to editable.
  693. this.attachListener( editor, 'beforeFocus', function() {
  694. var sel = editor.getSelection(),
  695. ieSel = sel && sel.getNative();
  696. // IE considers control-type element as separate
  697. // focus host when selected, avoid destroying the
  698. // selection in such case. (#5812) (#8949)
  699. if ( ieSel && ieSel.type == 'Control' )
  700. return;
  701. this.focus();
  702. }, this );
  703. this.attachListener( editor, 'insertHtml', function( evt ) {
  704. this.insertHtml( evt.data.dataValue, evt.data.mode, evt.data.range );
  705. }, this );
  706. this.attachListener( editor, 'insertElement', function( evt ) {
  707. this.insertElement( evt.data );
  708. }, this );
  709. this.attachListener( editor, 'insertText', function( evt ) {
  710. this.insertText( evt.data );
  711. }, this );
  712. // Update editable state.
  713. this.setReadOnly( editor.readOnly );
  714. // The editable class.
  715. this.attachClass( 'cke_editable' );
  716. // The element mode css class.
  717. if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ) {
  718. this.attachClass( 'cke_editable_inline' );
  719. } else if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ||
  720. editor.elementMode == CKEDITOR.ELEMENT_MODE_APPENDTO ) {
  721. this.attachClass( 'cke_editable_themed' );
  722. }
  723. this.attachClass( 'cke_contents_' + editor.config.contentsLangDirection );
  724. // Setup editor keystroke handlers on this element.
  725. var keystrokeHandler = editor.keystrokeHandler;
  726. // If editor is read-only, then make sure that BACKSPACE key
  727. // is blocked to prevent browser history navigation.
  728. keystrokeHandler.blockedKeystrokes[ 8 ] = +editor.readOnly;
  729. editor.keystrokeHandler.attach( this );
  730. // Update focus states.
  731. this.on( 'blur', function() {
  732. this.hasFocus = false;
  733. }, null, null, -1 );
  734. this.on( 'focus', function() {
  735. this.hasFocus = true;
  736. }, null, null, -1 );
  737. // Register to focus manager.
  738. editor.focusManager.add( this );
  739. // Inherit the initial focus on editable element.
  740. if ( this.equals( CKEDITOR.document.getActive() ) ) {
  741. this.hasFocus = true;
  742. // Pending until this editable has attached.
  743. editor.once( 'contentDom', function() {
  744. editor.focusManager.focus( this );
  745. }, this );
  746. }
  747. // Apply tab index on demand, with original direction saved.
  748. if ( this.isInline() ) {
  749. // tabIndex of the editable is different than editor's one.
  750. // Update the attribute of the editable.
  751. this.changeAttr( 'tabindex', editor.tabIndex );
  752. }
  753. // The above is all we'll be doing for a <textarea> editable.
  754. if ( this.is( 'textarea' ) )
  755. return;
  756. // The DOM document which the editing acts upon.
  757. editor.document = this.getDocument();
  758. editor.window = this.getWindow();
  759. var doc = editor.document;
  760. this.changeAttr( 'spellcheck', !editor.config.disableNativeSpellChecker );
  761. // Apply contents direction on demand, with original direction saved.
  762. var dir = editor.config.contentsLangDirection;
  763. if ( this.getDirection( 1 ) != dir )
  764. this.changeAttr( 'dir', dir );
  765. // Create the content stylesheet for this document.
  766. var styles = CKEDITOR.getCss();
  767. if ( styles ) {
  768. var head = doc.getHead();
  769. if ( !head.getCustomData( 'stylesheet' ) ) {
  770. var sheet = doc.appendStyleText( styles );
  771. sheet = new CKEDITOR.dom.element( sheet.ownerNode || sheet.owningElement );
  772. head.setCustomData( 'stylesheet', sheet );
  773. sheet.data( 'cke-temp', 1 );
  774. }
  775. }
  776. // Update the stylesheet sharing count.
  777. var ref = doc.getCustomData( 'stylesheet_ref' ) || 0;
  778. doc.setCustomData( 'stylesheet_ref', ref + 1 );
  779. // Pass this configuration to styles system.
  780. this.setCustomData( 'cke_includeReadonly', !editor.config.disableReadonlyStyling );
  781. // Prevent the browser opening read-only links. (#6032 & #10912)
  782. this.attachListener( this, 'click', function( evt ) {
  783. evt = evt.data;
  784. var link = new CKEDITOR.dom.elementPath( evt.getTarget(), this ).contains( 'a' );
  785. if ( link && evt.$.button != 2 && link.isReadOnly() )
  786. evt.preventDefault();
  787. } );
  788. var backspaceOrDelete = { 8: 1, 46: 1 };
  789. // Override keystrokes which should have deletion behavior
  790. // on fully selected element . (#4047) (#7645)
  791. this.attachListener( editor, 'key', function( evt ) {
  792. if ( editor.readOnly )
  793. return true;
  794. // Use getKey directly in order to ignore modifiers.
  795. // Justification: http://dev.ckeditor.com/ticket/11861#comment:13
  796. var keyCode = evt.data.domEvent.getKey(),
  797. isHandled;
  798. // Backspace OR Delete.
  799. if ( keyCode in backspaceOrDelete ) {
  800. var sel = editor.getSelection(),
  801. selected,
  802. range = sel.getRanges()[ 0 ],
  803. path = range.startPath(),
  804. block,
  805. parent,
  806. next,
  807. rtl = keyCode == 8;
  808. if (
  809. // [IE<11] Remove selected image/anchor/etc here to avoid going back in history. (#10055)
  810. ( CKEDITOR.env.ie && CKEDITOR.env.version < 11 && ( selected = sel.getSelectedElement() ) ) ||
  811. // Remove the entire list/table on fully selected content. (#7645)
  812. ( selected = getSelectedTableList( sel ) ) ) {
  813. // Make undo snapshot.
  814. editor.fire( 'saveSnapshot' );
  815. // Delete any element that 'hasLayout' (e.g. hr,table) in IE8 will
  816. // break up the selection, safely manage it here. (#4795)
  817. range.moveToPosition( selected, CKEDITOR.POSITION_BEFORE_START );
  818. // Remove the control manually.
  819. selected.remove();
  820. range.select();
  821. editor.fire( 'saveSnapshot' );
  822. isHandled = 1;
  823. } else if ( range.collapsed ) {
  824. // Handle the following special cases: (#6217)
  825. // 1. Del/Backspace key before/after table;
  826. // 2. Backspace Key after start of table.
  827. if ( ( block = path.block ) &&
  828. ( next = block[ rtl ? 'getPrevious' : 'getNext' ]( isNotWhitespace ) ) &&
  829. ( next.type == CKEDITOR.NODE_ELEMENT ) &&
  830. next.is( 'table' ) &&
  831. range[ rtl ? 'checkStartOfBlock' : 'checkEndOfBlock' ]() ) {
  832. editor.fire( 'saveSnapshot' );
  833. // Remove the current empty block.
  834. if ( range[ rtl ? 'checkEndOfBlock' : 'checkStartOfBlock' ]() )
  835. block.remove();
  836. // Move cursor to the beginning/end of table cell.
  837. range[ 'moveToElementEdit' + ( rtl ? 'End' : 'Start' ) ]( next );
  838. range.select();
  839. editor.fire( 'saveSnapshot' );
  840. isHandled = 1;
  841. }
  842. else if ( path.blockLimit && path.blockLimit.is( 'td' ) &&
  843. ( parent = path.blockLimit.getAscendant( 'table' ) ) &&
  844. range.checkBoundaryOfElement( parent, rtl ? CKEDITOR.START : CKEDITOR.END ) &&
  845. ( next = parent[ rtl ? 'getPrevious' : 'getNext' ]( isNotWhitespace ) ) ) {
  846. editor.fire( 'saveSnapshot' );
  847. // Move cursor to the end of previous block.
  848. range[ 'moveToElementEdit' + ( rtl ? 'End' : 'Start' ) ]( next );
  849. // Remove any previous empty block.
  850. if ( range.checkStartOfBlock() && range.checkEndOfBlock() )
  851. next.remove();
  852. else
  853. range.select();
  854. editor.fire( 'saveSnapshot' );
  855. isHandled = 1;
  856. }
  857. // BACKSPACE/DEL pressed at the start/end of table cell.
  858. else if ( ( parent = path.contains( [ 'td', 'th', 'caption' ] ) ) &&
  859. range.checkBoundaryOfElement( parent, rtl ? CKEDITOR.START : CKEDITOR.END ) ) {
  860. isHandled = 1;
  861. }
  862. }
  863. }
  864. return !isHandled;
  865. } );
  866. // On IE>=11 we need to fill blockless editable with <br> if it was deleted.
  867. if ( editor.blockless && CKEDITOR.env.ie && CKEDITOR.env.needsBrFiller ) {
  868. this.attachListener( this, 'keyup', function( evt ) {
  869. if ( evt.data.getKeystroke() in backspaceOrDelete && !this.getFirst( isNotEmpty ) ) {
  870. this.appendBogus();
  871. // Set the selection before bogus, because IE tends to put it after.
  872. var range = editor.createRange();
  873. range.moveToPosition( this, CKEDITOR.POSITION_AFTER_START );
  874. range.select();
  875. }
  876. } );
  877. }
  878. this.attachListener( this, 'dblclick', function( evt ) {
  879. if ( editor.readOnly )
  880. return false;
  881. var data = { element: evt.data.getTarget() };
  882. editor.fire( 'doubleclick', data );
  883. } );
  884. // Prevent automatic submission in IE #6336
  885. CKEDITOR.env.ie && this.attachListener( this, 'click', blockInputClick );
  886. // Gecko/Webkit need some help when selecting control type elements. (#3448)
  887. // We apply same behavior for IE Edge. (#13386)
  888. if ( !CKEDITOR.env.ie || CKEDITOR.env.edge ) {
  889. this.attachListener( this, 'mousedown', function( ev ) {
  890. var control = ev.data.getTarget();
  891. // #11727. Note: htmlDP assures that input/textarea/select have contenteditable=false
  892. // attributes. However, they also have data-cke-editable attribute, so isReadOnly() returns false,
  893. // and therefore those elements are correctly selected by this code.
  894. if ( control.is( 'img', 'hr', 'input', 'textarea', 'select' ) && !control.isReadOnly() ) {
  895. editor.getSelection().selectElement( control );
  896. // Prevent focus from stealing from the editable. (#9515)
  897. if ( control.is( 'input', 'textarea', 'select' ) )
  898. ev.data.preventDefault();
  899. }
  900. } );
  901. }
  902. // For some reason, after click event is done, IE Edge loses focus on the selected element. (#13386)
  903. if ( CKEDITOR.env.edge ) {
  904. this.attachListener( this, 'mouseup', function( ev ) {
  905. var selectedElement = ev.data.getTarget();
  906. if ( selectedElement && selectedElement.is( 'img' ) ) {
  907. editor.getSelection().selectElement( selectedElement );
  908. }
  909. } );
  910. }
  911. // Prevent right click from selecting an empty block even
  912. // when selection is anchored inside it. (#5845)
  913. if ( CKEDITOR.env.gecko ) {
  914. this.attachListener( this, 'mouseup', function( ev ) {
  915. if ( ev.data.$.button == 2 ) {
  916. var target = ev.data.getTarget();
  917. if ( !target.getOuterHtml().replace( emptyParagraphRegexp, '' ) ) {
  918. var range = editor.createRange();
  919. range.moveToElementEditStart( target );
  920. range.select( true );
  921. }
  922. }
  923. } );
  924. }
  925. // Webkit: avoid from editing form control elements content.
  926. if ( CKEDITOR.env.webkit ) {
  927. // Prevent from tick checkbox/radiobox/select
  928. this.attachListener( this, 'click', function( ev ) {
  929. if ( ev.data.getTarget().is( 'input', 'select' ) )
  930. ev.data.preventDefault();
  931. } );
  932. // Prevent from editig textfield/textarea value.
  933. this.attachListener( this, 'mouseup', function( ev ) {
  934. if ( ev.data.getTarget().is( 'input', 'textarea' ) )
  935. ev.data.preventDefault();
  936. } );
  937. }
  938. // Prevent Webkit/Blink from going rogue when joining
  939. // blocks on BACKSPACE/DEL (#11861,#9998).
  940. if ( CKEDITOR.env.webkit ) {
  941. this.attachListener( editor, 'key', function( evt ) {
  942. if ( editor.readOnly ) {
  943. return true;
  944. }
  945. // Use getKey directly in order to ignore modifiers.
  946. // Justification: http://dev.ckeditor.com/ticket/11861#comment:13
  947. var key = evt.data.domEvent.getKey();
  948. if ( !( key in backspaceOrDelete ) )
  949. return;
  950. var backspace = key == 8,
  951. range = editor.getSelection().getRanges()[ 0 ],
  952. startPath = range.startPath();
  953. if ( range.collapsed ) {
  954. if ( !mergeBlocksCollapsedSelection( editor, range, backspace, startPath ) )
  955. return;
  956. } else {
  957. if ( !mergeBlocksNonCollapsedSelection( editor, range, startPath ) )
  958. return;
  959. }
  960. // Scroll to the new position of the caret (#11960).
  961. editor.getSelection().scrollIntoView();
  962. editor.fire( 'saveSnapshot' );
  963. return false;
  964. }, this, null, 100 ); // Later is better – do not override existing listeners.
  965. }
  966. }
  967. },
  968. _: {
  969. detach: function() {
  970. // Update the editor cached data with current data.
  971. this.editor.setData( this.editor.getData(), 0, 1 );
  972. this.clearListeners();
  973. this.restoreAttrs();
  974. // Cleanup our custom classes.
  975. var classes;
  976. if ( ( classes = this.removeCustomData( 'classes' ) ) ) {
  977. while ( classes.length )
  978. this.removeClass( classes.pop() );
  979. }
  980. // Remove contents stylesheet from document if it's the last usage.
  981. if ( !this.is( 'textarea' ) ) {
  982. var doc = this.getDocument(),
  983. head = doc.getHead();
  984. if ( head.getCustomData( 'stylesheet' ) ) {
  985. var refs = doc.getCustomData( 'stylesheet_ref' );
  986. if ( !( --refs ) ) {
  987. doc.removeCustomData( 'stylesheet_ref' );
  988. var sheet = head.removeCustomData( 'stylesheet' );
  989. sheet.remove();
  990. } else {
  991. doc.setCustomData( 'stylesheet_ref', refs );
  992. }
  993. }
  994. }
  995. this.editor.fire( 'contentDomUnload' );
  996. // Free up the editor reference.
  997. delete this.editor;
  998. }
  999. }
  1000. } );
  1001. /**
  1002. * Creates, retrieves or detaches an editable element of the editor.
  1003. * This method should always be used instead of calling {@link CKEDITOR.editable} directly.
  1004. *
  1005. * @method editable
  1006. * @member CKEDITOR.editor
  1007. * @param {CKEDITOR.dom.element/CKEDITOR.editable} elementOrEditable The
  1008. * DOM element to become the editable or a {@link CKEDITOR.editable} object.
  1009. */
  1010. CKEDITOR.editor.prototype.editable = function( element ) {
  1011. var editable = this._.editable;
  1012. // This editor has already associated with
  1013. // an editable element, silently fails.
  1014. if ( editable && element )
  1015. return 0;
  1016. if ( arguments.length ) {
  1017. editable = this._.editable = element ? ( element instanceof CKEDITOR.editable ? element : new CKEDITOR.editable( this, element ) ) :
  1018. // Detach the editable from editor.
  1019. ( editable && editable.detach(), null );
  1020. }
  1021. // Just retrieve the editable.
  1022. return editable;
  1023. };
  1024. CKEDITOR.on( 'instanceLoaded', function( evt ) {
  1025. var editor = evt.editor;
  1026. // and flag that the element was locked by our code so it'll be editable by the editor functions (#6046).
  1027. editor.on( 'insertElement', function( evt ) {
  1028. var element = evt.data;
  1029. if ( element.type == CKEDITOR.NODE_ELEMENT && ( element.is( 'input' ) || element.is( 'textarea' ) ) ) {
  1030. // // The element is still not inserted yet, force attribute-based check.
  1031. if ( element.getAttribute( 'contentEditable' ) != 'false' )
  1032. element.data( 'cke-editable', element.hasAttribute( 'contenteditable' ) ? 'true' : '1' );
  1033. element.setAttribute( 'contentEditable', false );
  1034. }
  1035. } );
  1036. editor.on( 'selectionChange', function( evt ) {
  1037. if ( editor.readOnly )
  1038. return;
  1039. // Auto fixing on some document structure weakness to enhance usabilities. (#3190 and #3189)
  1040. var sel = editor.getSelection();
  1041. // Do it only when selection is not locked. (#8222)
  1042. if ( sel && !sel.isLocked ) {
  1043. var isDirty = editor.checkDirty();
  1044. // Lock undoM before touching DOM to prevent
  1045. // recording these changes as separate snapshot.
  1046. editor.fire( 'lockSnapshot' );
  1047. fixDom( evt );
  1048. editor.fire( 'unlockSnapshot' );
  1049. !isDirty && editor.resetDirty();
  1050. }
  1051. } );
  1052. } );
  1053. CKEDITOR.on( 'instanceCreated', function( evt ) {
  1054. var editor = evt.editor;
  1055. editor.on( 'mode', function() {
  1056. var editable = editor.editable();
  1057. // Setup proper ARIA roles and properties for inline editable, classic
  1058. // (iframe-based) editable is instead handled by plugin.
  1059. if ( editable && editable.isInline() ) {
  1060. var ariaLabel = editor.title;
  1061. editable.changeAttr( 'role', 'textbox' );
  1062. editable.changeAttr( 'aria-label', ariaLabel );
  1063. if ( ariaLabel )
  1064. editable.changeAttr( 'title', ariaLabel );
  1065. var helpLabel = editor.fire( 'ariaEditorHelpLabel', {} ).label;
  1066. if ( helpLabel ) {
  1067. // Put the voice label in different spaces, depending on element mode, so
  1068. // the DOM element get auto detached on mode reload or editor destroy.
  1069. var ct = this.ui.space( this.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ? 'top' : 'contents' );
  1070. if ( ct ) {
  1071. var ariaDescId = CKEDITOR.tools.getNextId(),
  1072. desc = CKEDITOR.dom.element.createFromHtml( '<span id="' + ariaDescId + '" class="cke_voice_label">' + helpLabel + '</span>' );
  1073. ct.append( desc );
  1074. editable.changeAttr( 'aria-describedby', ariaDescId );
  1075. }
  1076. }
  1077. }
  1078. } );
  1079. } );
  1080. // #9222: Show text cursor in Gecko.
  1081. // Show default cursor over control elements on all non-IEs.
  1082. CKEDITOR.addCss( '.cke_editable{cursor:text}.cke_editable img,.cke_editable input,.cke_editable textarea{cursor:default}' );
  1083. //
  1084. //
  1085. // Bazillion helpers for the editable class and above listeners.
  1086. //
  1087. //
  1088. var isNotWhitespace = CKEDITOR.dom.walker.whitespaces( true ),
  1089. isNotBookmark = CKEDITOR.dom.walker.bookmark( false, true ),
  1090. isEmpty = CKEDITOR.dom.walker.empty(),
  1091. isBogus = CKEDITOR.dom.walker.bogus(),
  1092. // Matching an empty paragraph at the end of document.
  1093. emptyParagraphRegexp = /(^|<body\b[^>]*>)\s*<(p|div|address|h\d|center|pre)[^>]*>\s*(?:<br[^>]*>|&nbsp;|\u00A0|&#160;)?\s*(:?<\/\2>)?\s*(?=$|<\/body>)/gi;
  1094. // Auto-fixing block-less content by wrapping paragraph (#3190), prevent
  1095. // non-exitable-block by padding extra br.(#3189)
  1096. // Returns truly value when dom was changed, falsy otherwise.
  1097. function fixDom( evt ) {
  1098. var editor = evt.editor,
  1099. path = evt.data.path,
  1100. blockLimit = path.blockLimit,
  1101. selection = evt.data.selection,
  1102. range = selection.getRanges()[ 0 ],
  1103. selectionUpdateNeeded;
  1104. if ( CKEDITOR.env.gecko || ( CKEDITOR.env.ie && CKEDITOR.env.needsBrFiller ) ) {
  1105. var blockNeedsFiller = needsBrFiller( selection, path );
  1106. if ( blockNeedsFiller ) {
  1107. blockNeedsFiller.appendBogus();
  1108. // IE tends to place selection after appended bogus, so we need to
  1109. // select the original range (placed before bogus).
  1110. selectionUpdateNeeded = CKEDITOR.env.ie;
  1111. }
  1112. }
  1113. // When we're in block enter mode, a new paragraph will be established
  1114. // to encapsulate inline contents inside editable. (#3657)
  1115. // Don't autoparagraph if browser (namely - IE) incorrectly anchored selection
  1116. // inside non-editable content. This happens e.g. if non-editable block is the only
  1117. // content of editable.
  1118. if ( shouldAutoParagraph( editor, path.block, blockLimit ) && range.collapsed && !range.getCommonAncestor().isReadOnly() ) {
  1119. var testRng = range.clone();
  1120. testRng.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
  1121. var walker = new CKEDITOR.dom.walker( testRng );
  1122. walker.guard = function( node ) {
  1123. return !isNotEmpty( node ) ||
  1124. node.type == CKEDITOR.NODE_COMMENT ||
  1125. node.isReadOnly();
  1126. };
  1127. // 1. Inline content discovered under cursor;
  1128. // 2. Empty editable.
  1129. if ( !walker.checkForward() || testRng.checkStartOfBlock() && testRng.checkEndOfBlock() ) {
  1130. var fixedBlock = range.fixBlock( true, editor.activeEnterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' );
  1131. // For IE<11, we should remove any filler node which was introduced before.
  1132. if ( !CKEDITOR.env.needsBrFiller ) {
  1133. var first = fixedBlock.getFirst( isNotEmpty );
  1134. if ( first && isNbsp( first ) )
  1135. first.remove();
  1136. }
  1137. selectionUpdateNeeded = 1;
  1138. // Cancel this selection change in favor of the next (correct). (#6811)
  1139. evt.cancel();
  1140. }
  1141. }
  1142. if ( selectionUpdateNeeded )
  1143. range.select();
  1144. }
  1145. // Checks whether current selection requires br filler to be appended.
  1146. // @returns Block which needs filler or falsy value.
  1147. function needsBrFiller( selection, path ) {
  1148. // Fake selection does not need filler, because it is fake.
  1149. if ( selection.isFake )
  1150. return 0;
  1151. // Ensure bogus br could help to move cursor (out of styles) to the end of block. (#7041)
  1152. var pathBlock = path.block || path.blockLimit,
  1153. lastNode = pathBlock && pathBlock.getLast( isNotEmpty );
  1154. // Check some specialities of the current path block:
  1155. // 1. It is really displayed as block; (#7221)
  1156. // 2. It doesn't end with one inner block; (#7467)
  1157. // 3. It doesn't have bogus br yet.
  1158. if (
  1159. pathBlock && pathBlock.isBlockBoundary() &&
  1160. !( lastNode && lastNode.type == CKEDITOR.NODE_ELEMENT && lastNode.isBlockBoundary() ) &&
  1161. !pathBlock.is( 'pre' ) && !pathBlock.getBogus()
  1162. )
  1163. return pathBlock;
  1164. }
  1165. function blockInputClick( evt ) {
  1166. var element = evt.data.getTarget();
  1167. if ( element.is( 'input' ) ) {
  1168. var type = element.getAttribute( 'type' );
  1169. if ( type == 'submit' || type == 'reset' )
  1170. evt.data.preventDefault();
  1171. }
  1172. }
  1173. function isNotEmpty( node ) {
  1174. return isNotWhitespace( node ) && isNotBookmark( node );
  1175. }
  1176. function isNbsp( node ) {
  1177. return node.type == CKEDITOR.NODE_TEXT && CKEDITOR.tools.trim( node.getText() ).match( /^(?:&nbsp;|\xa0)$/ );
  1178. }
  1179. function isNotBubbling( fn, src ) {
  1180. return function( evt ) {
  1181. var other = evt.data.$.toElement || evt.data.$.fromElement || evt.data.$.relatedTarget;
  1182. // First of all, other may simply be null/undefined.
  1183. // Second of all, at least early versions of Spartan returned empty objects from evt.relatedTarget,
  1184. // so let's also check the node type.
  1185. other = ( other && other.nodeType == CKEDITOR.NODE_ELEMENT ) ? new CKEDITOR.dom.element( other ) : null;
  1186. if ( !( other && ( src.equals( other ) || src.contains( other ) ) ) )
  1187. fn.call( this, evt );
  1188. };
  1189. }
  1190. function hasBookmarks( element ) {
  1191. // We use getElementsByTag() instead of find() to retain compatibility with IE quirks mode.
  1192. var potentialBookmarks = element.getElementsByTag( 'span' ),
  1193. i = 0,
  1194. child;
  1195. if ( potentialBookmarks ) {
  1196. while ( ( child = potentialBookmarks.getItem( i++ ) ) ) {
  1197. if ( !isNotBookmark( child ) ) {
  1198. return true;
  1199. }
  1200. }
  1201. }
  1202. return false;
  1203. }
  1204. // Check if the entire table/list contents is selected.
  1205. function getSelectedTableList( sel ) {
  1206. var selected,
  1207. range = sel.getRanges()[ 0 ],
  1208. editable = sel.root,
  1209. path = range.startPath(),
  1210. structural = { table: 1, ul: 1, ol: 1, dl: 1 };
  1211. if ( path.contains( structural ) ) {
  1212. // Clone the original range.
  1213. var walkerRng = range.clone();
  1214. // Enlarge the range: X<ul><li>[Y]</li></ul>X => [X<ul><li>]Y</li></ul>X
  1215. walkerRng.collapse( 1 );
  1216. walkerRng.setStartAt( editable, CKEDITOR.POSITION_AFTER_START );
  1217. // Create a new walker.
  1218. var walker = new CKEDITOR.dom.walker( walkerRng );
  1219. // Assign a new guard to the walker.
  1220. walker.guard = guard();
  1221. // Go backwards checking for selected structural node.
  1222. walker.checkBackward();
  1223. // If there's a selected structured element when checking backwards,
  1224. // then check the same forwards.
  1225. if ( selected ) {
  1226. // Clone the original range.
  1227. walkerRng = range.clone();
  1228. // Enlarge the range (assuming <ul> is selected element from guard):
  1229. //
  1230. // X<ul><li>[Y]</li></ul>X => X<ul><li>Y[</li></ul>]X
  1231. //
  1232. // If the walker went deeper down DOM than a while ago when traversing
  1233. // backwards, then it doesn't make sense: an element must be selected
  1234. // symmetrically. By placing range end **after previously selected node**,
  1235. // we make sure we don't go no deeper in DOM when going forwards.
  1236. walkerRng.collapse();
  1237. walkerRng.setEndAt( selected, CKEDITOR.POSITION_AFTER_END );
  1238. // Create a new walker.
  1239. walker = new CKEDITOR.dom.walker( walkerRng );
  1240. // Assign a new guard to the walker.
  1241. walker.guard = guard( true );
  1242. // Reset selected node.
  1243. selected = false;
  1244. // Go forwards checking for selected structural node.
  1245. walker.checkForward();
  1246. return selected;
  1247. }
  1248. }
  1249. return null;
  1250. function guard( forwardGuard ) {
  1251. return function( node, isWalkOut ) {
  1252. // Save the encountered node as selected if going down the DOM structure
  1253. // and the node is structured element.
  1254. if ( isWalkOut && node.type == CKEDITOR.NODE_ELEMENT && node.is( structural ) )
  1255. selected = node;
  1256. // Stop the walker when either traversing another non-empty node at the same
  1257. // DOM level as in previous step.
  1258. // NOTE: When going forwards, stop if encountered a bogus.
  1259. if ( !isWalkOut && isNotEmpty( node ) && !( forwardGuard && isBogus( node ) ) )
  1260. return false;
  1261. };
  1262. }
  1263. }
  1264. // Whether in given context (pathBlock, pathBlockLimit and editor settings)
  1265. // editor should automatically wrap inline contents with blocks.
  1266. function shouldAutoParagraph( editor, pathBlock, pathBlockLimit ) {
  1267. // Check whether pathBlock equals pathBlockLimit to support nested editable (#12162).
  1268. return editor.config.autoParagraph !== false &&
  1269. editor.activeEnterMode != CKEDITOR.ENTER_BR &&
  1270. (
  1271. ( editor.editable().equals( pathBlockLimit ) && !pathBlock ) ||
  1272. ( pathBlock && pathBlock.getAttribute( 'contenteditable' ) == 'true' )
  1273. );
  1274. }
  1275. function autoParagraphTag( editor ) {
  1276. return ( editor.activeEnterMode != CKEDITOR.ENTER_BR && editor.config.autoParagraph !== false ) ? editor.activeEnterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' : false;
  1277. }
  1278. //
  1279. // Functions related to insertXXX methods
  1280. //
  1281. var insert = ( function() {
  1282. 'use strict';
  1283. var DTD = CKEDITOR.dtd;
  1284. // Inserts the given (valid) HTML into the range position (with range content deleted),
  1285. // guarantee it's result to be a valid DOM tree.
  1286. function insert( editable, type, data, range ) {
  1287. var editor = editable.editor,
  1288. dontFilter = false;
  1289. if ( type == 'unfiltered_html' ) {
  1290. type = 'html';
  1291. dontFilter = true;
  1292. }
  1293. // Check range spans in non-editable.
  1294. if ( range.checkReadOnly() )
  1295. return;
  1296. // RANGE PREPARATIONS
  1297. var path = new CKEDITOR.dom.elementPath( range.startContainer, range.root ),
  1298. // Let root be the nearest block that's impossible to be split
  1299. // during html processing.
  1300. blockLimit = path.blockLimit || range.root,
  1301. // The "state" value.
  1302. that = {
  1303. type: type,
  1304. dontFilter: dontFilter,
  1305. editable: editable,
  1306. editor: editor,
  1307. range: range,
  1308. blockLimit: blockLimit,
  1309. // During pre-processing / preparations startContainer of affectedRange should be placed
  1310. // in this element in which inserted or moved (in case when we merge blocks) content
  1311. // could create situation that will need merging inline elements.
  1312. // Examples:
  1313. // <div><b>A</b>^B</div> + <b>C</b> => <div><b>A</b><b>C</b>B</div> - affected container is <div>.
  1314. // <p><b>A[B</b></p><p><b>C]D</b></p> + E => <p><b>AE</b></p><p><b>D</b></p> =>
  1315. // <p><b>AE</b><b>D</b></p> - affected container is <p> (in text mode).
  1316. mergeCandidates: [],
  1317. zombies: []
  1318. };
  1319. prepareRangeToDataInsertion( that );
  1320. // DATA PROCESSING
  1321. // Select range and stop execution.
  1322. // If data has been totally emptied after the filtering,
  1323. // any insertion is pointless (#10339).
  1324. if ( data && processDataForInsertion( that, data ) ) {
  1325. // DATA INSERTION
  1326. insertDataIntoRange( that );
  1327. }
  1328. // FINAL CLEANUP
  1329. // Set final range position and clean up.
  1330. cleanupAfterInsertion( that );
  1331. }
  1332. // Prepare range to its data deletion.
  1333. // Delete its contents.
  1334. // Prepare it to insertion.
  1335. function prepareRangeToDataInsertion( that ) {
  1336. var range = that.range,
  1337. mergeCandidates = that.mergeCandidates,
  1338. node, marker, path, startPath, endPath, previous, bm;
  1339. // If range starts in inline element then insert a marker, so empty
  1340. // inline elements won't be removed while range.deleteContents
  1341. // and we will be able to move range back into this element.
  1342. // E.g. 'aa<b>[bb</b>]cc' -> (after deleting) 'aa<b><span/></b>cc'
  1343. if ( that.type == 'text' && range.shrink( CKEDITOR.SHRINK_ELEMENT, true, false ) ) {
  1344. marker = CKEDITOR.dom.element.createFromHtml( '<span>&nbsp;</span>', range.document );
  1345. range.insertNode( marker );
  1346. range.setStartAfter( marker );
  1347. }
  1348. // By using path we can recover in which element was startContainer
  1349. // before deleting contents.
  1350. // Start and endPathElements will be used to squash selected blocks, after removing
  1351. // selection contents. See rule 5.
  1352. startPath = new CKEDITOR.dom.elementPath( range.startContainer );
  1353. that.endPath = endPath = new CKEDITOR.dom.elementPath( range.endContainer );
  1354. if ( !range.collapsed ) {
  1355. // Anticipate the possibly empty block at the end of range after deletion.
  1356. node = endPath.block || endPath.blockLimit;
  1357. var ancestor = range.getCommonAncestor();
  1358. if ( node && !( node.equals( ancestor ) || node.contains( ancestor ) ) && range.checkEndOfBlock() ) {
  1359. that.zombies.push( node );
  1360. }
  1361. range.deleteContents();
  1362. }
  1363. // Rule 4.
  1364. // Move range into the previous block.
  1365. while (
  1366. ( previous = getRangePrevious( range ) ) && checkIfElement( previous ) && previous.isBlockBoundary() &&
  1367. // Check if previousNode was parent of range's startContainer before deleteContents.
  1368. startPath.contains( previous )
  1369. )
  1370. range.moveToPosition( previous, CKEDITOR.POSITION_BEFORE_END );
  1371. // Rule 5.
  1372. mergeAncestorElementsOfSelectionEnds( range, that.blockLimit, startPath, endPath );
  1373. // Rule 1.
  1374. if ( marker ) {
  1375. // If marker was created then move collapsed range into its place.
  1376. range.setEndBefore( marker );
  1377. range.collapse();
  1378. marker.remove();
  1379. }
  1380. // Split inline elements so HTML will be inserted with its own styles.
  1381. path = range.startPath();
  1382. if ( ( node = path.contains( isInline, false, 1 ) ) ) {
  1383. range.splitElement( node );
  1384. that.inlineStylesRoot = node;
  1385. that.inlineStylesPeak = path.lastElement;
  1386. }
  1387. // Record inline merging candidates for later cleanup in place.
  1388. bm = range.createBookmark();
  1389. // 1. Inline siblings.
  1390. node = bm.startNode.getPrevious( isNotEmpty );
  1391. node && checkIfElement( node ) && isInline( node ) && mergeCandidates.push( node );
  1392. node = bm.startNode.getNext( isNotEmpty );
  1393. node && checkIfElement( node ) && isInline( node ) && mergeCandidates.push( node );
  1394. // 2. Inline parents.
  1395. node = bm.startNode;
  1396. while ( ( node = node.getParent() ) && isInline( node ) )
  1397. mergeCandidates.push( node );
  1398. range.moveToBookmark( bm );
  1399. }
  1400. function processDataForInsertion( that, data ) {
  1401. var range = that.range;
  1402. // Rule 8. - wrap entire data in inline styles.
  1403. // (e.g. <p><b>x^z</b></p> + <p>a</p><p>b</p> -> <b><p>a</p><p>b</p></b>)
  1404. // Incorrect tags order will be fixed by htmlDataProcessor.
  1405. if ( that.type == 'text' && that.inlineStylesRoot )
  1406. data = wrapDataWithInlineStyles( data, that );
  1407. var context = that.blockLimit.getName();
  1408. // Wrap data to be inserted, to avoid losing leading whitespaces
  1409. // when going through the below procedure.
  1410. if ( /^\s+|\s+$/.test( data ) && 'span' in CKEDITOR.dtd[ context ] ) {
  1411. var protect = '<span data-cke-marker="1">&nbsp;</span>';
  1412. data = protect + data + protect;
  1413. }
  1414. // Process the inserted html, in context of the insertion root.
  1415. // Don't use the "fix for body" feature as auto paragraphing must
  1416. // be handled during insertion.
  1417. data = that.editor.dataProcessor.toHtml( data, {
  1418. context: null,
  1419. fixForBody: false,
  1420. protectedWhitespaces: !!protect,
  1421. dontFilter: that.dontFilter,
  1422. // Use the current, contextual settings.
  1423. filter: that.editor.activeFilter,
  1424. enterMode: that.editor.activeEnterMode
  1425. } );
  1426. // Build the node list for insertion.
  1427. var doc = range.document,
  1428. wrapper = doc.createElement( 'body' );
  1429. wrapper.setHtml( data );
  1430. // Eventually remove the temporaries.
  1431. if ( protect ) {
  1432. wrapper.getFirst().remove();
  1433. wrapper.getLast().remove();
  1434. }
  1435. // Rule 7.
  1436. var block = range.startPath().block;
  1437. if ( block && // Apply when there exists path block after deleting selection's content...
  1438. !( block.getChildCount() == 1 && block.getBogus() ) ) { // ... and the only content of this block isn't a bogus.
  1439. stripBlockTagIfSingleLine( wrapper );
  1440. }
  1441. that.dataWrapper = wrapper;
  1442. return data;
  1443. }
  1444. function insertDataIntoRange( that ) {
  1445. var range = that.range,
  1446. doc = range.document,
  1447. path,
  1448. blockLimit = that.blockLimit,
  1449. nodesData, nodeData, node,
  1450. nodeIndex = 0,
  1451. bogus,
  1452. bogusNeededBlocks = [],
  1453. pathBlock, fixBlock,
  1454. splittingContainer = 0,
  1455. dontMoveCaret = 0,
  1456. insertionContainer, toSplit, newContainer,
  1457. startContainer = range.startContainer,
  1458. endContainer = that.endPath.elements[ 0 ],
  1459. filteredNodes,
  1460. // If endContainer was merged into startContainer: <p>a[b</p><p>c]d</p>
  1461. // or it's equal to startContainer: <p>a^b</p>
  1462. // or different situation happened :P
  1463. // then there's no separate container for the end of selection.
  1464. pos = endContainer.getPosition( startContainer ),
  1465. separateEndContainer = !!endContainer.getCommonAncestor( startContainer ) && // endC is not detached.
  1466. pos != CKEDITOR.POSITION_IDENTICAL && !( pos & CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_IS_CONTAINED ); // endC & endS are in separate branches.
  1467. nodesData = extractNodesData( that.dataWrapper, that );
  1468. removeBrsAdjacentToPastedBlocks( nodesData, range );
  1469. for ( ; nodeIndex < nodesData.length; nodeIndex++ ) {
  1470. nodeData = nodesData[ nodeIndex ];
  1471. // Ignore trailing <brs>
  1472. if ( nodeData.isLineBreak && splitOnLineBreak( range, blockLimit, nodeData ) ) {
  1473. // Do not move caret towards the text (in cleanupAfterInsertion),
  1474. // because caret was placed after a line break.
  1475. dontMoveCaret = nodeIndex > 0;
  1476. continue;
  1477. }
  1478. path = range.startPath();
  1479. // Auto paragraphing.
  1480. if ( !nodeData.isBlock && shouldAutoParagraph( that.editor, path.block, path.blockLimit ) && ( fixBlock = autoParagraphTag( that.editor ) ) ) {
  1481. fixBlock = doc.createElement( fixBlock );
  1482. fixBlock.appendBogus();
  1483. range.insertNode( fixBlock );
  1484. if ( CKEDITOR.env.needsBrFiller && ( bogus = fixBlock.getBogus() ) )
  1485. bogus.remove();
  1486. range.moveToPosition( fixBlock, CKEDITOR.POSITION_BEFORE_END );
  1487. }
  1488. node = range.startPath().block;
  1489. // Remove any bogus element on the current path block for now, and mark
  1490. // it for later compensation.
  1491. if ( node && !node.equals( pathBlock ) ) {
  1492. bogus = node.getBogus();
  1493. if ( bogus ) {
  1494. bogus.remove();
  1495. bogusNeededBlocks.push( node );
  1496. }
  1497. pathBlock = node;
  1498. }
  1499. // First not allowed node reached - start splitting original container
  1500. if ( nodeData.firstNotAllowed )
  1501. splittingContainer = 1;
  1502. if ( splittingContainer && nodeData.isElement ) {
  1503. insertionContainer = range.startContainer;
  1504. toSplit = null;
  1505. // Find the first ancestor that can contain current node.
  1506. // This one won't be split.
  1507. while ( insertionContainer && !DTD[ insertionContainer.getName() ][ nodeData.name ] ) {
  1508. if ( insertionContainer.equals( blockLimit ) ) {
  1509. insertionContainer = null;
  1510. break;
  1511. }
  1512. toSplit = insertionContainer;
  1513. insertionContainer = insertionContainer.getParent();
  1514. }
  1515. // If split has to be done - do it and mark both ends as a possible zombies.
  1516. if ( insertionContainer ) {
  1517. if ( toSplit ) {
  1518. newContainer = range.splitElement( toSplit );
  1519. that.zombies.push( newContainer );
  1520. that.zombies.push( toSplit );
  1521. }
  1522. }
  1523. // Unable to make the insertion happen in place, resort to the content filter.
  1524. else {
  1525. // If everything worked fine insertionContainer == blockLimit here.
  1526. filteredNodes = filterElement( nodeData.node, blockLimit.getName(), !nodeIndex, nodeIndex == nodesData.length - 1 );
  1527. }
  1528. }
  1529. if ( filteredNodes ) {
  1530. while ( ( node = filteredNodes.pop() ) )
  1531. range.insertNode( node );
  1532. filteredNodes = 0;
  1533. } else {
  1534. // Insert current node at the start of range.
  1535. range.insertNode( nodeData.node );
  1536. }
  1537. // Move range to the endContainer for the final allowed elements.
  1538. if ( nodeData.lastNotAllowed && nodeIndex < nodesData.length - 1 ) {
  1539. // If separateEndContainer exists move range there.
  1540. // Otherwise try to move range to container created during splitting.
  1541. // If this doesn't work - don't move range.
  1542. newContainer = separateEndContainer ? endContainer : newContainer;
  1543. newContainer && range.setEndAt( newContainer, CKEDITOR.POSITION_AFTER_START );
  1544. splittingContainer = 0;
  1545. }
  1546. // Collapse range after insertion to end.
  1547. range.collapse();
  1548. }
  1549. // Rule 9. Non-editable content should be selected as a whole.
  1550. if ( isSingleNonEditableElement( nodesData ) ) {
  1551. dontMoveCaret = true;
  1552. node = nodesData[ 0 ].node;
  1553. range.setStartAt( node, CKEDITOR.POSITION_BEFORE_START );
  1554. range.setEndAt( node, CKEDITOR.POSITION_AFTER_END );
  1555. }
  1556. that.dontMoveCaret = dontMoveCaret;
  1557. that.bogusNeededBlocks = bogusNeededBlocks;
  1558. }
  1559. function cleanupAfterInsertion( that ) {
  1560. var range = that.range,
  1561. node, testRange, movedIntoInline,
  1562. bogusNeededBlocks = that.bogusNeededBlocks,
  1563. // Create a bookmark to defend against the following range deconstructing operations.
  1564. bm = range.createBookmark();
  1565. // Remove all elements that could be created while splitting nodes
  1566. // with ranges at its start|end.
  1567. // E.g. remove <div><p></p></div>
  1568. // But not <div><p> </p></div>
  1569. // And replace <div><p><span data="cke-bookmark"/></p></div> with found bookmark.
  1570. while ( ( node = that.zombies.pop() ) ) {
  1571. // Detached element.
  1572. if ( !node.getParent() )
  1573. continue;
  1574. testRange = range.clone();
  1575. testRange.moveToElementEditStart( node );
  1576. testRange.removeEmptyBlocksAtEnd();
  1577. }
  1578. if ( bogusNeededBlocks ) {
  1579. // Bring back all block bogus nodes.
  1580. while ( ( node = bogusNeededBlocks.pop() ) ) {
  1581. if ( CKEDITOR.env.needsBrFiller )
  1582. node.appendBogus();
  1583. else
  1584. node.append( range.document.createText( '\u00a0' ) );
  1585. }
  1586. }
  1587. // Eventually merge identical inline elements.
  1588. while ( ( node = that.mergeCandidates.pop() ) )
  1589. node.mergeSiblings();
  1590. range.moveToBookmark( bm );
  1591. // Rule 3.
  1592. // Shrink range to the BEFOREEND of previous innermost editable node in source order.
  1593. if ( !that.dontMoveCaret ) {
  1594. node = getRangePrevious( range );
  1595. while ( node && checkIfElement( node ) && !node.is( DTD.$empty ) ) {
  1596. if ( node.isBlockBoundary() )
  1597. range.moveToPosition( node, CKEDITOR.POSITION_BEFORE_END );
  1598. else {
  1599. // Don't move into inline element (which ends with a text node)
  1600. // found which contains white-space at its end.
  1601. // If not - move range's end to the end of this element.
  1602. if ( isInline( node ) && node.getHtml().match( /(\s|&nbsp;)$/g ) ) {
  1603. movedIntoInline = null;
  1604. break;
  1605. }
  1606. movedIntoInline = range.clone();
  1607. movedIntoInline.moveToPosition( node, CKEDITOR.POSITION_BEFORE_END );
  1608. }
  1609. node = node.getLast( isNotEmpty );
  1610. }
  1611. movedIntoInline && range.moveToRange( movedIntoInline );
  1612. }
  1613. }
  1614. //
  1615. // HELPERS ------------------------------------------------------------
  1616. //
  1617. function checkIfElement( node ) {
  1618. return node.type == CKEDITOR.NODE_ELEMENT;
  1619. }
  1620. function extractNodesData( dataWrapper, that ) {
  1621. var node, sibling, nodeName, allowed,
  1622. nodesData = [],
  1623. startContainer = that.range.startContainer,
  1624. path = that.range.startPath(),
  1625. allowedNames = DTD[ startContainer.getName() ],
  1626. nodeIndex = 0,
  1627. nodesList = dataWrapper.getChildren(),
  1628. nodesCount = nodesList.count(),
  1629. firstNotAllowed = -1,
  1630. lastNotAllowed = -1,
  1631. lineBreak = 0,
  1632. blockSibling;
  1633. // Selection start within a list.
  1634. var insideOfList = path.contains( DTD.$list );
  1635. for ( ; nodeIndex < nodesCount; ++nodeIndex ) {
  1636. node = nodesList.getItem( nodeIndex );
  1637. if ( checkIfElement( node ) ) {
  1638. nodeName = node.getName();
  1639. // Extract only the list items, when insertion happens
  1640. // inside of a list, reads as rearrange list items. (#7957)
  1641. if ( insideOfList && nodeName in CKEDITOR.dtd.$list ) {
  1642. nodesData = nodesData.concat( extractNodesData( node, that ) );
  1643. continue;
  1644. }
  1645. allowed = !!allowedNames[ nodeName ];
  1646. // Mark <brs data-cke-eol="1"> at the beginning and at the end.
  1647. if ( nodeName == 'br' && node.data( 'cke-eol' ) && ( !nodeIndex || nodeIndex == nodesCount - 1 ) ) {
  1648. sibling = nodeIndex ? nodesData[ nodeIndex - 1 ].node : nodesList.getItem( nodeIndex + 1 );
  1649. // Line break has to have sibling which is not an <br>.
  1650. lineBreak = sibling && ( !checkIfElement( sibling ) || !sibling.is( 'br' ) );
  1651. // Line break has block element as a sibling.
  1652. blockSibling = sibling && checkIfElement( sibling ) && DTD.$block[ sibling.getName() ];
  1653. }
  1654. if ( firstNotAllowed == -1 && !allowed )
  1655. firstNotAllowed = nodeIndex;
  1656. if ( !allowed )
  1657. lastNotAllowed = nodeIndex;
  1658. nodesData.push( {
  1659. isElement: 1,
  1660. isLineBreak: lineBreak,
  1661. isBlock: node.isBlockBoundary(),
  1662. hasBlockSibling: blockSibling,
  1663. node: node,
  1664. name: nodeName,
  1665. allowed: allowed
  1666. } );
  1667. lineBreak = 0;
  1668. blockSibling = 0;
  1669. } else {
  1670. nodesData.push( { isElement: 0, node: node, allowed: 1 } );
  1671. }
  1672. }
  1673. // Mark first node that cannot be inserted directly into startContainer
  1674. // and last node for which startContainer has to be split.
  1675. if ( firstNotAllowed > -1 )
  1676. nodesData[ firstNotAllowed ].firstNotAllowed = 1;
  1677. if ( lastNotAllowed > -1 )
  1678. nodesData[ lastNotAllowed ].lastNotAllowed = 1;
  1679. return nodesData;
  1680. }
  1681. // TODO: Review content transformation rules on filtering element.
  1682. function filterElement( element, parentName, isFirst, isLast ) {
  1683. var nodes = filterElementInner( element, parentName ),
  1684. nodes2 = [],
  1685. nodesCount = nodes.length,
  1686. nodeIndex = 0,
  1687. node,
  1688. afterSpace = 0,
  1689. lastSpaceIndex = -1;
  1690. // Remove duplicated spaces and spaces at the:
  1691. // * beginnig if filtered element isFirst (isFirst that's going to be inserted)
  1692. // * end if filtered element isLast.
  1693. for ( ; nodeIndex < nodesCount; nodeIndex++ ) {
  1694. node = nodes[ nodeIndex ];
  1695. if ( node == ' ' ) {
  1696. // Don't push doubled space and if it's leading space for insertion.
  1697. if ( !afterSpace && !( isFirst && !nodeIndex ) ) {
  1698. nodes2.push( new CKEDITOR.dom.text( ' ' ) );
  1699. lastSpaceIndex = nodes2.length;
  1700. }
  1701. afterSpace = 1;
  1702. } else {
  1703. nodes2.push( node );
  1704. afterSpace = 0;
  1705. }
  1706. }
  1707. // Remove trailing space.
  1708. if ( isLast && lastSpaceIndex == nodes2.length )
  1709. nodes2.pop();
  1710. return nodes2;
  1711. }
  1712. function filterElementInner( element, parentName ) {
  1713. var nodes = [],
  1714. children = element.getChildren(),
  1715. childrenCount = children.count(),
  1716. child,
  1717. childIndex = 0,
  1718. allowedNames = DTD[ parentName ],
  1719. surroundBySpaces = !element.is( DTD.$inline ) || element.is( 'br' );
  1720. if ( surroundBySpaces )
  1721. nodes.push( ' ' );
  1722. for ( ; childIndex < childrenCount; childIndex++ ) {
  1723. child = children.getItem( childIndex );
  1724. if ( checkIfElement( child ) && !child.is( allowedNames ) )
  1725. nodes = nodes.concat( filterElementInner( child, parentName ) );
  1726. else
  1727. nodes.push( child );
  1728. }
  1729. if ( surroundBySpaces )
  1730. nodes.push( ' ' );
  1731. return nodes;
  1732. }
  1733. function getRangePrevious( range ) {
  1734. return checkIfElement( range.startContainer ) && range.startContainer.getChild( range.startOffset - 1 );
  1735. }
  1736. function isInline( node ) {
  1737. return node && checkIfElement( node ) && ( node.is( DTD.$removeEmpty ) || node.is( 'a' ) && !node.isBlockBoundary() );
  1738. }
  1739. // Checks if only non-editable element is being inserted.
  1740. function isSingleNonEditableElement( nodesData ) {
  1741. if ( nodesData.length != 1 )
  1742. return false;
  1743. var nodeData = nodesData[ 0 ];
  1744. return nodeData.isElement && ( nodeData.node.getAttribute( 'contenteditable' ) == 'false' );
  1745. }
  1746. var blockMergedTags = { p: 1, div: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ul: 1, ol: 1, li: 1, pre: 1, dl: 1, blockquote: 1 };
  1747. // See rule 5. in TCs.
  1748. // Initial situation:
  1749. // <ul><li>AA^</li></ul><ul><li>BB</li></ul>
  1750. // We're looking for 2nd <ul>, comparing with 1st <ul> and merging.
  1751. // We're not merging if caret is between these elements.
  1752. function mergeAncestorElementsOfSelectionEnds( range, blockLimit, startPath, endPath ) {
  1753. var walkerRange = range.clone(),
  1754. walker, nextNode, previousNode;
  1755. walkerRange.setEndAt( blockLimit, CKEDITOR.POSITION_BEFORE_END );
  1756. walker = new CKEDITOR.dom.walker( walkerRange );
  1757. if ( ( nextNode = walker.next() ) && // Find next source node
  1758. checkIfElement( nextNode ) && // which is an element
  1759. blockMergedTags[ nextNode.getName() ] && // that can be merged.
  1760. ( previousNode = nextNode.getPrevious() ) && // Take previous one
  1761. checkIfElement( previousNode ) && // which also has to be an element.
  1762. !previousNode.getParent().equals( range.startContainer ) && // Fail if caret is on the same level.
  1763. // This means that caret is between these nodes.
  1764. startPath.contains( previousNode ) && // Elements path of start of selection has
  1765. endPath.contains( nextNode ) && // to contain prevNode and vice versa.
  1766. nextNode.isIdentical( previousNode ) // Check if elements are identical.
  1767. ) {
  1768. // Merge blocks and repeat.
  1769. nextNode.moveChildren( previousNode );
  1770. nextNode.remove();
  1771. mergeAncestorElementsOfSelectionEnds( range, blockLimit, startPath, endPath );
  1772. }
  1773. }
  1774. // If last node that will be inserted is a block (but not a <br>)
  1775. // and it will be inserted right before <br> remove this <br>.
  1776. // Do the same for the first element that will be inserted and preceding <br>.
  1777. function removeBrsAdjacentToPastedBlocks( nodesData, range ) {
  1778. var succeedingNode = range.endContainer.getChild( range.endOffset ),
  1779. precedingNode = range.endContainer.getChild( range.endOffset - 1 );
  1780. if ( succeedingNode )
  1781. remove( succeedingNode, nodesData[ nodesData.length - 1 ] );
  1782. if ( precedingNode && remove( precedingNode, nodesData[ 0 ] ) ) {
  1783. // If preceding <br> was removed - move range left.
  1784. range.setEnd( range.endContainer, range.endOffset - 1 );
  1785. range.collapse();
  1786. }
  1787. function remove( maybeBr, maybeBlockData ) {
  1788. if ( maybeBlockData.isBlock && maybeBlockData.isElement && !maybeBlockData.node.is( 'br' ) &&
  1789. checkIfElement( maybeBr ) && maybeBr.is( 'br' ) ) {
  1790. maybeBr.remove();
  1791. return 1;
  1792. }
  1793. }
  1794. }
  1795. // Return 1 if <br> should be skipped when inserting, 0 otherwise.
  1796. function splitOnLineBreak( range, blockLimit, nodeData ) {
  1797. var firstBlockAscendant, pos;
  1798. if ( nodeData.hasBlockSibling )
  1799. return 1;
  1800. firstBlockAscendant = range.startContainer.getAscendant( DTD.$block, 1 );
  1801. if ( !firstBlockAscendant || !firstBlockAscendant.is( { div: 1, p: 1 } ) )
  1802. return 0;
  1803. pos = firstBlockAscendant.getPosition( blockLimit );
  1804. if ( pos == CKEDITOR.POSITION_IDENTICAL || pos == CKEDITOR.POSITION_CONTAINS )
  1805. return 0;
  1806. var newContainer = range.splitElement( firstBlockAscendant );
  1807. range.moveToPosition( newContainer, CKEDITOR.POSITION_AFTER_START );
  1808. return 1;
  1809. }
  1810. var stripSingleBlockTags = { p: 1, div: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1 },
  1811. inlineButNotBr = CKEDITOR.tools.extend( {}, DTD.$inline );
  1812. delete inlineButNotBr.br;
  1813. // Rule 7.
  1814. function stripBlockTagIfSingleLine( dataWrapper ) {
  1815. var block, children;
  1816. if ( dataWrapper.getChildCount() == 1 && // Only one node bein inserted.
  1817. checkIfElement( block = dataWrapper.getFirst() ) && // And it's an element.
  1818. block.is( stripSingleBlockTags ) && // That's <p> or <div> or header.
  1819. !block.hasAttribute( 'contenteditable' ) // It's not a non-editable block or nested editable.
  1820. ) {
  1821. // Check children not containing block.
  1822. children = block.getElementsByTag( '*' );
  1823. for ( var i = 0, child, count = children.count(); i < count; i++ ) {
  1824. child = children.getItem( i );
  1825. if ( !child.is( inlineButNotBr ) )
  1826. return;
  1827. }
  1828. block.moveChildren( block.getParent( 1 ) );
  1829. block.remove();
  1830. }
  1831. }
  1832. function wrapDataWithInlineStyles( data, that ) {
  1833. var element = that.inlineStylesPeak,
  1834. doc = element.getDocument(),
  1835. wrapper = doc.createText( '{cke-peak}' ),
  1836. limit = that.inlineStylesRoot.getParent();
  1837. while ( !element.equals( limit ) ) {
  1838. wrapper = wrapper.appendTo( element.clone() );
  1839. element = element.getParent();
  1840. }
  1841. // Don't use String.replace because it fails in IE7 if special replacement
  1842. // characters ($$, $&, etc.) are in data (#10367).
  1843. return wrapper.getOuterHtml().split( '{cke-peak}' ).join( data );
  1844. }
  1845. return insert;
  1846. } )();
  1847. function afterInsert( editable ) {
  1848. var editor = editable.editor;
  1849. // Scroll using selection, not ranges, to affect native pastes.
  1850. editor.getSelection().scrollIntoView();
  1851. // Save snaps after the whole execution completed.
  1852. // This's a workaround for make DOM modification's happened after
  1853. // 'insertElement' to be included either, e.g. Form-based dialogs' 'commitContents'
  1854. // call.
  1855. setTimeout( function() {
  1856. editor.fire( 'saveSnapshot' );
  1857. }, 0 );
  1858. }
  1859. // 1. Fixes a range which is a result of deleteContents() and is placed in an intermediate element (see dtd.$intermediate),
  1860. // inside a table. A goal is to find a closest <td> or <th> element and when this fails, recreate the structure of the table.
  1861. // 2. Fixes empty cells by appending bogus <br>s or deleting empty text nodes in IE<=8 case.
  1862. var fixTableAfterContentsDeletion = ( function() {
  1863. // Creates an element walker which can only "go deeper". It won't
  1864. // move out from any element. Therefore it can be used to find <td>x</td> in cases like:
  1865. // <table><tbody><tr><td>x</td></tr></tbody>^<tfoot>...
  1866. function getFixTableSelectionWalker( testRange ) {
  1867. var walker = new CKEDITOR.dom.walker( testRange );
  1868. walker.guard = function( node, isMovingOut ) {
  1869. if ( isMovingOut )
  1870. return false;
  1871. if ( node.type == CKEDITOR.NODE_ELEMENT )
  1872. return node.is( CKEDITOR.dtd.$tableContent );
  1873. };
  1874. walker.evaluator = function( node ) {
  1875. return node.type == CKEDITOR.NODE_ELEMENT;
  1876. };
  1877. return walker;
  1878. }
  1879. function fixTableStructure( element, newElementName, appendToStart ) {
  1880. var temp = element.getDocument().createElement( newElementName );
  1881. element.append( temp, appendToStart );
  1882. return temp;
  1883. }
  1884. // Fix empty cells. This means:
  1885. // * add bogus <br> if browser needs it
  1886. // * remove empty text nodes on IE8, because it will crash (http://dev.ckeditor.com/ticket/11183#comment:8).
  1887. function fixEmptyCells( cells ) {
  1888. var i = cells.count(),
  1889. cell;
  1890. for ( i; i-- > 0; ) {
  1891. cell = cells.getItem( i );
  1892. if ( !CKEDITOR.tools.trim( cell.getHtml() ) ) {
  1893. cell.appendBogus();
  1894. if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && cell.getChildCount() )
  1895. cell.getFirst().remove();
  1896. }
  1897. }
  1898. }
  1899. return function( range ) {
  1900. var container = range.startContainer,
  1901. table = container.getAscendant( 'table', 1 ),
  1902. testRange,
  1903. deeperSibling,
  1904. appendToStart = false;
  1905. fixEmptyCells( table.getElementsByTag( 'td' ) );
  1906. fixEmptyCells( table.getElementsByTag( 'th' ) );
  1907. // Look left.
  1908. testRange = range.clone();
  1909. testRange.setStart( container, 0 );
  1910. deeperSibling = getFixTableSelectionWalker( testRange ).lastBackward();
  1911. // If left is empty, look right.
  1912. if ( !deeperSibling ) {
  1913. testRange = range.clone();
  1914. testRange.setEndAt( container, CKEDITOR.POSITION_BEFORE_END );
  1915. deeperSibling = getFixTableSelectionWalker( testRange ).lastForward();
  1916. appendToStart = true;
  1917. }
  1918. // If there's no deeper nested element in both direction - container is empty - we'll use it then.
  1919. if ( !deeperSibling )
  1920. deeperSibling = container;
  1921. // Fix structure...
  1922. // We found a table what means that it's empty - remove it completely.
  1923. if ( deeperSibling.is( 'table' ) ) {
  1924. range.setStartAt( deeperSibling, CKEDITOR.POSITION_BEFORE_START );
  1925. range.collapse( true );
  1926. deeperSibling.remove();
  1927. return;
  1928. }
  1929. // Found an empty txxx element - append tr.
  1930. if ( deeperSibling.is( { tbody: 1, thead: 1, tfoot: 1 } ) )
  1931. deeperSibling = fixTableStructure( deeperSibling, 'tr', appendToStart );
  1932. // Found an empty tr element - append td/th.
  1933. if ( deeperSibling.is( 'tr' ) )
  1934. deeperSibling = fixTableStructure( deeperSibling, deeperSibling.getParent().is( 'thead' ) ? 'th' : 'td', appendToStart );
  1935. // To avoid setting selection after bogus, remove it from the current cell.
  1936. // We can safely do that, because we'll insert element into that cell.
  1937. var bogus = deeperSibling.getBogus();
  1938. if ( bogus )
  1939. bogus.remove();
  1940. range.moveToPosition( deeperSibling, appendToStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
  1941. };
  1942. } )();
  1943. function mergeBlocksCollapsedSelection( editor, range, backspace, startPath ) {
  1944. var startBlock = startPath.block;
  1945. // Selection must be collapsed and to be anchored in a block.
  1946. if ( !startBlock )
  1947. return false;
  1948. // Exclude cases where, i.e. if pressed arrow key, selection
  1949. // would move within the same block (merge inside a block).
  1950. if ( !range[ backspace ? 'checkStartOfBlock' : 'checkEndOfBlock' ]() )
  1951. return false;
  1952. // Make sure, there's an editable position to put selection,
  1953. // which i.e. would be used if pressed arrow key, but abort
  1954. // if such position exists but means a selected non-editable element.
  1955. if ( !range.moveToClosestEditablePosition( startBlock, !backspace ) || !range.collapsed )
  1956. return false;
  1957. // Handle special case, when block's sibling is a <hr>. Delete it and keep selection
  1958. // in the same place (http://dev.ckeditor.com/ticket/11861#comment:9).
  1959. if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT ) {
  1960. var touched = range.startContainer.getChild( range.startOffset - ( backspace ? 1 : 0 ) );
  1961. if ( touched && touched.type == CKEDITOR.NODE_ELEMENT && touched.is( 'hr' ) ) {
  1962. editor.fire( 'saveSnapshot' );
  1963. touched.remove();
  1964. return true;
  1965. }
  1966. }
  1967. var siblingBlock = range.startPath().block;
  1968. // Abort if an editable position exists, but either it's not
  1969. // in a block or that block is the parent of the start block
  1970. // (merging child into parent).
  1971. if ( !siblingBlock || ( siblingBlock && siblingBlock.contains( startBlock ) ) )
  1972. return;
  1973. editor.fire( 'saveSnapshot' );
  1974. // Remove bogus to avoid duplicated boguses.
  1975. var bogus;
  1976. if ( ( bogus = ( backspace ? siblingBlock : startBlock ).getBogus() ) )
  1977. bogus.remove();
  1978. // Save selection. It will be restored.
  1979. var selection = editor.getSelection(),
  1980. bookmarks = selection.createBookmarks();
  1981. // Merge blocks.
  1982. ( backspace ? startBlock : siblingBlock ).moveChildren( backspace ? siblingBlock : startBlock, false );
  1983. // Also merge children along with parents.
  1984. startPath.lastElement.mergeSiblings();
  1985. // Cut off removable branch of the DOM tree.
  1986. pruneEmptyDisjointAncestors( startBlock, siblingBlock, !backspace );
  1987. // Restore selection.
  1988. selection.selectBookmarks( bookmarks );
  1989. return true;
  1990. }
  1991. function mergeBlocksNonCollapsedSelection( editor, range, startPath ) {
  1992. var startBlock = startPath.block,
  1993. endPath = range.endPath(),
  1994. endBlock = endPath.block;
  1995. // Selection must be anchored in two different blocks.
  1996. if ( !startBlock || !endBlock || startBlock.equals( endBlock ) )
  1997. return false;
  1998. editor.fire( 'saveSnapshot' );
  1999. // Remove bogus to avoid duplicated boguses.
  2000. var bogus;
  2001. if ( ( bogus = startBlock.getBogus() ) )
  2002. bogus.remove();
  2003. // Changing end container to element from text node (#12503).
  2004. range.enlarge( CKEDITOR.ENLARGE_INLINE );
  2005. // Delete range contents. Do NOT merge. Merging is weird.
  2006. range.deleteContents();
  2007. // If something has left of the block to be merged, clean it up.
  2008. // It may happen when merging with list items.
  2009. if ( endBlock.getParent() ) {
  2010. // Move children to the first block.
  2011. endBlock.moveChildren( startBlock, false );
  2012. // ...and merge them if that's possible.
  2013. startPath.lastElement.mergeSiblings();
  2014. // If expanded selection, things are always merged like with BACKSPACE.
  2015. pruneEmptyDisjointAncestors( startBlock, endBlock, true );
  2016. }
  2017. // Make sure the result selection is collapsed.
  2018. range = editor.getSelection().getRanges()[ 0 ];
  2019. range.collapse( 1 );
  2020. // Optimizing range containers from text nodes to elements (#12503).
  2021. range.optimize();
  2022. if ( range.startContainer.getHtml() === '' ) {
  2023. range.startContainer.appendBogus();
  2024. }
  2025. range.select();
  2026. return true;
  2027. }
  2028. // Finds the innermost child of common parent, which,
  2029. // if removed, removes nothing but the contents of the element.
  2030. //
  2031. // before: <div><p><strong>first</strong></p><p>second</p></div>
  2032. // after: <div><p>second</p></div>
  2033. //
  2034. // before: <div><p>x<strong>first</strong></p><p>second</p></div>
  2035. // after: <div><p>x</p><p>second</p></div>
  2036. //
  2037. // isPruneToEnd=true
  2038. // before: <div><p><strong>first</strong></p><p>second</p></div>
  2039. // after: <div><p><strong>first</strong></p></div>
  2040. //
  2041. // @param {CKEDITOR.dom.element} first
  2042. // @param {CKEDITOR.dom.element} second
  2043. // @param {Boolean} isPruneToEnd
  2044. function pruneEmptyDisjointAncestors( first, second, isPruneToEnd ) {
  2045. var commonParent = first.getCommonAncestor( second ),
  2046. node = isPruneToEnd ? second : first,
  2047. removableParent = node;
  2048. while ( ( node = node.getParent() ) && !commonParent.equals( node ) && node.getChildCount() == 1 )
  2049. removableParent = node;
  2050. removableParent.remove();
  2051. }
  2052. //
  2053. // Helpers for editable.getHtmlFromRange.
  2054. //
  2055. var getHtmlFromRangeHelpers = {
  2056. eol: {
  2057. detect: function( that, editable ) {
  2058. var range = that.range,
  2059. rangeStart = range.clone(),
  2060. rangeEnd = range.clone(),
  2061. startPath = new CKEDITOR.dom.elementPath( range.startContainer, editable ),
  2062. endPath = new CKEDITOR.dom.elementPath( range.endContainer, editable );
  2063. // Note: checkBoundaryOfElement will not work on original range as CKEDITOR.START|END
  2064. // means that range start|end must be literally anchored at block start|end, e.g.
  2065. //
  2066. // <p>a{</p><p>}b</p>
  2067. //
  2068. // will return false for both paragraphs but two similar ranges
  2069. //
  2070. // <p>a{}</p><p>{}b</p>
  2071. //
  2072. // will return true if checked separately.
  2073. rangeStart.collapse( 1 );
  2074. rangeEnd.collapse();
  2075. if ( startPath.block && rangeStart.checkBoundaryOfElement( startPath.block, CKEDITOR.END ) ) {
  2076. range.setStartAfter( startPath.block );
  2077. that.prependEolBr = 1;
  2078. }
  2079. if ( endPath.block && rangeEnd.checkBoundaryOfElement( endPath.block, CKEDITOR.START ) ) {
  2080. range.setEndBefore( endPath.block );
  2081. that.appendEolBr = 1;
  2082. }
  2083. },
  2084. fix: function( that, editable ) {
  2085. var doc = editable.getDocument(),
  2086. appended;
  2087. // Append <br data-cke-eol="1"> to the fragment.
  2088. if ( that.appendEolBr ) {
  2089. appended = this.createEolBr( doc );
  2090. that.fragment.append( appended );
  2091. }
  2092. // Prepend <br data-cke-eol="1"> to the fragment but avoid duplicates. Such
  2093. // elements should never follow each other in DOM.
  2094. if ( that.prependEolBr && ( !appended || appended.getPrevious() ) ) {
  2095. that.fragment.append( this.createEolBr( doc ), 1 );
  2096. }
  2097. },
  2098. createEolBr: function( doc ) {
  2099. return doc.createElement( 'br', {
  2100. attributes: {
  2101. 'data-cke-eol': 1
  2102. }
  2103. } );
  2104. }
  2105. },
  2106. bogus: {
  2107. exclude: function( that ) {
  2108. var boundaryNodes = that.range.getBoundaryNodes(),
  2109. startNode = boundaryNodes.startNode,
  2110. endNode = boundaryNodes.endNode;
  2111. // If bogus is the last node in range but not the only node, exclude it.
  2112. if ( endNode && isBogus( endNode ) && ( !startNode || !startNode.equals( endNode ) ) )
  2113. that.range.setEndBefore( endNode );
  2114. }
  2115. },
  2116. tree: {
  2117. rebuild: function( that, editable ) {
  2118. var range = that.range,
  2119. node = range.getCommonAncestor(),
  2120. // A path relative to the common ancestor.
  2121. commonPath = new CKEDITOR.dom.elementPath( node, editable ),
  2122. startPath = new CKEDITOR.dom.elementPath( range.startContainer, editable ),
  2123. endPath = new CKEDITOR.dom.elementPath( range.endContainer, editable ),
  2124. limit;
  2125. if ( node.type == CKEDITOR.NODE_TEXT )
  2126. node = node.getParent();
  2127. // Fix DOM of partially enclosed tables
  2128. // <table><tbody><tr><td>a{b</td><td>c}d</td></tr></tbody></table>
  2129. // Full table is returned
  2130. // <table><tbody><tr><td>b</td><td>c</td></tr></tbody></table>
  2131. // instead of
  2132. // <td>b</td><td>c</td>
  2133. if ( commonPath.blockLimit.is( { tr: 1, table: 1 } ) ) {
  2134. var tableParent = commonPath.contains( 'table' ).getParent();
  2135. limit = function( node ) {
  2136. return !node.equals( tableParent );
  2137. };
  2138. }
  2139. // Fix DOM in the following case
  2140. // <ol><li>a{b<ul><li>c}d</li></ul></li></ol>
  2141. // Full list is returned
  2142. // <ol><li>b<ul><li>c</li></ul></li></ol>
  2143. // instead of
  2144. // b<ul><li>c</li></ul>
  2145. else if ( commonPath.block && commonPath.block.is( CKEDITOR.dtd.$listItem ) ) {
  2146. var startList = startPath.contains( CKEDITOR.dtd.$list ),
  2147. endList = endPath.contains( CKEDITOR.dtd.$list );
  2148. if ( !startList.equals( endList ) ) {
  2149. var listParent = commonPath.contains( CKEDITOR.dtd.$list ).getParent();
  2150. limit = function( node ) {
  2151. return !node.equals( listParent );
  2152. };
  2153. }
  2154. }
  2155. // If not defined, use generic limit function.
  2156. if ( !limit ) {
  2157. limit = function( node ) {
  2158. return !node.equals( commonPath.block ) && !node.equals( commonPath.blockLimit );
  2159. };
  2160. }
  2161. this.rebuildFragment( that, editable, node, limit );
  2162. },
  2163. rebuildFragment: function( that, editable, node, checkLimit ) {
  2164. var clone;
  2165. while ( node && !node.equals( editable ) && checkLimit( node ) ) {
  2166. // Don't clone children. Preserve element ids.
  2167. clone = node.clone( 0, 1 );
  2168. that.fragment.appendTo( clone );
  2169. that.fragment = clone;
  2170. node = node.getParent();
  2171. }
  2172. }
  2173. },
  2174. cell: {
  2175. // Handle range anchored in table row with a single cell enclosed:
  2176. // <table><tbody><tr>[<td>a</td>]</tr></tbody></table>
  2177. // becomes
  2178. // <table><tbody><tr><td>{a}</td></tr></tbody></table>
  2179. shrink: function( that ) {
  2180. var range = that.range,
  2181. startContainer = range.startContainer,
  2182. endContainer = range.endContainer,
  2183. startOffset = range.startOffset,
  2184. endOffset = range.endOffset;
  2185. if ( startContainer.type == CKEDITOR.NODE_ELEMENT && startContainer.equals( endContainer ) && startContainer.is( 'tr' ) && ++startOffset == endOffset ) {
  2186. range.shrink( CKEDITOR.SHRINK_TEXT );
  2187. }
  2188. }
  2189. }
  2190. };
  2191. //
  2192. // Helpers for editable.extractHtmlFromRange.
  2193. //
  2194. var extractHtmlFromRangeHelpers = ( function() {
  2195. function optimizeBookmarkNode( node, toStart ) {
  2196. var parent = node.getParent();
  2197. if ( parent.is( CKEDITOR.dtd.$inline ) )
  2198. node[ toStart ? 'insertBefore' : 'insertAfter' ]( parent );
  2199. }
  2200. function mergeElements( merged, startBookmark, endBookmark ) {
  2201. optimizeBookmarkNode( startBookmark );
  2202. optimizeBookmarkNode( endBookmark, 1 );
  2203. var next;
  2204. while ( ( next = endBookmark.getNext() ) ) {
  2205. next.insertAfter( startBookmark );
  2206. // Update startBookmark after insertion to avoid the reversal of nodes (#13449).
  2207. startBookmark = next;
  2208. }
  2209. if ( isEmpty( merged ) )
  2210. merged.remove();
  2211. }
  2212. function getPath( startElement, root ) {
  2213. return new CKEDITOR.dom.elementPath( startElement, root );
  2214. }
  2215. // Creates a range from a bookmark without removing the bookmark.
  2216. function createRangeFromBookmark( root, bookmark ) {
  2217. var range = new CKEDITOR.dom.range( root );
  2218. range.setStartAfter( bookmark.startNode );
  2219. range.setEndBefore( bookmark.endNode );
  2220. return range;
  2221. }
  2222. var list = {
  2223. detectMerge: function( that, editable ) {
  2224. var range = createRangeFromBookmark( editable, that.bookmark ),
  2225. startPath = range.startPath(),
  2226. endPath = range.endPath(),
  2227. startList = startPath.contains( CKEDITOR.dtd.$list ),
  2228. endList = endPath.contains( CKEDITOR.dtd.$list );
  2229. that.mergeList =
  2230. // Both lists must exist
  2231. startList && endList &&
  2232. // ...and be of the same type
  2233. // startList.getName() == endList.getName() &&
  2234. // ...and share the same parent (same level in the tree)
  2235. startList.getParent().equals( endList.getParent() ) &&
  2236. // ...and must be different.
  2237. !startList.equals( endList );
  2238. that.mergeListItems =
  2239. startPath.block && endPath.block &&
  2240. // Both containers must be list items
  2241. startPath.block.is( CKEDITOR.dtd.$listItem ) && endPath.block.is( CKEDITOR.dtd.$listItem );
  2242. // Create merge bookmark.
  2243. if ( that.mergeList || that.mergeListItems ) {
  2244. var rangeClone = range.clone();
  2245. rangeClone.setStartBefore( that.bookmark.startNode );
  2246. rangeClone.setEndAfter( that.bookmark.endNode );
  2247. that.mergeListBookmark = rangeClone.createBookmark();
  2248. }
  2249. },
  2250. merge: function( that, editable ) {
  2251. if ( !that.mergeListBookmark )
  2252. return;
  2253. var startNode = that.mergeListBookmark.startNode,
  2254. endNode = that.mergeListBookmark.endNode,
  2255. startPath = getPath( startNode, editable ),
  2256. endPath = getPath( endNode, editable );
  2257. if ( that.mergeList ) {
  2258. var firstList = startPath.contains( CKEDITOR.dtd.$list ),
  2259. secondList = endPath.contains( CKEDITOR.dtd.$list );
  2260. if ( !firstList.equals( secondList ) ) {
  2261. secondList.moveChildren( firstList );
  2262. secondList.remove();
  2263. }
  2264. }
  2265. if ( that.mergeListItems ) {
  2266. var firstListItem = startPath.contains( CKEDITOR.dtd.$listItem ),
  2267. secondListItem = endPath.contains( CKEDITOR.dtd.$listItem );
  2268. if ( !firstListItem.equals( secondListItem ) ) {
  2269. mergeElements( secondListItem, startNode, endNode );
  2270. }
  2271. }
  2272. // Remove bookmark nodes.
  2273. startNode.remove();
  2274. endNode.remove();
  2275. }
  2276. };
  2277. var block = {
  2278. // Detects whether blocks should be merged once contents are extracted.
  2279. detectMerge: function( that, editable ) {
  2280. // Don't merge blocks if lists or tables are already involved.
  2281. if ( that.tableContentsRanges || that.mergeListBookmark )
  2282. return;
  2283. var rangeClone = new CKEDITOR.dom.range( editable );
  2284. rangeClone.setStartBefore( that.bookmark.startNode );
  2285. rangeClone.setEndAfter( that.bookmark.endNode );
  2286. that.mergeBlockBookmark = rangeClone.createBookmark();
  2287. },
  2288. merge: function( that, editable ) {
  2289. if ( !that.mergeBlockBookmark || that.purgeTableBookmark )
  2290. return;
  2291. var startNode = that.mergeBlockBookmark.startNode,
  2292. endNode = that.mergeBlockBookmark.endNode,
  2293. startPath = getPath( startNode, editable ),
  2294. endPath = getPath( endNode, editable ),
  2295. firstBlock = startPath.block,
  2296. secondBlock = endPath.block;
  2297. if ( firstBlock && secondBlock && !firstBlock.equals( secondBlock ) ) {
  2298. mergeElements( secondBlock, startNode, endNode );
  2299. }
  2300. // Remove bookmark nodes.
  2301. startNode.remove();
  2302. endNode.remove();
  2303. }
  2304. };
  2305. var table = ( function() {
  2306. var tableEditable = { td: 1, th: 1, caption: 1 };
  2307. // Returns an array of ranges which should be entirely extracted.
  2308. //
  2309. // <table><tr>[<td>xx</td><td>y}y</td></tr></table>
  2310. // will find:
  2311. // <table><tr><td>[xx]</td><td>[y}y</td></tr></table>
  2312. function findTableContentsRanges( range ) {
  2313. // Leaving the below for debugging purposes.
  2314. //
  2315. // console.log( 'findTableContentsRanges' );
  2316. // console.log( bender.tools.range.getWithHtml( range.root, range ) );
  2317. var contentsRanges = [],
  2318. editableRange,
  2319. walker = new CKEDITOR.dom.walker( range ),
  2320. startCell = range.startPath().contains( tableEditable ),
  2321. endCell = range.endPath().contains( tableEditable ),
  2322. database = {};
  2323. walker.guard = function( node, leaving ) {
  2324. // Guard may be executed on some node boundaries multiple times,
  2325. // what results in creating more than one range for each selected cell. (#12964)
  2326. if ( node.type == CKEDITOR.NODE_ELEMENT ) {
  2327. var key = 'visited_' + ( leaving ? 'out' : 'in' );
  2328. if ( node.getCustomData( key ) ) {
  2329. return;
  2330. }
  2331. CKEDITOR.dom.element.setMarker( database, node, key, 1 );
  2332. }
  2333. // Handle partial selection in a cell in which the range starts:
  2334. // <td><p>x{xx</p></td>...
  2335. // will store:
  2336. // <td><p>x{xx</p>]</td>
  2337. if ( leaving && startCell && node.equals( startCell ) ) {
  2338. editableRange = range.clone();
  2339. editableRange.setEndAt( startCell, CKEDITOR.POSITION_BEFORE_END );
  2340. contentsRanges.push( editableRange );
  2341. return;
  2342. }
  2343. // Handle partial selection in a cell in which the range ends.
  2344. if ( !leaving && endCell && node.equals( endCell ) ) {
  2345. editableRange = range.clone();
  2346. editableRange.setStartAt( endCell, CKEDITOR.POSITION_AFTER_START );
  2347. contentsRanges.push( editableRange );
  2348. return;
  2349. }
  2350. // Handle all other cells visited by the walker.
  2351. // We need to check whether the cell is disjoint with
  2352. // the start and end cells to correctly handle case like:
  2353. // <td>x{x</td><td><table>..<td>y}y</td>..</table></td>
  2354. // without the check the second cell's content would be entirely removed.
  2355. if ( !leaving && checkRemoveCellContents( node ) ) {
  2356. editableRange = range.clone();
  2357. editableRange.selectNodeContents( node );
  2358. contentsRanges.push( editableRange );
  2359. }
  2360. };
  2361. walker.lastForward();
  2362. // Clear all markers so next extraction will not be affected by this one.
  2363. CKEDITOR.dom.element.clearAllMarkers( database );
  2364. return contentsRanges;
  2365. function checkRemoveCellContents( node ) {
  2366. return (
  2367. // Must be a cell.
  2368. node.type == CKEDITOR.NODE_ELEMENT && node.is( tableEditable ) &&
  2369. // Must be disjoint with the range's startCell if exists.
  2370. ( !startCell || checkDisjointNodes( node, startCell ) ) &&
  2371. // Must be disjoint with the range's endCell if exists.
  2372. ( !endCell || checkDisjointNodes( node, endCell ) )
  2373. );
  2374. }
  2375. }
  2376. // Returns a normalized common ancestor of a range.
  2377. // If the real common ancestor is located somewhere in between a table and a td/th/caption,
  2378. // then the table will be returned.
  2379. function getNormalizedAncestor( range ) {
  2380. var common = range.getCommonAncestor();
  2381. if ( common.is( CKEDITOR.dtd.$tableContent ) && !common.is( tableEditable ) ) {
  2382. common = common.getAscendant( 'table', true );
  2383. }
  2384. return common;
  2385. }
  2386. // Check whether node1 and node2 are disjoint, so are:
  2387. // * not identical,
  2388. // * not contained in each other.
  2389. function checkDisjointNodes( node1, node2 ) {
  2390. var disallowedPositions = CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_IS_CONTAINED,
  2391. pos = node1.getPosition( node2 );
  2392. // Baaah... IDENTICAL is 0, so we can't simplify this ;/.
  2393. return pos === CKEDITOR.POSITION_IDENTICAL ?
  2394. false :
  2395. ( ( pos & disallowedPositions ) === 0 );
  2396. }
  2397. return {
  2398. // Detects whether to purge entire list.
  2399. detectPurge: function( that ) {
  2400. var range = that.range,
  2401. walkerRange = range.clone();
  2402. walkerRange.enlarge( CKEDITOR.ENLARGE_ELEMENT );
  2403. var walker = new CKEDITOR.dom.walker( walkerRange ),
  2404. editablesCount = 0;
  2405. // Count the number of table editables in the range. If there's more than one,
  2406. // table MAY be removed completely (it's a cross-cell range). Otherwise, only
  2407. // the contents of the cell are usually removed.
  2408. walker.evaluator = function( node ) {
  2409. if ( node.type == CKEDITOR.NODE_ELEMENT && node.is( tableEditable ) ) {
  2410. ++editablesCount;
  2411. }
  2412. };
  2413. walker.checkForward();
  2414. if ( editablesCount > 1 ) {
  2415. var startTable = range.startPath().contains( 'table' ),
  2416. endTable = range.endPath().contains( 'table' );
  2417. if ( startTable && endTable && range.checkBoundaryOfElement( startTable, CKEDITOR.START ) && range.checkBoundaryOfElement( endTable, CKEDITOR.END ) ) {
  2418. var rangeClone = that.range.clone();
  2419. rangeClone.setStartBefore( startTable );
  2420. rangeClone.setEndAfter( endTable );
  2421. that.purgeTableBookmark = rangeClone.createBookmark();
  2422. }
  2423. }
  2424. },
  2425. // The magic.
  2426. //
  2427. // This method tries to discover whether the range starts or ends somewhere in a table
  2428. // (it is not interested whether the range contains a table, because in such case
  2429. // the extractContents() methods does the job correctly).
  2430. // If the range meets these criteria, then the method tries to discover and store the following:
  2431. //
  2432. // * that.tableSurroundingRange - a part of the range which is located outside of any table which
  2433. // will be touched (note: when range is located in a single cell it does not touch the table).
  2434. // This range can be placed at:
  2435. // * at the beginning: <p>he{re</p><table>..]..</table>
  2436. // * in the middle: <table>..[..</table><p>here</p><table>..]..</table>
  2437. // * at the end: <table>..[..</table><p>he}re</p>
  2438. // * that.tableContentsRanges - an array of ranges with contents of td/th/caption that should be removed.
  2439. // This assures that calling extractContents() does not change the structure of the table(s).
  2440. detectRanges: function( that, editable ) {
  2441. var range = createRangeFromBookmark( editable, that.bookmark ),
  2442. surroundingRange = range.clone(),
  2443. leftRange,
  2444. rightRange,
  2445. // Find a common ancestor and normalize it (so the following paths contain tables).
  2446. commonAncestor = getNormalizedAncestor( range ),
  2447. // Create paths using the normalized ancestor, so tables beyond the context
  2448. // of the input range are not found.
  2449. startPath = new CKEDITOR.dom.elementPath( range.startContainer, commonAncestor ),
  2450. endPath = new CKEDITOR.dom.elementPath( range.endContainer, commonAncestor ),
  2451. startTable = startPath.contains( 'table' ),
  2452. endTable = endPath.contains( 'table' ),
  2453. tableContentsRanges;
  2454. // Nothing to do here - the range doesn't touch any table or
  2455. // it contains a table, but that table is fully selected so it will be simply fully removed
  2456. // by the normal algorithm.
  2457. if ( !startTable && !endTable ) {
  2458. return;
  2459. }
  2460. // Handle two disjoint tables case:
  2461. // <table>..[..</table><p>ab</p><table>..]..</table>
  2462. // is handled as (respectively: findTableContents( left ), surroundingRange, findTableContents( right )):
  2463. // <table>..[..</table>][<p>ab</p>][<table>..]..</table>
  2464. // Check that tables are disjoint to exclude a case when start equals end or one is contained
  2465. // in the other.
  2466. if ( startTable && endTable && checkDisjointNodes( startTable, endTable ) ) {
  2467. that.tableSurroundingRange = surroundingRange;
  2468. surroundingRange.setStartAt( startTable, CKEDITOR.POSITION_AFTER_END );
  2469. surroundingRange.setEndAt( endTable, CKEDITOR.POSITION_BEFORE_START );
  2470. leftRange = range.clone();
  2471. leftRange.setEndAt( startTable, CKEDITOR.POSITION_AFTER_END );
  2472. rightRange = range.clone();
  2473. rightRange.setStartAt( endTable, CKEDITOR.POSITION_BEFORE_START );
  2474. tableContentsRanges = findTableContentsRanges( leftRange ).concat( findTableContentsRanges( rightRange ) );
  2475. }
  2476. // Divide the initial range into two parts:
  2477. // * range which contains the part containing the table,
  2478. // * surroundingRange which contains the part outside the table.
  2479. //
  2480. // The surroundingRange exists only if one of the range ends is
  2481. // located outside the table.
  2482. //
  2483. // <p>a{b</p><table>..]..</table><p>cd</p>
  2484. // becomes (respectively: surroundingRange, range):
  2485. // <p>a{b</p>][<table>..]..</table><p>cd</p>
  2486. else if ( !startTable ) {
  2487. that.tableSurroundingRange = surroundingRange;
  2488. surroundingRange.setEndAt( endTable, CKEDITOR.POSITION_BEFORE_START );
  2489. range.setStartAt( endTable, CKEDITOR.POSITION_AFTER_START );
  2490. }
  2491. // <p>ab</p><table>..[..</table><p>c}d</p>
  2492. // becomes (respectively range, surroundingRange):
  2493. // <p>ab</p><table>..[..</table>][<p>c}d</p>
  2494. else if ( !endTable ) {
  2495. that.tableSurroundingRange = surroundingRange;
  2496. surroundingRange.setStartAt( startTable, CKEDITOR.POSITION_AFTER_END );
  2497. range.setEndAt( startTable, CKEDITOR.POSITION_AFTER_END );
  2498. }
  2499. // Use already calculated or calculate for the remaining range.
  2500. that.tableContentsRanges = tableContentsRanges ? tableContentsRanges : findTableContentsRanges( range );
  2501. // Leaving the below for debugging purposes.
  2502. //
  2503. // if ( that.tableSurroundingRange ) {
  2504. // console.log( 'tableSurroundingRange' );
  2505. // console.log( bender.tools.range.getWithHtml( that.tableSurroundingRange.root, that.tableSurroundingRange ) );
  2506. // }
  2507. //
  2508. // console.log( 'tableContentsRanges' );
  2509. // that.tableContentsRanges.forEach( function( range ) {
  2510. // console.log( bender.tools.range.getWithHtml( range.root, range ) );
  2511. // } );
  2512. },
  2513. deleteRanges: function( that ) {
  2514. var range;
  2515. // Delete table cell contents.
  2516. while ( ( range = that.tableContentsRanges.pop() ) ) {
  2517. range.extractContents();
  2518. if ( isEmpty( range.startContainer ) )
  2519. range.startContainer.appendBogus();
  2520. }
  2521. // Finally delete surroundings of the table.
  2522. if ( that.tableSurroundingRange ) {
  2523. that.tableSurroundingRange.extractContents();
  2524. }
  2525. },
  2526. purge: function( that ) {
  2527. if ( !that.purgeTableBookmark )
  2528. return;
  2529. var doc = that.doc,
  2530. range = that.range,
  2531. rangeClone = range.clone(),
  2532. // How about different enter modes?
  2533. block = doc.createElement( 'p' );
  2534. block.insertBefore( that.purgeTableBookmark.startNode );
  2535. rangeClone.moveToBookmark( that.purgeTableBookmark );
  2536. rangeClone.deleteContents();
  2537. that.range.moveToPosition( block, CKEDITOR.POSITION_AFTER_START );
  2538. }
  2539. };
  2540. } )();
  2541. return {
  2542. list: list,
  2543. block: block,
  2544. table: table,
  2545. // Detects whether use "mergeThen" argument in range.extractContents().
  2546. detectExtractMerge: function( that ) {
  2547. // Don't merge if playing with lists.
  2548. return !(
  2549. that.range.startPath().contains( CKEDITOR.dtd.$listItem ) &&
  2550. that.range.endPath().contains( CKEDITOR.dtd.$listItem )
  2551. );
  2552. },
  2553. fixUneditableRangePosition: function( range ) {
  2554. if ( !range.startContainer.getDtd()[ '#' ] ) {
  2555. range.moveToClosestEditablePosition( null, true );
  2556. }
  2557. },
  2558. // Perform auto paragraphing if needed.
  2559. autoParagraph: function( editor, range ) {
  2560. var path = range.startPath(),
  2561. fixBlock;
  2562. if ( shouldAutoParagraph( editor, path.block, path.blockLimit ) && ( fixBlock = autoParagraphTag( editor ) ) ) {
  2563. fixBlock = range.document.createElement( fixBlock );
  2564. fixBlock.appendBogus();
  2565. range.insertNode( fixBlock );
  2566. range.moveToPosition( fixBlock, CKEDITOR.POSITION_AFTER_START );
  2567. }
  2568. }
  2569. };
  2570. } )();
  2571. } )();
  2572. /**
  2573. * Whether the editor must output an empty value (`''`) if its content only consists
  2574. * of an empty paragraph.
  2575. *
  2576. * config.ignoreEmptyParagraph = false;
  2577. *
  2578. * @cfg {Boolean} [ignoreEmptyParagraph=true]
  2579. * @member CKEDITOR.config
  2580. */
  2581. /**
  2582. * Event fired by the editor in order to get accessibility help label.
  2583. * The event is responded to by a component which provides accessibility
  2584. * help (i.e. the `a11yhelp` plugin) hence the editor is notified whether
  2585. * accessibility help is available.
  2586. *
  2587. * Providing info:
  2588. *
  2589. * editor.on( 'ariaEditorHelpLabel', function( evt ) {
  2590. * evt.data.label = editor.lang.common.editorHelp;
  2591. * } );
  2592. *
  2593. * Getting label:
  2594. *
  2595. * var helpLabel = editor.fire( 'ariaEditorHelpLabel', {} ).label;
  2596. *
  2597. * @since 4.4.3
  2598. * @event ariaEditorHelpLabel
  2599. * @param {String} label The label to be used.
  2600. * @member CKEDITOR.editor
  2601. */
  2602. /**
  2603. * Event fired when the user double-clicks in the editable area.
  2604. * The event allows to open a dialog window for a clicked element in a convenient way:
  2605. *
  2606. * editor.on( 'doubleclick', function( evt ) {
  2607. * var element = evt.data.element;
  2608. *
  2609. * if ( element.is( 'table' ) )
  2610. * evt.data.dialog = 'tableProperties';
  2611. * } );
  2612. *
  2613. * **Note:** To handle double-click on a widget use {@link CKEDITOR.plugins.widget#doubleclick}.
  2614. *
  2615. * @event doubleclick
  2616. * @param data
  2617. * @param {CKEDITOR.dom.element} data.element The double-clicked element.
  2618. * @param {String} data.dialog The dialog window to be opened. If set by the listener,
  2619. * the specified dialog window will be opened.
  2620. * @member CKEDITOR.editor
  2621. */