星火管控前端
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 159KB

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