Dashboard sipadu mbip
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

plugin.js 131KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or http://ckeditor.com/license
  4. */
  5. /**
  6. * @fileOverview [Widget](http://ckeditor.com/addon/widget) plugin.
  7. */
  8. 'use strict';
  9. ( function() {
  10. var DRAG_HANDLER_SIZE = 15;
  11. CKEDITOR.plugins.add( 'widget', {
  12. // jscs:disable maximumLineLength
  13. lang: 'af,ar,bg,ca,cs,cy,da,de,de-ch,el,en,en-gb,eo,es,eu,fa,fi,fr,gl,he,hr,hu,id,it,ja,km,ko,ku,lv,nb,nl,no,oc,pl,pt,pt-br,ru,sk,sl,sq,sv,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
  14. // jscs:enable maximumLineLength
  15. requires: 'lineutils,clipboard,widgetselection',
  16. onLoad: function() {
  17. CKEDITOR.addCss(
  18. '.cke_widget_wrapper{' +
  19. 'position:relative;' +
  20. 'outline:none' +
  21. '}' +
  22. '.cke_widget_inline{' +
  23. 'display:inline-block' +
  24. '}' +
  25. '.cke_widget_wrapper:hover>.cke_widget_element{' +
  26. 'outline:2px solid yellow;' +
  27. 'cursor:default' +
  28. '}' +
  29. '.cke_widget_wrapper:hover .cke_widget_editable{' +
  30. 'outline:2px solid yellow' +
  31. '}' +
  32. '.cke_widget_wrapper.cke_widget_focused>.cke_widget_element,' +
  33. // We need higher specificity than hover style.
  34. '.cke_widget_wrapper .cke_widget_editable.cke_widget_editable_focused{' +
  35. 'outline:2px solid #ace' +
  36. '}' +
  37. '.cke_widget_editable{' +
  38. 'cursor:text' +
  39. '}' +
  40. '.cke_widget_drag_handler_container{' +
  41. 'position:absolute;' +
  42. 'width:' + DRAG_HANDLER_SIZE + 'px;' +
  43. 'height:0;' +
  44. // Initially drag handler should not be visible, until its position will be
  45. // calculated (#11177).
  46. // We need to hide unpositined handlers, so they don't extend
  47. // widget's outline far to the left (#12024).
  48. 'display:none;' +
  49. 'opacity:0.75;' +
  50. 'transition:height 0s 0.2s;' + // Delay hiding drag handler.
  51. // Prevent drag handler from being misplaced (#11198).
  52. 'line-height:0' +
  53. '}' +
  54. '.cke_widget_wrapper:hover>.cke_widget_drag_handler_container{' +
  55. 'height:' + DRAG_HANDLER_SIZE + 'px;' +
  56. 'transition:none' +
  57. '}' +
  58. '.cke_widget_drag_handler_container:hover{' +
  59. 'opacity:1' +
  60. '}' +
  61. 'img.cke_widget_drag_handler{' +
  62. 'cursor:move;' +
  63. 'width:' + DRAG_HANDLER_SIZE + 'px;' +
  64. 'height:' + DRAG_HANDLER_SIZE + 'px;' +
  65. 'display:inline-block' +
  66. '}' +
  67. '.cke_widget_mask{' +
  68. 'position:absolute;' +
  69. 'top:0;' +
  70. 'left:0;' +
  71. 'width:100%;' +
  72. 'height:100%;' +
  73. 'display:block' +
  74. '}' +
  75. '.cke_editable.cke_widget_dragging, .cke_editable.cke_widget_dragging *{' +
  76. 'cursor:move !important' +
  77. '}'
  78. );
  79. },
  80. beforeInit: function( editor ) {
  81. /**
  82. * An instance of widget repository. It contains all
  83. * {@link CKEDITOR.plugins.widget.repository#registered registered widget definitions} and
  84. * {@link CKEDITOR.plugins.widget.repository#instances initialized instances}.
  85. *
  86. * editor.widgets.add( 'someName', {
  87. * // Widget definition...
  88. * } );
  89. *
  90. * editor.widgets.registered.someName; // -> Widget definition
  91. *
  92. * @since 4.3
  93. * @readonly
  94. * @property {CKEDITOR.plugins.widget.repository} widgets
  95. * @member CKEDITOR.editor
  96. */
  97. editor.widgets = new Repository( editor );
  98. },
  99. afterInit: function( editor ) {
  100. addWidgetButtons( editor );
  101. setupContextMenu( editor );
  102. }
  103. } );
  104. /**
  105. * Widget repository. It keeps track of all {@link #registered registered widget definitions} and
  106. * {@link #instances initialized instances}. An instance of the repository is available under
  107. * the {@link CKEDITOR.editor#widgets} property.
  108. *
  109. * @class CKEDITOR.plugins.widget.repository
  110. * @mixins CKEDITOR.event
  111. * @constructor Creates a widget repository instance. Note that the widget plugin automatically
  112. * creates a repository instance which is available under the {@link CKEDITOR.editor#widgets} property.
  113. * @param {CKEDITOR.editor} editor The editor instance for which the repository will be created.
  114. */
  115. function Repository( editor ) {
  116. /**
  117. * The editor instance for which this repository was created.
  118. *
  119. * @readonly
  120. * @property {CKEDITOR.editor} editor
  121. */
  122. this.editor = editor;
  123. /**
  124. * A hash of registered widget definitions (definition name => {@link CKEDITOR.plugins.widget.definition}).
  125. *
  126. * To register a definition use the {@link #add} method.
  127. *
  128. * @readonly
  129. */
  130. this.registered = {};
  131. /**
  132. * An object containing initialized widget instances (widget id => {@link CKEDITOR.plugins.widget}).
  133. *
  134. * @readonly
  135. */
  136. this.instances = {};
  137. /**
  138. * An array of selected widget instances.
  139. *
  140. * @readonly
  141. * @property {CKEDITOR.plugins.widget[]} selected
  142. */
  143. this.selected = [];
  144. /**
  145. * The focused widget instance. See also {@link CKEDITOR.plugins.widget#event-focus}
  146. * and {@link CKEDITOR.plugins.widget#event-blur} events.
  147. *
  148. * editor.on( 'selectionChange', function() {
  149. * if ( editor.widgets.focused ) {
  150. * // Do something when a widget is focused...
  151. * }
  152. * } );
  153. *
  154. * @readonly
  155. * @property {CKEDITOR.plugins.widget} focused
  156. */
  157. this.focused = null;
  158. /**
  159. * The widget instance that contains the nested editable which is currently focused.
  160. *
  161. * @readonly
  162. * @property {CKEDITOR.plugins.widget} widgetHoldingFocusedEditable
  163. */
  164. this.widgetHoldingFocusedEditable = null;
  165. this._ = {
  166. nextId: 0,
  167. upcasts: [],
  168. upcastCallbacks: [],
  169. filters: {}
  170. };
  171. setupWidgetsLifecycle( this );
  172. setupSelectionObserver( this );
  173. setupMouseObserver( this );
  174. setupKeyboardObserver( this );
  175. setupDragAndDrop( this );
  176. setupNativeCutAndCopy( this );
  177. }
  178. Repository.prototype = {
  179. /**
  180. * Minimum interval between selection checks.
  181. *
  182. * @private
  183. */
  184. MIN_SELECTION_CHECK_INTERVAL: 500,
  185. /**
  186. * Adds a widget definition to the repository. Fires the {@link CKEDITOR.editor#widgetDefinition} event
  187. * which allows to modify the widget definition which is going to be registered.
  188. *
  189. * @param {String} name The name of the widget definition.
  190. * @param {CKEDITOR.plugins.widget.definition} widgetDef Widget definition.
  191. * @returns {CKEDITOR.plugins.widget.definition}
  192. */
  193. add: function( name, widgetDef ) {
  194. // Create prototyped copy of original widget definition, so we won't modify it.
  195. widgetDef = CKEDITOR.tools.prototypedCopy( widgetDef );
  196. widgetDef.name = name;
  197. widgetDef._ = widgetDef._ || {};
  198. this.editor.fire( 'widgetDefinition', widgetDef );
  199. if ( widgetDef.template )
  200. widgetDef.template = new CKEDITOR.template( widgetDef.template );
  201. addWidgetCommand( this.editor, widgetDef );
  202. addWidgetProcessors( this, widgetDef );
  203. this.registered[ name ] = widgetDef;
  204. return widgetDef;
  205. },
  206. /**
  207. * Adds a callback for element upcasting. Each callback will be executed
  208. * for every element which is later tested by upcast methods. If a callback
  209. * returns `false`, the element will not be upcasted.
  210. *
  211. * // Images with the "banner" class will not be upcasted (e.g. to the image widget).
  212. * editor.widgets.addUpcastCallback( function( element ) {
  213. * if ( element.name == 'img' && element.hasClass( 'banner' ) )
  214. * return false;
  215. * } );
  216. *
  217. * @param {Function} callback
  218. * @param {CKEDITOR.htmlParser.element} callback.element
  219. */
  220. addUpcastCallback: function( callback ) {
  221. this._.upcastCallbacks.push( callback );
  222. },
  223. /**
  224. * Checks the selection to update widget states (selection and focus).
  225. *
  226. * This method is triggered by the {@link #event-checkSelection} event.
  227. */
  228. checkSelection: function() {
  229. var sel = this.editor.getSelection(),
  230. selectedElement = sel.getSelectedElement(),
  231. updater = stateUpdater( this ),
  232. widget;
  233. // Widget is focused so commit and finish checking.
  234. if ( selectedElement && ( widget = this.getByElement( selectedElement, true ) ) )
  235. return updater.focus( widget ).select( widget ).commit();
  236. var range = sel.getRanges()[ 0 ];
  237. // No ranges or collapsed range mean that nothing is selected, so commit and finish checking.
  238. if ( !range || range.collapsed )
  239. return updater.commit();
  240. // Range is not empty, so create walker checking for wrappers.
  241. var walker = new CKEDITOR.dom.walker( range ),
  242. wrapper;
  243. walker.evaluator = Widget.isDomWidgetWrapper;
  244. while ( ( wrapper = walker.next() ) )
  245. updater.select( this.getByElement( wrapper ) );
  246. updater.commit();
  247. },
  248. /**
  249. * Checks if all widget instances are still present in the DOM.
  250. * Destroys those instances that are not present.
  251. * Reinitializes widgets on widget wrappers for which widget instances
  252. * cannot be found. Takes nested widgets into account, too.
  253. *
  254. * This method triggers the {@link #event-checkWidgets} event whose listeners
  255. * can cancel the method's execution or modify its options.
  256. *
  257. * @param [options] The options object.
  258. * @param {Boolean} [options.initOnlyNew] Initializes widgets only on newly wrapped
  259. * widget elements (those which still have the `cke_widget_new` class). When this option is
  260. * set to `true`, widgets which were invalidated (e.g. by replacing with a cloned DOM structure)
  261. * will not be reinitialized. This makes the check faster.
  262. * @param {Boolean} [options.focusInited] If only one widget is initialized by
  263. * the method, it will be focused.
  264. */
  265. checkWidgets: function( options ) {
  266. this.fire( 'checkWidgets', CKEDITOR.tools.copy( options || {} ) );
  267. },
  268. /**
  269. * Removes the widget from the editor and moves the selection to the closest
  270. * editable position if the widget was focused before.
  271. *
  272. * @param {CKEDITOR.plugins.widget} widget The widget instance to be deleted.
  273. */
  274. del: function( widget ) {
  275. if ( this.focused === widget ) {
  276. var editor = widget.editor,
  277. range = editor.createRange(),
  278. found;
  279. // If haven't found place for caret on the default side,
  280. // try to find it on the other side.
  281. if ( !( found = range.moveToClosestEditablePosition( widget.wrapper, true ) ) )
  282. found = range.moveToClosestEditablePosition( widget.wrapper, false );
  283. if ( found )
  284. editor.getSelection().selectRanges( [ range ] );
  285. }
  286. widget.wrapper.remove();
  287. this.destroy( widget, true );
  288. },
  289. /**
  290. * Destroys the widget instance and all its nested widgets (widgets inside its nested editables).
  291. *
  292. * @param {CKEDITOR.plugins.widget} widget The widget instance to be destroyed.
  293. * @param {Boolean} [offline] Whether the widget is offline (detached from the DOM tree) —
  294. * in this case the DOM (attributes, classes, etc.) will not be cleaned up.
  295. */
  296. destroy: function( widget, offline ) {
  297. if ( this.widgetHoldingFocusedEditable === widget )
  298. setFocusedEditable( this, widget, null, offline );
  299. widget.destroy( offline );
  300. delete this.instances[ widget.id ];
  301. this.fire( 'instanceDestroyed', widget );
  302. },
  303. /**
  304. * Destroys all widget instances.
  305. *
  306. * @param {Boolean} [offline] Whether the widgets are offline (detached from the DOM tree) —
  307. * in this case the DOM (attributes, classes, etc.) will not be cleaned up.
  308. * @param {CKEDITOR.dom.element} [container] The container within widgets will be destroyed.
  309. * This option will be ignored if the `offline` flag was set to `true`, because in such case
  310. * it is not possible to find widgets within the passed block.
  311. */
  312. destroyAll: function( offline, container ) {
  313. var widget,
  314. id,
  315. instances = this.instances;
  316. if ( container && !offline ) {
  317. var wrappers = container.find( '.cke_widget_wrapper' ),
  318. l = wrappers.count(),
  319. i = 0;
  320. // Length is constant, because this is not a live node list.
  321. // Note: since querySelectorAll returns nodes in document order,
  322. // outer widgets are always placed before their nested widgets and therefore
  323. // are destroyed before them.
  324. for ( ; i < l; ++i ) {
  325. widget = this.getByElement( wrappers.getItem( i ), true );
  326. // Widget might not be found, because it could be a nested widget,
  327. // which would be destroyed when destroying its parent.
  328. if ( widget )
  329. this.destroy( widget );
  330. }
  331. return;
  332. }
  333. for ( id in instances ) {
  334. widget = instances[ id ];
  335. this.destroy( widget, offline );
  336. }
  337. },
  338. /**
  339. * Finalizes a process of widget creation. This includes:
  340. *
  341. * * inserting widget element into editor,
  342. * * marking widget instance as ready (see {@link CKEDITOR.plugins.widget#event-ready}),
  343. * * focusing widget instance.
  344. *
  345. * This method is used by the default widget's command and is called
  346. * after widget's dialog (if set) is closed. It may also be used in a
  347. * customized process of widget creation and insertion.
  348. *
  349. * widget.once( 'edit', function() {
  350. * // Finalize creation only of not ready widgets.
  351. * if ( widget.isReady() )
  352. * return;
  353. *
  354. * // Cancel edit event to prevent automatic widget insertion.
  355. * evt.cancel();
  356. *
  357. * CustomDialog.open( widget.data, function saveCallback( savedData ) {
  358. * // Cache the container, because widget may be destroyed while saving data,
  359. * // if this process will require some deep transformations.
  360. * var container = widget.wrapper.getParent();
  361. *
  362. * widget.setData( savedData );
  363. *
  364. * // Widget will be retrieved from container and inserted into editor.
  365. * editor.widgets.finalizeCreation( container );
  366. * } );
  367. * } );
  368. *
  369. * @param {CKEDITOR.dom.element/CKEDITOR.dom.documentFragment} container The element
  370. * or document fragment which contains widget wrapper. The container is used, so before
  371. * finalizing creation the widget can be freely transformed (even destroyed and reinitialized).
  372. */
  373. finalizeCreation: function( container ) {
  374. var wrapper = container.getFirst();
  375. if ( wrapper && Widget.isDomWidgetWrapper( wrapper ) ) {
  376. this.editor.insertElement( wrapper );
  377. var widget = this.getByElement( wrapper );
  378. // Fire postponed #ready event.
  379. widget.ready = true;
  380. widget.fire( 'ready' );
  381. widget.focus();
  382. }
  383. },
  384. /**
  385. * Finds a widget instance which contains a given element. The element will be the {@link CKEDITOR.plugins.widget#wrapper wrapper}
  386. * of the returned widget or a descendant of this {@link CKEDITOR.plugins.widget#wrapper wrapper}.
  387. *
  388. * editor.widgets.getByElement( someWidget.wrapper ); // -> someWidget
  389. * editor.widgets.getByElement( someWidget.parts.caption ); // -> someWidget
  390. *
  391. * // Check wrapper only:
  392. * editor.widgets.getByElement( someWidget.wrapper, true ); // -> someWidget
  393. * editor.widgets.getByElement( someWidget.parts.caption, true ); // -> null
  394. *
  395. * @param {CKEDITOR.dom.element} element The element to be checked.
  396. * @param {Boolean} [checkWrapperOnly] If set to `true`, the method will not check wrappers' descendants.
  397. * @returns {CKEDITOR.plugins.widget} The widget instance or `null`.
  398. */
  399. getByElement: ( function() {
  400. var validWrapperElements = { div: 1, span: 1 };
  401. function getWidgetId( element ) {
  402. return element.is( validWrapperElements ) && element.data( 'cke-widget-id' );
  403. }
  404. return function( element, checkWrapperOnly ) {
  405. if ( !element )
  406. return null;
  407. var id = getWidgetId( element );
  408. // There's no need to check element parents if element is a wrapper.
  409. if ( !checkWrapperOnly && !id ) {
  410. var limit = this.editor.editable();
  411. // Try to find a closest ascendant which is a widget wrapper.
  412. do {
  413. element = element.getParent();
  414. } while ( element && !element.equals( limit ) && !( id = getWidgetId( element ) ) );
  415. }
  416. return this.instances[ id ] || null;
  417. };
  418. } )(),
  419. /**
  420. * Initializes a widget on a given element if the widget has not been initialized on it yet.
  421. *
  422. * @param {CKEDITOR.dom.element} element The future widget element.
  423. * @param {String/CKEDITOR.plugins.widget.definition} [widgetDef] Name of a widget or a widget definition.
  424. * The widget definition should be previously registered by using the
  425. * {@link CKEDITOR.plugins.widget.repository#add} method.
  426. * @param [startupData] Widget startup data (has precedence over default one).
  427. * @returns {CKEDITOR.plugins.widget} The widget instance or `null` if a widget could not be initialized on
  428. * a given element.
  429. */
  430. initOn: function( element, widgetDef, startupData ) {
  431. if ( !widgetDef )
  432. widgetDef = this.registered[ element.data( 'widget' ) ];
  433. else if ( typeof widgetDef == 'string' )
  434. widgetDef = this.registered[ widgetDef ];
  435. if ( !widgetDef )
  436. return null;
  437. // Wrap element if still wasn't wrapped (was added during runtime by method that skips dataProcessor).
  438. var wrapper = this.wrapElement( element, widgetDef.name );
  439. if ( wrapper ) {
  440. // Check if widget wrapper is new (widget hasn't been initialized on it yet).
  441. // This class will be removed by widget constructor to avoid locking snapshot twice.
  442. if ( wrapper.hasClass( 'cke_widget_new' ) ) {
  443. var widget = new Widget( this, this._.nextId++, element, widgetDef, startupData );
  444. // Widget could be destroyed when initializing it.
  445. if ( widget.isInited() ) {
  446. this.instances[ widget.id ] = widget;
  447. return widget;
  448. } else {
  449. return null;
  450. }
  451. }
  452. // Widget already has been initialized, so try to get widget by element.
  453. // Note - it may happen that other instance will returned than the one created above,
  454. // if for example widget was destroyed and reinitialized.
  455. return this.getByElement( element );
  456. }
  457. // No wrapper means that there's no widget for this element.
  458. return null;
  459. },
  460. /**
  461. * Initializes widgets on all elements which were wrapped by {@link #wrapElement} and
  462. * have not been initialized yet.
  463. *
  464. * @param {CKEDITOR.dom.element} [container=editor.editable()] The container which will be checked for not
  465. * initialized widgets. Defaults to editor's {@link CKEDITOR.editor#editable editable} element.
  466. * @returns {CKEDITOR.plugins.widget[]} Array of widget instances which have been initialized.
  467. * Note: Only first-level widgets are returned &mdash; without nested widgets.
  468. */
  469. initOnAll: function( container ) {
  470. var newWidgets = ( container || this.editor.editable() ).find( '.cke_widget_new' ),
  471. newInstances = [],
  472. instance;
  473. for ( var i = newWidgets.count(); i--; ) {
  474. instance = this.initOn( newWidgets.getItem( i ).getFirst( Widget.isDomWidgetElement ) );
  475. if ( instance )
  476. newInstances.push( instance );
  477. }
  478. return newInstances;
  479. },
  480. /**
  481. * Allows to listen to events on specific types of widgets, even if they are not created yet.
  482. *
  483. * Please note that this method inherits parameters from the {@link CKEDITOR.event#method-on} method with one
  484. * extra parameter at the beginning which is the widget name.
  485. *
  486. * editor.widgets.onWidget( 'image', 'action', function( evt ) {
  487. * // Event `action` occurs on `image` widget.
  488. * } );
  489. *
  490. * @since 4.5
  491. * @param {String} widgetName
  492. * @param {String} eventName
  493. * @param {Function} listenerFunction
  494. * @param {Object} [scopeObj]
  495. * @param {Object} [listenerData]
  496. * @param {Number} [priority=10]
  497. */
  498. onWidget: function( widgetName ) {
  499. var args = Array.prototype.slice.call( arguments );
  500. args.shift();
  501. for ( var i in this.instances ) {
  502. var instance = this.instances[ i ];
  503. if ( instance.name == widgetName ) {
  504. instance.on.apply( instance, args );
  505. }
  506. }
  507. this.on( 'instanceCreated', function( evt ) {
  508. var widget = evt.data;
  509. if ( widget.name == widgetName ) {
  510. widget.on.apply( widget, args );
  511. }
  512. } );
  513. },
  514. /**
  515. * Parses element classes string and returns an object
  516. * whose keys contain class names. Skips all `cke_*` classes.
  517. *
  518. * This method is used by the {@link CKEDITOR.plugins.widget#getClasses} method and
  519. * may be used when overriding that method.
  520. *
  521. * @since 4.4
  522. * @param {String} classes String (value of `class` attribute).
  523. * @returns {Object} Object containing classes or `null` if no classes found.
  524. */
  525. parseElementClasses: function( classes ) {
  526. if ( !classes )
  527. return null;
  528. classes = CKEDITOR.tools.trim( classes ).split( /\s+/ );
  529. var cl,
  530. obj = {},
  531. hasClasses = 0;
  532. while ( ( cl = classes.pop() ) ) {
  533. if ( cl.indexOf( 'cke_' ) == -1 )
  534. obj[ cl ] = hasClasses = 1;
  535. }
  536. return hasClasses ? obj : null;
  537. },
  538. /**
  539. * Wraps an element with a widget's non-editable container.
  540. *
  541. * If this method is called on an {@link CKEDITOR.htmlParser.element}, then it will
  542. * also take care of fixing the DOM after wrapping (the wrapper may not be allowed in element's parent).
  543. *
  544. * @param {CKEDITOR.dom.element/CKEDITOR.htmlParser.element} element The widget element to be wrapped.
  545. * @param {String} [widgetName] The name of the widget definition. Defaults to element's `data-widget`
  546. * attribute value.
  547. * @returns {CKEDITOR.dom.element/CKEDITOR.htmlParser.element} The wrapper element or `null` if
  548. * the widget definition of this name is not registered.
  549. */
  550. wrapElement: function( element, widgetName ) {
  551. var wrapper = null,
  552. widgetDef,
  553. isInline;
  554. if ( element instanceof CKEDITOR.dom.element ) {
  555. widgetName = widgetName || element.data( 'widget' );
  556. widgetDef = this.registered[ widgetName ];
  557. if ( !widgetDef )
  558. return null;
  559. // Do not wrap already wrapped element.
  560. wrapper = element.getParent();
  561. if ( wrapper && wrapper.type == CKEDITOR.NODE_ELEMENT && wrapper.data( 'cke-widget-wrapper' ) )
  562. return wrapper;
  563. // If attribute isn't already set (e.g. for pasted widget), set it.
  564. if ( !element.hasAttribute( 'data-cke-widget-keep-attr' ) )
  565. element.data( 'cke-widget-keep-attr', element.data( 'widget' ) ? 1 : 0 );
  566. element.data( 'widget', widgetName );
  567. isInline = isWidgetInline( widgetDef, element.getName() );
  568. wrapper = new CKEDITOR.dom.element( isInline ? 'span' : 'div' );
  569. wrapper.setAttributes( getWrapperAttributes( isInline, widgetName ) );
  570. wrapper.data( 'cke-display-name', widgetDef.pathName ? widgetDef.pathName : element.getName() );
  571. // Replace element unless it is a detached one.
  572. if ( element.getParent( true ) )
  573. wrapper.replace( element );
  574. element.appendTo( wrapper );
  575. }
  576. else if ( element instanceof CKEDITOR.htmlParser.element ) {
  577. widgetName = widgetName || element.attributes[ 'data-widget' ];
  578. widgetDef = this.registered[ widgetName ];
  579. if ( !widgetDef )
  580. return null;
  581. wrapper = element.parent;
  582. if ( wrapper && wrapper.type == CKEDITOR.NODE_ELEMENT && wrapper.attributes[ 'data-cke-widget-wrapper' ] )
  583. return wrapper;
  584. // If attribute isn't already set (e.g. for pasted widget), set it.
  585. if ( !( 'data-cke-widget-keep-attr' in element.attributes ) )
  586. element.attributes[ 'data-cke-widget-keep-attr' ] = element.attributes[ 'data-widget' ] ? 1 : 0;
  587. if ( widgetName )
  588. element.attributes[ 'data-widget' ] = widgetName;
  589. isInline = isWidgetInline( widgetDef, element.name );
  590. wrapper = new CKEDITOR.htmlParser.element( isInline ? 'span' : 'div', getWrapperAttributes( isInline, widgetName ) );
  591. wrapper.attributes[ 'data-cke-display-name' ] = widgetDef.pathName ? widgetDef.pathName : element.name;
  592. var parent = element.parent,
  593. index;
  594. // Don't detach already detached element.
  595. if ( parent ) {
  596. index = element.getIndex();
  597. element.remove();
  598. }
  599. wrapper.add( element );
  600. // Insert wrapper fixing DOM (splitting parents if wrapper is not allowed inside them).
  601. parent && insertElement( parent, index, wrapper );
  602. }
  603. return wrapper;
  604. },
  605. // Expose for tests.
  606. _tests_createEditableFilter: createEditableFilter
  607. };
  608. CKEDITOR.event.implementOn( Repository.prototype );
  609. /**
  610. * An event fired when a widget instance is created, but before it is fully initialized.
  611. *
  612. * @event instanceCreated
  613. * @param {CKEDITOR.plugins.widget} data The widget instance.
  614. */
  615. /**
  616. * An event fired when a widget instance was destroyed.
  617. *
  618. * See also {@link CKEDITOR.plugins.widget#event-destroy}.
  619. *
  620. * @event instanceDestroyed
  621. * @param {CKEDITOR.plugins.widget} data The widget instance.
  622. */
  623. /**
  624. * An event fired to trigger the selection check.
  625. *
  626. * See the {@link #method-checkSelection} method.
  627. *
  628. * @event checkSelection
  629. */
  630. /**
  631. * An event fired by the the {@link #method-checkWidgets} method.
  632. *
  633. * It can be canceled in order to stop the {@link #method-checkWidgets}
  634. * method execution or the event listener can modify the method's options.
  635. *
  636. * @event checkWidgets
  637. * @param [data]
  638. * @param {Boolean} [data.initOnlyNew] Initialize widgets only on newly wrapped
  639. * widget elements (those which still have the `cke_widget_new` class). When this option is
  640. * set to `true`, widgets which were invalidated (e.g. by replacing with a cloned DOM structure)
  641. * will not be reinitialized. This makes the check faster.
  642. * @param {Boolean} [data.focusInited] If only one widget is initialized by
  643. * the method, it will be focused.
  644. */
  645. /**
  646. * An instance of a widget. Together with {@link CKEDITOR.plugins.widget.repository} these
  647. * two classes constitute the core of the Widget System.
  648. *
  649. * Note that neither the repository nor the widget instances can be created by using their constructors.
  650. * A repository instance is automatically set up by the Widget plugin and is accessible under
  651. * {@link CKEDITOR.editor#widgets}, while widget instances are created and destroyed by the repository.
  652. *
  653. * To create a widget, first you need to {@link CKEDITOR.plugins.widget.repository#add register} its
  654. * {@link CKEDITOR.plugins.widget.definition definition}:
  655. *
  656. * editor.widgets.add( 'simplebox', {
  657. * upcast: function( element ) {
  658. * // Defines which elements will become widgets.
  659. * if ( element.hasClass( 'simplebox' ) )
  660. * return true;
  661. * },
  662. * init: function() {
  663. * // ...
  664. * }
  665. * } );
  666. *
  667. * Once the widget definition is registered, widgets will be automatically
  668. * created when loading data:
  669. *
  670. * editor.setData( '<div class="simplebox">foo</div>', function() {
  671. * console.log( editor.widgets.instances ); // -> An object containing one instance.
  672. * } );
  673. *
  674. * It is also possible to create instances during runtime by using a command
  675. * (if a {@link CKEDITOR.plugins.widget.definition#template} property was defined):
  676. *
  677. * // You can execute an automatically defined command to
  678. * // insert a new simplebox widget or edit the one currently focused.
  679. * editor.execCommand( 'simplebox' );
  680. *
  681. * Note: Since CKEditor 4.5 widget's `startupData` can be passed as the command argument:
  682. *
  683. * editor.execCommand( 'simplebox', {
  684. * startupData: {
  685. * align: 'left'
  686. * }
  687. * } );
  688. *
  689. * A widget can also be created in a completely custom way:
  690. *
  691. * var element = editor.document.createElement( 'div' );
  692. * editor.insertElement( element );
  693. * var widget = editor.widgets.initOn( element, 'simplebox' );
  694. *
  695. * @since 4.3
  696. * @class CKEDITOR.plugins.widget
  697. * @mixins CKEDITOR.event
  698. * @extends CKEDITOR.plugins.widget.definition
  699. * @constructor Creates an instance of the widget class. Do not use it directly, but instead initialize widgets
  700. * by using the {@link CKEDITOR.plugins.widget.repository#initOn} method or by the upcasting system.
  701. * @param {CKEDITOR.plugins.widget.repository} widgetsRepo
  702. * @param {Number} id Unique ID of this widget instance.
  703. * @param {CKEDITOR.dom.element} element The widget element.
  704. * @param {CKEDITOR.plugins.widget.definition} widgetDef Widget's registered definition.
  705. * @param [startupData] Initial widget data. This data object will overwrite the default data and
  706. * the data loaded from the DOM.
  707. */
  708. function Widget( widgetsRepo, id, element, widgetDef, startupData ) {
  709. var editor = widgetsRepo.editor;
  710. // Extend this widget with widgetDef-specific methods and properties.
  711. CKEDITOR.tools.extend( this, widgetDef, {
  712. /**
  713. * The editor instance.
  714. *
  715. * @readonly
  716. * @property {CKEDITOR.editor}
  717. */
  718. editor: editor,
  719. /**
  720. * This widget's unique (per editor instance) ID.
  721. *
  722. * @readonly
  723. * @property {Number}
  724. */
  725. id: id,
  726. /**
  727. * Whether this widget is an inline widget (based on an inline element unless
  728. * forced otherwise by {@link CKEDITOR.plugins.widget.definition#inline}).
  729. *
  730. * **Note:** This option does not allow to turn a block element into an inline widget.
  731. * However, it makes it possible to turn an inline element into a block widget or to
  732. * force a correct type in case when automatic recognition fails.
  733. *
  734. * @readonly
  735. * @property {Boolean}
  736. */
  737. inline: element.getParent().getName() == 'span',
  738. /**
  739. * The widget element &mdash; the element on which the widget was initialized.
  740. *
  741. * @readonly
  742. * @property {CKEDITOR.dom.element} element
  743. */
  744. element: element,
  745. /**
  746. * Widget's data object.
  747. *
  748. * The data can only be set by using the {@link #setData} method.
  749. * Changes made to the data fire the {@link #event-data} event.
  750. *
  751. * @readonly
  752. */
  753. data: CKEDITOR.tools.extend( {}, typeof widgetDef.defaults == 'function' ? widgetDef.defaults() : widgetDef.defaults ),
  754. /**
  755. * Indicates if a widget is data-ready. Set to `true` when data from all sources
  756. * ({@link CKEDITOR.plugins.widget.definition#defaults}, set in the
  757. * {@link #init} method, loaded from the widget's element and startup data coming from the constructor)
  758. * are finally loaded. This is immediately followed by the first {@link #event-data}.
  759. *
  760. * @readonly
  761. */
  762. dataReady: false,
  763. /**
  764. * Whether a widget instance was initialized. This means that:
  765. *
  766. * * An instance was created,
  767. * * Its properties were set,
  768. * * The `init` method was executed.
  769. *
  770. * **Note**: The first {@link #event-data} event could not be fired yet which
  771. * means that the widget's DOM has not been set up yet. Wait for the {@link #event-ready}
  772. * event to be notified when a widget is fully initialized and ready.
  773. *
  774. * **Note**: Use the {@link #isInited} method to check whether a widget is initialized and
  775. * has not been destroyed.
  776. *
  777. * @readonly
  778. */
  779. inited: false,
  780. /**
  781. * Whether a widget instance is ready. This means that the widget is {@link #inited} and
  782. * that its DOM was finally set up.
  783. *
  784. * **Note:** Use the {@link #isReady} method to check whether a widget is ready and
  785. * has not been destroyed.
  786. *
  787. * @readonly
  788. */
  789. ready: false,
  790. // Revert what widgetDef could override (automatic #edit listener).
  791. edit: Widget.prototype.edit,
  792. /**
  793. * The nested editable element which is currently focused.
  794. *
  795. * @readonly
  796. * @property {CKEDITOR.plugins.widget.nestedEditable}
  797. */
  798. focusedEditable: null,
  799. /**
  800. * The widget definition from which this instance was created.
  801. *
  802. * @readonly
  803. * @property {CKEDITOR.plugins.widget.definition} definition
  804. */
  805. definition: widgetDef,
  806. /**
  807. * Link to the widget repository which created this instance.
  808. *
  809. * @readonly
  810. * @property {CKEDITOR.plugins.widget.repository} repository
  811. */
  812. repository: widgetsRepo,
  813. draggable: widgetDef.draggable !== false,
  814. // WAAARNING: Overwrite widgetDef's priv object, because otherwise violent unicorn's gonna visit you.
  815. _: {
  816. downcastFn: ( widgetDef.downcast && typeof widgetDef.downcast == 'string' ) ?
  817. widgetDef.downcasts[ widgetDef.downcast ] : widgetDef.downcast
  818. }
  819. }, true );
  820. /**
  821. * An object of widget component elements.
  822. *
  823. * For every `partName => selector` pair in {@link CKEDITOR.plugins.widget.definition#parts},
  824. * one `partName => element` pair is added to this object during the widget initialization.
  825. *
  826. * @readonly
  827. * @property {Object} parts
  828. */
  829. /**
  830. * The template which will be used to create a new widget element (when the widget's command is executed).
  831. * It will be populated with {@link #defaults default values}.
  832. *
  833. * @readonly
  834. * @property {CKEDITOR.template} template
  835. */
  836. /**
  837. * The widget wrapper &mdash; a non-editable `div` or `span` element (depending on {@link #inline})
  838. * which is a parent of the {@link #element} and widget compontents like the drag handler and the {@link #mask}.
  839. * It is the outermost widget element.
  840. *
  841. * @readonly
  842. * @property {CKEDITOR.dom.element} wrapper
  843. */
  844. widgetsRepo.fire( 'instanceCreated', this );
  845. setupWidget( this, widgetDef );
  846. this.init && this.init();
  847. // Finally mark widget as inited.
  848. this.inited = true;
  849. setupWidgetData( this, startupData );
  850. // If at some point (e.g. in #data listener) widget hasn't been destroyed
  851. // and widget is already attached To Document then fire #ready.
  852. if ( this.isInited() && editor.editable().contains( this.wrapper ) ) {
  853. this.ready = true;
  854. this.fire( 'ready' );
  855. }
  856. }
  857. Widget.prototype = {
  858. /**
  859. * Adds a class to the widget element. This method is used by
  860. * the {@link #applyStyle} method and should be overridden by widgets
  861. * which should handle classes differently (e.g. add them to other elements).
  862. *
  863. * Since 4.6.0 this method also adds a corresponding class prefixed with {@link #WRAPPER_CLASS_PREFIX}
  864. * to the widget wrapper element.
  865. *
  866. * **Note**: This method should not be used directly. Use the {@link #setData} method to
  867. * set the `classes` property. Read more in the {@link #setData} documentation.
  868. *
  869. * See also: {@link #removeClass}, {@link #hasClass}, {@link #getClasses}.
  870. *
  871. * @since 4.4
  872. * @param {String} className The class name to be added.
  873. */
  874. addClass: function( className ) {
  875. this.element.addClass( className );
  876. this.wrapper.addClass( Widget.WRAPPER_CLASS_PREFIX + className );
  877. },
  878. /**
  879. * Applies the specified style to the widget. It is highly recommended to use the
  880. * {@link CKEDITOR.editor#applyStyle} or {@link CKEDITOR.style#apply} methods instead of
  881. * using this method directly, because unlike editor's and style's methods, this one
  882. * does not perform any checks.
  883. *
  884. * By default this method handles only classes defined in the style. It clones existing
  885. * classes which are stored in the {@link #property-data widget data}'s `classes` property,
  886. * adds new classes, and calls the {@link #setData} method if at least one new class was added.
  887. * Then, using the {@link #event-data} event listener widget applies modifications passing
  888. * new classes to the {@link #addClass} method.
  889. *
  890. * If you need to handle classes differently than in the default way, you can override the
  891. * {@link #addClass} and related methods. You can also handle other style properties than `classes`
  892. * by overriding this method.
  893. *
  894. * See also: {@link #checkStyleActive}, {@link #removeStyle}.
  895. *
  896. * @since 4.4
  897. * @param {CKEDITOR.style} style The custom widget style to be applied.
  898. */
  899. applyStyle: function( style ) {
  900. applyRemoveStyle( this, style, 1 );
  901. },
  902. /**
  903. * Checks if the specified style is applied to this widget. It is highly recommended to use the
  904. * {@link CKEDITOR.style#checkActive} method instead of using this method directly,
  905. * because unlike style's method, this one does not perform any checks.
  906. *
  907. * By default this method handles only classes defined in the style and passes
  908. * them to the {@link #hasClass} method. You can override these methods to handle classes
  909. * differently or to handle more of the style properties.
  910. *
  911. * See also: {@link #applyStyle}, {@link #removeStyle}.
  912. *
  913. * @since 4.4
  914. * @param {CKEDITOR.style} style The custom widget style to be checked.
  915. * @returns {Boolean} Whether the style is applied to this widget.
  916. */
  917. checkStyleActive: function( style ) {
  918. var classes = getStyleClasses( style ),
  919. cl;
  920. if ( !classes )
  921. return false;
  922. while ( ( cl = classes.pop() ) ) {
  923. if ( !this.hasClass( cl ) )
  924. return false;
  925. }
  926. return true;
  927. },
  928. /**
  929. * Destroys this widget instance.
  930. *
  931. * Use {@link CKEDITOR.plugins.widget.repository#destroy} when possible instead of this method.
  932. *
  933. * This method fires the {#event-destroy} event.
  934. *
  935. * @param {Boolean} [offline] Whether a widget is offline (detached from the DOM tree) &mdash;
  936. * in this case the DOM (attributes, classes, etc.) will not be cleaned up.
  937. */
  938. destroy: function( offline ) {
  939. this.fire( 'destroy' );
  940. if ( this.editables ) {
  941. for ( var name in this.editables )
  942. this.destroyEditable( name, offline );
  943. }
  944. if ( !offline ) {
  945. if ( this.element.data( 'cke-widget-keep-attr' ) == '0' )
  946. this.element.removeAttribute( 'data-widget' );
  947. this.element.removeAttributes( [ 'data-cke-widget-data', 'data-cke-widget-keep-attr' ] );
  948. this.element.removeClass( 'cke_widget_element' );
  949. this.element.replace( this.wrapper );
  950. }
  951. this.wrapper = null;
  952. },
  953. /**
  954. * Destroys a nested editable and all nested widgets.
  955. *
  956. * @param {String} editableName Nested editable name.
  957. * @param {Boolean} [offline] See {@link #method-destroy} method.
  958. */
  959. destroyEditable: function( editableName, offline ) {
  960. var editable = this.editables[ editableName ];
  961. editable.removeListener( 'focus', onEditableFocus );
  962. editable.removeListener( 'blur', onEditableBlur );
  963. this.editor.focusManager.remove( editable );
  964. if ( !offline ) {
  965. this.repository.destroyAll( false, editable );
  966. editable.removeClass( 'cke_widget_editable' );
  967. editable.removeClass( 'cke_widget_editable_focused' );
  968. editable.removeAttributes( [ 'contenteditable', 'data-cke-widget-editable', 'data-cke-enter-mode' ] );
  969. }
  970. delete this.editables[ editableName ];
  971. },
  972. /**
  973. * Starts widget editing.
  974. *
  975. * This method fires the {@link CKEDITOR.plugins.widget#event-edit} event
  976. * which may be canceled in order to prevent it from opening a dialog window.
  977. *
  978. * The dialog window name is obtained from the event's data `dialog` property or
  979. * from {@link CKEDITOR.plugins.widget.definition#dialog}.
  980. *
  981. * @returns {Boolean} Returns `true` if a dialog window was opened.
  982. */
  983. edit: function() {
  984. var evtData = { dialog: this.dialog },
  985. that = this;
  986. // Edit event was blocked or there's no dialog to be automatically opened.
  987. if ( this.fire( 'edit', evtData ) === false || !evtData.dialog )
  988. return false;
  989. this.editor.openDialog( evtData.dialog, function( dialog ) {
  990. var showListener,
  991. okListener;
  992. // Allow to add a custom dialog handler.
  993. if ( that.fire( 'dialog', dialog ) === false )
  994. return;
  995. showListener = dialog.on( 'show', function() {
  996. dialog.setupContent( that );
  997. } );
  998. okListener = dialog.on( 'ok', function() {
  999. // Commit dialog's fields, but prevent from
  1000. // firing data event for every field. Fire only one,
  1001. // bulk event at the end.
  1002. var dataChanged,
  1003. dataListener = that.on( 'data', function( evt ) {
  1004. dataChanged = 1;
  1005. evt.cancel();
  1006. }, null, null, 0 );
  1007. // Create snapshot preceeding snapshot with changed widget...
  1008. // TODO it should not be required, but it is and I found similar
  1009. // code in dialog#ok listener in dialog/plugin.js.
  1010. that.editor.fire( 'saveSnapshot' );
  1011. dialog.commitContent( that );
  1012. dataListener.removeListener();
  1013. if ( dataChanged ) {
  1014. that.fire( 'data', that.data );
  1015. that.editor.fire( 'saveSnapshot' );
  1016. }
  1017. } );
  1018. dialog.once( 'hide', function() {
  1019. showListener.removeListener();
  1020. okListener.removeListener();
  1021. } );
  1022. } );
  1023. return true;
  1024. },
  1025. /**
  1026. * Returns widget element classes parsed to an object. This method
  1027. * is used to populate the `classes` property of widget's {@link #property-data}.
  1028. *
  1029. * This method reuses {@link CKEDITOR.plugins.widget.repository#parseElementClasses}.
  1030. * It should be overriden if a widget should handle classes differently (e.g. on other elements).
  1031. *
  1032. * See also: {@link #removeClass}, {@link #addClass}, {@link #hasClass}.
  1033. *
  1034. * @since 4.4
  1035. * @returns {Object}
  1036. */
  1037. getClasses: function() {
  1038. return this.repository.parseElementClasses( this.element.getAttribute( 'class' ) );
  1039. },
  1040. /**
  1041. * Checks if the widget element has specified class. This method is used by
  1042. * the {@link #checkStyleActive} method and should be overriden by widgets
  1043. * which should handle classes differently (e.g. on other elements).
  1044. *
  1045. * See also: {@link #removeClass}, {@link #addClass}, {@link #getClasses}.
  1046. *
  1047. * @since 4.4
  1048. * @param {String} className The class to be checked.
  1049. * @param {Boolean} Whether a widget has specified class.
  1050. */
  1051. hasClass: function( className ) {
  1052. return this.element.hasClass( className );
  1053. },
  1054. /**
  1055. * Initializes a nested editable.
  1056. *
  1057. * **Note**: Only elements from {@link CKEDITOR.dtd#$editable} may become editables.
  1058. *
  1059. * @param {String} editableName The nested editable name.
  1060. * @param {CKEDITOR.plugins.widget.nestedEditable.definition} definition The definition of the nested editable.
  1061. * @returns {Boolean} Whether an editable was successfully initialized.
  1062. */
  1063. initEditable: function( editableName, definition ) {
  1064. // Don't fetch just first element which matched selector but look for a correct one. (#13334)
  1065. var editable = this._findOneNotNested( definition.selector );
  1066. if ( editable && editable.is( CKEDITOR.dtd.$editable ) ) {
  1067. editable = new NestedEditable( this.editor, editable, {
  1068. filter: createEditableFilter.call( this.repository, this.name, editableName, definition )
  1069. } );
  1070. this.editables[ editableName ] = editable;
  1071. editable.setAttributes( {
  1072. contenteditable: 'true',
  1073. 'data-cke-widget-editable': editableName,
  1074. 'data-cke-enter-mode': editable.enterMode
  1075. } );
  1076. if ( editable.filter )
  1077. editable.data( 'cke-filter', editable.filter.id );
  1078. editable.addClass( 'cke_widget_editable' );
  1079. // This class may be left when d&ding widget which
  1080. // had focused editable. Clean this class here, not in
  1081. // cleanUpWidgetElement for performance and code size reasons.
  1082. editable.removeClass( 'cke_widget_editable_focused' );
  1083. if ( definition.pathName )
  1084. editable.data( 'cke-display-name', definition.pathName );
  1085. this.editor.focusManager.add( editable );
  1086. editable.on( 'focus', onEditableFocus, this );
  1087. CKEDITOR.env.ie && editable.on( 'blur', onEditableBlur, this );
  1088. // Finally, process editable's data. This data wasn't processed when loading
  1089. // editor's data, becuase they need to be processed separately, with its own filters and settings.
  1090. editable._.initialSetData = true;
  1091. editable.setData( editable.getHtml() );
  1092. return true;
  1093. }
  1094. return false;
  1095. },
  1096. /**
  1097. * Looks inside wrapper element to find a node that
  1098. * matches given selector and is not nested in other widget. (#13334)
  1099. *
  1100. * @since 4.5
  1101. * @private
  1102. * @param {String} selector Selector to match.
  1103. * @returns {CKEDITOR.dom.element} Matched element or `null` if a node has not been found.
  1104. */
  1105. _findOneNotNested: function( selector ) {
  1106. var matchedElements = this.wrapper.find( selector ),
  1107. match,
  1108. closestWrapper;
  1109. for ( var i = 0; i < matchedElements.count(); i++ ) {
  1110. match = matchedElements.getItem( i );
  1111. closestWrapper = match.getAscendant( Widget.isDomWidgetWrapper );
  1112. // The closest ascendant-wrapper of this match defines to which widget
  1113. // this match belongs. If the ascendant is this widget's wrapper
  1114. // it means that the match is not nested in other widget.
  1115. if ( this.wrapper.equals( closestWrapper ) ) {
  1116. return match;
  1117. }
  1118. }
  1119. return null;
  1120. },
  1121. /**
  1122. * Checks if a widget has already been initialized and has not been destroyed yet.
  1123. *
  1124. * See {@link #inited} for more details.
  1125. *
  1126. * @returns {Boolean}
  1127. */
  1128. isInited: function() {
  1129. return !!( this.wrapper && this.inited );
  1130. },
  1131. /**
  1132. * Checks if a widget is ready and has not been destroyed yet.
  1133. *
  1134. * See {@link #property-ready} for more details.
  1135. *
  1136. * @returns {Boolean}
  1137. */
  1138. isReady: function() {
  1139. return this.isInited() && this.ready;
  1140. },
  1141. /**
  1142. * Focuses a widget by selecting it.
  1143. */
  1144. focus: function() {
  1145. var sel = this.editor.getSelection();
  1146. // Fake the selection before focusing editor, to avoid unpreventable viewports scrolling
  1147. // on Webkit/Blink/IE which is done because there's no selection or selection was somewhere else than widget.
  1148. if ( sel ) {
  1149. var isDirty = this.editor.checkDirty();
  1150. sel.fake( this.wrapper );
  1151. !isDirty && this.editor.resetDirty();
  1152. }
  1153. // Always focus editor (not only when focusManger.hasFocus is false) (because of #10483).
  1154. this.editor.focus();
  1155. },
  1156. /**
  1157. * Removes a class from the widget element. This method is used by
  1158. * the {@link #removeStyle} method and should be overriden by widgets
  1159. * which should handle classes differently (e.g. on other elements).
  1160. *
  1161. * **Note**: This method should not be used directly. Use the {@link #setData} method to
  1162. * set the `classes` property. Read more in the {@link #setData} documentation.
  1163. *
  1164. * See also: {@link #hasClass}, {@link #addClass}.
  1165. *
  1166. * @since 4.4
  1167. * @param {String} className The class to be removed.
  1168. */
  1169. removeClass: function( className ) {
  1170. this.element.removeClass( className );
  1171. this.wrapper.removeClass( Widget.WRAPPER_CLASS_PREFIX + className );
  1172. },
  1173. /**
  1174. * Removes the specified style from the widget. It is highly recommended to use the
  1175. * {@link CKEDITOR.editor#removeStyle} or {@link CKEDITOR.style#remove} methods instead of
  1176. * using this method directly, because unlike editor's and style's methods, this one
  1177. * does not perform any checks.
  1178. *
  1179. * Read more about how applying/removing styles works in the {@link #applyStyle} method documentation.
  1180. *
  1181. * See also {@link #checkStyleActive}, {@link #applyStyle}, {@link #getClasses}.
  1182. *
  1183. * @since 4.4
  1184. * @param {CKEDITOR.style} style The custom widget style to be removed.
  1185. */
  1186. removeStyle: function( style ) {
  1187. applyRemoveStyle( this, style, 0 );
  1188. },
  1189. /**
  1190. * Sets widget value(s) in the {@link #property-data} object.
  1191. * If the given value(s) modifies current ones, the {@link #event-data} event is fired.
  1192. *
  1193. * this.setData( 'align', 'left' );
  1194. * this.data.align; // -> 'left'
  1195. *
  1196. * this.setData( { align: 'right', opened: false } );
  1197. * this.data.align; // -> 'right'
  1198. * this.data.opened; // -> false
  1199. *
  1200. * Set values are stored in {@link #element}'s attribute (`data-cke-widget-data`),
  1201. * in a JSON string, therefore {@link #property-data} should contain
  1202. * only serializable data.
  1203. *
  1204. * **Note:** A special data property, `classes`, exists. It contains an object with
  1205. * classes which were returned by the {@link #getClasses} method during the widget initialization.
  1206. * This property is then used by the {@link #applyStyle} and {@link #removeStyle} methods.
  1207. * When it is changed (the reference to object must be changed!), the widget updates its classes by
  1208. * using the {@link #addClass} and {@link #removeClass} methods.
  1209. *
  1210. * // Adding a new class.
  1211. * var classes = CKEDITOR.tools.clone( widget.data.classes );
  1212. * classes.newClass = 1;
  1213. * widget.setData( 'classes', classes );
  1214. *
  1215. * // Removing a class.
  1216. * var classes = CKEDITOR.tools.clone( widget.data.classes );
  1217. * delete classes.newClass;
  1218. * widget.setData( 'classes', classes );
  1219. *
  1220. * @param {String/Object} keyOrData
  1221. * @param {Object} value
  1222. * @chainable
  1223. */
  1224. setData: function( key, value ) {
  1225. var data = this.data,
  1226. modified = 0;
  1227. if ( typeof key == 'string' ) {
  1228. if ( data[ key ] !== value ) {
  1229. data[ key ] = value;
  1230. modified = 1;
  1231. }
  1232. }
  1233. else {
  1234. var newData = key;
  1235. for ( key in newData ) {
  1236. if ( data[ key ] !== newData[ key ] ) {
  1237. modified = 1;
  1238. data[ key ] = newData[ key ];
  1239. }
  1240. }
  1241. }
  1242. // Block firing data event and overwriting data element before setupWidgetData is executed.
  1243. if ( modified && this.dataReady ) {
  1244. writeDataToElement( this );
  1245. this.fire( 'data', data );
  1246. }
  1247. return this;
  1248. },
  1249. /**
  1250. * Changes the widget's focus state. This method is executed automatically after
  1251. * a widget was focused by the {@link #method-focus} method or the selection was moved
  1252. * out of the widget.
  1253. *
  1254. * This is a low-level method which is not integrated with e.g. the undo manager.
  1255. * Use the {@link #method-focus} method instead.
  1256. *
  1257. * @param {Boolean} selected Whether to select or deselect this widget.
  1258. * @chainable
  1259. */
  1260. setFocused: function( focused ) {
  1261. this.wrapper[ focused ? 'addClass' : 'removeClass' ]( 'cke_widget_focused' );
  1262. this.fire( focused ? 'focus' : 'blur' );
  1263. return this;
  1264. },
  1265. /**
  1266. * Changes the widget's select state. This method is executed automatically after
  1267. * a widget was selected by the {@link #method-focus} method or the selection
  1268. * was moved out of the widget.
  1269. *
  1270. * This is a low-level method which is not integrated with e.g. the undo manager.
  1271. * Use the {@link #method-focus} method instead or simply change the selection.
  1272. *
  1273. * @param {Boolean} selected Whether to select or deselect this widget.
  1274. * @chainable
  1275. */
  1276. setSelected: function( selected ) {
  1277. this.wrapper[ selected ? 'addClass' : 'removeClass' ]( 'cke_widget_selected' );
  1278. this.fire( selected ? 'select' : 'deselect' );
  1279. return this;
  1280. },
  1281. /**
  1282. * Repositions drag handler according to the widget's element position. Should be called from events, like mouseover.
  1283. */
  1284. updateDragHandlerPosition: function() {
  1285. var editor = this.editor,
  1286. domElement = this.element.$,
  1287. oldPos = this._.dragHandlerOffset,
  1288. newPos = {
  1289. x: domElement.offsetLeft,
  1290. y: domElement.offsetTop - DRAG_HANDLER_SIZE
  1291. };
  1292. if ( oldPos && newPos.x == oldPos.x && newPos.y == oldPos.y )
  1293. return;
  1294. // We need to make sure that dirty state is not changed (#11487).
  1295. var initialDirty = editor.checkDirty();
  1296. editor.fire( 'lockSnapshot' );
  1297. this.dragHandlerContainer.setStyles( {
  1298. top: newPos.y + 'px',
  1299. left: newPos.x + 'px',
  1300. display: 'block'
  1301. } );
  1302. editor.fire( 'unlockSnapshot' );
  1303. !initialDirty && editor.resetDirty();
  1304. this._.dragHandlerOffset = newPos;
  1305. }
  1306. };
  1307. CKEDITOR.event.implementOn( Widget.prototype );
  1308. /**
  1309. * Gets the {@link #isDomNestedEditable nested editable}
  1310. * (returned as a {@link CKEDITOR.dom.element}, not as a {@link CKEDITOR.plugins.widget.nestedEditable})
  1311. * closest to the `node` or the `node` if it is a nested editable itself.
  1312. *
  1313. * @since 4.5
  1314. * @static
  1315. * @param {CKEDITOR.dom.element} guard Stop ancestor search on this node (usually editor's editable).
  1316. * @param {CKEDITOR.dom.node} node Start the search from this node.
  1317. * @returns {CKEDITOR.dom.element/null} Element or `null` if not found.
  1318. */
  1319. Widget.getNestedEditable = function( guard, node ) {
  1320. if ( !node || node.equals( guard ) )
  1321. return null;
  1322. if ( Widget.isDomNestedEditable( node ) )
  1323. return node;
  1324. return Widget.getNestedEditable( guard, node.getParent() );
  1325. };
  1326. /**
  1327. * Checks whether the `node` is a widget's drag handle element.
  1328. *
  1329. * @since 4.5
  1330. * @static
  1331. * @param {CKEDITOR.dom.node} node
  1332. * @returns {Boolean}
  1333. */
  1334. Widget.isDomDragHandler = function( node ) {
  1335. return node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'data-cke-widget-drag-handler' );
  1336. };
  1337. /**
  1338. * Checks whether the `node` is a container of the widget's drag handle element.
  1339. *
  1340. * @since 4.5
  1341. * @static
  1342. * @param {CKEDITOR.dom.node} node
  1343. * @returns {Boolean}
  1344. */
  1345. Widget.isDomDragHandlerContainer = function( node ) {
  1346. return node.type == CKEDITOR.NODE_ELEMENT && node.hasClass( 'cke_widget_drag_handler_container' );
  1347. };
  1348. /**
  1349. * Checks whether the `node` is a {@link CKEDITOR.plugins.widget#editables nested editable}.
  1350. * Note that this function only checks whether it is the right element, not whether
  1351. * the passed `node` is an instance of {@link CKEDITOR.plugins.widget.nestedEditable}.
  1352. *
  1353. * @since 4.5
  1354. * @static
  1355. * @param {CKEDITOR.dom.node} node
  1356. * @returns {Boolean}
  1357. */
  1358. Widget.isDomNestedEditable = function( node ) {
  1359. return node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'data-cke-widget-editable' );
  1360. };
  1361. /**
  1362. * Checks whether the `node` is a {@link CKEDITOR.plugins.widget#element widget element}.
  1363. *
  1364. * @since 4.5
  1365. * @static
  1366. * @param {CKEDITOR.dom.node} node
  1367. * @returns {Boolean}
  1368. */
  1369. Widget.isDomWidgetElement = function( node ) {
  1370. return node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'data-widget' );
  1371. };
  1372. /**
  1373. * Checks whether the `node` is a {@link CKEDITOR.plugins.widget#wrapper widget wrapper}.
  1374. *
  1375. * @since 4.5
  1376. * @static
  1377. * @param {CKEDITOR.dom.element} node
  1378. * @returns {Boolean}
  1379. */
  1380. Widget.isDomWidgetWrapper = function( node ) {
  1381. return node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'data-cke-widget-wrapper' );
  1382. };
  1383. /**
  1384. * Checks whether the `node` is a {@link CKEDITOR.plugins.widget#element widget element}.
  1385. *
  1386. * @since 4.5
  1387. * @static
  1388. * @param {CKEDITOR.htmlParser.node} node
  1389. * @returns {Boolean}
  1390. */
  1391. Widget.isParserWidgetElement = function( node ) {
  1392. return node.type == CKEDITOR.NODE_ELEMENT && !!node.attributes[ 'data-widget' ];
  1393. };
  1394. /**
  1395. * Checks whether the `node` is a {@link CKEDITOR.plugins.widget#wrapper widget wrapper}.
  1396. *
  1397. * @since 4.5
  1398. * @static
  1399. * @param {CKEDITOR.htmlParser.element} node
  1400. * @returns {Boolean}
  1401. */
  1402. Widget.isParserWidgetWrapper = function( node ) {
  1403. return node.type == CKEDITOR.NODE_ELEMENT && !!node.attributes[ 'data-cke-widget-wrapper' ];
  1404. };
  1405. /**
  1406. * Prefix added to wrapper classes. Each class added to the widget element by the {@link #addClass}
  1407. * method will also be added to the wrapper prefixed with it.
  1408. *
  1409. * @since 4.6.0
  1410. * @static
  1411. * @readonly
  1412. * @property {String} [='cke_widget_wrapper_']
  1413. */
  1414. Widget.WRAPPER_CLASS_PREFIX = 'cke_widget_wrapper_';
  1415. /**
  1416. * An event fired when a widget is ready (fully initialized). This event is fired after:
  1417. *
  1418. * * {@link #init} is called,
  1419. * * The first {@link #event-data} event is fired,
  1420. * * A widget is attached to the document.
  1421. *
  1422. * Therefore, in case of widget creation with a command which opens a dialog window, this event
  1423. * will be delayed after the dialog window is closed and the widget is finally inserted into the document.
  1424. *
  1425. * **Note**: If your widget does not use automatic dialog window binding (i.e. you open the dialog window manually)
  1426. * or another situation in which the widget wrapper is not attached To Document at the time when it is
  1427. * initialized occurs, you need to take care of firing {@link #event-ready} yourself.
  1428. *
  1429. * See also {@link #property-ready} and {@link #property-inited} properties, and
  1430. * {@link #isReady} and {@link #isInited} methods.
  1431. *
  1432. * @event ready
  1433. */
  1434. /**
  1435. * An event fired when a widget is about to be destroyed, but before it is
  1436. * fully torn down.
  1437. *
  1438. * @event destroy
  1439. */
  1440. /**
  1441. * An event fired when a widget is focused.
  1442. *
  1443. * Widget can be focused by executing {@link #method-focus}.
  1444. *
  1445. * @event focus
  1446. */
  1447. /**
  1448. * An event fired when a widget is blurred.
  1449. *
  1450. * @event blur
  1451. */
  1452. /**
  1453. * An event fired when a widget is selected.
  1454. *
  1455. * @event select
  1456. */
  1457. /**
  1458. * An event fired when a widget is deselected.
  1459. *
  1460. * @event deselect
  1461. */
  1462. /**
  1463. * An event fired by the {@link #method-edit} method. It can be canceled
  1464. * in order to stop the default action (opening a dialog window and/or
  1465. * {@link CKEDITOR.plugins.widget.repository#finalizeCreation finalizing widget creation}).
  1466. *
  1467. * @event edit
  1468. * @param data
  1469. * @param {String} data.dialog Defaults to {@link CKEDITOR.plugins.widget.definition#dialog}
  1470. * and can be changed or set by the listener.
  1471. */
  1472. /**
  1473. * An event fired when a dialog window for widget editing is opened.
  1474. * This event can be canceled in order to handle the editing dialog in a custom manner.
  1475. *
  1476. * @event dialog
  1477. * @param {CKEDITOR.dialog} data The opened dialog window instance.
  1478. */
  1479. /**
  1480. * An event fired when a key is pressed on a focused widget.
  1481. * This event is forwarded from the {@link CKEDITOR.editor#key} event and
  1482. * has the ability to block editor keystrokes if it is canceled.
  1483. *
  1484. * @event key
  1485. * @param data
  1486. * @param {Number} data.keyCode A number representing the key code (or combination).
  1487. */
  1488. /**
  1489. * An event fired when a widget is double clicked.
  1490. *
  1491. * **Note:** If a default editing action is executed on double click (i.e. a widget has a
  1492. * {@link CKEDITOR.plugins.widget.definition#dialog dialog} defined and the {@link #event-doubleclick} event was not
  1493. * canceled), this event will be automatically canceled, so a listener added with the default priority (10)
  1494. * will not be executed. Use a listener with low priority (e.g. 5) to be sure that it will be executed.
  1495. *
  1496. * widget.on( 'doubleclick', function( evt ) {
  1497. * console.log( 'widget#doubleclick' );
  1498. * }, null, null, 5 );
  1499. *
  1500. * If your widget handles double click in a special way (so the default editing action is not executed),
  1501. * make sure you cancel this event, because otherwise it will be propagated to {@link CKEDITOR.editor#doubleclick}
  1502. * and another feature may step in (e.g. a Link dialog window may be opened if your widget was inside a link).
  1503. *
  1504. * @event doubleclick
  1505. * @param data
  1506. * @param {CKEDITOR.dom.element} data.element The double-clicked element.
  1507. */
  1508. /**
  1509. * An event fired when the context menu is opened for a widget.
  1510. *
  1511. * @event contextMenu
  1512. * @param data The object containing context menu options to be added
  1513. * for this widget. See {@link CKEDITOR.plugins.contextMenu#addListener}.
  1514. */
  1515. /**
  1516. * An event fired when the widget data changed. See the {@link #setData} method and the {@link #property-data} property.
  1517. *
  1518. * @event data
  1519. */
  1520. /**
  1521. * The wrapper class for editable elements inside widgets.
  1522. *
  1523. * Do not use directly. Use {@link CKEDITOR.plugins.widget.definition#editables} or
  1524. * {@link CKEDITOR.plugins.widget#initEditable}.
  1525. *
  1526. * @class CKEDITOR.plugins.widget.nestedEditable
  1527. * @extends CKEDITOR.dom.element
  1528. * @constructor
  1529. * @param {CKEDITOR.editor} editor
  1530. * @param {CKEDITOR.dom.element} element
  1531. * @param config
  1532. * @param {CKEDITOR.filter} [config.filter]
  1533. */
  1534. function NestedEditable( editor, element, config ) {
  1535. // Call the base constructor.
  1536. CKEDITOR.dom.element.call( this, element.$ );
  1537. this.editor = editor;
  1538. this._ = {};
  1539. var filter = this.filter = config.filter;
  1540. // If blockless editable - always use BR mode.
  1541. if ( !CKEDITOR.dtd[ this.getName() ].p )
  1542. this.enterMode = this.shiftEnterMode = CKEDITOR.ENTER_BR;
  1543. else {
  1544. this.enterMode = filter ? filter.getAllowedEnterMode( editor.enterMode ) : editor.enterMode;
  1545. this.shiftEnterMode = filter ? filter.getAllowedEnterMode( editor.shiftEnterMode, true ) : editor.shiftEnterMode;
  1546. }
  1547. }
  1548. NestedEditable.prototype = CKEDITOR.tools.extend( CKEDITOR.tools.prototypedCopy( CKEDITOR.dom.element.prototype ), {
  1549. /**
  1550. * Sets the editable data. The data will be passed through the {@link CKEDITOR.editor#dataProcessor}
  1551. * and the {@link CKEDITOR.editor#filter}. This ensures that the data was filtered and prepared to be
  1552. * edited like the {@link CKEDITOR.editor#method-setData editor data}.
  1553. *
  1554. * Before content is changed, all nested widgets are destroyed. Afterwards, after new content is loaded,
  1555. * all nested widgets are initialized.
  1556. *
  1557. * @param {String} data
  1558. */
  1559. setData: function( data ) {
  1560. // For performance reasons don't call destroyAll when initializing a nested editable,
  1561. // because there are no widgets inside.
  1562. if ( !this._.initialSetData ) {
  1563. // Destroy all nested widgets before setting data.
  1564. this.editor.widgets.destroyAll( false, this );
  1565. }
  1566. this._.initialSetData = false;
  1567. data = this.editor.dataProcessor.toHtml( data, {
  1568. context: this.getName(),
  1569. filter: this.filter,
  1570. enterMode: this.enterMode
  1571. } );
  1572. this.setHtml( data );
  1573. this.editor.widgets.initOnAll( this );
  1574. },
  1575. /**
  1576. * Gets the editable data. Like {@link #setData}, this method will process and filter the data.
  1577. *
  1578. * @returns {String}
  1579. */
  1580. getData: function() {
  1581. return this.editor.dataProcessor.toDataFormat( this.getHtml(), {
  1582. context: this.getName(),
  1583. filter: this.filter,
  1584. enterMode: this.enterMode
  1585. } );
  1586. }
  1587. } );
  1588. /**
  1589. * The editor instance.
  1590. *
  1591. * @readonly
  1592. * @property {CKEDITOR.editor} editor
  1593. */
  1594. /**
  1595. * The filter instance if allowed content rules were defined.
  1596. *
  1597. * @readonly
  1598. * @property {CKEDITOR.filter} filter
  1599. */
  1600. /**
  1601. * The enter mode active in this editable.
  1602. * It is determined from editable's name (whether it is a blockless editable),
  1603. * its allowed content rules (if defined) and the default editor's mode.
  1604. *
  1605. * @readonly
  1606. * @property {Number} enterMode
  1607. */
  1608. /**
  1609. * The shift enter move active in this editable.
  1610. *
  1611. * @readonly
  1612. * @property {Number} shiftEnterMode
  1613. */
  1614. //
  1615. // REPOSITORY helpers -----------------------------------------------------
  1616. //
  1617. function addWidgetButtons( editor ) {
  1618. var widgets = editor.widgets.registered,
  1619. widget,
  1620. widgetName,
  1621. widgetButton;
  1622. for ( widgetName in widgets ) {
  1623. widget = widgets[ widgetName ];
  1624. // Create button if defined.
  1625. widgetButton = widget.button;
  1626. if ( widgetButton && editor.ui.addButton ) {
  1627. editor.ui.addButton( CKEDITOR.tools.capitalize( widget.name, true ), {
  1628. label: widgetButton,
  1629. command: widget.name,
  1630. toolbar: 'insert,10'
  1631. } );
  1632. }
  1633. }
  1634. }
  1635. // Create a command creating and editing widget.
  1636. //
  1637. // @param editor
  1638. // @param {CKEDITOR.plugins.widget.definition} widgetDef
  1639. function addWidgetCommand( editor, widgetDef ) {
  1640. editor.addCommand( widgetDef.name, {
  1641. exec: function( editor, commandData ) {
  1642. var focused = editor.widgets.focused;
  1643. // If a widget of the same type is focused, start editing.
  1644. if ( focused && focused.name == widgetDef.name )
  1645. focused.edit();
  1646. // Otherwise...
  1647. // ... use insert method is was defined.
  1648. else if ( widgetDef.insert )
  1649. widgetDef.insert();
  1650. // ... or create a brand-new widget from template.
  1651. else if ( widgetDef.template ) {
  1652. var defaults = typeof widgetDef.defaults == 'function' ? widgetDef.defaults() : widgetDef.defaults,
  1653. element = CKEDITOR.dom.element.createFromHtml( widgetDef.template.output( defaults ) ),
  1654. instance,
  1655. wrapper = editor.widgets.wrapElement( element, widgetDef.name ),
  1656. temp = new CKEDITOR.dom.documentFragment( wrapper.getDocument() );
  1657. // Append wrapper to a temporary document. This will unify the environment
  1658. // in which #data listeners work when creating and editing widget.
  1659. temp.append( wrapper );
  1660. instance = editor.widgets.initOn( element, widgetDef, commandData && commandData.startupData );
  1661. // Instance could be destroyed during initialization.
  1662. // In this case finalize creation if some new widget
  1663. // was left in temporary document fragment.
  1664. if ( !instance ) {
  1665. finalizeCreation();
  1666. return;
  1667. }
  1668. // Listen on edit to finalize widget insertion.
  1669. //
  1670. // * If dialog was set, then insert widget after dialog was successfully saved or destroy this
  1671. // temporary instance.
  1672. // * If dialog wasn't set and edit wasn't canceled, insert widget.
  1673. var editListener = instance.once( 'edit', function( evt ) {
  1674. if ( evt.data.dialog ) {
  1675. instance.once( 'dialog', function( evt ) {
  1676. var dialog = evt.data,
  1677. okListener,
  1678. cancelListener;
  1679. // Finalize creation AFTER (20) new data was set.
  1680. okListener = dialog.once( 'ok', finalizeCreation, null, null, 20 );
  1681. cancelListener = dialog.once( 'cancel', function( evt ) {
  1682. if ( !( evt.data && evt.data.hide === false ) ) {
  1683. editor.widgets.destroy( instance, true );
  1684. }
  1685. } );
  1686. dialog.once( 'hide', function() {
  1687. okListener.removeListener();
  1688. cancelListener.removeListener();
  1689. } );
  1690. } );
  1691. } else {
  1692. // Dialog hasn't been set, so insert widget now.
  1693. finalizeCreation();
  1694. }
  1695. }, null, null, 999 );
  1696. instance.edit();
  1697. // Remove listener in case someone canceled it before this
  1698. // listener was executed.
  1699. editListener.removeListener();
  1700. }
  1701. function finalizeCreation() {
  1702. editor.widgets.finalizeCreation( temp );
  1703. }
  1704. },
  1705. allowedContent: widgetDef.allowedContent,
  1706. requiredContent: widgetDef.requiredContent,
  1707. contentForms: widgetDef.contentForms,
  1708. contentTransformations: widgetDef.contentTransformations
  1709. } );
  1710. }
  1711. function addWidgetProcessors( widgetsRepo, widgetDef ) {
  1712. var upcast = widgetDef.upcast,
  1713. upcasts,
  1714. priority = widgetDef.upcastPriority || 10;
  1715. if ( !upcast )
  1716. return;
  1717. // Multiple upcasts defined in string.
  1718. if ( typeof upcast == 'string' ) {
  1719. upcasts = upcast.split( ',' );
  1720. while ( upcasts.length ) {
  1721. addUpcast( widgetDef.upcasts[ upcasts.pop() ], widgetDef.name, priority );
  1722. }
  1723. }
  1724. // Single rule which is automatically activated.
  1725. else {
  1726. addUpcast( upcast, widgetDef.name, priority );
  1727. }
  1728. function addUpcast( upcast, name, priority ) {
  1729. // Find index of the first higher (in terms of value) priority upcast.
  1730. var index = CKEDITOR.tools.getIndex( widgetsRepo._.upcasts, function( element ) {
  1731. return element[ 2 ] > priority;
  1732. } );
  1733. // Add at the end if it is the highest priority so far.
  1734. if ( index < 0 ) {
  1735. index = widgetsRepo._.upcasts.length;
  1736. }
  1737. widgetsRepo._.upcasts.splice( index, 0, [ upcast, name, priority ] );
  1738. }
  1739. }
  1740. function blurWidget( widgetsRepo, widget ) {
  1741. widgetsRepo.focused = null;
  1742. if ( widget.isInited() ) {
  1743. var isDirty = widget.editor.checkDirty();
  1744. // Widget could be destroyed in the meantime - e.g. data could be set.
  1745. widgetsRepo.fire( 'widgetBlurred', { widget: widget } );
  1746. widget.setFocused( false );
  1747. !isDirty && widget.editor.resetDirty();
  1748. }
  1749. }
  1750. function checkWidgets( evt ) {
  1751. var options = evt.data;
  1752. if ( this.editor.mode != 'wysiwyg' )
  1753. return;
  1754. var editable = this.editor.editable(),
  1755. instances = this.instances,
  1756. newInstances, i, count, wrapper, notYetInitialized;
  1757. if ( !editable )
  1758. return;
  1759. // Remove widgets which have no corresponding elements in DOM.
  1760. for ( i in instances ) {
  1761. // #13410 Remove widgets that are ready. This prevents from destroying widgets that are during loading process.
  1762. if ( instances[ i ].isReady() && !editable.contains( instances[ i ].wrapper ) )
  1763. this.destroy( instances[ i ], true );
  1764. }
  1765. // Init on all (new) if initOnlyNew option was passed.
  1766. if ( options && options.initOnlyNew )
  1767. newInstances = this.initOnAll();
  1768. else {
  1769. var wrappers = editable.find( '.cke_widget_wrapper' );
  1770. newInstances = [];
  1771. // Create widgets on existing wrappers if they do not exists.
  1772. for ( i = 0, count = wrappers.count(); i < count; i++ ) {
  1773. wrapper = wrappers.getItem( i );
  1774. notYetInitialized = !this.getByElement( wrapper, true );
  1775. // Check if:
  1776. // * there's no instance for this widget
  1777. // * wrapper is not inside some temporary element like copybin (#11088)
  1778. // * it was a nested widget's wrapper which has been detached from DOM,
  1779. // when nested editable has been initialized (it overwrites its innerHTML
  1780. // and initializes nested widgets).
  1781. if ( notYetInitialized && !findParent( wrapper, isDomTemp ) && editable.contains( wrapper ) ) {
  1782. // Add cke_widget_new class because otherwise
  1783. // widget will not be created on such wrapper.
  1784. wrapper.addClass( 'cke_widget_new' );
  1785. newInstances.push( this.initOn( wrapper.getFirst( Widget.isDomWidgetElement ) ) );
  1786. }
  1787. }
  1788. }
  1789. // If only single widget was initialized and focusInited was passed, focus it.
  1790. if ( options && options.focusInited && newInstances.length == 1 )
  1791. newInstances[ 0 ].focus();
  1792. }
  1793. // Unwraps widget element and clean up element.
  1794. //
  1795. // This function is used to clean up pasted widgets.
  1796. // It should have similar result to widget#destroy plus
  1797. // some additional adjustments, specific for pasting.
  1798. //
  1799. // @param {CKEDITOR.htmlParser.element} el
  1800. function cleanUpWidgetElement( el ) {
  1801. var parent = el.parent;
  1802. if ( parent.type == CKEDITOR.NODE_ELEMENT && parent.attributes[ 'data-cke-widget-wrapper' ] )
  1803. parent.replaceWith( el );
  1804. }
  1805. // Similar to cleanUpWidgetElement, but works on DOM and finds
  1806. // widget elements by its own.
  1807. //
  1808. // Unlike cleanUpWidgetElement it will wrap element back.
  1809. //
  1810. // @param {CKEDITOR.dom.element} container
  1811. function cleanUpAllWidgetElements( widgetsRepo, container ) {
  1812. var wrappers = container.find( '.cke_widget_wrapper' ),
  1813. wrapper, element,
  1814. i = 0,
  1815. l = wrappers.count();
  1816. for ( ; i < l; ++i ) {
  1817. wrapper = wrappers.getItem( i );
  1818. element = wrapper.getFirst( Widget.isDomWidgetElement );
  1819. // If wrapper contains widget element - unwrap it and wrap again.
  1820. if ( element.type == CKEDITOR.NODE_ELEMENT && element.data( 'widget' ) ) {
  1821. element.replace( wrapper );
  1822. widgetsRepo.wrapElement( element );
  1823. } else {
  1824. // Otherwise - something is wrong... clean this up.
  1825. wrapper.remove();
  1826. }
  1827. }
  1828. }
  1829. // Creates {@link CKEDITOR.filter} instance for given widget, editable and rules.
  1830. //
  1831. // Once filter for widget-editable pair is created it is cached, so the same instance
  1832. // will be returned when method is executed again.
  1833. //
  1834. // @param {String} widgetName
  1835. // @param {String} editableName
  1836. // @param {CKEDITOR.plugins.widget.nestedEditableDefinition} editableDefinition The nested editable definition.
  1837. // @returns {CKEDITOR.filter} Filter instance or `null` if rules are not defined.
  1838. // @context CKEDITOR.plugins.widget.repository
  1839. function createEditableFilter( widgetName, editableName, editableDefinition ) {
  1840. if ( !editableDefinition.allowedContent )
  1841. return null;
  1842. var editables = this._.filters[ widgetName ];
  1843. if ( !editables )
  1844. this._.filters[ widgetName ] = editables = {};
  1845. var filter = editables[ editableName ];
  1846. if ( !filter )
  1847. editables[ editableName ] = filter = new CKEDITOR.filter( editableDefinition.allowedContent );
  1848. return filter;
  1849. }
  1850. // Creates an iterator function which when executed on all
  1851. // elements in DOM tree will gather elements that should be wrapped
  1852. // and initialized as widgets.
  1853. function createUpcastIterator( widgetsRepo ) {
  1854. var toBeWrapped = [],
  1855. upcasts = widgetsRepo._.upcasts,
  1856. upcastCallbacks = widgetsRepo._.upcastCallbacks;
  1857. return {
  1858. toBeWrapped: toBeWrapped,
  1859. iterator: function( element ) {
  1860. var upcast, upcasted,
  1861. data,
  1862. i,
  1863. upcastsLength,
  1864. upcastCallbacksLength;
  1865. // Wrapper found - find widget element, add it to be
  1866. // cleaned up (unwrapped) and wrapped and stop iterating in this branch.
  1867. if ( 'data-cke-widget-wrapper' in element.attributes ) {
  1868. element = element.getFirst( Widget.isParserWidgetElement );
  1869. if ( element )
  1870. toBeWrapped.push( [ element ] );
  1871. // Do not iterate over descendants.
  1872. return false;
  1873. }
  1874. // Widget element found - add it to be cleaned up (just in case)
  1875. // and wrapped and stop iterating in this branch.
  1876. else if ( 'data-widget' in element.attributes ) {
  1877. toBeWrapped.push( [ element ] );
  1878. // Do not iterate over descendants.
  1879. return false;
  1880. }
  1881. else if ( ( upcastsLength = upcasts.length ) ) {
  1882. // Ignore elements with data-cke-widget-upcasted to avoid multiple upcasts (#11533).
  1883. // Do not iterate over descendants.
  1884. if ( element.attributes[ 'data-cke-widget-upcasted' ] )
  1885. return false;
  1886. // Check element with upcast callbacks first.
  1887. // If any of them return false abort upcasting.
  1888. for ( i = 0, upcastCallbacksLength = upcastCallbacks.length; i < upcastCallbacksLength; ++i ) {
  1889. if ( upcastCallbacks[ i ]( element ) === false )
  1890. return;
  1891. // Return nothing in order to continue iterating over ascendants.
  1892. // See http://dev.ckeditor.com/ticket/11186#comment:6
  1893. }
  1894. for ( i = 0; i < upcastsLength; ++i ) {
  1895. upcast = upcasts[ i ];
  1896. data = {};
  1897. if ( ( upcasted = upcast[ 0 ]( element, data ) ) ) {
  1898. // If upcast function returned element, upcast this one.
  1899. // It can be e.g. a new element wrapping the original one.
  1900. if ( upcasted instanceof CKEDITOR.htmlParser.element )
  1901. element = upcasted;
  1902. // Set initial data attr with data from upcast method.
  1903. element.attributes[ 'data-cke-widget-data' ] = encodeURIComponent( JSON.stringify( data ) );
  1904. element.attributes[ 'data-cke-widget-upcasted' ] = 1;
  1905. toBeWrapped.push( [ element, upcast[ 1 ] ] );
  1906. // Do not iterate over descendants.
  1907. return false;
  1908. }
  1909. }
  1910. }
  1911. }
  1912. };
  1913. }
  1914. // Finds a first parent that matches query.
  1915. //
  1916. // @param {CKEDITOR.dom.element} element
  1917. // @param {Function} query
  1918. function findParent( element, query ) {
  1919. var parent = element;
  1920. while ( ( parent = parent.getParent() ) ) {
  1921. if ( query( parent ) )
  1922. return true;
  1923. }
  1924. return false;
  1925. }
  1926. function getWrapperAttributes( inlineWidget, name ) {
  1927. return {
  1928. // tabindex="-1" means that it can receive focus by code.
  1929. tabindex: -1,
  1930. contenteditable: 'false',
  1931. 'data-cke-widget-wrapper': 1,
  1932. 'data-cke-filter': 'off',
  1933. // Class cke_widget_new marks widgets which haven't been initialized yet.
  1934. 'class': 'cke_widget_wrapper cke_widget_new cke_widget_' +
  1935. ( inlineWidget ? 'inline' : 'block' ) +
  1936. ( name ? ' cke_widget_' + name : '' )
  1937. };
  1938. }
  1939. // Inserts element at given index.
  1940. // It will check DTD and split ancestor elements up to the first
  1941. // that can contain this element.
  1942. //
  1943. // @param {CKEDITOR.htmlParser.element} parent
  1944. // @param {Number} index
  1945. // @param {CKEDITOR.htmlParser.element} element
  1946. function insertElement( parent, index, element ) {
  1947. // Do not split doc fragment...
  1948. if ( parent.type == CKEDITOR.NODE_ELEMENT ) {
  1949. var parentAllows = CKEDITOR.dtd[ parent.name ];
  1950. // Parent element is known (included in DTD) and cannot contain
  1951. // this element.
  1952. if ( parentAllows && !parentAllows[ element.name ] ) {
  1953. var parent2 = parent.split( index ),
  1954. parentParent = parent.parent;
  1955. // Element will now be inserted at right parent's index.
  1956. index = parent2.getIndex();
  1957. // If left part of split is empty - remove it.
  1958. if ( !parent.children.length ) {
  1959. index -= 1;
  1960. parent.remove();
  1961. }
  1962. // If right part of split is empty - remove it.
  1963. if ( !parent2.children.length )
  1964. parent2.remove();
  1965. // Try inserting as grandpas' children.
  1966. return insertElement( parentParent, index, element );
  1967. }
  1968. }
  1969. // Finally we can add this element.
  1970. parent.add( element, index );
  1971. }
  1972. // Checks whether for the given widget definition and element widget should be created in inline or block mode.
  1973. //
  1974. // See also: {@link CKEDITOR.plugins.widget.definition#inline} and {@link CKEDITOR.plugins.widget#element}.
  1975. //
  1976. // @param {CKEDITOR.plugins.widget.definition} widgetDef The widget definition.
  1977. // @param {String} elementName The name of the widget element.
  1978. // @returns {Boolean}
  1979. function isWidgetInline( widgetDef, elementName ) {
  1980. return typeof widgetDef.inline == 'boolean' ? widgetDef.inline : !!CKEDITOR.dtd.$inline[ elementName ];
  1981. }
  1982. // @param {CKEDITOR.dom.element}
  1983. // @returns {Boolean}
  1984. function isDomTemp( element ) {
  1985. return element.hasAttribute( 'data-cke-temp' );
  1986. }
  1987. function onEditableKey( widget, keyCode ) {
  1988. var focusedEditable = widget.focusedEditable,
  1989. range;
  1990. // CTRL+A.
  1991. if ( keyCode == CKEDITOR.CTRL + 65 ) {
  1992. var bogus = focusedEditable.getBogus();
  1993. range = widget.editor.createRange();
  1994. range.selectNodeContents( focusedEditable );
  1995. // Exclude bogus if exists.
  1996. if ( bogus )
  1997. range.setEndAt( bogus, CKEDITOR.POSITION_BEFORE_START );
  1998. range.select();
  1999. // Cancel event - block default.
  2000. return false;
  2001. }
  2002. // DEL or BACKSPACE.
  2003. else if ( keyCode == 8 || keyCode == 46 ) {
  2004. var ranges = widget.editor.getSelection().getRanges();
  2005. range = ranges[ 0 ];
  2006. // Block del or backspace if at editable's boundary.
  2007. return !( ranges.length == 1 && range.collapsed &&
  2008. range.checkBoundaryOfElement( focusedEditable, CKEDITOR[ keyCode == 8 ? 'START' : 'END' ] ) );
  2009. }
  2010. }
  2011. function setFocusedEditable( widgetsRepo, widget, editableElement, offline ) {
  2012. var editor = widgetsRepo.editor;
  2013. editor.fire( 'lockSnapshot' );
  2014. if ( editableElement ) {
  2015. var editableName = editableElement.data( 'cke-widget-editable' ),
  2016. editableInstance = widget.editables[ editableName ];
  2017. widgetsRepo.widgetHoldingFocusedEditable = widget;
  2018. widget.focusedEditable = editableInstance;
  2019. editableElement.addClass( 'cke_widget_editable_focused' );
  2020. if ( editableInstance.filter )
  2021. editor.setActiveFilter( editableInstance.filter );
  2022. editor.setActiveEnterMode( editableInstance.enterMode, editableInstance.shiftEnterMode );
  2023. } else {
  2024. if ( !offline )
  2025. widget.focusedEditable.removeClass( 'cke_widget_editable_focused' );
  2026. widget.focusedEditable = null;
  2027. widgetsRepo.widgetHoldingFocusedEditable = null;
  2028. editor.setActiveFilter( null );
  2029. editor.setActiveEnterMode( null, null );
  2030. }
  2031. editor.fire( 'unlockSnapshot' );
  2032. }
  2033. function setupContextMenu( editor ) {
  2034. if ( !editor.contextMenu )
  2035. return;
  2036. editor.contextMenu.addListener( function( element ) {
  2037. var widget = editor.widgets.getByElement( element, true );
  2038. if ( widget )
  2039. return widget.fire( 'contextMenu', {} );
  2040. } );
  2041. }
  2042. // And now we've got two problems - original problem and RegExp.
  2043. // Some softeners:
  2044. // * FF tends to copy all blocks up to the copybin container.
  2045. // * IE tends to copy only the copybin, without its container.
  2046. // * We use spans on IE and blockless editors, but divs in other cases.
  2047. var pasteReplaceRegex = new RegExp(
  2048. '^' +
  2049. '(?:<(?:div|span)(?: data-cke-temp="1")?(?: id="cke_copybin")?(?: data-cke-temp="1")?>)?' +
  2050. '(?:<(?:div|span)(?: style="[^"]+")?>)?' +
  2051. '<span [^>]*data-cke-copybin-start="1"[^>]*>.?</span>([\\s\\S]+)<span [^>]*data-cke-copybin-end="1"[^>]*>.?</span>' +
  2052. '(?:</(?:div|span)>)?' +
  2053. '(?:</(?:div|span)>)?' +
  2054. '$',
  2055. // IE8 prefers uppercase when browsers stick to lowercase HTML (#13460).
  2056. 'i'
  2057. );
  2058. function pasteReplaceFn( match, wrapperHtml ) {
  2059. // Avoid polluting pasted data with any whitspaces,
  2060. // what's going to break check whether only one widget was pasted.
  2061. return CKEDITOR.tools.trim( wrapperHtml );
  2062. }
  2063. function setupDragAndDrop( widgetsRepo ) {
  2064. var editor = widgetsRepo.editor,
  2065. lineutils = CKEDITOR.plugins.lineutils;
  2066. // These listeners handle inline and block widgets drag and drop.
  2067. // The only thing we need To Do to make block widgets custom drag and drop functionality
  2068. // is to fire those events with the right properties (like the target which must be the drag handle).
  2069. editor.on( 'dragstart', function( evt ) {
  2070. var target = evt.data.target;
  2071. if ( Widget.isDomDragHandler( target ) ) {
  2072. var widget = widgetsRepo.getByElement( target );
  2073. evt.data.dataTransfer.setData( 'cke/widget-id', widget.id );
  2074. // IE needs focus.
  2075. editor.focus();
  2076. // and widget need to be focused on drag start (#12172#comment:10).
  2077. widget.focus();
  2078. }
  2079. } );
  2080. editor.on( 'drop', function( evt ) {
  2081. var dataTransfer = evt.data.dataTransfer,
  2082. id = dataTransfer.getData( 'cke/widget-id' ),
  2083. transferType = dataTransfer.getTransferType( editor ),
  2084. dragRange = editor.createRange(),
  2085. sourceWidget;
  2086. // Disable cross-editor drag & drop for widgets - #13599.
  2087. if ( id !== '' && transferType === CKEDITOR.DATA_TRANSFER_CROSS_EDITORS ) {
  2088. evt.cancel();
  2089. return;
  2090. }
  2091. if ( id === '' || transferType != CKEDITOR.DATA_TRANSFER_INTERNAL ) {
  2092. return;
  2093. }
  2094. sourceWidget = widgetsRepo.instances[ id ];
  2095. if ( !sourceWidget ) {
  2096. return;
  2097. }
  2098. dragRange.setStartBefore( sourceWidget.wrapper );
  2099. dragRange.setEndAfter( sourceWidget.wrapper );
  2100. evt.data.dragRange = dragRange;
  2101. // [IE8-9] Reset state of the clipboard#fixSplitNodesAfterDrop fix because by setting evt.data.dragRange
  2102. // (see above) after drop happened we do not need it. That fix is needed only if dragRange was created
  2103. // before drop (before text node was split).
  2104. delete CKEDITOR.plugins.clipboard.dragStartContainerChildCount;
  2105. delete CKEDITOR.plugins.clipboard.dragEndContainerChildCount;
  2106. evt.data.dataTransfer.setData( 'text/html', editor.editable().getHtmlFromRange( dragRange ).getHtml() );
  2107. editor.widgets.destroy( sourceWidget, true );
  2108. } );
  2109. editor.on( 'contentDom', function() {
  2110. var editable = editor.editable();
  2111. // Register Lineutils's utilities as properties of repo.
  2112. CKEDITOR.tools.extend( widgetsRepo, {
  2113. finder: new lineutils.finder( editor, {
  2114. lookups: {
  2115. // Element is block but not list item and not in nested editable.
  2116. 'default': function( el ) {
  2117. if ( el.is( CKEDITOR.dtd.$listItem ) )
  2118. return;
  2119. if ( !el.is( CKEDITOR.dtd.$block ) )
  2120. return;
  2121. // Allow drop line inside, but never before or after nested editable (#12006).
  2122. if ( Widget.isDomNestedEditable( el ) )
  2123. return;
  2124. // Do not allow droping inside the widget being dragged (#13397).
  2125. if ( widgetsRepo._.draggedWidget.wrapper.contains( el ) ) {
  2126. return;
  2127. }
  2128. // If element is nested editable, make sure widget can be dropped there (#12006).
  2129. var nestedEditable = Widget.getNestedEditable( editable, el );
  2130. if ( nestedEditable ) {
  2131. var draggedWidget = widgetsRepo._.draggedWidget;
  2132. // Don't let the widget to be dropped into its own nested editable.
  2133. if ( widgetsRepo.getByElement( nestedEditable ) == draggedWidget )
  2134. return;
  2135. var filter = CKEDITOR.filter.instances[ nestedEditable.data( 'cke-filter' ) ],
  2136. draggedRequiredContent = draggedWidget.requiredContent;
  2137. // There will be no relation if the filter of nested editable does not allow
  2138. // requiredContent of dragged widget.
  2139. if ( filter && draggedRequiredContent && !filter.check( draggedRequiredContent ) )
  2140. return;
  2141. }
  2142. return CKEDITOR.LINEUTILS_BEFORE | CKEDITOR.LINEUTILS_AFTER;
  2143. }
  2144. }
  2145. } ),
  2146. locator: new lineutils.locator( editor ),
  2147. liner: new lineutils.liner( editor, {
  2148. lineStyle: {
  2149. cursor: 'move !important',
  2150. 'border-top-color': '#666'
  2151. },
  2152. tipLeftStyle: {
  2153. 'border-left-color': '#666'
  2154. },
  2155. tipRightStyle: {
  2156. 'border-right-color': '#666'
  2157. }
  2158. } )
  2159. }, true );
  2160. } );
  2161. }
  2162. // Setup mouse observer which will trigger:
  2163. // * widget focus on widget click,
  2164. // * widget#doubleclick forwarded from editor#doubleclick.
  2165. function setupMouseObserver( widgetsRepo ) {
  2166. var editor = widgetsRepo.editor;
  2167. editor.on( 'contentDom', function() {
  2168. var editable = editor.editable(),
  2169. evtRoot = editable.isInline() ? editable : editor.document,
  2170. widget,
  2171. mouseDownOnDragHandler;
  2172. editable.attachListener( evtRoot, 'mousedown', function( evt ) {
  2173. var target = evt.data.getTarget();
  2174. // #10887 Clicking scrollbar in IE8 will invoke event with empty target object.
  2175. if ( !target.type )
  2176. return false;
  2177. widget = widgetsRepo.getByElement( target );
  2178. mouseDownOnDragHandler = 0; // Reset.
  2179. // Widget was clicked, but not editable nested in it.
  2180. if ( widget ) {
  2181. // Ignore mousedown on drag and drop handler if the widget is inline.
  2182. // Block widgets are handled by Lineutils.
  2183. if ( widget.inline && target.type == CKEDITOR.NODE_ELEMENT && target.hasAttribute( 'data-cke-widget-drag-handler' ) ) {
  2184. mouseDownOnDragHandler = 1;
  2185. // When drag handler is pressed we have to clear current selection if it wasn't already on this widget.
  2186. // Otherwise, the selection may be in a fillingChar, which prevents dragging a widget. (#13284, see comment 8 and 9.)
  2187. if ( widgetsRepo.focused != widget )
  2188. editor.getSelection().removeAllRanges();
  2189. return;
  2190. }
  2191. if ( !Widget.getNestedEditable( widget.wrapper, target ) ) {
  2192. evt.data.preventDefault();
  2193. if ( !CKEDITOR.env.ie )
  2194. widget.focus();
  2195. } else {
  2196. // Reset widget so mouseup listener is not confused.
  2197. widget = null;
  2198. }
  2199. }
  2200. } );
  2201. // Focus widget on mouseup if mousedown was fired on drag handler.
  2202. // Note: mouseup won't be fired at all if widget was dragged and dropped, so
  2203. // this code will be executed only when drag handler was clicked.
  2204. editable.attachListener( evtRoot, 'mouseup', function() {
  2205. // Check if widget is not destroyed (if widget is destroyed the wrapper will be null).
  2206. if ( mouseDownOnDragHandler && widget && widget.wrapper ) {
  2207. mouseDownOnDragHandler = 0;
  2208. widget.focus();
  2209. }
  2210. } );
  2211. // On IE it is not enough to block mousedown. If widget wrapper (element with
  2212. // contenteditable=false attribute) is clicked directly (it is a target),
  2213. // then after mouseup/click IE will select that element.
  2214. // It is not possible to prevent that default action,
  2215. // so we force fake selection after everything happened.
  2216. if ( CKEDITOR.env.ie ) {
  2217. editable.attachListener( evtRoot, 'mouseup', function() {
  2218. setTimeout( function() {
  2219. // Check if widget is not destroyed (if widget is destroyed the wrapper will be null) and
  2220. // in editable contains widget (it could be dragged and removed).
  2221. if ( widget && widget.wrapper && editable.contains( widget.wrapper ) ) {
  2222. widget.focus();
  2223. widget = null;
  2224. }
  2225. } );
  2226. } );
  2227. }
  2228. } );
  2229. editor.on( 'doubleclick', function( evt ) {
  2230. var widget = widgetsRepo.getByElement( evt.data.element );
  2231. // Not in widget or in nested editable.
  2232. if ( !widget || Widget.getNestedEditable( widget.wrapper, evt.data.element ) )
  2233. return;
  2234. return widget.fire( 'doubleclick', { element: evt.data.element } );
  2235. }, null, null, 1 );
  2236. }
  2237. // Setup editor#key observer which will forward it
  2238. // to focused widget.
  2239. function setupKeyboardObserver( widgetsRepo ) {
  2240. var editor = widgetsRepo.editor;
  2241. editor.on( 'key', function( evt ) {
  2242. var focused = widgetsRepo.focused,
  2243. widgetHoldingFocusedEditable = widgetsRepo.widgetHoldingFocusedEditable,
  2244. ret;
  2245. if ( focused )
  2246. ret = focused.fire( 'key', { keyCode: evt.data.keyCode } );
  2247. else if ( widgetHoldingFocusedEditable )
  2248. ret = onEditableKey( widgetHoldingFocusedEditable, evt.data.keyCode );
  2249. return ret;
  2250. }, null, null, 1 );
  2251. }
  2252. // Setup copybin on native copy and cut events in order to handle copy and cut commands
  2253. // if user accepted security alert on IEs.
  2254. // Note: when copying or cutting using keystroke, copySingleWidget will be first executed
  2255. // by the keydown listener. Conflict between two calls will be resolved by copy_bin existence check.
  2256. function setupNativeCutAndCopy( widgetsRepo ) {
  2257. var editor = widgetsRepo.editor;
  2258. editor.on( 'contentDom', function() {
  2259. var editable = editor.editable();
  2260. editable.attachListener( editable, 'copy', eventListener );
  2261. editable.attachListener( editable, 'cut', eventListener );
  2262. } );
  2263. function eventListener( evt ) {
  2264. if ( widgetsRepo.focused )
  2265. copySingleWidget( widgetsRepo.focused, evt.name == 'cut' );
  2266. }
  2267. }
  2268. // Setup selection observer which will trigger:
  2269. // * widget select & focus on selection change,
  2270. // * nested editable focus (related properites and classes) on selection change,
  2271. // * deselecting and blurring all widgets on data,
  2272. // * blurring widget on editor blur.
  2273. function setupSelectionObserver( widgetsRepo ) {
  2274. var editor = widgetsRepo.editor;
  2275. editor.on( 'selectionCheck', function() {
  2276. widgetsRepo.fire( 'checkSelection' );
  2277. } );
  2278. widgetsRepo.on( 'checkSelection', widgetsRepo.checkSelection, widgetsRepo );
  2279. editor.on( 'selectionChange', function( evt ) {
  2280. var nestedEditable = Widget.getNestedEditable( editor.editable(), evt.data.selection.getStartElement() ),
  2281. newWidget = nestedEditable && widgetsRepo.getByElement( nestedEditable ),
  2282. oldWidget = widgetsRepo.widgetHoldingFocusedEditable;
  2283. if ( oldWidget ) {
  2284. if ( oldWidget !== newWidget || !oldWidget.focusedEditable.equals( nestedEditable ) ) {
  2285. setFocusedEditable( widgetsRepo, oldWidget, null );
  2286. if ( newWidget && nestedEditable )
  2287. setFocusedEditable( widgetsRepo, newWidget, nestedEditable );
  2288. }
  2289. }
  2290. // It may happen that there's no widget even if editable was found -
  2291. // e.g. if selection was automatically set in editable although widget wasn't initialized yet.
  2292. else if ( newWidget && nestedEditable ) {
  2293. setFocusedEditable( widgetsRepo, newWidget, nestedEditable );
  2294. }
  2295. } );
  2296. // Invalidate old widgets early - immediately on dataReady.
  2297. editor.on( 'dataReady', function() {
  2298. // Deselect and blur all widgets.
  2299. stateUpdater( widgetsRepo ).commit();
  2300. } );
  2301. editor.on( 'blur', function() {
  2302. var widget;
  2303. if ( ( widget = widgetsRepo.focused ) )
  2304. blurWidget( widgetsRepo, widget );
  2305. if ( ( widget = widgetsRepo.widgetHoldingFocusedEditable ) )
  2306. setFocusedEditable( widgetsRepo, widget, null );
  2307. } );
  2308. }
  2309. // Set up actions like:
  2310. // * processing in toHtml/toDataFormat,
  2311. // * pasting handling,
  2312. // * insertion handling,
  2313. // * editable reload handling (setData, mode switch, undo/redo),
  2314. // * DOM invalidation handling,
  2315. // * widgets checks.
  2316. function setupWidgetsLifecycle( widgetsRepo ) {
  2317. setupWidgetsLifecycleStart( widgetsRepo );
  2318. setupWidgetsLifecycleEnd( widgetsRepo );
  2319. widgetsRepo.on( 'checkWidgets', checkWidgets );
  2320. widgetsRepo.editor.on( 'contentDomInvalidated', widgetsRepo.checkWidgets, widgetsRepo );
  2321. }
  2322. function setupWidgetsLifecycleEnd( widgetsRepo ) {
  2323. var editor = widgetsRepo.editor,
  2324. downcastingSessions = {};
  2325. // Listen before htmlDP#htmlFilter is applied to cache all widgets, because we'll
  2326. // loose data-cke-* attributes.
  2327. editor.on( 'toDataFormat', function( evt ) {
  2328. // To avoid conflicts between htmlDP#toDF calls done at the same time
  2329. // (e.g. nestedEditable#getData called during downcasting some widget)
  2330. // mark every toDataFormat event chain with the downcasting session id.
  2331. var id = CKEDITOR.tools.getNextNumber(),
  2332. toBeDowncasted = [];
  2333. evt.data.downcastingSessionId = id;
  2334. downcastingSessions[ id ] = toBeDowncasted;
  2335. evt.data.dataValue.forEach( function( element ) {
  2336. var attrs = element.attributes,
  2337. widget, widgetElement;
  2338. // Wrapper.
  2339. // Perform first part of downcasting (cleanup) and cache widgets,
  2340. // because after applying DP's filter all data-cke-* attributes will be gone.
  2341. if ( 'data-cke-widget-id' in attrs ) {
  2342. widget = widgetsRepo.instances[ attrs[ 'data-cke-widget-id' ] ];
  2343. if ( widget ) {
  2344. widgetElement = element.getFirst( Widget.isParserWidgetElement );
  2345. toBeDowncasted.push( {
  2346. wrapper: element,
  2347. element: widgetElement,
  2348. widget: widget,
  2349. editables: {}
  2350. } );
  2351. // If widget did not have data-cke-widget attribute before upcasting remove it.
  2352. if ( widgetElement.attributes[ 'data-cke-widget-keep-attr' ] != '1' )
  2353. delete widgetElement.attributes[ 'data-widget' ];
  2354. }
  2355. }
  2356. // Nested editable.
  2357. else if ( 'data-cke-widget-editable' in attrs ) {
  2358. // Save the reference to this nested editable in the closest widget to be downcasted.
  2359. // Nested editables are downcasted in the successive toDataFormat to create an opportunity
  2360. // for dataFilter's "excludeNestedEditable" option To Do its job (that option relies on
  2361. // contenteditable="true" attribute) (#11372).
  2362. toBeDowncasted[ toBeDowncasted.length - 1 ].editables[ attrs[ 'data-cke-widget-editable' ] ] = element;
  2363. // Don't check children - there won't be next wrapper or nested editable which we
  2364. // should process in this session.
  2365. return false;
  2366. }
  2367. }, CKEDITOR.NODE_ELEMENT, true );
  2368. }, null, null, 8 );
  2369. // Listen after dataProcessor.htmlFilter and ACF were applied
  2370. // so wrappers securing widgets' contents are removed after all filtering was done.
  2371. editor.on( 'toDataFormat', function( evt ) {
  2372. // Ignore some unmarked sessions.
  2373. if ( !evt.data.downcastingSessionId )
  2374. return;
  2375. var toBeDowncasted = downcastingSessions[ evt.data.downcastingSessionId ],
  2376. toBe, widget, widgetElement, retElement, editableElement, e;
  2377. while ( ( toBe = toBeDowncasted.shift() ) ) {
  2378. widget = toBe.widget;
  2379. widgetElement = toBe.element;
  2380. retElement = widget._.downcastFn && widget._.downcastFn.call( widget, widgetElement );
  2381. // Replace nested editables' content with their output data.
  2382. for ( e in toBe.editables ) {
  2383. editableElement = toBe.editables[ e ];
  2384. delete editableElement.attributes.contenteditable;
  2385. editableElement.setHtml( widget.editables[ e ].getData() );
  2386. }
  2387. // Returned element always defaults to widgetElement.
  2388. if ( !retElement )
  2389. retElement = widgetElement;
  2390. toBe.wrapper.replaceWith( retElement );
  2391. }
  2392. }, null, null, 13 );
  2393. editor.on( 'contentDomUnload', function() {
  2394. widgetsRepo.destroyAll( true );
  2395. } );
  2396. }
  2397. function setupWidgetsLifecycleStart( widgetsRepo ) {
  2398. var editor = widgetsRepo.editor,
  2399. processedWidgetOnly,
  2400. snapshotLoaded;
  2401. // Listen after ACF (so data are filtered),
  2402. // but before dataProcessor.dataFilter was applied (so we can secure widgets' internals).
  2403. editor.on( 'toHtml', function( evt ) {
  2404. var upcastIterator = createUpcastIterator( widgetsRepo ),
  2405. toBeWrapped;
  2406. evt.data.dataValue.forEach( upcastIterator.iterator, CKEDITOR.NODE_ELEMENT, true );
  2407. // Clean up and wrap all queued elements.
  2408. while ( ( toBeWrapped = upcastIterator.toBeWrapped.pop() ) ) {
  2409. cleanUpWidgetElement( toBeWrapped[ 0 ] );
  2410. widgetsRepo.wrapElement( toBeWrapped[ 0 ], toBeWrapped[ 1 ] );
  2411. }
  2412. // Used to determine whether only widget was pasted.
  2413. if ( evt.data.protectedWhitespaces ) {
  2414. // Whitespaces are protected by wrapping content with spans. Take the middle node only.
  2415. processedWidgetOnly = evt.data.dataValue.children.length == 3 &&
  2416. Widget.isParserWidgetWrapper( evt.data.dataValue.children[ 1 ] );
  2417. } else {
  2418. processedWidgetOnly = evt.data.dataValue.children.length == 1 &&
  2419. Widget.isParserWidgetWrapper( evt.data.dataValue.children[ 0 ] );
  2420. }
  2421. }, null, null, 8 );
  2422. editor.on( 'dataReady', function() {
  2423. // Clean up all widgets loaded from snapshot.
  2424. if ( snapshotLoaded )
  2425. cleanUpAllWidgetElements( widgetsRepo, editor.editable() );
  2426. snapshotLoaded = 0;
  2427. // Some widgets were destroyed on contentDomUnload,
  2428. // some on loadSnapshot, but that does not include
  2429. // e.g. setHtml on inline editor or widgets removed just
  2430. // before setting data.
  2431. widgetsRepo.destroyAll( true );
  2432. widgetsRepo.initOnAll();
  2433. } );
  2434. // Set flag so dataReady will know that additional
  2435. // cleanup is needed, because snapshot containing widgets was loaded.
  2436. editor.on( 'loadSnapshot', function( evt ) {
  2437. // Primitive but sufficient check which will prevent from executing
  2438. // heavier cleanUpAllWidgetElements if not needed.
  2439. if ( ( /data-cke-widget/ ).test( evt.data ) )
  2440. snapshotLoaded = 1;
  2441. widgetsRepo.destroyAll( true );
  2442. }, null, null, 9 );
  2443. // Handle pasted single widget.
  2444. editor.on( 'paste', function( evt ) {
  2445. var data = evt.data;
  2446. data.dataValue = data.dataValue.replace( pasteReplaceRegex, pasteReplaceFn );
  2447. // If drag'n'drop kind of paste into nested editable (data.range), selection is set AFTER
  2448. // data is pasted, which means editor has no chance to change activeFilter's context.
  2449. // As a result, pasted data is filtered with default editor's filter instead of NE's and
  2450. // funny things get inserted. Changing the filter by analysis of the paste range below (#13186).
  2451. if ( data.range ) {
  2452. // Check if pasting into nested editable.
  2453. var nestedEditable = Widget.getNestedEditable( editor.editable(), data.range.startContainer );
  2454. if ( nestedEditable ) {
  2455. // Retrieve the filter from NE's data and set it active before editor.insertHtml is done
  2456. // in clipboard plugin.
  2457. var filter = CKEDITOR.filter.instances[ nestedEditable.data( 'cke-filter' ) ];
  2458. if ( filter ) {
  2459. editor.setActiveFilter( filter );
  2460. }
  2461. }
  2462. }
  2463. } );
  2464. // Listen with high priority to check widgets after data was inserted.
  2465. editor.on( 'afterInsertHtml', function( evt ) {
  2466. if ( evt.data.intoRange ) {
  2467. widgetsRepo.checkWidgets( { initOnlyNew: true } );
  2468. } else {
  2469. editor.fire( 'lockSnapshot' );
  2470. // Init only new for performance reason.
  2471. // Focus inited if only widget was processed.
  2472. widgetsRepo.checkWidgets( { initOnlyNew: true, focusInited: processedWidgetOnly } );
  2473. editor.fire( 'unlockSnapshot' );
  2474. }
  2475. } );
  2476. }
  2477. // Helper for coordinating which widgets should be
  2478. // selected/deselected and which one should be focused/blurred.
  2479. function stateUpdater( widgetsRepo ) {
  2480. var currentlySelected = widgetsRepo.selected,
  2481. toBeSelected = [],
  2482. toBeDeselected = currentlySelected.slice( 0 ),
  2483. focused = null;
  2484. return {
  2485. select: function( widget ) {
  2486. if ( CKEDITOR.tools.indexOf( currentlySelected, widget ) < 0 )
  2487. toBeSelected.push( widget );
  2488. var index = CKEDITOR.tools.indexOf( toBeDeselected, widget );
  2489. if ( index >= 0 )
  2490. toBeDeselected.splice( index, 1 );
  2491. return this;
  2492. },
  2493. focus: function( widget ) {
  2494. focused = widget;
  2495. return this;
  2496. },
  2497. commit: function() {
  2498. var focusedChanged = widgetsRepo.focused !== focused,
  2499. widget, isDirty;
  2500. widgetsRepo.editor.fire( 'lockSnapshot' );
  2501. if ( focusedChanged && ( widget = widgetsRepo.focused ) )
  2502. blurWidget( widgetsRepo, widget );
  2503. while ( ( widget = toBeDeselected.pop() ) ) {
  2504. currentlySelected.splice( CKEDITOR.tools.indexOf( currentlySelected, widget ), 1 );
  2505. // Widget could be destroyed in the meantime - e.g. data could be set.
  2506. if ( widget.isInited() ) {
  2507. isDirty = widget.editor.checkDirty();
  2508. widget.setSelected( false );
  2509. !isDirty && widget.editor.resetDirty();
  2510. }
  2511. }
  2512. if ( focusedChanged && focused ) {
  2513. isDirty = widgetsRepo.editor.checkDirty();
  2514. widgetsRepo.focused = focused;
  2515. widgetsRepo.fire( 'widgetFocused', { widget: focused } );
  2516. focused.setFocused( true );
  2517. !isDirty && widgetsRepo.editor.resetDirty();
  2518. }
  2519. while ( ( widget = toBeSelected.pop() ) ) {
  2520. currentlySelected.push( widget );
  2521. widget.setSelected( true );
  2522. }
  2523. widgetsRepo.editor.fire( 'unlockSnapshot' );
  2524. }
  2525. };
  2526. }
  2527. //
  2528. // WIDGET helpers ---------------------------------------------------------
  2529. //
  2530. // LEFT, RIGHT, UP, DOWN, DEL, BACKSPACE - unblock default fake sel handlers.
  2531. var keystrokesNotBlockedByWidget = { 37: 1, 38: 1, 39: 1, 40: 1, 8: 1, 46: 1 };
  2532. // Applies or removes style's classes from widget.
  2533. // @param {CKEDITOR.style} style Custom widget style.
  2534. // @param {Boolean} apply Whether to apply or remove style.
  2535. function applyRemoveStyle( widget, style, apply ) {
  2536. var changed = 0,
  2537. classes = getStyleClasses( style ),
  2538. updatedClasses = widget.data.classes || {},
  2539. cl;
  2540. // Ee... Something is wrong with this style.
  2541. if ( !classes )
  2542. return;
  2543. // Clone, because we need to break reference.
  2544. updatedClasses = CKEDITOR.tools.clone( updatedClasses );
  2545. while ( ( cl = classes.pop() ) ) {
  2546. if ( apply ) {
  2547. if ( !updatedClasses[ cl ] )
  2548. changed = updatedClasses[ cl ] = 1;
  2549. } else {
  2550. if ( updatedClasses[ cl ] ) {
  2551. delete updatedClasses[ cl ];
  2552. changed = 1;
  2553. }
  2554. }
  2555. }
  2556. if ( changed )
  2557. widget.setData( 'classes', updatedClasses );
  2558. }
  2559. function cancel( evt ) {
  2560. evt.cancel();
  2561. }
  2562. function copySingleWidget( widget, isCut ) {
  2563. var editor = widget.editor,
  2564. doc = editor.document;
  2565. // We're still handling previous copy/cut.
  2566. // When keystroke is used to copy/cut this will also prevent
  2567. // conflict with copySingleWidget called again for native copy/cut event.
  2568. if ( doc.getById( 'cke_copybin' ) )
  2569. return;
  2570. // [IE] Use span for copybin and its container to avoid bug with expanding editable height by
  2571. // absolutely positioned element.
  2572. var copybinName = ( editor.blockless || CKEDITOR.env.ie ) ? 'span' : 'div',
  2573. copybin = doc.createElement( copybinName ),
  2574. copybinContainer = doc.createElement( copybinName ),
  2575. // IE8 always jumps to the end of document.
  2576. needsScrollHack = CKEDITOR.env.ie && CKEDITOR.env.version < 9;
  2577. copybinContainer.setAttributes( {
  2578. id: 'cke_copybin',
  2579. 'data-cke-temp': '1'
  2580. } );
  2581. // Position copybin element outside current viewport.
  2582. copybin.setStyles( {
  2583. position: 'absolute',
  2584. width: '1px',
  2585. height: '1px',
  2586. overflow: 'hidden'
  2587. } );
  2588. copybin.setStyle( editor.config.contentsLangDirection == 'ltr' ? 'left' : 'right', '-5000px' );
  2589. var range = editor.createRange();
  2590. range.setStartBefore( widget.wrapper );
  2591. range.setEndAfter( widget.wrapper );
  2592. copybin.setHtml(
  2593. '<span data-cke-copybin-start="1">\u200b</span>' +
  2594. editor.editable().getHtmlFromRange( range ).getHtml() +
  2595. '<span data-cke-copybin-end="1">\u200b</span>' );
  2596. // Save snapshot with the current state.
  2597. editor.fire( 'saveSnapshot' );
  2598. // Ignore copybin.
  2599. editor.fire( 'lockSnapshot' );
  2600. copybinContainer.append( copybin );
  2601. editor.editable().append( copybinContainer );
  2602. var listener1 = editor.on( 'selectionChange', cancel, null, null, 0 ),
  2603. listener2 = widget.repository.on( 'checkSelection', cancel, null, null, 0 );
  2604. if ( needsScrollHack ) {
  2605. var docElement = doc.getDocumentElement().$,
  2606. scrollTop = docElement.scrollTop;
  2607. }
  2608. // Once the clone of the widget is inside of copybin, select
  2609. // the entire contents. This selection will be copied by the
  2610. // native browser's clipboard system.
  2611. range = editor.createRange();
  2612. range.selectNodeContents( copybin );
  2613. range.select();
  2614. if ( needsScrollHack )
  2615. docElement.scrollTop = scrollTop;
  2616. setTimeout( function() {
  2617. // [IE] Focus widget before removing copybin to avoid scroll jump.
  2618. if ( !isCut )
  2619. widget.focus();
  2620. copybinContainer.remove();
  2621. listener1.removeListener();
  2622. listener2.removeListener();
  2623. editor.fire( 'unlockSnapshot' );
  2624. if ( isCut ) {
  2625. widget.repository.del( widget );
  2626. editor.fire( 'saveSnapshot' );
  2627. }
  2628. }, 100 ); // Use 100ms, so Chrome (@Mac) will be able to grab the content.
  2629. }
  2630. // Extracts classes array from style instance.
  2631. function getStyleClasses( style ) {
  2632. var attrs = style.getDefinition().attributes,
  2633. classes = attrs && attrs[ 'class' ];
  2634. return classes ? classes.split( /\s+/ ) : null;
  2635. }
  2636. // [IE] Force keeping focus because IE sometimes forgets to fire focus on main editable
  2637. // when blurring nested editable.
  2638. // @context widget
  2639. function onEditableBlur() {
  2640. var active = CKEDITOR.document.getActive(),
  2641. editor = this.editor,
  2642. editable = editor.editable();
  2643. // If focus stays within editor override blur and set currentActive because it should be
  2644. // automatically changed to editable on editable#focus but it is not fired.
  2645. if ( ( editable.isInline() ? editable : editor.document.getWindow().getFrame() ).equals( active ) )
  2646. editor.focusManager.focus( editable );
  2647. }
  2648. // Force selectionChange when editable was focused.
  2649. // Similar to hack in selection.js#~620.
  2650. // @context widget
  2651. function onEditableFocus() {
  2652. // Gecko does not support 'DOMFocusIn' event on which we unlock selection
  2653. // in selection.js to prevent selection locking when entering nested editables.
  2654. if ( CKEDITOR.env.gecko )
  2655. this.editor.unlockSelection();
  2656. // We don't need to force selectionCheck on Webkit, because on Webkit
  2657. // we do that on DOMFocusIn in selection.js.
  2658. if ( !CKEDITOR.env.webkit ) {
  2659. this.editor.forceNextSelectionCheck();
  2660. this.editor.selectionChange( 1 );
  2661. }
  2662. }
  2663. // Setup listener on widget#data which will update (remove/add) classes
  2664. // by comparing newly set classes with the old ones.
  2665. function setupDataClassesListener( widget ) {
  2666. // Note: previousClasses and newClasses may be null!
  2667. // Tip: for ( cl in null ) is correct.
  2668. var previousClasses = null;
  2669. widget.on( 'data', function() {
  2670. var newClasses = this.data.classes,
  2671. cl;
  2672. // When setting new classes one need to remember
  2673. // that he must break reference.
  2674. if ( previousClasses == newClasses )
  2675. return;
  2676. for ( cl in previousClasses ) {
  2677. // Avoid removing and adding classes again.
  2678. if ( !( newClasses && newClasses[ cl ] ) )
  2679. this.removeClass( cl );
  2680. }
  2681. for ( cl in newClasses )
  2682. this.addClass( cl );
  2683. previousClasses = newClasses;
  2684. } );
  2685. }
  2686. // Add a listener to data event that will set/change widget's label (#14539).
  2687. function setupA11yListener( widget ) {
  2688. // Note, the function gets executed in a context of widget instance.
  2689. function getLabelDefault() {
  2690. return this.editor.lang.widget.label.replace( /%1/, this.pathName || this.element.getName() );
  2691. }
  2692. // Setting a listener on data is enough, there's no need to perform it on widget initialization, as
  2693. // setupWidgetData fires this event anyway.
  2694. widget.on( 'data', function() {
  2695. // In some cases widget might get destroyed in an earlier data listener. For instance, image2 plugin, does
  2696. // so when changing its internal state.
  2697. if ( !widget.wrapper ) {
  2698. return;
  2699. }
  2700. var label = this.getLabel ? this.getLabel() : getLabelDefault.call( this );
  2701. widget.wrapper.setAttribute( 'role', 'region' );
  2702. widget.wrapper.setAttribute( 'aria-label', label );
  2703. }, null, null, 9999 );
  2704. }
  2705. function setupDragHandler( widget ) {
  2706. if ( !widget.draggable )
  2707. return;
  2708. var editor = widget.editor,
  2709. // Use getLast to find wrapper's direct descendant (#12022).
  2710. container = widget.wrapper.getLast( Widget.isDomDragHandlerContainer ),
  2711. img;
  2712. // Reuse drag handler if already exists (#11281).
  2713. if ( container )
  2714. img = container.findOne( 'img' );
  2715. else {
  2716. container = new CKEDITOR.dom.element( 'span', editor.document );
  2717. container.setAttributes( {
  2718. 'class': 'cke_reset cke_widget_drag_handler_container',
  2719. // Split background and background-image for IE8 which will break on rgba().
  2720. style: 'background:rgba(220,220,220,0.5);background-image:url(' + editor.plugins.widget.path + 'images/handle.png)'
  2721. } );
  2722. img = new CKEDITOR.dom.element( 'img', editor.document );
  2723. img.setAttributes( {
  2724. 'class': 'cke_reset cke_widget_drag_handler',
  2725. 'data-cke-widget-drag-handler': '1',
  2726. src: CKEDITOR.tools.transparentImageData,
  2727. width: DRAG_HANDLER_SIZE,
  2728. title: editor.lang.widget.move,
  2729. height: DRAG_HANDLER_SIZE,
  2730. role: 'presentation'
  2731. } );
  2732. widget.inline && img.setAttribute( 'draggable', 'true' );
  2733. container.append( img );
  2734. widget.wrapper.append( container );
  2735. }
  2736. // Preventing page reload when dropped content on widget wrapper (#13015).
  2737. // Widget is not editable so by default drop on it isn't allowed what means that
  2738. // browser handles it (there's no editable#drop event). If there's no drop event we cannot block
  2739. // the drop, so page is reloaded. This listener enables drop on widget wrappers.
  2740. widget.wrapper.on( 'dragover', function( evt ) {
  2741. evt.data.preventDefault();
  2742. } );
  2743. widget.wrapper.on( 'mouseenter', widget.updateDragHandlerPosition, widget );
  2744. setTimeout( function() {
  2745. widget.on( 'data', widget.updateDragHandlerPosition, widget );
  2746. }, 50 );
  2747. if ( !widget.inline ) {
  2748. img.on( 'mousedown', onBlockWidgetDrag, widget );
  2749. // On IE8 'dragstart' is propagated to editable, so editor#dragstart is fired twice on block widgets.
  2750. if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) {
  2751. img.on( 'dragstart', function( evt ) {
  2752. evt.data.preventDefault( true );
  2753. } );
  2754. }
  2755. }
  2756. widget.dragHandlerContainer = container;
  2757. }
  2758. function onBlockWidgetDrag( evt ) {
  2759. var finder = this.repository.finder,
  2760. locator = this.repository.locator,
  2761. liner = this.repository.liner,
  2762. editor = this.editor,
  2763. editable = editor.editable(),
  2764. listeners = [],
  2765. sorted = [],
  2766. locations,
  2767. y;
  2768. // Mark dragged widget for repository#finder.
  2769. this.repository._.draggedWidget = this;
  2770. // Harvest all possible relations and display some closest.
  2771. var relations = finder.greedySearch(),
  2772. buffer = CKEDITOR.tools.eventsBuffer( 50, function() {
  2773. locations = locator.locate( relations );
  2774. // There's only a single line displayed for D&D.
  2775. sorted = locator.sort( y, 1 );
  2776. if ( sorted.length ) {
  2777. liner.prepare( relations, locations );
  2778. liner.placeLine( sorted[ 0 ] );
  2779. liner.cleanup();
  2780. }
  2781. } );
  2782. // Let's have the "dragging cursor" over entire editable.
  2783. editable.addClass( 'cke_widget_dragging' );
  2784. // Cache mouse position so it is re-used in events buffer.
  2785. listeners.push( editable.on( 'mousemove', function( evt ) {
  2786. y = evt.data.$.clientY;
  2787. buffer.input();
  2788. } ) );
  2789. // Fire drag start as it happens during the native D&D.
  2790. editor.fire( 'dragstart', { target: evt.sender } );
  2791. function onMouseUp() {
  2792. var l;
  2793. buffer.reset();
  2794. // Stop observing events.
  2795. while ( ( l = listeners.pop() ) )
  2796. l.removeListener();
  2797. onBlockWidgetDrop.call( this, sorted, evt.sender );
  2798. }
  2799. // Mouseup means "drop". This is when the widget is being detached
  2800. // from DOM and placed at range determined by the line (location).
  2801. listeners.push( editor.document.once( 'mouseup', onMouseUp, this ) );
  2802. // Prevent calling 'onBlockWidgetDrop' twice in the inline editor.
  2803. // `removeListener` does not work if it is called at the same time event is fired.
  2804. if ( !editable.isInline() ) {
  2805. // Mouseup may occur when user hovers the line, which belongs to
  2806. // the outer document. This is, of course, a valid listener too.
  2807. listeners.push( CKEDITOR.document.once( 'mouseup', onMouseUp, this ) );
  2808. }
  2809. }
  2810. function onBlockWidgetDrop( sorted, dragTarget ) {
  2811. var finder = this.repository.finder,
  2812. liner = this.repository.liner,
  2813. editor = this.editor,
  2814. editable = this.editor.editable();
  2815. if ( !CKEDITOR.tools.isEmpty( liner.visible ) ) {
  2816. // Retrieve range for the closest location.
  2817. var dropRange = finder.getRange( sorted[ 0 ] );
  2818. // Focus widget (it could lost focus after mousedown+mouseup)
  2819. // and save this state as the one where we want to be taken back when undoing.
  2820. this.focus();
  2821. // Drag range will be set in the drop listener.
  2822. editor.fire( 'drop', {
  2823. dropRange: dropRange,
  2824. target: dropRange.startContainer
  2825. } );
  2826. }
  2827. // Clean-up custom cursor for editable.
  2828. editable.removeClass( 'cke_widget_dragging' );
  2829. // Clean-up all remaining lines.
  2830. liner.hideVisible();
  2831. // Clean-up drag & drop.
  2832. editor.fire( 'dragend', { target: dragTarget } );
  2833. }
  2834. function setupEditables( widget ) {
  2835. var editableName,
  2836. editableDef,
  2837. definedEditables = widget.editables;
  2838. widget.editables = {};
  2839. if ( !widget.editables )
  2840. return;
  2841. for ( editableName in definedEditables ) {
  2842. editableDef = definedEditables[ editableName ];
  2843. widget.initEditable( editableName, typeof editableDef == 'string' ? { selector: editableDef } : editableDef );
  2844. }
  2845. }
  2846. function setupMask( widget ) {
  2847. if ( !widget.mask )
  2848. return;
  2849. // Reuse mask if already exists (#11281).
  2850. var img = widget.wrapper.findOne( '.cke_widget_mask' );
  2851. if ( !img ) {
  2852. img = new CKEDITOR.dom.element( 'img', widget.editor.document );
  2853. img.setAttributes( {
  2854. src: CKEDITOR.tools.transparentImageData,
  2855. 'class': 'cke_reset cke_widget_mask'
  2856. } );
  2857. widget.wrapper.append( img );
  2858. }
  2859. widget.mask = img;
  2860. }
  2861. // Replace parts object containing:
  2862. // partName => selector pairs
  2863. // with:
  2864. // partName => element pairs
  2865. function setupParts( widget ) {
  2866. if ( widget.parts ) {
  2867. var parts = {},
  2868. el, partName;
  2869. for ( partName in widget.parts ) {
  2870. el = widget.wrapper.findOne( widget.parts[ partName ] );
  2871. parts[ partName ] = el;
  2872. }
  2873. widget.parts = parts;
  2874. }
  2875. }
  2876. function setupWidget( widget, widgetDef ) {
  2877. setupWrapper( widget );
  2878. setupParts( widget );
  2879. setupEditables( widget );
  2880. setupMask( widget );
  2881. setupDragHandler( widget );
  2882. setupDataClassesListener( widget );
  2883. setupA11yListener( widget );
  2884. // #11145: [IE8] Non-editable content of widget is draggable.
  2885. if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) {
  2886. widget.wrapper.on( 'dragstart', function( evt ) {
  2887. var target = evt.data.getTarget();
  2888. // Allow text dragging inside nested editables or dragging inline widget's drag handler.
  2889. if ( !Widget.getNestedEditable( widget, target ) && !( widget.inline && Widget.isDomDragHandler( target ) ) )
  2890. evt.data.preventDefault();
  2891. } );
  2892. }
  2893. widget.wrapper.removeClass( 'cke_widget_new' );
  2894. widget.element.addClass( 'cke_widget_element' );
  2895. widget.on( 'key', function( evt ) {
  2896. var keyCode = evt.data.keyCode;
  2897. // ENTER.
  2898. if ( keyCode == 13 ) {
  2899. widget.edit();
  2900. // CTRL+C or CTRL+X.
  2901. } else if ( keyCode == CKEDITOR.CTRL + 67 || keyCode == CKEDITOR.CTRL + 88 ) {
  2902. copySingleWidget( widget, keyCode == CKEDITOR.CTRL + 88 );
  2903. return; // Do not preventDefault.
  2904. } else if ( keyCode in keystrokesNotBlockedByWidget || ( CKEDITOR.CTRL & keyCode ) || ( CKEDITOR.ALT & keyCode ) ) {
  2905. // Pass chosen keystrokes to other plugins or default fake sel handlers.
  2906. // Pass all CTRL/ALT keystrokes.
  2907. return;
  2908. }
  2909. return false;
  2910. }, null, null, 999 );
  2911. // Listen with high priority so it's possible
  2912. // to overwrite this callback.
  2913. widget.on( 'doubleclick', function( evt ) {
  2914. if ( widget.edit() ) {
  2915. // We have to cancel event if edit method opens a dialog, otherwise
  2916. // link plugin may open extra dialog (#12140).
  2917. evt.cancel();
  2918. }
  2919. } );
  2920. if ( widgetDef.data )
  2921. widget.on( 'data', widgetDef.data );
  2922. if ( widgetDef.edit )
  2923. widget.on( 'edit', widgetDef.edit );
  2924. }
  2925. function setupWidgetData( widget, startupData ) {
  2926. var widgetDataAttr = widget.element.data( 'cke-widget-data' );
  2927. if ( widgetDataAttr )
  2928. widget.setData( JSON.parse( decodeURIComponent( widgetDataAttr ) ) );
  2929. if ( startupData )
  2930. widget.setData( startupData );
  2931. // Populate classes if they are not preset.
  2932. if ( !widget.data.classes )
  2933. widget.setData( 'classes', widget.getClasses() );
  2934. // Unblock data and...
  2935. widget.dataReady = true;
  2936. // Write data to element because this was blocked when data wasn't ready.
  2937. writeDataToElement( widget );
  2938. // Fire data event first time, because this was blocked when data wasn't ready.
  2939. widget.fire( 'data', widget.data );
  2940. }
  2941. function setupWrapper( widget ) {
  2942. // Retrieve widget wrapper. Assign an id to it.
  2943. var wrapper = widget.wrapper = widget.element.getParent();
  2944. wrapper.setAttribute( 'data-cke-widget-id', widget.id );
  2945. }
  2946. function writeDataToElement( widget ) {
  2947. widget.element.data( 'cke-widget-data', encodeURIComponent( JSON.stringify( widget.data ) ) );
  2948. }
  2949. //
  2950. // WIDGET STYLE HANDLER ---------------------------------------------------
  2951. //
  2952. ( function() {
  2953. /**
  2954. * The class representing a widget style. It is an {@link CKEDITOR#STYLE_OBJECT object} like
  2955. * the styles handler for widgets.
  2956. *
  2957. * **Note:** This custom style handler does not support all methods of the {@link CKEDITOR.style} class.
  2958. * Not supported methods: {@link #applyToRange}, {@link #removeFromRange}, {@link #applyToObject}.
  2959. *
  2960. * @since 4.4
  2961. * @class CKEDITOR.style.customHandlers.widget
  2962. * @extends CKEDITOR.style
  2963. */
  2964. CKEDITOR.style.addCustomHandler( {
  2965. type: 'widget',
  2966. setup: function( styleDefinition ) {
  2967. /**
  2968. * The name of widget to which this style can be applied.
  2969. * It is extracted from style definition's `widget` property.
  2970. *
  2971. * @property {String} widget
  2972. */
  2973. this.widget = styleDefinition.widget;
  2974. },
  2975. apply: function( editor ) {
  2976. // Before CKEditor 4.4 wasn't a required argument, so we need to
  2977. // handle a case when it wasn't provided.
  2978. if ( !( editor instanceof CKEDITOR.editor ) )
  2979. return;
  2980. // Theoretically we could bypass checkApplicable, get widget from
  2981. // widgets.focused and check its name, what would be faster, but then
  2982. // this custom style would work differently than the default style
  2983. // which checks if it's applicable before applying or removeing itself.
  2984. if ( this.checkApplicable( editor.elementPath(), editor ) )
  2985. editor.widgets.focused.applyStyle( this );
  2986. },
  2987. remove: function( editor ) {
  2988. // Before CKEditor 4.4 wasn't a required argument, so we need to
  2989. // handle a case when it wasn't provided.
  2990. if ( !( editor instanceof CKEDITOR.editor ) )
  2991. return;
  2992. if ( this.checkApplicable( editor.elementPath(), editor ) )
  2993. editor.widgets.focused.removeStyle( this );
  2994. },
  2995. checkActive: function( elementPath, editor ) {
  2996. return this.checkElementMatch( elementPath.lastElement, 0, editor );
  2997. },
  2998. checkApplicable: function( elementPath, editor ) {
  2999. // Before CKEditor 4.4 wasn't a required argument, so we need to
  3000. // handle a case when it wasn't provided.
  3001. if ( !( editor instanceof CKEDITOR.editor ) )
  3002. return false;
  3003. return this.checkElement( elementPath.lastElement );
  3004. },
  3005. checkElementMatch: checkElementMatch,
  3006. checkElementRemovable: checkElementMatch,
  3007. /**
  3008. * Checks if an element is a {@link CKEDITOR.plugins.widget#wrapper wrapper} of a
  3009. * widget whose name matches the {@link #widget widget name} specified in the style definition.
  3010. *
  3011. * @param {CKEDITOR.dom.element} element
  3012. * @returns {Boolean}
  3013. */
  3014. checkElement: function( element ) {
  3015. if ( !Widget.isDomWidgetWrapper( element ) )
  3016. return false;
  3017. var widgetElement = element.getFirst( Widget.isDomWidgetElement );
  3018. return widgetElement && widgetElement.data( 'widget' ) == this.widget;
  3019. },
  3020. buildPreview: function( label ) {
  3021. return label || this._.definition.name;
  3022. },
  3023. /**
  3024. * Returns allowed content rules which should be registered for this style.
  3025. * Uses widget's {@link CKEDITOR.plugins.widget.definition#styleableElements} to make a rule
  3026. * allowing classes on specified elements or use widget's
  3027. * {@link CKEDITOR.plugins.widget.definition#styleToAllowedContentRules} method to transform a style
  3028. * into allowed content rules.
  3029. *
  3030. * @param {CKEDITOR.editor} The editor instance.
  3031. * @returns {CKEDITOR.filter.allowedContentRules}
  3032. */
  3033. toAllowedContentRules: function( editor ) {
  3034. if ( !editor )
  3035. return null;
  3036. var widgetDef = editor.widgets.registered[ this.widget ],
  3037. classes,
  3038. rule = {};
  3039. if ( !widgetDef )
  3040. return null;
  3041. if ( widgetDef.styleableElements ) {
  3042. classes = this.getClassesArray();
  3043. if ( !classes )
  3044. return null;
  3045. rule[ widgetDef.styleableElements ] = {
  3046. classes: classes,
  3047. propertiesOnly: true
  3048. };
  3049. return rule;
  3050. }
  3051. if ( widgetDef.styleToAllowedContentRules )
  3052. return widgetDef.styleToAllowedContentRules( this );
  3053. return null;
  3054. },
  3055. /**
  3056. * Returns classes defined in the style in form of an array.
  3057. *
  3058. * @returns {String[]}
  3059. */
  3060. getClassesArray: function() {
  3061. var classes = this._.definition.attributes && this._.definition.attributes[ 'class' ];
  3062. return classes ? CKEDITOR.tools.trim( classes ).split( /\s+/ ) : null;
  3063. },
  3064. /**
  3065. * Not implemented.
  3066. *
  3067. * @method applyToRange
  3068. */
  3069. applyToRange: notImplemented,
  3070. /**
  3071. * Not implemented.
  3072. *
  3073. * @method removeFromRange
  3074. */
  3075. removeFromRange: notImplemented,
  3076. /**
  3077. * Not implemented.
  3078. *
  3079. * @method applyToObject
  3080. */
  3081. applyToObject: notImplemented
  3082. } );
  3083. function notImplemented() {}
  3084. // @context style
  3085. function checkElementMatch( element, fullMatch, editor ) {
  3086. // Before CKEditor 4.4 wasn't a required argument, so we need to
  3087. // handle a case when it wasn't provided.
  3088. if ( !editor )
  3089. return false;
  3090. if ( !this.checkElement( element ) )
  3091. return false;
  3092. var widget = editor.widgets.getByElement( element, true );
  3093. return widget && widget.checkStyleActive( this );
  3094. }
  3095. } )();
  3096. //
  3097. // EXPOSE PUBLIC API ------------------------------------------------------
  3098. //
  3099. CKEDITOR.plugins.widget = Widget;
  3100. Widget.repository = Repository;
  3101. Widget.nestedEditable = NestedEditable;
  3102. } )();
  3103. /**
  3104. * An event fired when a widget definition is registered by the {@link CKEDITOR.plugins.widget.repository#add} method.
  3105. * It is possible to modify the definition being registered.
  3106. *
  3107. * @event widgetDefinition
  3108. * @member CKEDITOR.editor
  3109. * @param {CKEDITOR.plugins.widget.definition} data Widget definition.
  3110. */
  3111. /**
  3112. * This is an abstract class that describes the definition of a widget.
  3113. * It is a type of {@link CKEDITOR.plugins.widget.repository#add} method's second argument.
  3114. *
  3115. * Widget instances inherit from registered widget definitions, although not in a prototypal way.
  3116. * They are simply extended with corresponding widget definitions. Note that not all properties of
  3117. * the widget definition become properties of a widget. Some, like {@link #data} or {@link #edit}, become
  3118. * widget's events listeners.
  3119. *
  3120. * @class CKEDITOR.plugins.widget.definition
  3121. * @abstract
  3122. * @mixins CKEDITOR.feature
  3123. */
  3124. /**
  3125. * Widget definition name. It is automatically set when the definition is
  3126. * {@link CKEDITOR.plugins.widget.repository#add registered}.
  3127. *
  3128. * @property {String} name
  3129. */
  3130. /**
  3131. * The method executed while initializing a widget, after a widget instance
  3132. * is created, but before it is ready. It is executed before the first
  3133. * {@link CKEDITOR.plugins.widget#event-data} is fired so it is common to
  3134. * use the `init` method to populate widget data with information loaded from
  3135. * the DOM, like for exmaple:
  3136. *
  3137. * init: function() {
  3138. * this.setData( 'width', this.element.getStyle( 'width' ) );
  3139. *
  3140. * if ( this.parts.caption.getStyle( 'display' ) != 'none' )
  3141. * this.setData( 'showCaption', true );
  3142. * }
  3143. *
  3144. * @property {Function} init
  3145. */
  3146. /**
  3147. * The function to be used to upcast an element to this widget or a
  3148. * comma-separated list of upcast methods from the {@link #upcasts} object.
  3149. *
  3150. * The upcast function **is not** executed in the widget context (because the widget
  3151. * does not exist yet) and two arguments are passed:
  3152. *
  3153. * * `element` ({@link CKEDITOR.htmlParser.element}) &ndash; The element to be checked.
  3154. * * `data` (`Object`) &ndash; The object which can be extended with data which will then be passed to the widget.
  3155. *
  3156. * An element will be upcasted if a function returned `true` or an instance of
  3157. * a {@link CKEDITOR.htmlParser.element} if upcasting meant DOM structure changes
  3158. * (in this case the widget will be initialized on the returned element).
  3159. *
  3160. * @property {String/Function} upcast
  3161. */
  3162. /**
  3163. * The object containing functions which can be used to upcast this widget.
  3164. * Only those pointed by the {@link #upcast} property will be used.
  3165. *
  3166. * In most cases it is appropriate to use {@link #upcast} directly,
  3167. * because majority of widgets need just one method.
  3168. * However, in some cases the widget author may want to expose more than one variant
  3169. * and then this property may be used.
  3170. *
  3171. * upcasts: {
  3172. * // This function may upcast only figure elements.
  3173. * figure: function() {
  3174. * // ...
  3175. * },
  3176. * // This function may upcast only image elements.
  3177. * image: function() {
  3178. * // ...
  3179. * },
  3180. * // More variants...
  3181. * }
  3182. *
  3183. * // Then, widget user may choose which upcast methods will be enabled.
  3184. * editor.on( 'widgetDefinition', function( evt ) {
  3185. * if ( evt.data.name == 'image' )
  3186. * evt.data.upcast = 'figure,image'; // Use both methods.
  3187. * } );
  3188. *
  3189. * @property {Object} upcasts
  3190. */
  3191. /**
  3192. * The {@link #upcast} method(s) priority. The upcast with a lower priority number will be called before
  3193. * the one with a higher number. The default priority is `10`.
  3194. *
  3195. * @since 4.5
  3196. * @property {Number} [upcastPriority=10]
  3197. */
  3198. /**
  3199. * The function to be used To Downcast this widget or
  3200. * a name of the downcast option from the {@link #downcasts} object.
  3201. *
  3202. * The downcast funciton will be executed in the {@link CKEDITOR.plugins.widget} context
  3203. * and with `widgetElement` ({@link CKEDITOR.htmlParser.element}) argument which is
  3204. * the widget's main element.
  3205. *
  3206. * The function may return an instance of the {@link CKEDITOR.htmlParser.node} class if the widget
  3207. * needs to be downcasted to a different node than the widget's main element.
  3208. *
  3209. * @property {String/Function} downcast
  3210. */
  3211. /**
  3212. * The object containing functions which can be used To Downcast this widget.
  3213. * Only the one pointed by the {@link #downcast} property will be used.
  3214. *
  3215. * In most cases it is appropriate to use {@link #downcast} directly,
  3216. * because majority of widgets have just one variant of downcasting (or none at all).
  3217. * However, in some cases the widget author may want to expose more than one variant
  3218. * and then this property may be used.
  3219. *
  3220. * downcasts: {
  3221. * // This downcast may transform the widget into the figure element.
  3222. * figure: function() {
  3223. * // ...
  3224. * },
  3225. * // This downcast may transform the widget into the image element with data-* attributes.
  3226. * image: function() {
  3227. * // ...
  3228. * }
  3229. * }
  3230. *
  3231. * // Then, the widget user may choose one of the downcast options when setting up his editor.
  3232. * editor.on( 'widgetDefinition', function( evt ) {
  3233. * if ( evt.data.name == 'image' )
  3234. * evt.data.downcast = 'figure';
  3235. * } );
  3236. *
  3237. * @property downcasts
  3238. */
  3239. /**
  3240. * If set, it will be added as the {@link CKEDITOR.plugins.widget#event-edit} event listener.
  3241. * This means that it will be executed when a widget is being edited.
  3242. * See the {@link CKEDITOR.plugins.widget#method-edit} method.
  3243. *
  3244. * @property {Function} edit
  3245. */
  3246. /**
  3247. * If set, it will be added as the {@link CKEDITOR.plugins.widget#event-data} event listener.
  3248. * This means that it will be executed every time the {@link CKEDITOR.plugins.widget#property-data widget data} changes.
  3249. *
  3250. * @property {Function} data
  3251. */
  3252. /**
  3253. * The method to be executed when the widget's command is executed in order to insert a new widget
  3254. * (widget of this type is not focused). If not defined, then the default action will be
  3255. * performed which means that:
  3256. *
  3257. * * An instance of the widget will be created in a detached {@link CKEDITOR.dom.documentFragment document fragment},
  3258. * * The {@link CKEDITOR.plugins.widget#method-edit} method will be called to trigger widget editing,
  3259. * * The widget element will be inserted inTo DoM.
  3260. *
  3261. * @property {Function} insert
  3262. */
  3263. /**
  3264. * The name of a dialog window which will be opened on {@link CKEDITOR.plugins.widget#method-edit}.
  3265. * If not defined, then the {@link CKEDITOR.plugins.widget#method-edit} method will not perform any action and
  3266. * widget's command will insert a new widget without opening a dialog window first.
  3267. *
  3268. * @property {String} dialog
  3269. */
  3270. /**
  3271. * The template which will be used to create a new widget element (when the widget's command is executed).
  3272. * This string is populated with {@link #defaults default values} by using the {@link CKEDITOR.template} format.
  3273. * Therefore it has to be a valid {@link CKEDITOR.template} argument.
  3274. *
  3275. * @property {String} template
  3276. */
  3277. /**
  3278. * The data object which will be used to populate the data of a newly created widget.
  3279. * See {@link CKEDITOR.plugins.widget#property-data}.
  3280. *
  3281. * defaults: {
  3282. * showCaption: true,
  3283. * align: 'none'
  3284. * }
  3285. *
  3286. * @property defaults
  3287. */
  3288. /**
  3289. * An object containing definitions of widget components (part name => CSS selector).
  3290. *
  3291. * parts: {
  3292. * image: 'img',
  3293. * caption: 'div.caption'
  3294. * }
  3295. *
  3296. * @property parts
  3297. */
  3298. /**
  3299. * An object containing definitions of nested editables (editable name => {@link CKEDITOR.plugins.widget.nestedEditable.definition}).
  3300. * Note that editables *have to* be defined in the same order as they are in DOM / {@link CKEDITOR.plugins.widget.definition#template template}.
  3301. * Otherwise errors will occur when nesting widgets inside each other.
  3302. *
  3303. * editables: {
  3304. * header: 'h1',
  3305. * content: {
  3306. * selector: 'div.content',
  3307. * allowedContent: 'p strong em; a[!href]'
  3308. * }
  3309. * }
  3310. *
  3311. * @property editables
  3312. */
  3313. /**
  3314. * The function used to obtain an accessibility label for the widget. It might be used to make
  3315. * the widget labels as precise as possible, since it has access to the widget instance.
  3316. *
  3317. * If not specified, the default implementation will use the {@link #pathName} or the main
  3318. * {@link CKEDITOR.plugins.widget#element element} tag name.
  3319. *
  3320. * @property {Function} getLabel
  3321. */
  3322. /**
  3323. * The widget name displayed in the elements path.
  3324. *
  3325. * @property {String} pathName
  3326. */
  3327. /**
  3328. * If set to `true`, the widget's element will be covered with a transparent mask.
  3329. * This will prevent its content from being clickable, which matters in case
  3330. * of special elements like embedded Flash or iframes that generate a separate "context".
  3331. *
  3332. * @property {Boolean} mask
  3333. */
  3334. /**
  3335. * If set to `true/false`, it will force the widget to be either an inline or a block widget.
  3336. * If not set, the widget type will be determined from the widget element.
  3337. *
  3338. * Widget type influences whether a block (`div`) or an inline (`span`) element is used
  3339. * for the wrapper.
  3340. *
  3341. * @property {Boolean} inline
  3342. */
  3343. /**
  3344. * The label for the widget toolbar button.
  3345. *
  3346. * editor.widgets.add( 'simplebox', {
  3347. * button: 'Create a simple box'
  3348. * } );
  3349. *
  3350. * editor.widgets.add( 'simplebox', {
  3351. * button: editor.lang.simplebox.title
  3352. * } );
  3353. *
  3354. * @property {String} button
  3355. */
  3356. /**
  3357. * Whether widget should be draggable. Defaults to `true`.
  3358. * If set to `false` drag handler will not be displayed when hovering widget.
  3359. *
  3360. * @property {Boolean} draggable
  3361. */
  3362. /**
  3363. * Names of element(s) (separated by spaces) for which the {@link CKEDITOR.filter} should allow classes
  3364. * defined in the widget styles. For example if your widget is upcasted from a simple `<div>`
  3365. * element, then in order to make it styleable you can set:
  3366. *
  3367. * editor.widgets.add( 'customWidget', {
  3368. * upcast: function( element ) {
  3369. * return element.name == 'div';
  3370. * },
  3371. *
  3372. * // ...
  3373. *
  3374. * styleableElements: 'div'
  3375. * } );
  3376. *
  3377. * Then, when the following style is defined:
  3378. *
  3379. * {
  3380. * name: 'Thick border', type: 'widget', widget: 'customWidget',
  3381. * attributes: { 'class': 'thickBorder' }
  3382. * }
  3383. *
  3384. * a rule allowing the `thickBorder` class for `div` elements will be registered in the {@link CKEDITOR.filter}.
  3385. *
  3386. * If you need to have more freedom when transforming widget style to allowed content rules,
  3387. * you can use the {@link #styleToAllowedContentRules} callback.
  3388. *
  3389. * @since 4.4
  3390. * @property {String} styleableElements
  3391. */
  3392. /**
  3393. * Function transforming custom widget's {@link CKEDITOR.style} instance into
  3394. * {@link CKEDITOR.filter.allowedContentRules}. It may be used when a static
  3395. * {@link #styleableElements} property is not enough to inform the {@link CKEDITOR.filter}
  3396. * what HTML features should be enabled when allowing the given style.
  3397. *
  3398. * In most cases, when style's classes just have to be added to element name(s) used by
  3399. * the widget element, it is recommended to use simpler {@link #styleableElements} property.
  3400. *
  3401. * In order to get parsed classes from the style definition you can use
  3402. * {@link CKEDITOR.style.customHandlers.widget#getClassesArray}.
  3403. *
  3404. * For example, if you want to use the [object format of allowed content rules](#!/guide/dev_allowed_content_rules-section-object-format),
  3405. * to specify `match` validator, your implementation could look like this:
  3406. *
  3407. * editor.widgets.add( 'customWidget', {
  3408. * // ...
  3409. *
  3410. * styleToAllowedContentRules: funciton( style ) {
  3411. * // Retrieve classes defined in the style.
  3412. * var classes = style.getClassesArray();
  3413. *
  3414. * // Do something crazy - for example return allowed content rules in object format,
  3415. * // with custom match property and propertiesOnly flag.
  3416. * return {
  3417. * h1: {
  3418. * match: isWidgetElement,
  3419. * propertiesOnly: true,
  3420. * classes: classes
  3421. * }
  3422. * };
  3423. * }
  3424. * } );
  3425. *
  3426. * @since 4.4
  3427. * @property {Function} styleToAllowedContentRules
  3428. * @param {CKEDITOR.style.customHandlers.widget} style The style to be transformed.
  3429. * @returns {CKEDITOR.filter.allowedContentRules}
  3430. */
  3431. /**
  3432. * This is an abstract class that describes the definition of a widget's nested editable.
  3433. * It is a type of values in the {@link CKEDITOR.plugins.widget.definition#editables} object.
  3434. *
  3435. * In the simplest case the definition is a string which is a CSS selector used to
  3436. * find an element that will become a nested editable inside the widget. Note that
  3437. * the widget element can be a nested editable, too.
  3438. *
  3439. * In the more advanced case a definition is an object with a required `selector` property.
  3440. *
  3441. * editables: {
  3442. * header: 'h1',
  3443. * content: {
  3444. * selector: 'div.content',
  3445. * allowedContent: 'p strong em; a[!href]'
  3446. * }
  3447. * }
  3448. *
  3449. * @class CKEDITOR.plugins.widget.nestedEditable.definition
  3450. * @abstract
  3451. */
  3452. /**
  3453. * The CSS selector used to find an element which will become a nested editable.
  3454. *
  3455. * @property {String} selector
  3456. */
  3457. /**
  3458. * The [Advanced Content Filter](#!/guide/dev_advanced_content_filter) rules
  3459. * which will be used to limit the content allowed in this nested editable.
  3460. * This option is similar to {@link CKEDITOR.config#allowedContent} and one can
  3461. * use it to limit the editor features available in the nested editable.
  3462. *
  3463. * @property {CKEDITOR.filter.allowedContentRules} allowedContent
  3464. */
  3465. /**
  3466. * Nested editable name displayed in elements path.
  3467. *
  3468. * @property {String} pathName
  3469. */