tinymce.js 1.7 MB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214821582168217821882198220822182228223822482258226822782288229823082318232823382348235823682378238823982408241824282438244824582468247824882498250825182528253825482558256825782588259826082618262826382648265826682678268826982708271827282738274827582768277827882798280828182828283828482858286828782888289829082918292829382948295829682978298829983008301830283038304830583068307830883098310831183128313831483158316831783188319832083218322832383248325832683278328832983308331833283338334833583368337833883398340834183428343834483458346834783488349835083518352835383548355835683578358835983608361836283638364836583668367836883698370837183728373837483758376837783788379838083818382838383848385838683878388838983908391839283938394839583968397839883998400840184028403840484058406840784088409841084118412841384148415841684178418841984208421842284238424842584268427842884298430843184328433843484358436843784388439844084418442844384448445844684478448844984508451845284538454845584568457845884598460846184628463846484658466846784688469847084718472847384748475847684778478847984808481848284838484848584868487848884898490849184928493849484958496849784988499850085018502850385048505850685078508850985108511851285138514851585168517851885198520852185228523852485258526852785288529853085318532853385348535853685378538853985408541854285438544854585468547854885498550855185528553855485558556855785588559856085618562856385648565856685678568856985708571857285738574857585768577857885798580858185828583858485858586858785888589859085918592859385948595859685978598859986008601860286038604860586068607860886098610861186128613861486158616861786188619862086218622862386248625862686278628862986308631863286338634863586368637863886398640864186428643864486458646864786488649865086518652865386548655865686578658865986608661866286638664866586668667866886698670867186728673867486758676867786788679868086818682868386848685868686878688868986908691869286938694869586968697869886998700870187028703870487058706870787088709871087118712871387148715871687178718871987208721872287238724872587268727872887298730873187328733873487358736873787388739874087418742874387448745874687478748874987508751875287538754875587568757875887598760876187628763876487658766876787688769877087718772877387748775877687778778877987808781878287838784878587868787878887898790879187928793879487958796879787988799880088018802880388048805880688078808880988108811881288138814881588168817881888198820882188228823882488258826882788288829883088318832883388348835883688378838883988408841884288438844884588468847884888498850885188528853885488558856885788588859886088618862886388648865886688678868886988708871887288738874887588768877887888798880888188828883888488858886888788888889889088918892889388948895889688978898889989008901890289038904890589068907890889098910891189128913891489158916891789188919892089218922892389248925892689278928892989308931893289338934893589368937893889398940894189428943894489458946894789488949895089518952895389548955895689578958895989608961896289638964896589668967896889698970897189728973897489758976897789788979898089818982898389848985898689878988898989908991899289938994899589968997899889999000900190029003900490059006900790089009901090119012901390149015901690179018901990209021902290239024902590269027902890299030903190329033903490359036903790389039904090419042904390449045904690479048904990509051905290539054905590569057905890599060906190629063906490659066906790689069907090719072907390749075907690779078907990809081908290839084908590869087908890899090909190929093909490959096909790989099910091019102910391049105910691079108910991109111911291139114911591169117911891199120912191229123912491259126912791289129913091319132913391349135913691379138913991409141914291439144914591469147914891499150915191529153915491559156915791589159916091619162916391649165916691679168916991709171917291739174917591769177917891799180918191829183918491859186918791889189919091919192919391949195919691979198919992009201920292039204920592069207920892099210921192129213921492159216921792189219922092219222922392249225922692279228922992309231923292339234923592369237923892399240924192429243924492459246924792489249925092519252925392549255925692579258925992609261926292639264926592669267926892699270927192729273927492759276927792789279928092819282928392849285928692879288928992909291929292939294929592969297929892999300930193029303930493059306930793089309931093119312931393149315931693179318931993209321932293239324932593269327932893299330933193329333933493359336933793389339934093419342934393449345934693479348934993509351935293539354935593569357935893599360936193629363936493659366936793689369937093719372937393749375937693779378937993809381938293839384938593869387938893899390939193929393939493959396939793989399940094019402940394049405940694079408940994109411941294139414941594169417941894199420942194229423942494259426942794289429943094319432943394349435943694379438943994409441944294439444944594469447944894499450945194529453945494559456945794589459946094619462946394649465946694679468946994709471947294739474947594769477947894799480948194829483948494859486948794889489949094919492949394949495949694979498949995009501950295039504950595069507950895099510951195129513951495159516951795189519952095219522952395249525952695279528952995309531953295339534953595369537953895399540954195429543954495459546954795489549955095519552955395549555955695579558955995609561956295639564956595669567956895699570957195729573957495759576957795789579958095819582958395849585958695879588958995909591959295939594959595969597959895999600960196029603960496059606960796089609961096119612961396149615961696179618961996209621962296239624962596269627962896299630963196329633963496359636963796389639964096419642964396449645964696479648964996509651965296539654965596569657965896599660966196629663966496659666966796689669967096719672967396749675967696779678967996809681968296839684968596869687968896899690969196929693969496959696969796989699970097019702970397049705970697079708970997109711971297139714971597169717971897199720972197229723972497259726972797289729973097319732973397349735973697379738973997409741974297439744974597469747974897499750975197529753975497559756975797589759976097619762976397649765976697679768976997709771977297739774977597769777977897799780978197829783978497859786978797889789979097919792979397949795979697979798979998009801980298039804980598069807980898099810981198129813981498159816981798189819982098219822982398249825982698279828982998309831983298339834983598369837983898399840984198429843984498459846984798489849985098519852985398549855985698579858985998609861986298639864986598669867986898699870987198729873987498759876987798789879988098819882988398849885988698879888988998909891989298939894989598969897989898999900990199029903990499059906990799089909991099119912991399149915991699179918991999209921992299239924992599269927992899299930993199329933993499359936993799389939994099419942994399449945994699479948994999509951995299539954995599569957995899599960996199629963996499659966996799689969997099719972997399749975997699779978997999809981998299839984998599869987998899899990999199929993999499959996999799989999100001000110002100031000410005100061000710008100091001010011100121001310014100151001610017100181001910020100211002210023100241002510026100271002810029100301003110032100331003410035100361003710038100391004010041100421004310044100451004610047100481004910050100511005210053100541005510056100571005810059100601006110062100631006410065100661006710068100691007010071100721007310074100751007610077100781007910080100811008210083100841008510086100871008810089100901009110092100931009410095100961009710098100991010010101101021010310104101051010610107101081010910110101111011210113101141011510116101171011810119101201012110122101231012410125101261012710128101291013010131101321013310134101351013610137101381013910140101411014210143101441014510146101471014810149101501015110152101531015410155101561015710158101591016010161101621016310164101651016610167101681016910170101711017210173101741017510176101771017810179101801018110182101831018410185101861018710188101891019010191101921019310194101951019610197101981019910200102011020210203102041020510206102071020810209102101021110212102131021410215102161021710218102191022010221102221022310224102251022610227102281022910230102311023210233102341023510236102371023810239102401024110242102431024410245102461024710248102491025010251102521025310254102551025610257102581025910260102611026210263102641026510266102671026810269102701027110272102731027410275102761027710278102791028010281102821028310284102851028610287102881028910290102911029210293102941029510296102971029810299103001030110302103031030410305103061030710308103091031010311103121031310314103151031610317103181031910320103211032210323103241032510326103271032810329103301033110332103331033410335103361033710338103391034010341103421034310344103451034610347103481034910350103511035210353103541035510356103571035810359103601036110362103631036410365103661036710368103691037010371103721037310374103751037610377103781037910380103811038210383103841038510386103871038810389103901039110392103931039410395103961039710398103991040010401104021040310404104051040610407104081040910410104111041210413104141041510416104171041810419104201042110422104231042410425104261042710428104291043010431104321043310434104351043610437104381043910440104411044210443104441044510446104471044810449104501045110452104531045410455104561045710458104591046010461104621046310464104651046610467104681046910470104711047210473104741047510476104771047810479104801048110482104831048410485104861048710488104891049010491104921049310494104951049610497104981049910500105011050210503105041050510506105071050810509105101051110512105131051410515105161051710518105191052010521105221052310524105251052610527105281052910530105311053210533105341053510536105371053810539105401054110542105431054410545105461054710548105491055010551105521055310554105551055610557105581055910560105611056210563105641056510566105671056810569105701057110572105731057410575105761057710578105791058010581105821058310584105851058610587105881058910590105911059210593105941059510596105971059810599106001060110602106031060410605106061060710608106091061010611106121061310614106151061610617106181061910620106211062210623106241062510626106271062810629106301063110632106331063410635106361063710638106391064010641106421064310644106451064610647106481064910650106511065210653106541065510656106571065810659106601066110662106631066410665106661066710668106691067010671106721067310674106751067610677106781067910680106811068210683106841068510686106871068810689106901069110692106931069410695106961069710698106991070010701107021070310704107051070610707107081070910710107111071210713107141071510716107171071810719107201072110722107231072410725107261072710728107291073010731107321073310734107351073610737107381073910740107411074210743107441074510746107471074810749107501075110752107531075410755107561075710758107591076010761107621076310764107651076610767107681076910770107711077210773107741077510776107771077810779107801078110782107831078410785107861078710788107891079010791107921079310794107951079610797107981079910800108011080210803108041080510806108071080810809108101081110812108131081410815108161081710818108191082010821108221082310824108251082610827108281082910830108311083210833108341083510836108371083810839108401084110842108431084410845108461084710848108491085010851108521085310854108551085610857108581085910860108611086210863108641086510866108671086810869108701087110872108731087410875108761087710878108791088010881108821088310884108851088610887108881088910890108911089210893108941089510896108971089810899109001090110902109031090410905109061090710908109091091010911109121091310914109151091610917109181091910920109211092210923109241092510926109271092810929109301093110932109331093410935109361093710938109391094010941109421094310944109451094610947109481094910950109511095210953109541095510956109571095810959109601096110962109631096410965109661096710968109691097010971109721097310974109751097610977109781097910980109811098210983109841098510986109871098810989109901099110992109931099410995109961099710998109991100011001110021100311004110051100611007110081100911010110111101211013110141101511016110171101811019110201102111022110231102411025110261102711028110291103011031110321103311034110351103611037110381103911040110411104211043110441104511046110471104811049110501105111052110531105411055110561105711058110591106011061110621106311064110651106611067110681106911070110711107211073110741107511076110771107811079110801108111082110831108411085110861108711088110891109011091110921109311094110951109611097110981109911100111011110211103111041110511106111071110811109111101111111112111131111411115111161111711118111191112011121111221112311124111251112611127111281112911130111311113211133111341113511136111371113811139111401114111142111431114411145111461114711148111491115011151111521115311154111551115611157111581115911160111611116211163111641116511166111671116811169111701117111172111731117411175111761117711178111791118011181111821118311184111851118611187111881118911190111911119211193111941119511196111971119811199112001120111202112031120411205112061120711208112091121011211112121121311214112151121611217112181121911220112211122211223112241122511226112271122811229112301123111232112331123411235112361123711238112391124011241112421124311244112451124611247112481124911250112511125211253112541125511256112571125811259112601126111262112631126411265112661126711268112691127011271112721127311274112751127611277112781127911280112811128211283112841128511286112871128811289112901129111292112931129411295112961129711298112991130011301113021130311304113051130611307113081130911310113111131211313113141131511316113171131811319113201132111322113231132411325113261132711328113291133011331113321133311334113351133611337113381133911340113411134211343113441134511346113471134811349113501135111352113531135411355113561135711358113591136011361113621136311364113651136611367113681136911370113711137211373113741137511376113771137811379113801138111382113831138411385113861138711388113891139011391113921139311394113951139611397113981139911400114011140211403114041140511406114071140811409114101141111412114131141411415114161141711418114191142011421114221142311424114251142611427114281142911430114311143211433114341143511436114371143811439114401144111442114431144411445114461144711448114491145011451114521145311454114551145611457114581145911460114611146211463114641146511466114671146811469114701147111472114731147411475114761147711478114791148011481114821148311484114851148611487114881148911490114911149211493114941149511496114971149811499115001150111502115031150411505115061150711508115091151011511115121151311514115151151611517115181151911520115211152211523115241152511526115271152811529115301153111532115331153411535115361153711538115391154011541115421154311544115451154611547115481154911550115511155211553115541155511556115571155811559115601156111562115631156411565115661156711568115691157011571115721157311574115751157611577115781157911580115811158211583115841158511586115871158811589115901159111592115931159411595115961159711598115991160011601116021160311604116051160611607116081160911610116111161211613116141161511616116171161811619116201162111622116231162411625116261162711628116291163011631116321163311634116351163611637116381163911640116411164211643116441164511646116471164811649116501165111652116531165411655116561165711658116591166011661116621166311664116651166611667116681166911670116711167211673116741167511676116771167811679116801168111682116831168411685116861168711688116891169011691116921169311694116951169611697116981169911700117011170211703117041170511706117071170811709117101171111712117131171411715117161171711718117191172011721117221172311724117251172611727117281172911730117311173211733117341173511736117371173811739117401174111742117431174411745117461174711748117491175011751117521175311754117551175611757117581175911760117611176211763117641176511766117671176811769117701177111772117731177411775117761177711778117791178011781117821178311784117851178611787117881178911790117911179211793117941179511796117971179811799118001180111802118031180411805118061180711808118091181011811118121181311814118151181611817118181181911820118211182211823118241182511826118271182811829118301183111832118331183411835118361183711838118391184011841118421184311844118451184611847118481184911850118511185211853118541185511856118571185811859118601186111862118631186411865118661186711868118691187011871118721187311874118751187611877118781187911880118811188211883118841188511886118871188811889118901189111892118931189411895118961189711898118991190011901119021190311904119051190611907119081190911910119111191211913119141191511916119171191811919119201192111922119231192411925119261192711928119291193011931119321193311934119351193611937119381193911940119411194211943119441194511946119471194811949119501195111952119531195411955119561195711958119591196011961119621196311964119651196611967119681196911970119711197211973119741197511976119771197811979119801198111982119831198411985119861198711988119891199011991119921199311994119951199611997119981199912000120011200212003120041200512006120071200812009120101201112012120131201412015120161201712018120191202012021120221202312024120251202612027120281202912030120311203212033120341203512036120371203812039120401204112042120431204412045120461204712048120491205012051120521205312054120551205612057120581205912060120611206212063120641206512066120671206812069120701207112072120731207412075120761207712078120791208012081120821208312084120851208612087120881208912090120911209212093120941209512096120971209812099121001210112102121031210412105121061210712108121091211012111121121211312114121151211612117121181211912120121211212212123121241212512126121271212812129121301213112132121331213412135121361213712138121391214012141121421214312144121451214612147121481214912150121511215212153121541215512156121571215812159121601216112162121631216412165121661216712168121691217012171121721217312174121751217612177121781217912180121811218212183121841218512186121871218812189121901219112192121931219412195121961219712198121991220012201122021220312204122051220612207122081220912210122111221212213122141221512216122171221812219122201222112222122231222412225122261222712228122291223012231122321223312234122351223612237122381223912240122411224212243122441224512246122471224812249122501225112252122531225412255122561225712258122591226012261122621226312264122651226612267122681226912270122711227212273122741227512276122771227812279122801228112282122831228412285122861228712288122891229012291122921229312294122951229612297122981229912300123011230212303123041230512306123071230812309123101231112312123131231412315123161231712318123191232012321123221232312324123251232612327123281232912330123311233212333123341233512336123371233812339123401234112342123431234412345123461234712348123491235012351123521235312354123551235612357123581235912360123611236212363123641236512366123671236812369123701237112372123731237412375123761237712378123791238012381123821238312384123851238612387123881238912390123911239212393123941239512396123971239812399124001240112402124031240412405124061240712408124091241012411124121241312414124151241612417124181241912420124211242212423124241242512426124271242812429124301243112432124331243412435124361243712438124391244012441124421244312444124451244612447124481244912450124511245212453124541245512456124571245812459124601246112462124631246412465124661246712468124691247012471124721247312474124751247612477124781247912480124811248212483124841248512486124871248812489124901249112492124931249412495124961249712498124991250012501125021250312504125051250612507125081250912510125111251212513125141251512516125171251812519125201252112522125231252412525125261252712528125291253012531125321253312534125351253612537125381253912540125411254212543125441254512546125471254812549125501255112552125531255412555125561255712558125591256012561125621256312564125651256612567125681256912570125711257212573125741257512576125771257812579125801258112582125831258412585125861258712588125891259012591125921259312594125951259612597125981259912600126011260212603126041260512606126071260812609126101261112612126131261412615126161261712618126191262012621126221262312624126251262612627126281262912630126311263212633126341263512636126371263812639126401264112642126431264412645126461264712648126491265012651126521265312654126551265612657126581265912660126611266212663126641266512666126671266812669126701267112672126731267412675126761267712678126791268012681126821268312684126851268612687126881268912690126911269212693126941269512696126971269812699127001270112702127031270412705127061270712708127091271012711127121271312714127151271612717127181271912720127211272212723127241272512726127271272812729127301273112732127331273412735127361273712738127391274012741127421274312744127451274612747127481274912750127511275212753127541275512756127571275812759127601276112762127631276412765127661276712768127691277012771127721277312774127751277612777127781277912780127811278212783127841278512786127871278812789127901279112792127931279412795127961279712798127991280012801128021280312804128051280612807128081280912810128111281212813128141281512816128171281812819128201282112822128231282412825128261282712828128291283012831128321283312834128351283612837128381283912840128411284212843128441284512846128471284812849128501285112852128531285412855128561285712858128591286012861128621286312864128651286612867128681286912870128711287212873128741287512876128771287812879128801288112882128831288412885128861288712888128891289012891128921289312894128951289612897128981289912900129011290212903129041290512906129071290812909129101291112912129131291412915129161291712918129191292012921129221292312924129251292612927129281292912930129311293212933129341293512936129371293812939129401294112942129431294412945129461294712948129491295012951129521295312954129551295612957129581295912960129611296212963129641296512966129671296812969129701297112972129731297412975129761297712978129791298012981129821298312984129851298612987129881298912990129911299212993129941299512996129971299812999130001300113002130031300413005130061300713008130091301013011130121301313014130151301613017130181301913020130211302213023130241302513026130271302813029130301303113032130331303413035130361303713038130391304013041130421304313044130451304613047130481304913050130511305213053130541305513056130571305813059130601306113062130631306413065130661306713068130691307013071130721307313074130751307613077130781307913080130811308213083130841308513086130871308813089130901309113092130931309413095130961309713098130991310013101131021310313104131051310613107131081310913110131111311213113131141311513116131171311813119131201312113122131231312413125131261312713128131291313013131131321313313134131351313613137131381313913140131411314213143131441314513146131471314813149131501315113152131531315413155131561315713158131591316013161131621316313164131651316613167131681316913170131711317213173131741317513176131771317813179131801318113182131831318413185131861318713188131891319013191131921319313194131951319613197131981319913200132011320213203132041320513206132071320813209132101321113212132131321413215132161321713218132191322013221132221322313224132251322613227132281322913230132311323213233132341323513236132371323813239132401324113242132431324413245132461324713248132491325013251132521325313254132551325613257132581325913260132611326213263132641326513266132671326813269132701327113272132731327413275132761327713278132791328013281132821328313284132851328613287132881328913290132911329213293132941329513296132971329813299133001330113302133031330413305133061330713308133091331013311133121331313314133151331613317133181331913320133211332213323133241332513326133271332813329133301333113332133331333413335133361333713338133391334013341133421334313344133451334613347133481334913350133511335213353133541335513356133571335813359133601336113362133631336413365133661336713368133691337013371133721337313374133751337613377133781337913380133811338213383133841338513386133871338813389133901339113392133931339413395133961339713398133991340013401134021340313404134051340613407134081340913410134111341213413134141341513416134171341813419134201342113422134231342413425134261342713428134291343013431134321343313434134351343613437134381343913440134411344213443134441344513446134471344813449134501345113452134531345413455134561345713458134591346013461134621346313464134651346613467134681346913470134711347213473134741347513476134771347813479134801348113482134831348413485134861348713488134891349013491134921349313494134951349613497134981349913500135011350213503135041350513506135071350813509135101351113512135131351413515135161351713518135191352013521135221352313524135251352613527135281352913530135311353213533135341353513536135371353813539135401354113542135431354413545135461354713548135491355013551135521355313554135551355613557135581355913560135611356213563135641356513566135671356813569135701357113572135731357413575135761357713578135791358013581135821358313584135851358613587135881358913590135911359213593135941359513596135971359813599136001360113602136031360413605136061360713608136091361013611136121361313614136151361613617136181361913620136211362213623136241362513626136271362813629136301363113632136331363413635136361363713638136391364013641136421364313644136451364613647136481364913650136511365213653136541365513656136571365813659136601366113662136631366413665136661366713668136691367013671136721367313674136751367613677136781367913680136811368213683136841368513686136871368813689136901369113692136931369413695136961369713698136991370013701137021370313704137051370613707137081370913710137111371213713137141371513716137171371813719137201372113722137231372413725137261372713728137291373013731137321373313734137351373613737137381373913740137411374213743137441374513746137471374813749137501375113752137531375413755137561375713758137591376013761137621376313764137651376613767137681376913770137711377213773137741377513776137771377813779137801378113782137831378413785137861378713788137891379013791137921379313794137951379613797137981379913800138011380213803138041380513806138071380813809138101381113812138131381413815138161381713818138191382013821138221382313824138251382613827138281382913830138311383213833138341383513836138371383813839138401384113842138431384413845138461384713848138491385013851138521385313854138551385613857138581385913860138611386213863138641386513866138671386813869138701387113872138731387413875138761387713878138791388013881138821388313884138851388613887138881388913890138911389213893138941389513896138971389813899139001390113902139031390413905139061390713908139091391013911139121391313914139151391613917139181391913920139211392213923139241392513926139271392813929139301393113932139331393413935139361393713938139391394013941139421394313944139451394613947139481394913950139511395213953139541395513956139571395813959139601396113962139631396413965139661396713968139691397013971139721397313974139751397613977139781397913980139811398213983139841398513986139871398813989139901399113992139931399413995139961399713998139991400014001140021400314004140051400614007140081400914010140111401214013140141401514016140171401814019140201402114022140231402414025140261402714028140291403014031140321403314034140351403614037140381403914040140411404214043140441404514046140471404814049140501405114052140531405414055140561405714058140591406014061140621406314064140651406614067140681406914070140711407214073140741407514076140771407814079140801408114082140831408414085140861408714088140891409014091140921409314094140951409614097140981409914100141011410214103141041410514106141071410814109141101411114112141131411414115141161411714118141191412014121141221412314124141251412614127141281412914130141311413214133141341413514136141371413814139141401414114142141431414414145141461414714148141491415014151141521415314154141551415614157141581415914160141611416214163141641416514166141671416814169141701417114172141731417414175141761417714178141791418014181141821418314184141851418614187141881418914190141911419214193141941419514196141971419814199142001420114202142031420414205142061420714208142091421014211142121421314214142151421614217142181421914220142211422214223142241422514226142271422814229142301423114232142331423414235142361423714238142391424014241142421424314244142451424614247142481424914250142511425214253142541425514256142571425814259142601426114262142631426414265142661426714268142691427014271142721427314274142751427614277142781427914280142811428214283142841428514286142871428814289142901429114292142931429414295142961429714298142991430014301143021430314304143051430614307143081430914310143111431214313143141431514316143171431814319143201432114322143231432414325143261432714328143291433014331143321433314334143351433614337143381433914340143411434214343143441434514346143471434814349143501435114352143531435414355143561435714358143591436014361143621436314364143651436614367143681436914370143711437214373143741437514376143771437814379143801438114382143831438414385143861438714388143891439014391143921439314394143951439614397143981439914400144011440214403144041440514406144071440814409144101441114412144131441414415144161441714418144191442014421144221442314424144251442614427144281442914430144311443214433144341443514436144371443814439144401444114442144431444414445144461444714448144491445014451144521445314454144551445614457144581445914460144611446214463144641446514466144671446814469144701447114472144731447414475144761447714478144791448014481144821448314484144851448614487144881448914490144911449214493144941449514496144971449814499145001450114502145031450414505145061450714508145091451014511145121451314514145151451614517145181451914520145211452214523145241452514526145271452814529145301453114532145331453414535145361453714538145391454014541145421454314544145451454614547145481454914550145511455214553145541455514556145571455814559145601456114562145631456414565145661456714568145691457014571145721457314574145751457614577145781457914580145811458214583145841458514586145871458814589145901459114592145931459414595145961459714598145991460014601146021460314604146051460614607146081460914610146111461214613146141461514616146171461814619146201462114622146231462414625146261462714628146291463014631146321463314634146351463614637146381463914640146411464214643146441464514646146471464814649146501465114652146531465414655146561465714658146591466014661146621466314664146651466614667146681466914670146711467214673146741467514676146771467814679146801468114682146831468414685146861468714688146891469014691146921469314694146951469614697146981469914700147011470214703147041470514706147071470814709147101471114712147131471414715147161471714718147191472014721147221472314724147251472614727147281472914730147311473214733147341473514736147371473814739147401474114742147431474414745147461474714748147491475014751147521475314754147551475614757147581475914760147611476214763147641476514766147671476814769147701477114772147731477414775147761477714778147791478014781147821478314784147851478614787147881478914790147911479214793147941479514796147971479814799148001480114802148031480414805148061480714808148091481014811148121481314814148151481614817148181481914820148211482214823148241482514826148271482814829148301483114832148331483414835148361483714838148391484014841148421484314844148451484614847148481484914850148511485214853148541485514856148571485814859148601486114862148631486414865148661486714868148691487014871148721487314874148751487614877148781487914880148811488214883148841488514886148871488814889148901489114892148931489414895148961489714898148991490014901149021490314904149051490614907149081490914910149111491214913149141491514916149171491814919149201492114922149231492414925149261492714928149291493014931149321493314934149351493614937149381493914940149411494214943149441494514946149471494814949149501495114952149531495414955149561495714958149591496014961149621496314964149651496614967149681496914970149711497214973149741497514976149771497814979149801498114982149831498414985149861498714988149891499014991149921499314994149951499614997149981499915000150011500215003150041500515006150071500815009150101501115012150131501415015150161501715018150191502015021150221502315024150251502615027150281502915030150311503215033150341503515036150371503815039150401504115042150431504415045150461504715048150491505015051150521505315054150551505615057150581505915060150611506215063150641506515066150671506815069150701507115072150731507415075150761507715078150791508015081150821508315084150851508615087150881508915090150911509215093150941509515096150971509815099151001510115102151031510415105151061510715108151091511015111151121511315114151151511615117151181511915120151211512215123151241512515126151271512815129151301513115132151331513415135151361513715138151391514015141151421514315144151451514615147151481514915150151511515215153151541515515156151571515815159151601516115162151631516415165151661516715168151691517015171151721517315174151751517615177151781517915180151811518215183151841518515186151871518815189151901519115192151931519415195151961519715198151991520015201152021520315204152051520615207152081520915210152111521215213152141521515216152171521815219152201522115222152231522415225152261522715228152291523015231152321523315234152351523615237152381523915240152411524215243152441524515246152471524815249152501525115252152531525415255152561525715258152591526015261152621526315264152651526615267152681526915270152711527215273152741527515276152771527815279152801528115282152831528415285152861528715288152891529015291152921529315294152951529615297152981529915300153011530215303153041530515306153071530815309153101531115312153131531415315153161531715318153191532015321153221532315324153251532615327153281532915330153311533215333153341533515336153371533815339153401534115342153431534415345153461534715348153491535015351153521535315354153551535615357153581535915360153611536215363153641536515366153671536815369153701537115372153731537415375153761537715378153791538015381153821538315384153851538615387153881538915390153911539215393153941539515396153971539815399154001540115402154031540415405154061540715408154091541015411154121541315414154151541615417154181541915420154211542215423154241542515426154271542815429154301543115432154331543415435154361543715438154391544015441154421544315444154451544615447154481544915450154511545215453154541545515456154571545815459154601546115462154631546415465154661546715468154691547015471154721547315474154751547615477154781547915480154811548215483154841548515486154871548815489154901549115492154931549415495154961549715498154991550015501155021550315504155051550615507155081550915510155111551215513155141551515516155171551815519155201552115522155231552415525155261552715528155291553015531155321553315534155351553615537155381553915540155411554215543155441554515546155471554815549155501555115552155531555415555155561555715558155591556015561155621556315564155651556615567155681556915570155711557215573155741557515576155771557815579155801558115582155831558415585155861558715588155891559015591155921559315594155951559615597155981559915600156011560215603156041560515606156071560815609156101561115612156131561415615156161561715618156191562015621156221562315624156251562615627156281562915630156311563215633156341563515636156371563815639156401564115642156431564415645156461564715648156491565015651156521565315654156551565615657156581565915660156611566215663156641566515666156671566815669156701567115672156731567415675156761567715678156791568015681156821568315684156851568615687156881568915690156911569215693156941569515696156971569815699157001570115702157031570415705157061570715708157091571015711157121571315714157151571615717157181571915720157211572215723157241572515726157271572815729157301573115732157331573415735157361573715738157391574015741157421574315744157451574615747157481574915750157511575215753157541575515756157571575815759157601576115762157631576415765157661576715768157691577015771157721577315774157751577615777157781577915780157811578215783157841578515786157871578815789157901579115792157931579415795157961579715798157991580015801158021580315804158051580615807158081580915810158111581215813158141581515816158171581815819158201582115822158231582415825158261582715828158291583015831158321583315834158351583615837158381583915840158411584215843158441584515846158471584815849158501585115852158531585415855158561585715858158591586015861158621586315864158651586615867158681586915870158711587215873158741587515876158771587815879158801588115882158831588415885158861588715888158891589015891158921589315894158951589615897158981589915900159011590215903159041590515906159071590815909159101591115912159131591415915159161591715918159191592015921159221592315924159251592615927159281592915930159311593215933159341593515936159371593815939159401594115942159431594415945159461594715948159491595015951159521595315954159551595615957159581595915960159611596215963159641596515966159671596815969159701597115972159731597415975159761597715978159791598015981159821598315984159851598615987159881598915990159911599215993159941599515996159971599815999160001600116002160031600416005160061600716008160091601016011160121601316014160151601616017160181601916020160211602216023160241602516026160271602816029160301603116032160331603416035160361603716038160391604016041160421604316044160451604616047160481604916050160511605216053160541605516056160571605816059160601606116062160631606416065160661606716068160691607016071160721607316074160751607616077160781607916080160811608216083160841608516086160871608816089160901609116092160931609416095160961609716098160991610016101161021610316104161051610616107161081610916110161111611216113161141611516116161171611816119161201612116122161231612416125161261612716128161291613016131161321613316134161351613616137161381613916140161411614216143161441614516146161471614816149161501615116152161531615416155161561615716158161591616016161161621616316164161651616616167161681616916170161711617216173161741617516176161771617816179161801618116182161831618416185161861618716188161891619016191161921619316194161951619616197161981619916200162011620216203162041620516206162071620816209162101621116212162131621416215162161621716218162191622016221162221622316224162251622616227162281622916230162311623216233162341623516236162371623816239162401624116242162431624416245162461624716248162491625016251162521625316254162551625616257162581625916260162611626216263162641626516266162671626816269162701627116272162731627416275162761627716278162791628016281162821628316284162851628616287162881628916290162911629216293162941629516296162971629816299163001630116302163031630416305163061630716308163091631016311163121631316314163151631616317163181631916320163211632216323163241632516326163271632816329163301633116332163331633416335163361633716338163391634016341163421634316344163451634616347163481634916350163511635216353163541635516356163571635816359163601636116362163631636416365163661636716368163691637016371163721637316374163751637616377163781637916380163811638216383163841638516386163871638816389163901639116392163931639416395163961639716398163991640016401164021640316404164051640616407164081640916410164111641216413164141641516416164171641816419164201642116422164231642416425164261642716428164291643016431164321643316434164351643616437164381643916440164411644216443164441644516446164471644816449164501645116452164531645416455164561645716458164591646016461164621646316464164651646616467164681646916470164711647216473164741647516476164771647816479164801648116482164831648416485164861648716488164891649016491164921649316494164951649616497164981649916500165011650216503165041650516506165071650816509165101651116512165131651416515165161651716518165191652016521165221652316524165251652616527165281652916530165311653216533165341653516536165371653816539165401654116542165431654416545165461654716548165491655016551165521655316554165551655616557165581655916560165611656216563165641656516566165671656816569165701657116572165731657416575165761657716578165791658016581165821658316584165851658616587165881658916590165911659216593165941659516596165971659816599166001660116602166031660416605166061660716608166091661016611166121661316614166151661616617166181661916620166211662216623166241662516626166271662816629166301663116632166331663416635166361663716638166391664016641166421664316644166451664616647166481664916650166511665216653166541665516656166571665816659166601666116662166631666416665166661666716668166691667016671166721667316674166751667616677166781667916680166811668216683166841668516686166871668816689166901669116692166931669416695166961669716698166991670016701167021670316704167051670616707167081670916710167111671216713167141671516716167171671816719167201672116722167231672416725167261672716728167291673016731167321673316734167351673616737167381673916740167411674216743167441674516746167471674816749167501675116752167531675416755167561675716758167591676016761167621676316764167651676616767167681676916770167711677216773167741677516776167771677816779167801678116782167831678416785167861678716788167891679016791167921679316794167951679616797167981679916800168011680216803168041680516806168071680816809168101681116812168131681416815168161681716818168191682016821168221682316824168251682616827168281682916830168311683216833168341683516836168371683816839168401684116842168431684416845168461684716848168491685016851168521685316854168551685616857168581685916860168611686216863168641686516866168671686816869168701687116872168731687416875168761687716878168791688016881168821688316884168851688616887168881688916890168911689216893168941689516896168971689816899169001690116902169031690416905169061690716908169091691016911169121691316914169151691616917169181691916920169211692216923169241692516926169271692816929169301693116932169331693416935169361693716938169391694016941169421694316944169451694616947169481694916950169511695216953169541695516956169571695816959169601696116962169631696416965169661696716968169691697016971169721697316974169751697616977169781697916980169811698216983169841698516986169871698816989169901699116992169931699416995169961699716998169991700017001170021700317004170051700617007170081700917010170111701217013170141701517016170171701817019170201702117022170231702417025170261702717028170291703017031170321703317034170351703617037170381703917040170411704217043170441704517046170471704817049170501705117052170531705417055170561705717058170591706017061170621706317064170651706617067170681706917070170711707217073170741707517076170771707817079170801708117082170831708417085170861708717088170891709017091170921709317094170951709617097170981709917100171011710217103171041710517106171071710817109171101711117112171131711417115171161711717118171191712017121171221712317124171251712617127171281712917130171311713217133171341713517136171371713817139171401714117142171431714417145171461714717148171491715017151171521715317154171551715617157171581715917160171611716217163171641716517166171671716817169171701717117172171731717417175171761717717178171791718017181171821718317184171851718617187171881718917190171911719217193171941719517196171971719817199172001720117202172031720417205172061720717208172091721017211172121721317214172151721617217172181721917220172211722217223172241722517226172271722817229172301723117232172331723417235172361723717238172391724017241172421724317244172451724617247172481724917250172511725217253172541725517256172571725817259172601726117262172631726417265172661726717268172691727017271172721727317274172751727617277172781727917280172811728217283172841728517286172871728817289172901729117292172931729417295172961729717298172991730017301173021730317304173051730617307173081730917310173111731217313173141731517316173171731817319173201732117322173231732417325173261732717328173291733017331173321733317334173351733617337173381733917340173411734217343173441734517346173471734817349173501735117352173531735417355173561735717358173591736017361173621736317364173651736617367173681736917370173711737217373173741737517376173771737817379173801738117382173831738417385173861738717388173891739017391173921739317394173951739617397173981739917400174011740217403174041740517406174071740817409174101741117412174131741417415174161741717418174191742017421174221742317424174251742617427174281742917430174311743217433174341743517436174371743817439174401744117442174431744417445174461744717448174491745017451174521745317454174551745617457174581745917460174611746217463174641746517466174671746817469174701747117472174731747417475174761747717478174791748017481174821748317484174851748617487174881748917490174911749217493174941749517496174971749817499175001750117502175031750417505175061750717508175091751017511175121751317514175151751617517175181751917520175211752217523175241752517526175271752817529175301753117532175331753417535175361753717538175391754017541175421754317544175451754617547175481754917550175511755217553175541755517556175571755817559175601756117562175631756417565175661756717568175691757017571175721757317574175751757617577175781757917580175811758217583175841758517586175871758817589175901759117592175931759417595175961759717598175991760017601176021760317604176051760617607176081760917610176111761217613176141761517616176171761817619176201762117622176231762417625176261762717628176291763017631176321763317634176351763617637176381763917640176411764217643176441764517646176471764817649176501765117652176531765417655176561765717658176591766017661176621766317664176651766617667176681766917670176711767217673176741767517676176771767817679176801768117682176831768417685176861768717688176891769017691176921769317694176951769617697176981769917700177011770217703177041770517706177071770817709177101771117712177131771417715177161771717718177191772017721177221772317724177251772617727177281772917730177311773217733177341773517736177371773817739177401774117742177431774417745177461774717748177491775017751177521775317754177551775617757177581775917760177611776217763177641776517766177671776817769177701777117772177731777417775177761777717778177791778017781177821778317784177851778617787177881778917790177911779217793177941779517796177971779817799178001780117802178031780417805178061780717808178091781017811178121781317814178151781617817178181781917820178211782217823178241782517826178271782817829178301783117832178331783417835178361783717838178391784017841178421784317844178451784617847178481784917850178511785217853178541785517856178571785817859178601786117862178631786417865178661786717868178691787017871178721787317874178751787617877178781787917880178811788217883178841788517886178871788817889178901789117892178931789417895178961789717898178991790017901179021790317904179051790617907179081790917910179111791217913179141791517916179171791817919179201792117922179231792417925179261792717928179291793017931179321793317934179351793617937179381793917940179411794217943179441794517946179471794817949179501795117952179531795417955179561795717958179591796017961179621796317964179651796617967179681796917970179711797217973179741797517976179771797817979179801798117982179831798417985179861798717988179891799017991179921799317994179951799617997179981799918000180011800218003180041800518006180071800818009180101801118012180131801418015180161801718018180191802018021180221802318024180251802618027180281802918030180311803218033180341803518036180371803818039180401804118042180431804418045180461804718048180491805018051180521805318054180551805618057180581805918060180611806218063180641806518066180671806818069180701807118072180731807418075180761807718078180791808018081180821808318084180851808618087180881808918090180911809218093180941809518096180971809818099181001810118102181031810418105181061810718108181091811018111181121811318114181151811618117181181811918120181211812218123181241812518126181271812818129181301813118132181331813418135181361813718138181391814018141181421814318144181451814618147181481814918150181511815218153181541815518156181571815818159181601816118162181631816418165181661816718168181691817018171181721817318174181751817618177181781817918180181811818218183181841818518186181871818818189181901819118192181931819418195181961819718198181991820018201182021820318204182051820618207182081820918210182111821218213182141821518216182171821818219182201822118222182231822418225182261822718228182291823018231182321823318234182351823618237182381823918240182411824218243182441824518246182471824818249182501825118252182531825418255182561825718258182591826018261182621826318264182651826618267182681826918270182711827218273182741827518276182771827818279182801828118282182831828418285182861828718288182891829018291182921829318294182951829618297182981829918300183011830218303183041830518306183071830818309183101831118312183131831418315183161831718318183191832018321183221832318324183251832618327183281832918330183311833218333183341833518336183371833818339183401834118342183431834418345183461834718348183491835018351183521835318354183551835618357183581835918360183611836218363183641836518366183671836818369183701837118372183731837418375183761837718378183791838018381183821838318384183851838618387183881838918390183911839218393183941839518396183971839818399184001840118402184031840418405184061840718408184091841018411184121841318414184151841618417184181841918420184211842218423184241842518426184271842818429184301843118432184331843418435184361843718438184391844018441184421844318444184451844618447184481844918450184511845218453184541845518456184571845818459184601846118462184631846418465184661846718468184691847018471184721847318474184751847618477184781847918480184811848218483184841848518486184871848818489184901849118492184931849418495184961849718498184991850018501185021850318504185051850618507185081850918510185111851218513185141851518516185171851818519185201852118522185231852418525185261852718528185291853018531185321853318534185351853618537185381853918540185411854218543185441854518546185471854818549185501855118552185531855418555185561855718558185591856018561185621856318564185651856618567185681856918570185711857218573185741857518576185771857818579185801858118582185831858418585185861858718588185891859018591185921859318594185951859618597185981859918600186011860218603186041860518606186071860818609186101861118612186131861418615186161861718618186191862018621186221862318624186251862618627186281862918630186311863218633186341863518636186371863818639186401864118642186431864418645186461864718648186491865018651186521865318654186551865618657186581865918660186611866218663186641866518666186671866818669186701867118672186731867418675186761867718678186791868018681186821868318684186851868618687186881868918690186911869218693186941869518696186971869818699187001870118702187031870418705187061870718708187091871018711187121871318714187151871618717187181871918720187211872218723187241872518726187271872818729187301873118732187331873418735187361873718738187391874018741187421874318744187451874618747187481874918750187511875218753187541875518756187571875818759187601876118762187631876418765187661876718768187691877018771187721877318774187751877618777187781877918780187811878218783187841878518786187871878818789187901879118792187931879418795187961879718798187991880018801188021880318804188051880618807188081880918810188111881218813188141881518816188171881818819188201882118822188231882418825188261882718828188291883018831188321883318834188351883618837188381883918840188411884218843188441884518846188471884818849188501885118852188531885418855188561885718858188591886018861188621886318864188651886618867188681886918870188711887218873188741887518876188771887818879188801888118882188831888418885188861888718888188891889018891188921889318894188951889618897188981889918900189011890218903189041890518906189071890818909189101891118912189131891418915189161891718918189191892018921189221892318924189251892618927189281892918930189311893218933189341893518936189371893818939189401894118942189431894418945189461894718948189491895018951189521895318954189551895618957189581895918960189611896218963189641896518966189671896818969189701897118972189731897418975189761897718978189791898018981189821898318984189851898618987189881898918990189911899218993189941899518996189971899818999190001900119002190031900419005190061900719008190091901019011190121901319014190151901619017190181901919020190211902219023190241902519026190271902819029190301903119032190331903419035190361903719038190391904019041190421904319044190451904619047190481904919050190511905219053190541905519056190571905819059190601906119062190631906419065190661906719068190691907019071190721907319074190751907619077190781907919080190811908219083190841908519086190871908819089190901909119092190931909419095190961909719098190991910019101191021910319104191051910619107191081910919110191111911219113191141911519116191171911819119191201912119122191231912419125191261912719128191291913019131191321913319134191351913619137191381913919140191411914219143191441914519146191471914819149191501915119152191531915419155191561915719158191591916019161191621916319164191651916619167191681916919170191711917219173191741917519176191771917819179191801918119182191831918419185191861918719188191891919019191191921919319194191951919619197191981919919200192011920219203192041920519206192071920819209192101921119212192131921419215192161921719218192191922019221192221922319224192251922619227192281922919230192311923219233192341923519236192371923819239192401924119242192431924419245192461924719248192491925019251192521925319254192551925619257192581925919260192611926219263192641926519266192671926819269192701927119272192731927419275192761927719278192791928019281192821928319284192851928619287192881928919290192911929219293192941929519296192971929819299193001930119302193031930419305193061930719308193091931019311193121931319314193151931619317193181931919320193211932219323193241932519326193271932819329193301933119332193331933419335193361933719338193391934019341193421934319344193451934619347193481934919350193511935219353193541935519356193571935819359193601936119362193631936419365193661936719368193691937019371193721937319374193751937619377193781937919380193811938219383193841938519386193871938819389193901939119392193931939419395193961939719398193991940019401194021940319404194051940619407194081940919410194111941219413194141941519416194171941819419194201942119422194231942419425194261942719428194291943019431194321943319434194351943619437194381943919440194411944219443194441944519446194471944819449194501945119452194531945419455194561945719458194591946019461194621946319464194651946619467194681946919470194711947219473194741947519476194771947819479194801948119482194831948419485194861948719488194891949019491194921949319494194951949619497194981949919500195011950219503195041950519506195071950819509195101951119512195131951419515195161951719518195191952019521195221952319524195251952619527195281952919530195311953219533195341953519536195371953819539195401954119542195431954419545195461954719548195491955019551195521955319554195551955619557195581955919560195611956219563195641956519566195671956819569195701957119572195731957419575195761957719578195791958019581195821958319584195851958619587195881958919590195911959219593195941959519596195971959819599196001960119602196031960419605196061960719608196091961019611196121961319614196151961619617196181961919620196211962219623196241962519626196271962819629196301963119632196331963419635196361963719638196391964019641196421964319644196451964619647196481964919650196511965219653196541965519656196571965819659196601966119662196631966419665196661966719668196691967019671196721967319674196751967619677196781967919680196811968219683196841968519686196871968819689196901969119692196931969419695196961969719698196991970019701197021970319704197051970619707197081970919710197111971219713197141971519716197171971819719197201972119722197231972419725197261972719728197291973019731197321973319734197351973619737197381973919740197411974219743197441974519746197471974819749197501975119752197531975419755197561975719758197591976019761197621976319764197651976619767197681976919770197711977219773197741977519776197771977819779197801978119782197831978419785197861978719788197891979019791197921979319794197951979619797197981979919800198011980219803198041980519806198071980819809198101981119812198131981419815198161981719818198191982019821198221982319824198251982619827198281982919830198311983219833198341983519836198371983819839198401984119842198431984419845198461984719848198491985019851198521985319854198551985619857198581985919860198611986219863198641986519866198671986819869198701987119872198731987419875198761987719878198791988019881198821988319884198851988619887198881988919890198911989219893198941989519896198971989819899199001990119902199031990419905199061990719908199091991019911199121991319914199151991619917199181991919920199211992219923199241992519926199271992819929199301993119932199331993419935199361993719938199391994019941199421994319944199451994619947199481994919950199511995219953199541995519956199571995819959199601996119962199631996419965199661996719968199691997019971199721997319974199751997619977199781997919980199811998219983199841998519986199871998819989199901999119992199931999419995199961999719998199992000020001200022000320004200052000620007200082000920010200112001220013200142001520016200172001820019200202002120022200232002420025200262002720028200292003020031200322003320034200352003620037200382003920040200412004220043200442004520046200472004820049200502005120052200532005420055200562005720058200592006020061200622006320064200652006620067200682006920070200712007220073200742007520076200772007820079200802008120082200832008420085200862008720088200892009020091200922009320094200952009620097200982009920100201012010220103201042010520106201072010820109201102011120112201132011420115201162011720118201192012020121201222012320124201252012620127201282012920130201312013220133201342013520136201372013820139201402014120142201432014420145201462014720148201492015020151201522015320154201552015620157201582015920160201612016220163201642016520166201672016820169201702017120172201732017420175201762017720178201792018020181201822018320184201852018620187201882018920190201912019220193201942019520196201972019820199202002020120202202032020420205202062020720208202092021020211202122021320214202152021620217202182021920220202212022220223202242022520226202272022820229202302023120232202332023420235202362023720238202392024020241202422024320244202452024620247202482024920250202512025220253202542025520256202572025820259202602026120262202632026420265202662026720268202692027020271202722027320274202752027620277202782027920280202812028220283202842028520286202872028820289202902029120292202932029420295202962029720298202992030020301203022030320304203052030620307203082030920310203112031220313203142031520316203172031820319203202032120322203232032420325203262032720328203292033020331203322033320334203352033620337203382033920340203412034220343203442034520346203472034820349203502035120352203532035420355203562035720358203592036020361203622036320364203652036620367203682036920370203712037220373203742037520376203772037820379203802038120382203832038420385203862038720388203892039020391203922039320394203952039620397203982039920400204012040220403204042040520406204072040820409204102041120412204132041420415204162041720418204192042020421204222042320424204252042620427204282042920430204312043220433204342043520436204372043820439204402044120442204432044420445204462044720448204492045020451204522045320454204552045620457204582045920460204612046220463204642046520466204672046820469204702047120472204732047420475204762047720478204792048020481204822048320484204852048620487204882048920490204912049220493204942049520496204972049820499205002050120502205032050420505205062050720508205092051020511205122051320514205152051620517205182051920520205212052220523205242052520526205272052820529205302053120532205332053420535205362053720538205392054020541205422054320544205452054620547205482054920550205512055220553205542055520556205572055820559205602056120562205632056420565205662056720568205692057020571205722057320574205752057620577205782057920580205812058220583205842058520586205872058820589205902059120592205932059420595205962059720598205992060020601206022060320604206052060620607206082060920610206112061220613206142061520616206172061820619206202062120622206232062420625206262062720628206292063020631206322063320634206352063620637206382063920640206412064220643206442064520646206472064820649206502065120652206532065420655206562065720658206592066020661206622066320664206652066620667206682066920670206712067220673206742067520676206772067820679206802068120682206832068420685206862068720688206892069020691206922069320694206952069620697206982069920700207012070220703207042070520706207072070820709207102071120712207132071420715207162071720718207192072020721207222072320724207252072620727207282072920730207312073220733207342073520736207372073820739207402074120742207432074420745207462074720748207492075020751207522075320754207552075620757207582075920760207612076220763207642076520766207672076820769207702077120772207732077420775207762077720778207792078020781207822078320784207852078620787207882078920790207912079220793207942079520796207972079820799208002080120802208032080420805208062080720808208092081020811208122081320814208152081620817208182081920820208212082220823208242082520826208272082820829208302083120832208332083420835208362083720838208392084020841208422084320844208452084620847208482084920850208512085220853208542085520856208572085820859208602086120862208632086420865208662086720868208692087020871208722087320874208752087620877208782087920880208812088220883208842088520886208872088820889208902089120892208932089420895208962089720898208992090020901209022090320904209052090620907209082090920910209112091220913209142091520916209172091820919209202092120922209232092420925209262092720928209292093020931209322093320934209352093620937209382093920940209412094220943209442094520946209472094820949209502095120952209532095420955209562095720958209592096020961209622096320964209652096620967209682096920970209712097220973209742097520976209772097820979209802098120982209832098420985209862098720988209892099020991209922099320994209952099620997209982099921000210012100221003210042100521006210072100821009210102101121012210132101421015210162101721018210192102021021210222102321024210252102621027210282102921030210312103221033210342103521036210372103821039210402104121042210432104421045210462104721048210492105021051210522105321054210552105621057210582105921060210612106221063210642106521066210672106821069210702107121072210732107421075210762107721078210792108021081210822108321084210852108621087210882108921090210912109221093210942109521096210972109821099211002110121102211032110421105211062110721108211092111021111211122111321114211152111621117211182111921120211212112221123211242112521126211272112821129211302113121132211332113421135211362113721138211392114021141211422114321144211452114621147211482114921150211512115221153211542115521156211572115821159211602116121162211632116421165211662116721168211692117021171211722117321174211752117621177211782117921180211812118221183211842118521186211872118821189211902119121192211932119421195211962119721198211992120021201212022120321204212052120621207212082120921210212112121221213212142121521216212172121821219212202122121222212232122421225212262122721228212292123021231212322123321234212352123621237212382123921240212412124221243212442124521246212472124821249212502125121252212532125421255212562125721258212592126021261212622126321264212652126621267212682126921270212712127221273212742127521276212772127821279212802128121282212832128421285212862128721288212892129021291212922129321294212952129621297212982129921300213012130221303213042130521306213072130821309213102131121312213132131421315213162131721318213192132021321213222132321324213252132621327213282132921330213312133221333213342133521336213372133821339213402134121342213432134421345213462134721348213492135021351213522135321354213552135621357213582135921360213612136221363213642136521366213672136821369213702137121372213732137421375213762137721378213792138021381213822138321384213852138621387213882138921390213912139221393213942139521396213972139821399214002140121402214032140421405214062140721408214092141021411214122141321414214152141621417214182141921420214212142221423214242142521426214272142821429214302143121432214332143421435214362143721438214392144021441214422144321444214452144621447214482144921450214512145221453214542145521456214572145821459214602146121462214632146421465214662146721468214692147021471214722147321474214752147621477214782147921480214812148221483214842148521486214872148821489214902149121492214932149421495214962149721498214992150021501215022150321504215052150621507215082150921510215112151221513215142151521516215172151821519215202152121522215232152421525215262152721528215292153021531215322153321534215352153621537215382153921540215412154221543215442154521546215472154821549215502155121552215532155421555215562155721558215592156021561215622156321564215652156621567215682156921570215712157221573215742157521576215772157821579215802158121582215832158421585215862158721588215892159021591215922159321594215952159621597215982159921600216012160221603216042160521606216072160821609216102161121612216132161421615216162161721618216192162021621216222162321624216252162621627216282162921630216312163221633216342163521636216372163821639216402164121642216432164421645216462164721648216492165021651216522165321654216552165621657216582165921660216612166221663216642166521666216672166821669216702167121672216732167421675216762167721678216792168021681216822168321684216852168621687216882168921690216912169221693216942169521696216972169821699217002170121702217032170421705217062170721708217092171021711217122171321714217152171621717217182171921720217212172221723217242172521726217272172821729217302173121732217332173421735217362173721738217392174021741217422174321744217452174621747217482174921750217512175221753217542175521756217572175821759217602176121762217632176421765217662176721768217692177021771217722177321774217752177621777217782177921780217812178221783217842178521786217872178821789217902179121792217932179421795217962179721798217992180021801218022180321804218052180621807218082180921810218112181221813218142181521816218172181821819218202182121822218232182421825218262182721828218292183021831218322183321834218352183621837218382183921840218412184221843218442184521846218472184821849218502185121852218532185421855218562185721858218592186021861218622186321864218652186621867218682186921870218712187221873218742187521876218772187821879218802188121882218832188421885218862188721888218892189021891218922189321894218952189621897218982189921900219012190221903219042190521906219072190821909219102191121912219132191421915219162191721918219192192021921219222192321924219252192621927219282192921930219312193221933219342193521936219372193821939219402194121942219432194421945219462194721948219492195021951219522195321954219552195621957219582195921960219612196221963219642196521966219672196821969219702197121972219732197421975219762197721978219792198021981219822198321984219852198621987219882198921990219912199221993219942199521996219972199821999220002200122002220032200422005220062200722008220092201022011220122201322014220152201622017220182201922020220212202222023220242202522026220272202822029220302203122032220332203422035220362203722038220392204022041220422204322044220452204622047220482204922050220512205222053220542205522056220572205822059220602206122062220632206422065220662206722068220692207022071220722207322074220752207622077220782207922080220812208222083220842208522086220872208822089220902209122092220932209422095220962209722098220992210022101221022210322104221052210622107221082210922110221112211222113221142211522116221172211822119221202212122122221232212422125221262212722128221292213022131221322213322134221352213622137221382213922140221412214222143221442214522146221472214822149221502215122152221532215422155221562215722158221592216022161221622216322164221652216622167221682216922170221712217222173221742217522176221772217822179221802218122182221832218422185221862218722188221892219022191221922219322194221952219622197221982219922200222012220222203222042220522206222072220822209222102221122212222132221422215222162221722218222192222022221222222222322224222252222622227222282222922230222312223222233222342223522236222372223822239222402224122242222432224422245222462224722248222492225022251222522225322254222552225622257222582225922260222612226222263222642226522266222672226822269222702227122272222732227422275222762227722278222792228022281222822228322284222852228622287222882228922290222912229222293222942229522296222972229822299223002230122302223032230422305223062230722308223092231022311223122231322314223152231622317223182231922320223212232222323223242232522326223272232822329223302233122332223332233422335223362233722338223392234022341223422234322344223452234622347223482234922350223512235222353223542235522356223572235822359223602236122362223632236422365223662236722368223692237022371223722237322374223752237622377223782237922380223812238222383223842238522386223872238822389223902239122392223932239422395223962239722398223992240022401224022240322404224052240622407224082240922410224112241222413224142241522416224172241822419224202242122422224232242422425224262242722428224292243022431224322243322434224352243622437224382243922440224412244222443224442244522446224472244822449224502245122452224532245422455224562245722458224592246022461224622246322464224652246622467224682246922470224712247222473224742247522476224772247822479224802248122482224832248422485224862248722488224892249022491224922249322494224952249622497224982249922500225012250222503225042250522506225072250822509225102251122512225132251422515225162251722518225192252022521225222252322524225252252622527225282252922530225312253222533225342253522536225372253822539225402254122542225432254422545225462254722548225492255022551225522255322554225552255622557225582255922560225612256222563225642256522566225672256822569225702257122572225732257422575225762257722578225792258022581225822258322584225852258622587225882258922590225912259222593225942259522596225972259822599226002260122602226032260422605226062260722608226092261022611226122261322614226152261622617226182261922620226212262222623226242262522626226272262822629226302263122632226332263422635226362263722638226392264022641226422264322644226452264622647226482264922650226512265222653226542265522656226572265822659226602266122662226632266422665226662266722668226692267022671226722267322674226752267622677226782267922680226812268222683226842268522686226872268822689226902269122692226932269422695226962269722698226992270022701227022270322704227052270622707227082270922710227112271222713227142271522716227172271822719227202272122722227232272422725227262272722728227292273022731227322273322734227352273622737227382273922740227412274222743227442274522746227472274822749227502275122752227532275422755227562275722758227592276022761227622276322764227652276622767227682276922770227712277222773227742277522776227772277822779227802278122782227832278422785227862278722788227892279022791227922279322794227952279622797227982279922800228012280222803228042280522806228072280822809228102281122812228132281422815228162281722818228192282022821228222282322824228252282622827228282282922830228312283222833228342283522836228372283822839228402284122842228432284422845228462284722848228492285022851228522285322854228552285622857228582285922860228612286222863228642286522866228672286822869228702287122872228732287422875228762287722878228792288022881228822288322884228852288622887228882288922890228912289222893228942289522896228972289822899229002290122902229032290422905229062290722908229092291022911229122291322914229152291622917229182291922920229212292222923229242292522926229272292822929229302293122932229332293422935229362293722938229392294022941229422294322944229452294622947229482294922950229512295222953229542295522956229572295822959229602296122962229632296422965229662296722968229692297022971229722297322974229752297622977229782297922980229812298222983229842298522986229872298822989229902299122992229932299422995229962299722998229992300023001230022300323004230052300623007230082300923010230112301223013230142301523016230172301823019230202302123022230232302423025230262302723028230292303023031230322303323034230352303623037230382303923040230412304223043230442304523046230472304823049230502305123052230532305423055230562305723058230592306023061230622306323064230652306623067230682306923070230712307223073230742307523076230772307823079230802308123082230832308423085230862308723088230892309023091230922309323094230952309623097230982309923100231012310223103231042310523106231072310823109231102311123112231132311423115231162311723118231192312023121231222312323124231252312623127231282312923130231312313223133231342313523136231372313823139231402314123142231432314423145231462314723148231492315023151231522315323154231552315623157231582315923160231612316223163231642316523166231672316823169231702317123172231732317423175231762317723178231792318023181231822318323184231852318623187231882318923190231912319223193231942319523196231972319823199232002320123202232032320423205232062320723208232092321023211232122321323214232152321623217232182321923220232212322223223232242322523226232272322823229232302323123232232332323423235232362323723238232392324023241232422324323244232452324623247232482324923250232512325223253232542325523256232572325823259232602326123262232632326423265232662326723268232692327023271232722327323274232752327623277232782327923280232812328223283232842328523286232872328823289232902329123292232932329423295232962329723298232992330023301233022330323304233052330623307233082330923310233112331223313233142331523316233172331823319233202332123322233232332423325233262332723328233292333023331233322333323334233352333623337233382333923340233412334223343233442334523346233472334823349233502335123352233532335423355233562335723358233592336023361233622336323364233652336623367233682336923370233712337223373233742337523376233772337823379233802338123382233832338423385233862338723388233892339023391233922339323394233952339623397233982339923400234012340223403234042340523406234072340823409234102341123412234132341423415234162341723418234192342023421234222342323424234252342623427234282342923430234312343223433234342343523436234372343823439234402344123442234432344423445234462344723448234492345023451234522345323454234552345623457234582345923460234612346223463234642346523466234672346823469234702347123472234732347423475234762347723478234792348023481234822348323484234852348623487234882348923490234912349223493234942349523496234972349823499235002350123502235032350423505235062350723508235092351023511235122351323514235152351623517235182351923520235212352223523235242352523526235272352823529235302353123532235332353423535235362353723538235392354023541235422354323544235452354623547235482354923550235512355223553235542355523556235572355823559235602356123562235632356423565235662356723568235692357023571235722357323574235752357623577235782357923580235812358223583235842358523586235872358823589235902359123592235932359423595235962359723598235992360023601236022360323604236052360623607236082360923610236112361223613236142361523616236172361823619236202362123622236232362423625236262362723628236292363023631236322363323634236352363623637236382363923640236412364223643236442364523646236472364823649236502365123652236532365423655236562365723658236592366023661236622366323664236652366623667236682366923670236712367223673236742367523676236772367823679236802368123682236832368423685236862368723688236892369023691236922369323694236952369623697236982369923700237012370223703237042370523706237072370823709237102371123712237132371423715237162371723718237192372023721237222372323724237252372623727237282372923730237312373223733237342373523736237372373823739237402374123742237432374423745237462374723748237492375023751237522375323754237552375623757237582375923760237612376223763237642376523766237672376823769237702377123772237732377423775237762377723778237792378023781237822378323784237852378623787237882378923790237912379223793237942379523796237972379823799238002380123802238032380423805238062380723808238092381023811238122381323814238152381623817238182381923820238212382223823238242382523826238272382823829238302383123832238332383423835238362383723838238392384023841238422384323844238452384623847238482384923850238512385223853238542385523856238572385823859238602386123862238632386423865238662386723868238692387023871238722387323874238752387623877238782387923880238812388223883238842388523886238872388823889238902389123892238932389423895238962389723898238992390023901239022390323904239052390623907239082390923910239112391223913239142391523916239172391823919239202392123922239232392423925239262392723928239292393023931239322393323934239352393623937239382393923940239412394223943239442394523946239472394823949239502395123952239532395423955239562395723958239592396023961239622396323964239652396623967239682396923970239712397223973239742397523976239772397823979239802398123982239832398423985239862398723988239892399023991239922399323994239952399623997239982399924000240012400224003240042400524006240072400824009240102401124012240132401424015240162401724018240192402024021240222402324024240252402624027240282402924030240312403224033240342403524036240372403824039240402404124042240432404424045240462404724048240492405024051240522405324054240552405624057240582405924060240612406224063240642406524066240672406824069240702407124072240732407424075240762407724078240792408024081240822408324084240852408624087240882408924090240912409224093240942409524096240972409824099241002410124102241032410424105241062410724108241092411024111241122411324114241152411624117241182411924120241212412224123241242412524126241272412824129241302413124132241332413424135241362413724138241392414024141241422414324144241452414624147241482414924150241512415224153241542415524156241572415824159241602416124162241632416424165241662416724168241692417024171241722417324174241752417624177241782417924180241812418224183241842418524186241872418824189241902419124192241932419424195241962419724198241992420024201242022420324204242052420624207242082420924210242112421224213242142421524216242172421824219242202422124222242232422424225242262422724228242292423024231242322423324234242352423624237242382423924240242412424224243242442424524246242472424824249242502425124252242532425424255242562425724258242592426024261242622426324264242652426624267242682426924270242712427224273242742427524276242772427824279242802428124282242832428424285242862428724288242892429024291242922429324294242952429624297242982429924300243012430224303243042430524306243072430824309243102431124312243132431424315243162431724318243192432024321243222432324324243252432624327243282432924330243312433224333243342433524336243372433824339243402434124342243432434424345243462434724348243492435024351243522435324354243552435624357243582435924360243612436224363243642436524366243672436824369243702437124372243732437424375243762437724378243792438024381243822438324384243852438624387243882438924390243912439224393243942439524396243972439824399244002440124402244032440424405244062440724408244092441024411244122441324414244152441624417244182441924420244212442224423244242442524426244272442824429244302443124432244332443424435244362443724438244392444024441244422444324444244452444624447244482444924450244512445224453244542445524456244572445824459244602446124462244632446424465244662446724468244692447024471244722447324474244752447624477244782447924480244812448224483244842448524486244872448824489244902449124492244932449424495244962449724498244992450024501245022450324504245052450624507245082450924510245112451224513245142451524516245172451824519245202452124522245232452424525245262452724528245292453024531245322453324534245352453624537245382453924540245412454224543245442454524546245472454824549245502455124552245532455424555245562455724558245592456024561245622456324564245652456624567245682456924570245712457224573245742457524576245772457824579245802458124582245832458424585245862458724588245892459024591245922459324594245952459624597245982459924600246012460224603246042460524606246072460824609246102461124612246132461424615246162461724618246192462024621246222462324624246252462624627246282462924630246312463224633246342463524636246372463824639246402464124642246432464424645246462464724648246492465024651246522465324654246552465624657246582465924660246612466224663246642466524666246672466824669246702467124672246732467424675246762467724678246792468024681246822468324684246852468624687246882468924690246912469224693246942469524696246972469824699247002470124702247032470424705247062470724708247092471024711247122471324714247152471624717247182471924720247212472224723247242472524726247272472824729247302473124732247332473424735247362473724738247392474024741247422474324744247452474624747247482474924750247512475224753247542475524756247572475824759247602476124762247632476424765247662476724768247692477024771247722477324774247752477624777247782477924780247812478224783247842478524786247872478824789247902479124792247932479424795247962479724798247992480024801248022480324804248052480624807248082480924810248112481224813248142481524816248172481824819248202482124822248232482424825248262482724828248292483024831248322483324834248352483624837248382483924840248412484224843248442484524846248472484824849248502485124852248532485424855248562485724858248592486024861248622486324864248652486624867248682486924870248712487224873248742487524876248772487824879248802488124882248832488424885248862488724888248892489024891248922489324894248952489624897248982489924900249012490224903249042490524906249072490824909249102491124912249132491424915249162491724918249192492024921249222492324924249252492624927249282492924930249312493224933249342493524936249372493824939249402494124942249432494424945249462494724948249492495024951249522495324954249552495624957249582495924960249612496224963249642496524966249672496824969249702497124972249732497424975249762497724978249792498024981249822498324984249852498624987249882498924990249912499224993249942499524996249972499824999250002500125002250032500425005250062500725008250092501025011250122501325014250152501625017250182501925020250212502225023250242502525026250272502825029250302503125032250332503425035250362503725038250392504025041250422504325044250452504625047250482504925050250512505225053250542505525056250572505825059250602506125062250632506425065250662506725068250692507025071250722507325074250752507625077250782507925080250812508225083250842508525086250872508825089250902509125092250932509425095250962509725098250992510025101251022510325104251052510625107251082510925110251112511225113251142511525116251172511825119251202512125122251232512425125251262512725128251292513025131251322513325134251352513625137251382513925140251412514225143251442514525146251472514825149251502515125152251532515425155251562515725158251592516025161251622516325164251652516625167251682516925170251712517225173251742517525176251772517825179251802518125182251832518425185251862518725188251892519025191251922519325194251952519625197251982519925200252012520225203252042520525206252072520825209252102521125212252132521425215252162521725218252192522025221252222522325224252252522625227252282522925230252312523225233252342523525236252372523825239252402524125242252432524425245252462524725248252492525025251252522525325254252552525625257252582525925260252612526225263252642526525266252672526825269252702527125272252732527425275252762527725278252792528025281252822528325284252852528625287252882528925290252912529225293252942529525296252972529825299253002530125302253032530425305253062530725308253092531025311253122531325314253152531625317253182531925320253212532225323253242532525326253272532825329253302533125332253332533425335253362533725338253392534025341253422534325344253452534625347253482534925350253512535225353253542535525356253572535825359253602536125362253632536425365253662536725368253692537025371253722537325374253752537625377253782537925380253812538225383253842538525386253872538825389253902539125392253932539425395253962539725398253992540025401254022540325404254052540625407254082540925410254112541225413254142541525416254172541825419254202542125422254232542425425254262542725428254292543025431254322543325434254352543625437254382543925440254412544225443254442544525446254472544825449254502545125452254532545425455254562545725458254592546025461254622546325464254652546625467254682546925470254712547225473254742547525476254772547825479254802548125482254832548425485254862548725488254892549025491254922549325494254952549625497254982549925500255012550225503255042550525506255072550825509255102551125512255132551425515255162551725518255192552025521255222552325524255252552625527255282552925530255312553225533255342553525536255372553825539255402554125542255432554425545255462554725548255492555025551255522555325554255552555625557255582555925560255612556225563255642556525566255672556825569255702557125572255732557425575255762557725578255792558025581255822558325584255852558625587255882558925590255912559225593255942559525596255972559825599256002560125602256032560425605256062560725608256092561025611256122561325614256152561625617256182561925620256212562225623256242562525626256272562825629256302563125632256332563425635256362563725638256392564025641256422564325644256452564625647256482564925650256512565225653256542565525656256572565825659256602566125662256632566425665256662566725668256692567025671256722567325674256752567625677256782567925680256812568225683256842568525686256872568825689256902569125692256932569425695256962569725698256992570025701257022570325704257052570625707257082570925710257112571225713257142571525716257172571825719257202572125722257232572425725257262572725728257292573025731257322573325734257352573625737257382573925740257412574225743257442574525746257472574825749257502575125752257532575425755257562575725758257592576025761257622576325764257652576625767257682576925770257712577225773257742577525776257772577825779257802578125782257832578425785257862578725788257892579025791257922579325794257952579625797257982579925800258012580225803258042580525806258072580825809258102581125812258132581425815258162581725818258192582025821258222582325824258252582625827258282582925830258312583225833258342583525836258372583825839258402584125842258432584425845258462584725848258492585025851258522585325854258552585625857258582585925860258612586225863258642586525866258672586825869258702587125872258732587425875258762587725878258792588025881258822588325884258852588625887258882588925890258912589225893258942589525896258972589825899259002590125902259032590425905259062590725908259092591025911259122591325914259152591625917259182591925920259212592225923259242592525926259272592825929259302593125932259332593425935259362593725938259392594025941259422594325944259452594625947259482594925950259512595225953259542595525956259572595825959259602596125962259632596425965259662596725968259692597025971259722597325974259752597625977259782597925980259812598225983259842598525986259872598825989259902599125992259932599425995259962599725998259992600026001260022600326004260052600626007260082600926010260112601226013260142601526016260172601826019260202602126022260232602426025260262602726028260292603026031260322603326034260352603626037260382603926040260412604226043260442604526046260472604826049260502605126052260532605426055260562605726058260592606026061260622606326064260652606626067260682606926070260712607226073260742607526076260772607826079260802608126082260832608426085260862608726088260892609026091260922609326094260952609626097260982609926100261012610226103261042610526106261072610826109261102611126112261132611426115261162611726118261192612026121261222612326124261252612626127261282612926130261312613226133261342613526136261372613826139261402614126142261432614426145261462614726148261492615026151261522615326154261552615626157261582615926160261612616226163261642616526166261672616826169261702617126172261732617426175261762617726178261792618026181261822618326184261852618626187261882618926190261912619226193261942619526196261972619826199262002620126202262032620426205262062620726208262092621026211262122621326214262152621626217262182621926220262212622226223262242622526226262272622826229262302623126232262332623426235262362623726238262392624026241262422624326244262452624626247262482624926250262512625226253262542625526256262572625826259262602626126262262632626426265262662626726268262692627026271262722627326274262752627626277262782627926280262812628226283262842628526286262872628826289262902629126292262932629426295262962629726298262992630026301263022630326304263052630626307263082630926310263112631226313263142631526316263172631826319263202632126322263232632426325263262632726328263292633026331263322633326334263352633626337263382633926340263412634226343263442634526346263472634826349263502635126352263532635426355263562635726358263592636026361263622636326364263652636626367263682636926370263712637226373263742637526376263772637826379263802638126382263832638426385263862638726388263892639026391263922639326394263952639626397263982639926400264012640226403264042640526406264072640826409264102641126412264132641426415264162641726418264192642026421264222642326424264252642626427264282642926430264312643226433264342643526436264372643826439264402644126442264432644426445264462644726448264492645026451264522645326454264552645626457264582645926460264612646226463264642646526466264672646826469264702647126472264732647426475264762647726478264792648026481264822648326484264852648626487264882648926490264912649226493264942649526496264972649826499265002650126502265032650426505265062650726508265092651026511265122651326514265152651626517265182651926520265212652226523265242652526526265272652826529265302653126532265332653426535265362653726538265392654026541265422654326544265452654626547265482654926550265512655226553265542655526556265572655826559265602656126562265632656426565265662656726568265692657026571265722657326574265752657626577265782657926580265812658226583265842658526586265872658826589265902659126592265932659426595265962659726598265992660026601266022660326604266052660626607266082660926610266112661226613266142661526616266172661826619266202662126622266232662426625266262662726628266292663026631266322663326634266352663626637266382663926640266412664226643266442664526646266472664826649266502665126652266532665426655266562665726658266592666026661266622666326664266652666626667266682666926670266712667226673266742667526676266772667826679266802668126682266832668426685266862668726688266892669026691266922669326694266952669626697266982669926700267012670226703267042670526706267072670826709267102671126712267132671426715267162671726718267192672026721267222672326724267252672626727267282672926730267312673226733267342673526736267372673826739267402674126742267432674426745267462674726748267492675026751267522675326754267552675626757267582675926760267612676226763267642676526766267672676826769267702677126772267732677426775267762677726778267792678026781267822678326784267852678626787267882678926790267912679226793267942679526796267972679826799268002680126802268032680426805268062680726808268092681026811268122681326814268152681626817268182681926820268212682226823268242682526826268272682826829268302683126832268332683426835268362683726838268392684026841268422684326844268452684626847268482684926850268512685226853268542685526856268572685826859268602686126862268632686426865268662686726868268692687026871268722687326874268752687626877268782687926880268812688226883268842688526886268872688826889268902689126892268932689426895268962689726898268992690026901269022690326904269052690626907269082690926910269112691226913269142691526916269172691826919269202692126922269232692426925269262692726928269292693026931269322693326934269352693626937269382693926940269412694226943269442694526946269472694826949269502695126952269532695426955269562695726958269592696026961269622696326964269652696626967269682696926970269712697226973269742697526976269772697826979269802698126982269832698426985269862698726988269892699026991269922699326994269952699626997269982699927000270012700227003270042700527006270072700827009270102701127012270132701427015270162701727018270192702027021270222702327024270252702627027270282702927030270312703227033270342703527036270372703827039270402704127042270432704427045270462704727048270492705027051270522705327054270552705627057270582705927060270612706227063270642706527066270672706827069270702707127072270732707427075270762707727078270792708027081270822708327084270852708627087270882708927090270912709227093270942709527096270972709827099271002710127102271032710427105271062710727108271092711027111271122711327114271152711627117271182711927120271212712227123271242712527126271272712827129271302713127132271332713427135271362713727138271392714027141271422714327144271452714627147271482714927150271512715227153271542715527156271572715827159271602716127162271632716427165271662716727168271692717027171271722717327174271752717627177271782717927180271812718227183271842718527186271872718827189271902719127192271932719427195271962719727198271992720027201272022720327204272052720627207272082720927210272112721227213272142721527216272172721827219272202722127222272232722427225272262722727228272292723027231272322723327234272352723627237272382723927240272412724227243272442724527246272472724827249272502725127252272532725427255272562725727258272592726027261272622726327264272652726627267272682726927270272712727227273272742727527276272772727827279272802728127282272832728427285272862728727288272892729027291272922729327294272952729627297272982729927300273012730227303273042730527306273072730827309273102731127312273132731427315273162731727318273192732027321273222732327324273252732627327273282732927330273312733227333273342733527336273372733827339273402734127342273432734427345273462734727348273492735027351273522735327354273552735627357273582735927360273612736227363273642736527366273672736827369273702737127372273732737427375273762737727378273792738027381273822738327384273852738627387273882738927390273912739227393273942739527396273972739827399274002740127402274032740427405274062740727408274092741027411274122741327414274152741627417274182741927420274212742227423274242742527426274272742827429274302743127432274332743427435274362743727438274392744027441274422744327444274452744627447274482744927450274512745227453274542745527456274572745827459274602746127462274632746427465274662746727468274692747027471274722747327474274752747627477274782747927480274812748227483274842748527486274872748827489274902749127492274932749427495274962749727498274992750027501275022750327504275052750627507275082750927510275112751227513275142751527516275172751827519275202752127522275232752427525275262752727528275292753027531275322753327534275352753627537275382753927540275412754227543275442754527546275472754827549275502755127552275532755427555275562755727558275592756027561275622756327564275652756627567275682756927570275712757227573275742757527576275772757827579275802758127582275832758427585275862758727588275892759027591275922759327594275952759627597275982759927600276012760227603276042760527606276072760827609276102761127612276132761427615276162761727618276192762027621276222762327624276252762627627276282762927630276312763227633276342763527636276372763827639276402764127642276432764427645276462764727648276492765027651276522765327654276552765627657276582765927660276612766227663276642766527666276672766827669276702767127672276732767427675276762767727678276792768027681276822768327684276852768627687276882768927690276912769227693276942769527696276972769827699277002770127702277032770427705277062770727708277092771027711277122771327714277152771627717277182771927720277212772227723277242772527726277272772827729277302773127732277332773427735277362773727738277392774027741277422774327744277452774627747277482774927750277512775227753277542775527756277572775827759277602776127762277632776427765277662776727768277692777027771277722777327774277752777627777277782777927780277812778227783277842778527786277872778827789277902779127792277932779427795277962779727798277992780027801278022780327804278052780627807278082780927810278112781227813278142781527816278172781827819278202782127822278232782427825278262782727828278292783027831278322783327834278352783627837278382783927840278412784227843278442784527846278472784827849278502785127852278532785427855278562785727858278592786027861278622786327864278652786627867278682786927870278712787227873278742787527876278772787827879278802788127882278832788427885278862788727888278892789027891278922789327894278952789627897278982789927900279012790227903279042790527906279072790827909279102791127912279132791427915279162791727918279192792027921279222792327924279252792627927279282792927930279312793227933279342793527936279372793827939279402794127942279432794427945279462794727948279492795027951279522795327954279552795627957279582795927960279612796227963279642796527966279672796827969279702797127972279732797427975279762797727978279792798027981279822798327984279852798627987279882798927990279912799227993279942799527996279972799827999280002800128002280032800428005280062800728008280092801028011280122801328014280152801628017280182801928020280212802228023280242802528026280272802828029280302803128032280332803428035280362803728038280392804028041280422804328044280452804628047280482804928050280512805228053280542805528056280572805828059280602806128062280632806428065280662806728068280692807028071280722807328074280752807628077280782807928080280812808228083280842808528086280872808828089280902809128092280932809428095280962809728098280992810028101281022810328104281052810628107281082810928110281112811228113281142811528116281172811828119281202812128122281232812428125281262812728128281292813028131281322813328134281352813628137281382813928140281412814228143281442814528146281472814828149281502815128152281532815428155281562815728158281592816028161281622816328164281652816628167281682816928170281712817228173281742817528176281772817828179281802818128182281832818428185281862818728188281892819028191281922819328194281952819628197281982819928200282012820228203282042820528206282072820828209282102821128212282132821428215282162821728218282192822028221282222822328224282252822628227282282822928230282312823228233282342823528236282372823828239282402824128242282432824428245282462824728248282492825028251282522825328254282552825628257282582825928260282612826228263282642826528266282672826828269282702827128272282732827428275282762827728278282792828028281282822828328284282852828628287282882828928290282912829228293282942829528296282972829828299283002830128302283032830428305283062830728308283092831028311283122831328314283152831628317283182831928320283212832228323283242832528326283272832828329283302833128332283332833428335283362833728338283392834028341283422834328344283452834628347283482834928350283512835228353283542835528356283572835828359283602836128362283632836428365283662836728368283692837028371283722837328374283752837628377283782837928380283812838228383283842838528386283872838828389283902839128392283932839428395283962839728398283992840028401284022840328404284052840628407284082840928410284112841228413284142841528416284172841828419284202842128422284232842428425284262842728428284292843028431284322843328434284352843628437284382843928440284412844228443284442844528446284472844828449284502845128452284532845428455284562845728458284592846028461284622846328464284652846628467284682846928470284712847228473284742847528476284772847828479284802848128482284832848428485284862848728488284892849028491284922849328494284952849628497284982849928500285012850228503285042850528506285072850828509285102851128512285132851428515285162851728518285192852028521285222852328524285252852628527285282852928530285312853228533285342853528536285372853828539285402854128542285432854428545285462854728548285492855028551285522855328554285552855628557285582855928560285612856228563285642856528566285672856828569285702857128572285732857428575285762857728578285792858028581285822858328584285852858628587285882858928590285912859228593285942859528596285972859828599286002860128602286032860428605286062860728608286092861028611286122861328614286152861628617286182861928620286212862228623286242862528626286272862828629286302863128632286332863428635286362863728638286392864028641286422864328644286452864628647286482864928650286512865228653286542865528656286572865828659286602866128662286632866428665286662866728668286692867028671286722867328674286752867628677286782867928680286812868228683286842868528686286872868828689286902869128692286932869428695286962869728698286992870028701287022870328704287052870628707287082870928710287112871228713287142871528716287172871828719287202872128722287232872428725287262872728728287292873028731287322873328734287352873628737287382873928740287412874228743287442874528746287472874828749287502875128752287532875428755287562875728758287592876028761287622876328764287652876628767287682876928770287712877228773287742877528776287772877828779287802878128782287832878428785287862878728788287892879028791287922879328794287952879628797287982879928800288012880228803288042880528806288072880828809288102881128812288132881428815288162881728818288192882028821288222882328824288252882628827288282882928830288312883228833288342883528836288372883828839288402884128842288432884428845288462884728848288492885028851288522885328854288552885628857288582885928860288612886228863288642886528866288672886828869288702887128872288732887428875288762887728878288792888028881288822888328884288852888628887288882888928890288912889228893288942889528896288972889828899289002890128902289032890428905289062890728908289092891028911289122891328914289152891628917289182891928920289212892228923289242892528926289272892828929289302893128932289332893428935289362893728938289392894028941289422894328944289452894628947289482894928950289512895228953289542895528956289572895828959289602896128962289632896428965289662896728968289692897028971289722897328974289752897628977289782897928980289812898228983289842898528986289872898828989289902899128992289932899428995289962899728998289992900029001290022900329004290052900629007290082900929010290112901229013290142901529016290172901829019290202902129022290232902429025290262902729028290292903029031290322903329034290352903629037290382903929040290412904229043290442904529046290472904829049290502905129052290532905429055290562905729058290592906029061290622906329064290652906629067290682906929070290712907229073290742907529076290772907829079290802908129082290832908429085290862908729088290892909029091290922909329094290952909629097290982909929100291012910229103291042910529106291072910829109291102911129112291132911429115291162911729118291192912029121291222912329124291252912629127291282912929130291312913229133291342913529136291372913829139291402914129142291432914429145291462914729148291492915029151291522915329154291552915629157291582915929160291612916229163291642916529166291672916829169291702917129172291732917429175291762917729178291792918029181291822918329184291852918629187291882918929190291912919229193291942919529196291972919829199292002920129202292032920429205292062920729208292092921029211292122921329214292152921629217292182921929220292212922229223292242922529226292272922829229292302923129232292332923429235292362923729238292392924029241292422924329244292452924629247292482924929250292512925229253292542925529256292572925829259292602926129262292632926429265292662926729268292692927029271292722927329274292752927629277292782927929280292812928229283292842928529286292872928829289292902929129292292932929429295292962929729298292992930029301293022930329304293052930629307293082930929310293112931229313293142931529316293172931829319293202932129322293232932429325293262932729328293292933029331293322933329334293352933629337293382933929340293412934229343293442934529346293472934829349293502935129352293532935429355293562935729358293592936029361293622936329364293652936629367293682936929370293712937229373293742937529376293772937829379293802938129382293832938429385293862938729388293892939029391293922939329394293952939629397293982939929400294012940229403294042940529406294072940829409294102941129412294132941429415294162941729418294192942029421294222942329424294252942629427294282942929430294312943229433294342943529436294372943829439294402944129442294432944429445294462944729448294492945029451294522945329454294552945629457294582945929460294612946229463294642946529466294672946829469294702947129472294732947429475294762947729478294792948029481294822948329484294852948629487294882948929490294912949229493294942949529496294972949829499295002950129502295032950429505295062950729508295092951029511295122951329514295152951629517295182951929520295212952229523295242952529526295272952829529295302953129532295332953429535295362953729538295392954029541295422954329544295452954629547295482954929550295512955229553295542955529556295572955829559295602956129562295632956429565295662956729568295692957029571295722957329574295752957629577295782957929580295812958229583295842958529586295872958829589295902959129592295932959429595295962959729598295992960029601296022960329604296052960629607296082960929610296112961229613296142961529616296172961829619296202962129622296232962429625296262962729628296292963029631296322963329634296352963629637296382963929640296412964229643296442964529646296472964829649296502965129652296532965429655296562965729658296592966029661296622966329664296652966629667296682966929670296712967229673296742967529676296772967829679296802968129682296832968429685296862968729688296892969029691296922969329694296952969629697296982969929700297012970229703297042970529706297072970829709297102971129712297132971429715297162971729718297192972029721297222972329724297252972629727297282972929730297312973229733297342973529736297372973829739297402974129742297432974429745297462974729748297492975029751297522975329754297552975629757297582975929760297612976229763297642976529766297672976829769297702977129772297732977429775297762977729778297792978029781297822978329784297852978629787297882978929790297912979229793297942979529796297972979829799298002980129802298032980429805298062980729808298092981029811298122981329814298152981629817298182981929820298212982229823298242982529826298272982829829298302983129832298332983429835298362983729838298392984029841298422984329844298452984629847298482984929850298512985229853298542985529856298572985829859298602986129862298632986429865298662986729868298692987029871298722987329874298752987629877298782987929880298812988229883298842988529886298872988829889298902989129892298932989429895298962989729898298992990029901299022990329904299052990629907299082990929910299112991229913299142991529916299172991829919299202992129922299232992429925299262992729928299292993029931299322993329934299352993629937299382993929940299412994229943299442994529946299472994829949299502995129952299532995429955299562995729958299592996029961299622996329964299652996629967299682996929970299712997229973299742997529976299772997829979299802998129982299832998429985299862998729988299892999029991299922999329994299952999629997299982999930000300013000230003300043000530006300073000830009300103001130012300133001430015300163001730018300193002030021300223002330024300253002630027300283002930030300313003230033300343003530036300373003830039300403004130042300433004430045300463004730048300493005030051300523005330054300553005630057300583005930060300613006230063300643006530066300673006830069300703007130072300733007430075300763007730078300793008030081300823008330084300853008630087300883008930090300913009230093300943009530096300973009830099301003010130102301033010430105301063010730108301093011030111301123011330114301153011630117301183011930120301213012230123301243012530126301273012830129301303013130132301333013430135301363013730138301393014030141301423014330144301453014630147301483014930150301513015230153301543015530156301573015830159301603016130162301633016430165301663016730168301693017030171301723017330174301753017630177301783017930180301813018230183301843018530186301873018830189301903019130192301933019430195301963019730198301993020030201302023020330204302053020630207302083020930210302113021230213302143021530216302173021830219302203022130222302233022430225302263022730228302293023030231302323023330234302353023630237302383023930240302413024230243302443024530246302473024830249302503025130252302533025430255302563025730258302593026030261302623026330264302653026630267302683026930270302713027230273302743027530276302773027830279302803028130282302833028430285302863028730288302893029030291302923029330294302953029630297302983029930300303013030230303303043030530306303073030830309303103031130312303133031430315303163031730318303193032030321303223032330324303253032630327303283032930330303313033230333303343033530336303373033830339303403034130342303433034430345303463034730348303493035030351303523035330354303553035630357303583035930360303613036230363303643036530366303673036830369303703037130372303733037430375303763037730378303793038030381303823038330384303853038630387303883038930390303913039230393303943039530396303973039830399304003040130402304033040430405304063040730408304093041030411304123041330414304153041630417304183041930420304213042230423304243042530426304273042830429304303043130432304333043430435304363043730438304393044030441304423044330444304453044630447304483044930450304513045230453304543045530456304573045830459304603046130462304633046430465304663046730468304693047030471304723047330474304753047630477304783047930480304813048230483304843048530486304873048830489304903049130492304933049430495304963049730498304993050030501305023050330504305053050630507305083050930510305113051230513305143051530516305173051830519305203052130522305233052430525305263052730528305293053030531305323053330534305353053630537305383053930540305413054230543305443054530546305473054830549305503055130552305533055430555305563055730558305593056030561305623056330564305653056630567305683056930570305713057230573305743057530576305773057830579305803058130582305833058430585305863058730588305893059030591305923059330594305953059630597305983059930600306013060230603306043060530606306073060830609306103061130612306133061430615306163061730618306193062030621306223062330624306253062630627306283062930630306313063230633306343063530636306373063830639306403064130642306433064430645306463064730648306493065030651306523065330654306553065630657306583065930660306613066230663306643066530666306673066830669306703067130672306733067430675306763067730678306793068030681306823068330684306853068630687306883068930690306913069230693306943069530696306973069830699307003070130702307033070430705307063070730708307093071030711307123071330714307153071630717307183071930720307213072230723307243072530726307273072830729307303073130732307333073430735307363073730738307393074030741307423074330744307453074630747307483074930750307513075230753307543075530756307573075830759307603076130762307633076430765307663076730768307693077030771307723077330774307753077630777307783077930780307813078230783307843078530786307873078830789307903079130792307933079430795307963079730798307993080030801308023080330804308053080630807308083080930810308113081230813308143081530816308173081830819308203082130822308233082430825308263082730828308293083030831308323083330834308353083630837308383083930840308413084230843308443084530846308473084830849308503085130852308533085430855308563085730858308593086030861308623086330864308653086630867308683086930870308713087230873308743087530876308773087830879308803088130882308833088430885308863088730888308893089030891308923089330894308953089630897308983089930900309013090230903309043090530906309073090830909309103091130912309133091430915309163091730918309193092030921309223092330924309253092630927309283092930930309313093230933309343093530936309373093830939309403094130942309433094430945309463094730948309493095030951309523095330954309553095630957309583095930960309613096230963309643096530966309673096830969309703097130972309733097430975309763097730978309793098030981309823098330984309853098630987309883098930990309913099230993309943099530996309973099830999310003100131002310033100431005310063100731008310093101031011310123101331014310153101631017310183101931020310213102231023310243102531026310273102831029310303103131032310333103431035310363103731038310393104031041310423104331044310453104631047310483104931050310513105231053310543105531056310573105831059310603106131062310633106431065310663106731068310693107031071310723107331074310753107631077310783107931080310813108231083310843108531086310873108831089310903109131092310933109431095310963109731098310993110031101311023110331104311053110631107311083110931110311113111231113311143111531116311173111831119311203112131122311233112431125311263112731128311293113031131311323113331134311353113631137311383113931140311413114231143311443114531146311473114831149311503115131152311533115431155311563115731158311593116031161311623116331164311653116631167311683116931170311713117231173311743117531176311773117831179311803118131182311833118431185311863118731188311893119031191311923119331194311953119631197311983119931200312013120231203312043120531206312073120831209312103121131212312133121431215312163121731218312193122031221312223122331224312253122631227312283122931230312313123231233312343123531236312373123831239312403124131242312433124431245312463124731248312493125031251312523125331254312553125631257312583125931260312613126231263312643126531266312673126831269312703127131272312733127431275312763127731278312793128031281312823128331284312853128631287312883128931290312913129231293312943129531296312973129831299313003130131302313033130431305313063130731308313093131031311313123131331314313153131631317313183131931320313213132231323313243132531326313273132831329313303133131332313333133431335313363133731338313393134031341313423134331344313453134631347313483134931350313513135231353313543135531356313573135831359313603136131362313633136431365313663136731368313693137031371313723137331374313753137631377313783137931380313813138231383313843138531386313873138831389313903139131392313933139431395313963139731398313993140031401314023140331404314053140631407314083140931410314113141231413314143141531416314173141831419314203142131422314233142431425314263142731428314293143031431314323143331434314353143631437314383143931440314413144231443314443144531446314473144831449314503145131452314533145431455314563145731458314593146031461314623146331464314653146631467314683146931470314713147231473314743147531476314773147831479314803148131482314833148431485314863148731488314893149031491314923149331494314953149631497314983149931500315013150231503315043150531506315073150831509315103151131512315133151431515315163151731518315193152031521315223152331524315253152631527315283152931530315313153231533315343153531536315373153831539315403154131542315433154431545315463154731548315493155031551315523155331554315553155631557315583155931560315613156231563315643156531566315673156831569315703157131572315733157431575315763157731578315793158031581315823158331584315853158631587315883158931590315913159231593315943159531596315973159831599316003160131602316033160431605316063160731608316093161031611316123161331614316153161631617316183161931620316213162231623316243162531626316273162831629316303163131632316333163431635316363163731638316393164031641316423164331644316453164631647316483164931650316513165231653316543165531656316573165831659316603166131662316633166431665316663166731668316693167031671316723167331674316753167631677316783167931680316813168231683316843168531686316873168831689316903169131692316933169431695316963169731698316993170031701317023170331704317053170631707317083170931710317113171231713317143171531716317173171831719317203172131722317233172431725317263172731728317293173031731317323173331734317353173631737317383173931740317413174231743317443174531746317473174831749317503175131752317533175431755317563175731758317593176031761317623176331764317653176631767317683176931770317713177231773317743177531776317773177831779317803178131782317833178431785317863178731788317893179031791317923179331794317953179631797317983179931800318013180231803318043180531806318073180831809318103181131812318133181431815318163181731818318193182031821318223182331824318253182631827318283182931830318313183231833318343183531836318373183831839318403184131842318433184431845318463184731848318493185031851318523185331854318553185631857318583185931860318613186231863318643186531866318673186831869318703187131872318733187431875318763187731878318793188031881318823188331884318853188631887318883188931890318913189231893318943189531896318973189831899319003190131902319033190431905319063190731908319093191031911319123191331914319153191631917319183191931920319213192231923319243192531926319273192831929319303193131932319333193431935319363193731938319393194031941319423194331944319453194631947319483194931950319513195231953319543195531956319573195831959319603196131962319633196431965319663196731968319693197031971319723197331974319753197631977319783197931980319813198231983319843198531986319873198831989319903199131992319933199431995319963199731998319993200032001320023200332004320053200632007320083200932010320113201232013320143201532016320173201832019320203202132022320233202432025320263202732028320293203032031320323203332034320353203632037320383203932040320413204232043320443204532046320473204832049320503205132052320533205432055320563205732058320593206032061320623206332064320653206632067320683206932070320713207232073320743207532076320773207832079320803208132082320833208432085320863208732088320893209032091320923209332094320953209632097320983209932100321013210232103321043210532106321073210832109321103211132112321133211432115321163211732118321193212032121321223212332124321253212632127321283212932130321313213232133321343213532136321373213832139321403214132142321433214432145321463214732148321493215032151321523215332154321553215632157321583215932160321613216232163321643216532166321673216832169321703217132172321733217432175321763217732178321793218032181321823218332184321853218632187321883218932190321913219232193321943219532196321973219832199322003220132202322033220432205322063220732208322093221032211322123221332214322153221632217322183221932220322213222232223322243222532226322273222832229322303223132232322333223432235322363223732238322393224032241322423224332244322453224632247322483224932250322513225232253322543225532256322573225832259322603226132262322633226432265322663226732268322693227032271322723227332274322753227632277322783227932280322813228232283322843228532286322873228832289322903229132292322933229432295322963229732298322993230032301323023230332304323053230632307323083230932310323113231232313323143231532316323173231832319323203232132322323233232432325323263232732328323293233032331323323233332334323353233632337323383233932340323413234232343323443234532346323473234832349323503235132352323533235432355323563235732358323593236032361323623236332364323653236632367323683236932370323713237232373323743237532376323773237832379323803238132382323833238432385323863238732388323893239032391323923239332394323953239632397323983239932400324013240232403324043240532406324073240832409324103241132412324133241432415324163241732418324193242032421324223242332424324253242632427324283242932430324313243232433324343243532436324373243832439324403244132442324433244432445324463244732448324493245032451324523245332454324553245632457324583245932460324613246232463324643246532466324673246832469324703247132472324733247432475324763247732478324793248032481324823248332484324853248632487324883248932490324913249232493324943249532496324973249832499325003250132502325033250432505325063250732508325093251032511325123251332514325153251632517325183251932520325213252232523325243252532526325273252832529325303253132532325333253432535325363253732538325393254032541325423254332544325453254632547325483254932550325513255232553325543255532556325573255832559325603256132562325633256432565325663256732568325693257032571325723257332574325753257632577325783257932580325813258232583325843258532586325873258832589325903259132592325933259432595325963259732598325993260032601326023260332604326053260632607326083260932610326113261232613326143261532616326173261832619326203262132622326233262432625326263262732628326293263032631326323263332634326353263632637326383263932640326413264232643326443264532646326473264832649326503265132652326533265432655326563265732658326593266032661326623266332664326653266632667326683266932670326713267232673326743267532676326773267832679326803268132682326833268432685326863268732688326893269032691326923269332694326953269632697326983269932700327013270232703327043270532706327073270832709327103271132712327133271432715327163271732718327193272032721327223272332724327253272632727327283272932730327313273232733327343273532736327373273832739327403274132742327433274432745327463274732748327493275032751327523275332754327553275632757327583275932760327613276232763327643276532766327673276832769327703277132772327733277432775327763277732778327793278032781327823278332784327853278632787327883278932790327913279232793327943279532796327973279832799328003280132802328033280432805328063280732808328093281032811328123281332814328153281632817328183281932820328213282232823328243282532826328273282832829328303283132832328333283432835328363283732838328393284032841328423284332844328453284632847328483284932850328513285232853328543285532856328573285832859328603286132862328633286432865328663286732868328693287032871328723287332874328753287632877328783287932880328813288232883328843288532886328873288832889328903289132892328933289432895328963289732898328993290032901329023290332904329053290632907329083290932910329113291232913329143291532916329173291832919329203292132922329233292432925329263292732928329293293032931329323293332934329353293632937329383293932940329413294232943329443294532946329473294832949329503295132952329533295432955329563295732958329593296032961329623296332964329653296632967329683296932970329713297232973329743297532976329773297832979329803298132982329833298432985329863298732988329893299032991329923299332994329953299632997329983299933000330013300233003330043300533006330073300833009330103301133012330133301433015330163301733018330193302033021330223302333024330253302633027330283302933030330313303233033330343303533036330373303833039330403304133042330433304433045330463304733048330493305033051330523305333054330553305633057330583305933060330613306233063330643306533066330673306833069330703307133072330733307433075330763307733078330793308033081330823308333084330853308633087330883308933090330913309233093330943309533096330973309833099331003310133102331033310433105331063310733108331093311033111331123311333114331153311633117331183311933120331213312233123331243312533126331273312833129331303313133132331333313433135331363313733138331393314033141331423314333144331453314633147331483314933150331513315233153331543315533156331573315833159331603316133162331633316433165331663316733168331693317033171331723317333174331753317633177331783317933180331813318233183331843318533186331873318833189331903319133192331933319433195331963319733198331993320033201332023320333204332053320633207332083320933210332113321233213332143321533216332173321833219332203322133222332233322433225332263322733228332293323033231332323323333234332353323633237332383323933240332413324233243332443324533246332473324833249332503325133252332533325433255332563325733258332593326033261332623326333264332653326633267332683326933270332713327233273332743327533276332773327833279332803328133282332833328433285332863328733288332893329033291332923329333294332953329633297332983329933300333013330233303333043330533306333073330833309333103331133312333133331433315333163331733318333193332033321333223332333324333253332633327333283332933330333313333233333333343333533336333373333833339333403334133342333433334433345333463334733348333493335033351333523335333354333553335633357333583335933360333613336233363333643336533366333673336833369333703337133372333733337433375333763337733378333793338033381333823338333384333853338633387333883338933390333913339233393333943339533396333973339833399334003340133402334033340433405334063340733408334093341033411334123341333414334153341633417334183341933420334213342233423334243342533426334273342833429334303343133432334333343433435334363343733438334393344033441334423344333444334453344633447334483344933450334513345233453334543345533456334573345833459334603346133462334633346433465334663346733468334693347033471334723347333474334753347633477334783347933480334813348233483334843348533486334873348833489334903349133492334933349433495334963349733498334993350033501335023350333504335053350633507335083350933510335113351233513335143351533516335173351833519335203352133522335233352433525335263352733528335293353033531335323353333534335353353633537335383353933540335413354233543335443354533546335473354833549335503355133552335533355433555335563355733558335593356033561335623356333564335653356633567335683356933570335713357233573335743357533576335773357833579335803358133582335833358433585335863358733588335893359033591335923359333594335953359633597335983359933600336013360233603336043360533606336073360833609336103361133612336133361433615336163361733618336193362033621336223362333624336253362633627336283362933630336313363233633336343363533636336373363833639336403364133642336433364433645336463364733648336493365033651336523365333654336553365633657336583365933660336613366233663336643366533666336673366833669336703367133672336733367433675336763367733678336793368033681336823368333684336853368633687336883368933690336913369233693336943369533696336973369833699337003370133702337033370433705337063370733708337093371033711337123371333714337153371633717337183371933720337213372233723337243372533726337273372833729337303373133732337333373433735337363373733738337393374033741337423374333744337453374633747337483374933750337513375233753337543375533756337573375833759337603376133762337633376433765337663376733768337693377033771337723377333774337753377633777337783377933780337813378233783337843378533786337873378833789337903379133792337933379433795337963379733798337993380033801338023380333804338053380633807338083380933810338113381233813338143381533816338173381833819338203382133822338233382433825338263382733828338293383033831338323383333834338353383633837338383383933840338413384233843338443384533846338473384833849338503385133852338533385433855338563385733858338593386033861338623386333864338653386633867338683386933870338713387233873338743387533876338773387833879338803388133882338833388433885338863388733888338893389033891338923389333894338953389633897338983389933900339013390233903339043390533906339073390833909339103391133912339133391433915339163391733918339193392033921339223392333924339253392633927339283392933930339313393233933339343393533936339373393833939339403394133942339433394433945339463394733948339493395033951339523395333954339553395633957339583395933960339613396233963339643396533966339673396833969339703397133972339733397433975339763397733978339793398033981339823398333984339853398633987339883398933990339913399233993339943399533996339973399833999340003400134002340033400434005340063400734008340093401034011340123401334014340153401634017340183401934020340213402234023340243402534026340273402834029340303403134032340333403434035340363403734038340393404034041340423404334044340453404634047340483404934050340513405234053340543405534056340573405834059340603406134062340633406434065340663406734068340693407034071340723407334074340753407634077340783407934080340813408234083340843408534086340873408834089340903409134092340933409434095340963409734098340993410034101341023410334104341053410634107341083410934110341113411234113341143411534116341173411834119341203412134122341233412434125341263412734128341293413034131341323413334134341353413634137341383413934140341413414234143341443414534146341473414834149341503415134152341533415434155341563415734158341593416034161341623416334164341653416634167341683416934170341713417234173341743417534176341773417834179341803418134182341833418434185341863418734188341893419034191341923419334194341953419634197341983419934200342013420234203342043420534206342073420834209342103421134212342133421434215342163421734218342193422034221342223422334224342253422634227342283422934230342313423234233342343423534236342373423834239342403424134242342433424434245342463424734248342493425034251342523425334254342553425634257342583425934260342613426234263342643426534266342673426834269342703427134272342733427434275342763427734278342793428034281342823428334284342853428634287342883428934290342913429234293342943429534296342973429834299343003430134302343033430434305343063430734308343093431034311343123431334314343153431634317343183431934320343213432234323343243432534326343273432834329343303433134332343333433434335343363433734338343393434034341343423434334344343453434634347343483434934350343513435234353343543435534356343573435834359343603436134362343633436434365343663436734368343693437034371343723437334374343753437634377343783437934380343813438234383343843438534386343873438834389343903439134392343933439434395343963439734398343993440034401344023440334404344053440634407344083440934410344113441234413344143441534416344173441834419344203442134422344233442434425344263442734428344293443034431344323443334434344353443634437344383443934440344413444234443344443444534446344473444834449344503445134452344533445434455344563445734458344593446034461344623446334464344653446634467344683446934470344713447234473344743447534476344773447834479344803448134482344833448434485344863448734488344893449034491344923449334494344953449634497344983449934500345013450234503345043450534506345073450834509345103451134512345133451434515345163451734518345193452034521345223452334524345253452634527345283452934530345313453234533345343453534536345373453834539345403454134542345433454434545345463454734548345493455034551345523455334554345553455634557345583455934560345613456234563345643456534566345673456834569345703457134572345733457434575345763457734578345793458034581345823458334584345853458634587345883458934590345913459234593345943459534596345973459834599346003460134602346033460434605346063460734608346093461034611346123461334614346153461634617346183461934620346213462234623346243462534626346273462834629346303463134632346333463434635346363463734638346393464034641346423464334644346453464634647346483464934650346513465234653346543465534656346573465834659346603466134662346633466434665346663466734668346693467034671346723467334674346753467634677346783467934680346813468234683346843468534686346873468834689346903469134692346933469434695346963469734698346993470034701347023470334704347053470634707347083470934710347113471234713347143471534716347173471834719347203472134722347233472434725347263472734728347293473034731347323473334734347353473634737347383473934740347413474234743347443474534746347473474834749347503475134752347533475434755347563475734758347593476034761347623476334764347653476634767347683476934770347713477234773347743477534776347773477834779347803478134782347833478434785347863478734788347893479034791347923479334794347953479634797347983479934800348013480234803348043480534806348073480834809348103481134812348133481434815348163481734818348193482034821348223482334824348253482634827348283482934830348313483234833348343483534836348373483834839348403484134842348433484434845348463484734848348493485034851348523485334854348553485634857348583485934860348613486234863348643486534866348673486834869348703487134872348733487434875348763487734878348793488034881348823488334884348853488634887348883488934890348913489234893348943489534896348973489834899349003490134902349033490434905349063490734908349093491034911349123491334914349153491634917349183491934920349213492234923349243492534926349273492834929349303493134932349333493434935349363493734938349393494034941349423494334944349453494634947349483494934950349513495234953349543495534956349573495834959349603496134962349633496434965349663496734968349693497034971349723497334974349753497634977349783497934980349813498234983349843498534986349873498834989349903499134992349933499434995349963499734998349993500035001350023500335004350053500635007350083500935010350113501235013350143501535016350173501835019350203502135022350233502435025350263502735028350293503035031350323503335034350353503635037350383503935040350413504235043350443504535046350473504835049350503505135052350533505435055350563505735058350593506035061350623506335064350653506635067350683506935070350713507235073350743507535076350773507835079350803508135082350833508435085350863508735088350893509035091350923509335094350953509635097350983509935100351013510235103351043510535106351073510835109351103511135112351133511435115351163511735118351193512035121351223512335124351253512635127351283512935130351313513235133351343513535136351373513835139351403514135142351433514435145351463514735148351493515035151351523515335154351553515635157351583515935160351613516235163351643516535166351673516835169351703517135172351733517435175351763517735178351793518035181351823518335184351853518635187351883518935190351913519235193351943519535196351973519835199352003520135202352033520435205352063520735208352093521035211352123521335214352153521635217352183521935220352213522235223352243522535226352273522835229352303523135232352333523435235352363523735238352393524035241352423524335244352453524635247352483524935250352513525235253352543525535256352573525835259352603526135262352633526435265352663526735268352693527035271352723527335274352753527635277352783527935280352813528235283352843528535286352873528835289352903529135292352933529435295352963529735298352993530035301353023530335304353053530635307353083530935310353113531235313353143531535316353173531835319353203532135322353233532435325353263532735328353293533035331353323533335334353353533635337353383533935340353413534235343353443534535346353473534835349353503535135352353533535435355353563535735358353593536035361353623536335364353653536635367353683536935370353713537235373353743537535376353773537835379353803538135382353833538435385353863538735388353893539035391353923539335394353953539635397353983539935400354013540235403354043540535406354073540835409354103541135412354133541435415354163541735418354193542035421354223542335424354253542635427354283542935430354313543235433354343543535436354373543835439354403544135442354433544435445354463544735448354493545035451354523545335454354553545635457354583545935460354613546235463354643546535466354673546835469354703547135472354733547435475354763547735478354793548035481354823548335484354853548635487354883548935490354913549235493354943549535496354973549835499355003550135502355033550435505355063550735508355093551035511355123551335514355153551635517355183551935520355213552235523355243552535526355273552835529355303553135532355333553435535355363553735538355393554035541355423554335544355453554635547355483554935550355513555235553355543555535556355573555835559355603556135562355633556435565355663556735568355693557035571355723557335574355753557635577355783557935580355813558235583355843558535586355873558835589355903559135592355933559435595355963559735598355993560035601356023560335604356053560635607356083560935610356113561235613356143561535616356173561835619356203562135622356233562435625356263562735628356293563035631356323563335634356353563635637356383563935640356413564235643356443564535646356473564835649356503565135652356533565435655356563565735658356593566035661356623566335664356653566635667356683566935670356713567235673356743567535676356773567835679356803568135682356833568435685356863568735688356893569035691356923569335694356953569635697356983569935700357013570235703357043570535706357073570835709357103571135712357133571435715357163571735718357193572035721357223572335724357253572635727357283572935730357313573235733357343573535736357373573835739357403574135742357433574435745357463574735748357493575035751357523575335754357553575635757357583575935760357613576235763357643576535766357673576835769357703577135772357733577435775357763577735778357793578035781357823578335784357853578635787357883578935790357913579235793357943579535796357973579835799358003580135802358033580435805358063580735808358093581035811358123581335814358153581635817358183581935820358213582235823358243582535826358273582835829358303583135832358333583435835358363583735838358393584035841358423584335844358453584635847358483584935850358513585235853358543585535856358573585835859358603586135862358633586435865358663586735868358693587035871358723587335874358753587635877358783587935880358813588235883358843588535886358873588835889358903589135892358933589435895358963589735898358993590035901359023590335904359053590635907359083590935910359113591235913359143591535916359173591835919359203592135922359233592435925359263592735928359293593035931359323593335934359353593635937359383593935940359413594235943359443594535946359473594835949359503595135952359533595435955359563595735958359593596035961359623596335964359653596635967359683596935970359713597235973359743597535976359773597835979359803598135982359833598435985359863598735988359893599035991359923599335994359953599635997359983599936000360013600236003360043600536006360073600836009360103601136012360133601436015360163601736018360193602036021360223602336024360253602636027360283602936030360313603236033360343603536036360373603836039360403604136042360433604436045360463604736048360493605036051360523605336054360553605636057360583605936060360613606236063360643606536066360673606836069360703607136072360733607436075360763607736078360793608036081360823608336084360853608636087360883608936090360913609236093360943609536096360973609836099361003610136102361033610436105361063610736108361093611036111361123611336114361153611636117361183611936120361213612236123361243612536126361273612836129361303613136132361333613436135361363613736138361393614036141361423614336144361453614636147361483614936150361513615236153361543615536156361573615836159361603616136162361633616436165361663616736168361693617036171361723617336174361753617636177361783617936180361813618236183361843618536186361873618836189361903619136192361933619436195361963619736198361993620036201362023620336204362053620636207362083620936210362113621236213362143621536216362173621836219362203622136222362233622436225362263622736228362293623036231362323623336234362353623636237362383623936240362413624236243362443624536246362473624836249362503625136252362533625436255362563625736258362593626036261362623626336264362653626636267362683626936270362713627236273362743627536276362773627836279362803628136282362833628436285362863628736288362893629036291362923629336294362953629636297362983629936300363013630236303363043630536306363073630836309363103631136312363133631436315363163631736318363193632036321363223632336324363253632636327363283632936330363313633236333363343633536336363373633836339363403634136342363433634436345363463634736348363493635036351363523635336354363553635636357363583635936360363613636236363363643636536366363673636836369363703637136372363733637436375363763637736378363793638036381363823638336384363853638636387363883638936390363913639236393363943639536396363973639836399364003640136402364033640436405364063640736408364093641036411364123641336414364153641636417364183641936420364213642236423364243642536426364273642836429364303643136432364333643436435364363643736438364393644036441364423644336444364453644636447364483644936450364513645236453364543645536456364573645836459364603646136462364633646436465364663646736468364693647036471364723647336474364753647636477364783647936480364813648236483364843648536486364873648836489364903649136492364933649436495364963649736498364993650036501365023650336504365053650636507365083650936510365113651236513365143651536516365173651836519365203652136522365233652436525365263652736528365293653036531365323653336534365353653636537365383653936540365413654236543365443654536546365473654836549365503655136552365533655436555365563655736558365593656036561365623656336564365653656636567365683656936570365713657236573365743657536576365773657836579365803658136582365833658436585365863658736588365893659036591365923659336594365953659636597365983659936600366013660236603366043660536606366073660836609366103661136612366133661436615366163661736618366193662036621366223662336624366253662636627366283662936630366313663236633366343663536636366373663836639366403664136642366433664436645366463664736648366493665036651366523665336654366553665636657366583665936660366613666236663366643666536666366673666836669366703667136672366733667436675366763667736678366793668036681366823668336684366853668636687366883668936690366913669236693366943669536696366973669836699367003670136702367033670436705367063670736708367093671036711367123671336714367153671636717367183671936720367213672236723367243672536726367273672836729367303673136732367333673436735367363673736738367393674036741367423674336744367453674636747367483674936750367513675236753367543675536756367573675836759367603676136762367633676436765367663676736768367693677036771367723677336774367753677636777367783677936780367813678236783367843678536786367873678836789367903679136792367933679436795367963679736798367993680036801368023680336804368053680636807368083680936810368113681236813368143681536816368173681836819368203682136822368233682436825368263682736828368293683036831368323683336834368353683636837368383683936840368413684236843368443684536846368473684836849368503685136852368533685436855368563685736858368593686036861368623686336864368653686636867368683686936870368713687236873368743687536876368773687836879368803688136882368833688436885368863688736888368893689036891368923689336894368953689636897368983689936900369013690236903369043690536906369073690836909369103691136912369133691436915369163691736918369193692036921369223692336924369253692636927369283692936930369313693236933369343693536936369373693836939369403694136942369433694436945369463694736948369493695036951369523695336954369553695636957369583695936960369613696236963369643696536966369673696836969369703697136972369733697436975369763697736978369793698036981369823698336984369853698636987369883698936990369913699236993369943699536996369973699836999370003700137002370033700437005370063700737008370093701037011370123701337014370153701637017370183701937020370213702237023370243702537026370273702837029370303703137032370333703437035370363703737038370393704037041370423704337044370453704637047370483704937050370513705237053370543705537056370573705837059370603706137062370633706437065370663706737068370693707037071370723707337074370753707637077370783707937080370813708237083370843708537086370873708837089370903709137092370933709437095370963709737098370993710037101371023710337104371053710637107371083710937110371113711237113371143711537116371173711837119371203712137122371233712437125371263712737128371293713037131371323713337134371353713637137371383713937140371413714237143371443714537146371473714837149371503715137152371533715437155371563715737158371593716037161371623716337164371653716637167371683716937170371713717237173371743717537176371773717837179371803718137182371833718437185371863718737188371893719037191371923719337194371953719637197371983719937200372013720237203372043720537206372073720837209372103721137212372133721437215372163721737218372193722037221372223722337224372253722637227372283722937230372313723237233372343723537236372373723837239372403724137242372433724437245372463724737248372493725037251372523725337254372553725637257372583725937260372613726237263372643726537266372673726837269372703727137272372733727437275372763727737278372793728037281372823728337284372853728637287372883728937290372913729237293372943729537296372973729837299373003730137302373033730437305373063730737308373093731037311373123731337314373153731637317373183731937320373213732237323373243732537326373273732837329373303733137332373333733437335373363733737338373393734037341373423734337344373453734637347373483734937350373513735237353373543735537356373573735837359373603736137362373633736437365373663736737368373693737037371373723737337374373753737637377373783737937380373813738237383373843738537386373873738837389373903739137392373933739437395373963739737398373993740037401374023740337404374053740637407374083740937410374113741237413374143741537416374173741837419374203742137422374233742437425374263742737428374293743037431374323743337434374353743637437374383743937440374413744237443374443744537446374473744837449374503745137452374533745437455374563745737458374593746037461374623746337464374653746637467374683746937470374713747237473374743747537476374773747837479374803748137482374833748437485374863748737488374893749037491374923749337494374953749637497374983749937500375013750237503375043750537506375073750837509375103751137512375133751437515375163751737518375193752037521375223752337524375253752637527375283752937530375313753237533375343753537536375373753837539375403754137542375433754437545375463754737548375493755037551375523755337554375553755637557375583755937560375613756237563375643756537566375673756837569375703757137572375733757437575375763757737578375793758037581375823758337584375853758637587375883758937590375913759237593375943759537596375973759837599376003760137602376033760437605376063760737608376093761037611376123761337614376153761637617376183761937620376213762237623376243762537626376273762837629376303763137632376333763437635376363763737638376393764037641376423764337644376453764637647376483764937650376513765237653376543765537656376573765837659376603766137662376633766437665376663766737668376693767037671376723767337674376753767637677376783767937680376813768237683376843768537686376873768837689376903769137692376933769437695376963769737698376993770037701377023770337704377053770637707377083770937710377113771237713377143771537716377173771837719377203772137722377233772437725377263772737728377293773037731377323773337734377353773637737377383773937740377413774237743377443774537746377473774837749377503775137752377533775437755377563775737758377593776037761377623776337764377653776637767377683776937770377713777237773377743777537776377773777837779377803778137782377833778437785377863778737788377893779037791377923779337794377953779637797377983779937800378013780237803378043780537806378073780837809378103781137812378133781437815378163781737818378193782037821378223782337824378253782637827378283782937830378313783237833378343783537836378373783837839378403784137842378433784437845378463784737848378493785037851378523785337854378553785637857378583785937860378613786237863378643786537866378673786837869378703787137872378733787437875378763787737878378793788037881378823788337884378853788637887378883788937890378913789237893378943789537896378973789837899379003790137902379033790437905379063790737908379093791037911379123791337914379153791637917379183791937920379213792237923379243792537926379273792837929379303793137932379333793437935379363793737938379393794037941379423794337944379453794637947379483794937950379513795237953379543795537956379573795837959379603796137962379633796437965379663796737968379693797037971379723797337974379753797637977379783797937980379813798237983379843798537986379873798837989379903799137992379933799437995379963799737998379993800038001380023800338004380053800638007380083800938010380113801238013380143801538016380173801838019380203802138022380233802438025380263802738028380293803038031380323803338034380353803638037380383803938040380413804238043380443804538046380473804838049380503805138052380533805438055380563805738058380593806038061380623806338064380653806638067380683806938070380713807238073380743807538076380773807838079380803808138082380833808438085380863808738088380893809038091380923809338094380953809638097380983809938100381013810238103381043810538106381073810838109381103811138112381133811438115381163811738118381193812038121381223812338124381253812638127381283812938130381313813238133381343813538136381373813838139381403814138142381433814438145381463814738148381493815038151381523815338154381553815638157381583815938160381613816238163381643816538166381673816838169381703817138172381733817438175381763817738178381793818038181381823818338184381853818638187381883818938190381913819238193381943819538196381973819838199382003820138202382033820438205382063820738208382093821038211382123821338214382153821638217382183821938220382213822238223382243822538226382273822838229382303823138232382333823438235382363823738238382393824038241382423824338244382453824638247382483824938250382513825238253382543825538256382573825838259382603826138262382633826438265382663826738268382693827038271382723827338274382753827638277382783827938280382813828238283382843828538286382873828838289382903829138292382933829438295382963829738298382993830038301383023830338304383053830638307383083830938310383113831238313383143831538316383173831838319383203832138322383233832438325383263832738328383293833038331383323833338334383353833638337383383833938340383413834238343383443834538346383473834838349383503835138352383533835438355383563835738358383593836038361383623836338364383653836638367383683836938370383713837238373383743837538376383773837838379383803838138382383833838438385383863838738388383893839038391383923839338394383953839638397383983839938400384013840238403384043840538406384073840838409384103841138412384133841438415384163841738418384193842038421384223842338424384253842638427384283842938430384313843238433384343843538436384373843838439384403844138442384433844438445384463844738448384493845038451384523845338454384553845638457384583845938460384613846238463384643846538466384673846838469384703847138472384733847438475384763847738478384793848038481384823848338484384853848638487384883848938490384913849238493384943849538496384973849838499385003850138502385033850438505385063850738508385093851038511385123851338514385153851638517385183851938520385213852238523385243852538526385273852838529385303853138532385333853438535385363853738538385393854038541385423854338544385453854638547385483854938550385513855238553385543855538556385573855838559385603856138562385633856438565385663856738568385693857038571385723857338574385753857638577385783857938580385813858238583385843858538586385873858838589385903859138592385933859438595385963859738598385993860038601386023860338604386053860638607386083860938610386113861238613386143861538616386173861838619386203862138622386233862438625386263862738628386293863038631386323863338634386353863638637386383863938640386413864238643386443864538646386473864838649386503865138652386533865438655386563865738658386593866038661386623866338664386653866638667386683866938670386713867238673386743867538676386773867838679386803868138682386833868438685386863868738688386893869038691386923869338694386953869638697386983869938700387013870238703387043870538706387073870838709387103871138712387133871438715387163871738718387193872038721387223872338724387253872638727387283872938730387313873238733387343873538736387373873838739387403874138742387433874438745387463874738748387493875038751387523875338754387553875638757387583875938760387613876238763387643876538766387673876838769387703877138772387733877438775387763877738778387793878038781387823878338784387853878638787387883878938790387913879238793387943879538796387973879838799388003880138802388033880438805388063880738808388093881038811388123881338814388153881638817388183881938820388213882238823388243882538826388273882838829388303883138832388333883438835388363883738838388393884038841388423884338844388453884638847388483884938850388513885238853388543885538856388573885838859388603886138862388633886438865388663886738868388693887038871388723887338874388753887638877388783887938880388813888238883388843888538886388873888838889388903889138892388933889438895388963889738898388993890038901389023890338904389053890638907389083890938910389113891238913389143891538916389173891838919389203892138922389233892438925389263892738928389293893038931389323893338934389353893638937389383893938940389413894238943389443894538946389473894838949389503895138952389533895438955389563895738958389593896038961389623896338964389653896638967389683896938970389713897238973389743897538976389773897838979389803898138982389833898438985389863898738988389893899038991389923899338994389953899638997389983899939000390013900239003390043900539006390073900839009390103901139012390133901439015390163901739018390193902039021390223902339024390253902639027390283902939030390313903239033390343903539036390373903839039390403904139042390433904439045390463904739048390493905039051390523905339054390553905639057390583905939060390613906239063390643906539066390673906839069390703907139072390733907439075390763907739078390793908039081390823908339084390853908639087390883908939090390913909239093390943909539096390973909839099391003910139102391033910439105391063910739108391093911039111391123911339114391153911639117391183911939120391213912239123391243912539126391273912839129391303913139132391333913439135391363913739138391393914039141391423914339144391453914639147391483914939150391513915239153391543915539156391573915839159391603916139162391633916439165391663916739168391693917039171391723917339174391753917639177391783917939180391813918239183391843918539186391873918839189391903919139192391933919439195391963919739198391993920039201392023920339204392053920639207392083920939210392113921239213392143921539216392173921839219392203922139222392233922439225392263922739228392293923039231392323923339234392353923639237392383923939240392413924239243392443924539246392473924839249392503925139252392533925439255392563925739258392593926039261392623926339264392653926639267392683926939270392713927239273392743927539276392773927839279392803928139282392833928439285392863928739288392893929039291392923929339294392953929639297392983929939300393013930239303393043930539306393073930839309393103931139312393133931439315393163931739318393193932039321393223932339324393253932639327393283932939330393313933239333393343933539336393373933839339393403934139342393433934439345393463934739348393493935039351393523935339354393553935639357393583935939360393613936239363393643936539366393673936839369393703937139372393733937439375393763937739378393793938039381393823938339384393853938639387393883938939390393913939239393393943939539396393973939839399394003940139402394033940439405394063940739408394093941039411394123941339414394153941639417394183941939420394213942239423394243942539426394273942839429394303943139432394333943439435394363943739438394393944039441394423944339444394453944639447394483944939450394513945239453394543945539456394573945839459394603946139462394633946439465394663946739468394693947039471394723947339474394753947639477394783947939480394813948239483394843948539486394873948839489394903949139492394933949439495394963949739498394993950039501395023950339504395053950639507395083950939510395113951239513395143951539516395173951839519395203952139522395233952439525395263952739528395293953039531395323953339534395353953639537395383953939540395413954239543395443954539546395473954839549395503955139552395533955439555395563955739558395593956039561395623956339564395653956639567395683956939570395713957239573395743957539576395773957839579395803958139582395833958439585395863958739588395893959039591395923959339594395953959639597395983959939600396013960239603396043960539606396073960839609396103961139612396133961439615396163961739618396193962039621396223962339624396253962639627396283962939630396313963239633396343963539636396373963839639396403964139642396433964439645396463964739648396493965039651396523965339654396553965639657396583965939660396613966239663396643966539666396673966839669396703967139672396733967439675396763967739678396793968039681396823968339684396853968639687396883968939690396913969239693396943969539696396973969839699397003970139702397033970439705397063970739708397093971039711397123971339714397153971639717397183971939720397213972239723397243972539726397273972839729397303973139732397333973439735397363973739738397393974039741397423974339744397453974639747397483974939750397513975239753397543975539756397573975839759397603976139762397633976439765397663976739768397693977039771397723977339774397753977639777397783977939780397813978239783397843978539786397873978839789397903979139792397933979439795397963979739798397993980039801398023980339804398053980639807398083980939810398113981239813398143981539816398173981839819398203982139822398233982439825398263982739828398293983039831398323983339834398353983639837398383983939840398413984239843398443984539846398473984839849398503985139852398533985439855398563985739858398593986039861398623986339864398653986639867398683986939870398713987239873398743987539876398773987839879398803988139882398833988439885398863988739888398893989039891398923989339894398953989639897398983989939900399013990239903399043990539906399073990839909399103991139912399133991439915399163991739918399193992039921399223992339924399253992639927399283992939930399313993239933399343993539936399373993839939399403994139942399433994439945399463994739948399493995039951399523995339954399553995639957399583995939960399613996239963399643996539966399673996839969399703997139972399733997439975399763997739978399793998039981399823998339984399853998639987399883998939990399913999239993399943999539996399973999839999400004000140002400034000440005400064000740008400094001040011400124001340014400154001640017400184001940020400214002240023400244002540026400274002840029400304003140032400334003440035400364003740038400394004040041400424004340044400454004640047400484004940050400514005240053400544005540056400574005840059400604006140062400634006440065400664006740068400694007040071400724007340074400754007640077400784007940080400814008240083400844008540086400874008840089400904009140092400934009440095400964009740098400994010040101401024010340104401054010640107401084010940110401114011240113401144011540116401174011840119401204012140122401234012440125401264012740128401294013040131401324013340134401354013640137401384013940140401414014240143401444014540146401474014840149401504015140152401534015440155401564015740158401594016040161401624016340164401654016640167401684016940170401714017240173401744017540176401774017840179401804018140182401834018440185401864018740188401894019040191401924019340194401954019640197401984019940200402014020240203402044020540206402074020840209402104021140212402134021440215402164021740218402194022040221402224022340224402254022640227402284022940230402314023240233402344023540236402374023840239402404024140242402434024440245402464024740248402494025040251402524025340254402554025640257402584025940260402614026240263402644026540266402674026840269402704027140272402734027440275402764027740278402794028040281402824028340284402854028640287402884028940290402914029240293402944029540296402974029840299403004030140302403034030440305403064030740308403094031040311403124031340314403154031640317403184031940320403214032240323403244032540326403274032840329403304033140332403334033440335403364033740338403394034040341403424034340344403454034640347403484034940350403514035240353403544035540356403574035840359403604036140362403634036440365403664036740368403694037040371403724037340374403754037640377403784037940380403814038240383403844038540386403874038840389403904039140392403934039440395403964039740398403994040040401404024040340404404054040640407404084040940410404114041240413404144041540416404174041840419404204042140422404234042440425404264042740428404294043040431404324043340434404354043640437404384043940440404414044240443404444044540446404474044840449404504045140452404534045440455404564045740458404594046040461404624046340464404654046640467404684046940470404714047240473404744047540476404774047840479404804048140482404834048440485404864048740488404894049040491404924049340494404954049640497404984049940500405014050240503405044050540506405074050840509405104051140512405134051440515405164051740518405194052040521405224052340524405254052640527405284052940530405314053240533405344053540536405374053840539405404054140542405434054440545405464054740548405494055040551405524055340554405554055640557405584055940560405614056240563405644056540566405674056840569405704057140572405734057440575405764057740578405794058040581405824058340584405854058640587405884058940590405914059240593405944059540596405974059840599406004060140602406034060440605406064060740608406094061040611406124061340614406154061640617406184061940620406214062240623406244062540626406274062840629406304063140632406334063440635406364063740638406394064040641406424064340644406454064640647406484064940650406514065240653406544065540656406574065840659406604066140662406634066440665406664066740668406694067040671406724067340674406754067640677406784067940680406814068240683406844068540686406874068840689406904069140692406934069440695406964069740698406994070040701407024070340704407054070640707407084070940710407114071240713407144071540716407174071840719407204072140722407234072440725407264072740728407294073040731407324073340734407354073640737407384073940740407414074240743407444074540746407474074840749407504075140752407534075440755407564075740758407594076040761407624076340764407654076640767407684076940770407714077240773407744077540776407774077840779407804078140782407834078440785407864078740788407894079040791407924079340794407954079640797407984079940800408014080240803408044080540806408074080840809408104081140812408134081440815408164081740818408194082040821408224082340824408254082640827408284082940830408314083240833408344083540836408374083840839408404084140842408434084440845408464084740848408494085040851408524085340854408554085640857408584085940860408614086240863408644086540866408674086840869408704087140872408734087440875408764087740878408794088040881408824088340884408854088640887408884088940890408914089240893408944089540896408974089840899409004090140902409034090440905409064090740908409094091040911409124091340914409154091640917409184091940920409214092240923409244092540926409274092840929409304093140932409334093440935409364093740938409394094040941409424094340944409454094640947409484094940950409514095240953409544095540956409574095840959409604096140962409634096440965409664096740968409694097040971409724097340974409754097640977409784097940980409814098240983409844098540986409874098840989409904099140992409934099440995409964099740998409994100041001410024100341004410054100641007410084100941010410114101241013410144101541016410174101841019410204102141022410234102441025410264102741028410294103041031410324103341034410354103641037410384103941040410414104241043410444104541046410474104841049410504105141052410534105441055410564105741058410594106041061410624106341064410654106641067410684106941070410714107241073410744107541076410774107841079410804108141082410834108441085410864108741088410894109041091410924109341094410954109641097410984109941100411014110241103411044110541106411074110841109411104111141112411134111441115411164111741118411194112041121411224112341124411254112641127411284112941130411314113241133411344113541136411374113841139411404114141142411434114441145411464114741148411494115041151411524115341154411554115641157411584115941160411614116241163411644116541166411674116841169411704117141172411734117441175411764117741178411794118041181411824118341184411854118641187411884118941190411914119241193411944119541196411974119841199412004120141202412034120441205412064120741208412094121041211412124121341214412154121641217412184121941220412214122241223412244122541226412274122841229412304123141232412334123441235412364123741238412394124041241412424124341244412454124641247412484124941250412514125241253412544125541256412574125841259412604126141262412634126441265412664126741268
  1. /**
  2. * TinyMCE version 8.0.2 (2025-08-14)
  3. */
  4. (function () {
  5. 'use strict';
  6. var typeOf$1 = function (x) {
  7. if (x === null) {
  8. return 'null';
  9. }
  10. if (x === undefined) {
  11. return 'undefined';
  12. }
  13. var t = typeof x;
  14. if (t === 'object' && (Array.prototype.isPrototypeOf(x) || x.constructor && x.constructor.name === 'Array')) {
  15. return 'array';
  16. }
  17. if (t === 'object' && (String.prototype.isPrototypeOf(x) || x.constructor && x.constructor.name === 'String')) {
  18. return 'string';
  19. }
  20. return t;
  21. };
  22. var isEquatableType = function (x) {
  23. return ['undefined', 'boolean', 'number', 'string', 'function', 'xml', 'null'].indexOf(x) !== -1;
  24. };
  25. var sort$1 = function (xs, compareFn) {
  26. var clone = Array.prototype.slice.call(xs);
  27. return clone.sort(compareFn);
  28. };
  29. var contramap = function (eqa, f) {
  30. return eq$2(function (x, y) { return eqa.eq(f(x), f(y)); });
  31. };
  32. var eq$2 = function (f) {
  33. return ({ eq: f });
  34. };
  35. var tripleEq = eq$2(function (x, y) { return x === y; });
  36. var eqString = tripleEq;
  37. var eqArray = function (eqa) { return eq$2(function (x, y) {
  38. if (x.length !== y.length) {
  39. return false;
  40. }
  41. var len = x.length;
  42. for (var i = 0; i < len; i++) {
  43. if (!eqa.eq(x[i], y[i])) {
  44. return false;
  45. }
  46. }
  47. return true;
  48. }); };
  49. // TODO: Make an Ord typeclass
  50. var eqSortedArray = function (eqa, compareFn) {
  51. return contramap(eqArray(eqa), function (xs) { return sort$1(xs, compareFn); });
  52. };
  53. var eqRecord = function (eqa) { return eq$2(function (x, y) {
  54. var kx = Object.keys(x);
  55. var ky = Object.keys(y);
  56. if (!eqSortedArray(eqString).eq(kx, ky)) {
  57. return false;
  58. }
  59. var len = kx.length;
  60. for (var i = 0; i < len; i++) {
  61. var q = kx[i];
  62. if (!eqa.eq(x[q], y[q])) {
  63. return false;
  64. }
  65. }
  66. return true;
  67. }); };
  68. var eqAny = eq$2(function (x, y) {
  69. if (x === y) {
  70. return true;
  71. }
  72. var tx = typeOf$1(x);
  73. var ty = typeOf$1(y);
  74. if (tx !== ty) {
  75. return false;
  76. }
  77. if (isEquatableType(tx)) {
  78. return x === y;
  79. }
  80. else if (tx === 'array') {
  81. return eqArray(eqAny).eq(x, y);
  82. }
  83. else if (tx === 'object') {
  84. return eqRecord(eqAny).eq(x, y);
  85. }
  86. return false;
  87. });
  88. /* eslint-disable @typescript-eslint/no-wrapper-object-types */
  89. const getPrototypeOf$2 = Object.getPrototypeOf;
  90. const hasProto = (v, constructor, predicate) => {
  91. var _a;
  92. if (predicate(v, constructor.prototype)) {
  93. return true;
  94. }
  95. else {
  96. // String-based fallback time
  97. return ((_a = v.constructor) === null || _a === void 0 ? void 0 : _a.name) === constructor.name;
  98. }
  99. };
  100. const typeOf = (x) => {
  101. const t = typeof x;
  102. if (x === null) {
  103. return 'null';
  104. }
  105. else if (t === 'object' && Array.isArray(x)) {
  106. return 'array';
  107. }
  108. else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) {
  109. return 'string';
  110. }
  111. else {
  112. return t;
  113. }
  114. };
  115. const isType$1 = (type) => (value) => typeOf(value) === type;
  116. const isSimpleType = (type) => (value) => typeof value === type;
  117. const eq$1 = (t) => (a) => t === a;
  118. const is$5 = (value, constructor) => isObject(value) && hasProto(value, constructor, (o, proto) => getPrototypeOf$2(o) === proto);
  119. const isString = isType$1('string');
  120. const isObject = isType$1('object');
  121. const isPlainObject = (value) => is$5(value, Object);
  122. const isArray$1 = isType$1('array');
  123. const isNull = eq$1(null);
  124. const isBoolean = isSimpleType('boolean');
  125. const isUndefined = eq$1(undefined);
  126. const isNullable = (a) => a === null || a === undefined;
  127. const isNonNullable = (a) => !isNullable(a);
  128. const isFunction = isSimpleType('function');
  129. const isNumber = isSimpleType('number');
  130. const isArrayOf = (value, pred) => {
  131. if (isArray$1(value)) {
  132. for (let i = 0, len = value.length; i < len; ++i) {
  133. if (!(pred(value[i]))) {
  134. return false;
  135. }
  136. }
  137. return true;
  138. }
  139. return false;
  140. };
  141. const noop = () => { };
  142. /** Compose a unary function with an n-ary function */
  143. const compose = (fa, fb) => {
  144. return (...args) => {
  145. return fa(fb.apply(null, args));
  146. };
  147. };
  148. /** Compose two unary functions. Similar to compose, but avoids using Function.prototype.apply. */
  149. const compose1 = (fbc, fab) => (a) => fbc(fab(a));
  150. const constant = (value) => {
  151. return () => {
  152. return value;
  153. };
  154. };
  155. const identity = (x) => {
  156. return x;
  157. };
  158. const tripleEquals = (a, b) => {
  159. return a === b;
  160. };
  161. function curry(fn, ...initialArgs) {
  162. return (...restArgs) => {
  163. const all = initialArgs.concat(restArgs);
  164. return fn.apply(null, all);
  165. };
  166. }
  167. const not = (f) => (t) => !f(t);
  168. const die = (msg) => {
  169. return () => {
  170. throw new Error(msg);
  171. };
  172. };
  173. const apply$1 = (f) => {
  174. return f();
  175. };
  176. const call = (f) => {
  177. f();
  178. };
  179. const never = constant(false);
  180. const always = constant(true);
  181. /**
  182. * The `Optional` type represents a value (of any type) that potentially does
  183. * not exist. Any `Optional<T>` can either be a `Some<T>` (in which case the
  184. * value does exist) or a `None` (in which case the value does not exist). This
  185. * module defines a whole lot of FP-inspired utility functions for dealing with
  186. * `Optional` objects.
  187. *
  188. * Comparison with null or undefined:
  189. * - We don't get fancy null coalescing operators with `Optional`
  190. * - We do get fancy helper functions with `Optional`
  191. * - `Optional` support nesting, and allow for the type to still be nullable (or
  192. * another `Optional`)
  193. * - There is no option to turn off strict-optional-checks like there is for
  194. * strict-null-checks
  195. */
  196. class Optional {
  197. // The internal representation has a `tag` and a `value`, but both are
  198. // private: able to be console.logged, but not able to be accessed by code
  199. constructor(tag, value) {
  200. this.tag = tag;
  201. this.value = value;
  202. }
  203. // --- Identities ---
  204. /**
  205. * Creates a new `Optional<T>` that **does** contain a value.
  206. */
  207. static some(value) {
  208. return new Optional(true, value);
  209. }
  210. /**
  211. * Create a new `Optional<T>` that **does not** contain a value. `T` can be
  212. * any type because we don't actually have a `T`.
  213. */
  214. static none() {
  215. return Optional.singletonNone;
  216. }
  217. /**
  218. * Perform a transform on an `Optional` type. Regardless of whether this
  219. * `Optional` contains a value or not, `fold` will return a value of type `U`.
  220. * If this `Optional` does not contain a value, the `U` will be created by
  221. * calling `onNone`. If this `Optional` does contain a value, the `U` will be
  222. * created by calling `onSome`.
  223. *
  224. * For the FP enthusiasts in the room, this function:
  225. * 1. Could be used to implement all of the functions below
  226. * 2. Forms a catamorphism
  227. */
  228. fold(onNone, onSome) {
  229. if (this.tag) {
  230. return onSome(this.value);
  231. }
  232. else {
  233. return onNone();
  234. }
  235. }
  236. /**
  237. * Determine if this `Optional` object contains a value.
  238. */
  239. isSome() {
  240. return this.tag;
  241. }
  242. /**
  243. * Determine if this `Optional` object **does not** contain a value.
  244. */
  245. isNone() {
  246. return !this.tag;
  247. }
  248. // --- Functor (name stolen from Haskell / maths) ---
  249. /**
  250. * Perform a transform on an `Optional` object, **if** there is a value. If
  251. * you provide a function to turn a T into a U, this is the function you use
  252. * to turn an `Optional<T>` into an `Optional<U>`. If this **does** contain
  253. * a value then the output will also contain a value (that value being the
  254. * output of `mapper(this.value)`), and if this **does not** contain a value
  255. * then neither will the output.
  256. */
  257. map(mapper) {
  258. if (this.tag) {
  259. return Optional.some(mapper(this.value));
  260. }
  261. else {
  262. return Optional.none();
  263. }
  264. }
  265. // --- Monad (name stolen from Haskell / maths) ---
  266. /**
  267. * Perform a transform on an `Optional` object, **if** there is a value.
  268. * Unlike `map`, here the transform itself also returns an `Optional`.
  269. */
  270. bind(binder) {
  271. if (this.tag) {
  272. return binder(this.value);
  273. }
  274. else {
  275. return Optional.none();
  276. }
  277. }
  278. // --- Traversable (name stolen from Haskell / maths) ---
  279. /**
  280. * For a given predicate, this function finds out if there **exists** a value
  281. * inside this `Optional` object that meets the predicate. In practice, this
  282. * means that for `Optional`s that do not contain a value it returns false (as
  283. * no predicate-meeting value exists).
  284. */
  285. exists(predicate) {
  286. return this.tag && predicate(this.value);
  287. }
  288. /**
  289. * For a given predicate, this function finds out if **all** the values inside
  290. * this `Optional` object meet the predicate. In practice, this means that
  291. * for `Optional`s that do not contain a value it returns true (as all 0
  292. * objects do meet the predicate).
  293. */
  294. forall(predicate) {
  295. return !this.tag || predicate(this.value);
  296. }
  297. filter(predicate) {
  298. if (!this.tag || predicate(this.value)) {
  299. return this;
  300. }
  301. else {
  302. return Optional.none();
  303. }
  304. }
  305. // --- Getters ---
  306. /**
  307. * Get the value out of the inside of the `Optional` object, using a default
  308. * `replacement` value if the provided `Optional` object does not contain a
  309. * value.
  310. */
  311. getOr(replacement) {
  312. return this.tag ? this.value : replacement;
  313. }
  314. /**
  315. * Get the value out of the inside of the `Optional` object, using a default
  316. * `replacement` value if the provided `Optional` object does not contain a
  317. * value. Unlike `getOr`, in this method the `replacement` object is also
  318. * `Optional` - meaning that this method will always return an `Optional`.
  319. */
  320. or(replacement) {
  321. return this.tag ? this : replacement;
  322. }
  323. /**
  324. * Get the value out of the inside of the `Optional` object, using a default
  325. * `replacement` value if the provided `Optional` object does not contain a
  326. * value. Unlike `getOr`, in this method the `replacement` value is
  327. * "thunked" - that is to say that you don't pass a value to `getOrThunk`, you
  328. * pass a function which (if called) will **return** the `value` you want to
  329. * use.
  330. */
  331. getOrThunk(thunk) {
  332. return this.tag ? this.value : thunk();
  333. }
  334. /**
  335. * Get the value out of the inside of the `Optional` object, using a default
  336. * `replacement` value if the provided Optional object does not contain a
  337. * value.
  338. *
  339. * Unlike `or`, in this method the `replacement` value is "thunked" - that is
  340. * to say that you don't pass a value to `orThunk`, you pass a function which
  341. * (if called) will **return** the `value` you want to use.
  342. *
  343. * Unlike `getOrThunk`, in this method the `replacement` value is also
  344. * `Optional`, meaning that this method will always return an `Optional`.
  345. */
  346. orThunk(thunk) {
  347. return this.tag ? this : thunk();
  348. }
  349. /**
  350. * Get the value out of the inside of the `Optional` object, throwing an
  351. * exception if the provided `Optional` object does not contain a value.
  352. *
  353. * WARNING:
  354. * You should only be using this function if you know that the `Optional`
  355. * object **is not** empty (otherwise you're throwing exceptions in production
  356. * code, which is bad).
  357. *
  358. * In tests this is more acceptable.
  359. *
  360. * Prefer other methods to this, such as `.each`.
  361. */
  362. getOrDie(message) {
  363. if (!this.tag) {
  364. throw new Error(message !== null && message !== void 0 ? message : 'Called getOrDie on None');
  365. }
  366. else {
  367. return this.value;
  368. }
  369. }
  370. // --- Interop with null and undefined ---
  371. /**
  372. * Creates an `Optional` value from a nullable (or undefined-able) input.
  373. * Null, or undefined, is converted to `None`, and anything else is converted
  374. * to `Some`.
  375. */
  376. static from(value) {
  377. return isNonNullable(value) ? Optional.some(value) : Optional.none();
  378. }
  379. /**
  380. * Converts an `Optional` to a nullable type, by getting the value if it
  381. * exists, or returning `null` if it does not.
  382. */
  383. getOrNull() {
  384. return this.tag ? this.value : null;
  385. }
  386. /**
  387. * Converts an `Optional` to an undefined-able type, by getting the value if
  388. * it exists, or returning `undefined` if it does not.
  389. */
  390. getOrUndefined() {
  391. return this.value;
  392. }
  393. // --- Utilities ---
  394. /**
  395. * If the `Optional` contains a value, perform an action on that value.
  396. * Unlike the rest of the methods on this type, `.each` has side-effects. If
  397. * you want to transform an `Optional<T>` **into** something, then this is not
  398. * the method for you. If you want to use an `Optional<T>` to **do**
  399. * something, then this is the method for you - provided you're okay with not
  400. * doing anything in the case where the `Optional` doesn't have a value inside
  401. * it. If you're not sure whether your use-case fits into transforming
  402. * **into** something or **doing** something, check whether it has a return
  403. * value. If it does, you should be performing a transform.
  404. */
  405. each(worker) {
  406. if (this.tag) {
  407. worker(this.value);
  408. }
  409. }
  410. /**
  411. * Turn the `Optional` object into an array that contains all of the values
  412. * stored inside the `Optional`. In practice, this means the output will have
  413. * either 0 or 1 elements.
  414. */
  415. toArray() {
  416. return this.tag ? [this.value] : [];
  417. }
  418. /**
  419. * Turn the `Optional` object into a string for debugging or printing. Not
  420. * recommended for production code, but good for debugging. Also note that
  421. * these days an `Optional` object can be logged to the console directly, and
  422. * its inner value (if it exists) will be visible.
  423. */
  424. toString() {
  425. return this.tag ? `some(${this.value})` : 'none()';
  426. }
  427. }
  428. // Sneaky optimisation: every instance of Optional.none is identical, so just
  429. // reuse the same object
  430. Optional.singletonNone = new Optional(false);
  431. const nativeSlice = Array.prototype.slice;
  432. const nativeIndexOf = Array.prototype.indexOf;
  433. const nativePush = Array.prototype.push;
  434. const rawIndexOf = (ts, t) => nativeIndexOf.call(ts, t);
  435. const indexOf$1 = (xs, x) => {
  436. // The rawIndexOf method does not wrap up in an option. This is for performance reasons.
  437. const r = rawIndexOf(xs, x);
  438. return r === -1 ? Optional.none() : Optional.some(r);
  439. };
  440. const contains$2 = (xs, x) => rawIndexOf(xs, x) > -1;
  441. const exists = (xs, pred) => {
  442. for (let i = 0, len = xs.length; i < len; i++) {
  443. const x = xs[i];
  444. if (pred(x, i)) {
  445. return true;
  446. }
  447. }
  448. return false;
  449. };
  450. const map$3 = (xs, f) => {
  451. // pre-allocating array size when it's guaranteed to be known
  452. // http://jsperf.com/push-allocated-vs-dynamic/22
  453. const len = xs.length;
  454. const r = new Array(len);
  455. for (let i = 0; i < len; i++) {
  456. const x = xs[i];
  457. r[i] = f(x, i);
  458. }
  459. return r;
  460. };
  461. // Unwound implementing other functions in terms of each.
  462. // The code size is roughly the same, and it should allow for better optimisation.
  463. // const each = function<T, U>(xs: T[], f: (x: T, i?: number, xs?: T[]) => void): void {
  464. const each$e = (xs, f) => {
  465. for (let i = 0, len = xs.length; i < len; i++) {
  466. const x = xs[i];
  467. f(x, i);
  468. }
  469. };
  470. const eachr = (xs, f) => {
  471. for (let i = xs.length - 1; i >= 0; i--) {
  472. const x = xs[i];
  473. f(x, i);
  474. }
  475. };
  476. const partition$2 = (xs, pred) => {
  477. const pass = [];
  478. const fail = [];
  479. for (let i = 0, len = xs.length; i < len; i++) {
  480. const x = xs[i];
  481. const arr = pred(x, i) ? pass : fail;
  482. arr.push(x);
  483. }
  484. return { pass, fail };
  485. };
  486. const filter$5 = (xs, pred) => {
  487. const r = [];
  488. for (let i = 0, len = xs.length; i < len; i++) {
  489. const x = xs[i];
  490. if (pred(x, i)) {
  491. r.push(x);
  492. }
  493. }
  494. return r;
  495. };
  496. /*
  497. * Groups an array into contiguous arrays of like elements. Whether an element is like or not depends on f.
  498. *
  499. * f is a function that derives a value from an element - e.g. true or false, or a string.
  500. * Elements are like if this function generates the same value for them (according to ===).
  501. *
  502. *
  503. * Order of the elements is preserved. Arr.flatten() on the result will return the original list, as with Haskell groupBy function.
  504. * For a good explanation, see the group function (which is a special case of groupBy)
  505. * http://hackage.haskell.org/package/base-4.7.0.0/docs/Data-List.html#v:group
  506. */
  507. const groupBy = (xs, f) => {
  508. if (xs.length === 0) {
  509. return [];
  510. }
  511. else {
  512. let wasType = f(xs[0]); // initial case for matching
  513. const r = [];
  514. let group = [];
  515. for (let i = 0, len = xs.length; i < len; i++) {
  516. const x = xs[i];
  517. const type = f(x);
  518. if (type !== wasType) {
  519. r.push(group);
  520. group = [];
  521. }
  522. wasType = type;
  523. group.push(x);
  524. }
  525. if (group.length !== 0) {
  526. r.push(group);
  527. }
  528. return r;
  529. }
  530. };
  531. const foldr = (xs, f, acc) => {
  532. eachr(xs, (x, i) => {
  533. acc = f(acc, x, i);
  534. });
  535. return acc;
  536. };
  537. const foldl = (xs, f, acc) => {
  538. each$e(xs, (x, i) => {
  539. acc = f(acc, x, i);
  540. });
  541. return acc;
  542. };
  543. const findUntil$1 = (xs, pred, until) => {
  544. for (let i = 0, len = xs.length; i < len; i++) {
  545. const x = xs[i];
  546. if (pred(x, i)) {
  547. return Optional.some(x);
  548. }
  549. else if (until(x, i)) {
  550. break;
  551. }
  552. }
  553. return Optional.none();
  554. };
  555. const find$2 = (xs, pred) => {
  556. return findUntil$1(xs, pred, never);
  557. };
  558. const findIndex$2 = (xs, pred) => {
  559. for (let i = 0, len = xs.length; i < len; i++) {
  560. const x = xs[i];
  561. if (pred(x, i)) {
  562. return Optional.some(i);
  563. }
  564. }
  565. return Optional.none();
  566. };
  567. const findLastIndex = (arr, pred) => {
  568. for (let i = arr.length - 1; i >= 0; i--) {
  569. if (pred(arr[i], i)) {
  570. return Optional.some(i);
  571. }
  572. }
  573. return Optional.none();
  574. };
  575. const flatten = (xs) => {
  576. // Note, this is possible because push supports multiple arguments:
  577. // http://jsperf.com/concat-push/6
  578. // Note that in the past, concat() would silently work (very slowly) for array-like objects.
  579. // With this change it will throw an error.
  580. const r = [];
  581. for (let i = 0, len = xs.length; i < len; ++i) {
  582. // Ensure that each value is an array itself
  583. if (!isArray$1(xs[i])) {
  584. throw new Error('Arr.flatten item ' + i + ' was not an array, input: ' + xs);
  585. }
  586. nativePush.apply(r, xs[i]);
  587. }
  588. return r;
  589. };
  590. const bind$3 = (xs, f) => flatten(map$3(xs, f));
  591. const forall = (xs, pred) => {
  592. for (let i = 0, len = xs.length; i < len; ++i) {
  593. const x = xs[i];
  594. if (pred(x, i) !== true) {
  595. return false;
  596. }
  597. }
  598. return true;
  599. };
  600. const reverse = (xs) => {
  601. const r = nativeSlice.call(xs, 0);
  602. r.reverse();
  603. return r;
  604. };
  605. const difference = (a1, a2) => filter$5(a1, (x) => !contains$2(a2, x));
  606. const mapToObject = (xs, f) => {
  607. const r = {};
  608. for (let i = 0, len = xs.length; i < len; i++) {
  609. const x = xs[i];
  610. r[String(x)] = f(x, i);
  611. }
  612. return r;
  613. };
  614. const sort = (xs, comparator) => {
  615. const copy = nativeSlice.call(xs, 0);
  616. copy.sort(comparator);
  617. return copy;
  618. };
  619. const get$b = (xs, i) => i >= 0 && i < xs.length ? Optional.some(xs[i]) : Optional.none();
  620. const head = (xs) => get$b(xs, 0);
  621. const last$2 = (xs) => get$b(xs, xs.length - 1);
  622. const from = isFunction(Array.from) ? Array.from : (x) => nativeSlice.call(x);
  623. const findMap = (arr, f) => {
  624. for (let i = 0; i < arr.length; i++) {
  625. const r = f(arr[i], i);
  626. if (r.isSome()) {
  627. return r;
  628. }
  629. }
  630. return Optional.none();
  631. };
  632. const unique$1 = (xs, comparator) => {
  633. const r = [];
  634. const isDuplicated = isFunction(comparator) ?
  635. (x) => exists(r, (i) => comparator(i, x)) :
  636. (x) => contains$2(r, x);
  637. for (let i = 0, len = xs.length; i < len; i++) {
  638. const x = xs[i];
  639. if (!isDuplicated(x)) {
  640. r.push(x);
  641. }
  642. }
  643. return r;
  644. };
  645. // There are many variations of Object iteration that are faster than the 'for-in' style:
  646. // http://jsperf.com/object-keys-iteration/107
  647. //
  648. // Use the native keys if it is available (IE9+), otherwise fall back to manually filtering
  649. const keys = Object.keys;
  650. const hasOwnProperty$1 = Object.hasOwnProperty;
  651. const each$d = (obj, f) => {
  652. const props = keys(obj);
  653. for (let k = 0, len = props.length; k < len; k++) {
  654. const i = props[k];
  655. const x = obj[i];
  656. f(x, i);
  657. }
  658. };
  659. const map$2 = (obj, f) => {
  660. return tupleMap(obj, (x, i) => ({
  661. k: i,
  662. v: f(x, i)
  663. }));
  664. };
  665. const tupleMap = (obj, f) => {
  666. const r = {};
  667. each$d(obj, (x, i) => {
  668. const tuple = f(x, i);
  669. r[tuple.k] = tuple.v;
  670. });
  671. return r;
  672. };
  673. const objAcc = (r) => (x, i) => {
  674. r[i] = x;
  675. };
  676. const internalFilter = (obj, pred, onTrue, onFalse) => {
  677. each$d(obj, (x, i) => {
  678. (pred(x, i) ? onTrue : onFalse)(x, i);
  679. });
  680. };
  681. const bifilter = (obj, pred) => {
  682. const t = {};
  683. const f = {};
  684. internalFilter(obj, pred, objAcc(t), objAcc(f));
  685. return { t, f };
  686. };
  687. const filter$4 = (obj, pred) => {
  688. const t = {};
  689. internalFilter(obj, pred, objAcc(t), noop);
  690. return t;
  691. };
  692. const mapToArray = (obj, f) => {
  693. const r = [];
  694. each$d(obj, (value, name) => {
  695. r.push(f(value, name));
  696. });
  697. return r;
  698. };
  699. const values = (obj) => {
  700. return mapToArray(obj, identity);
  701. };
  702. const get$a = (obj, key) => {
  703. return has$2(obj, key) ? Optional.from(obj[key]) : Optional.none();
  704. };
  705. const has$2 = (obj, key) => hasOwnProperty$1.call(obj, key);
  706. const hasNonNullableKey = (obj, key) => has$2(obj, key) && obj[key] !== undefined && obj[key] !== null;
  707. const equal$1 = (a1, a2, eq = eqAny) => eqRecord(eq).eq(a1, a2);
  708. /*
  709. * Generates a church encoded ADT (https://en.wikipedia.org/wiki/Church_encoding)
  710. * For syntax and use, look at the test code.
  711. */
  712. const generate$2 = (cases) => {
  713. // validation
  714. if (!isArray$1(cases)) {
  715. throw new Error('cases must be an array');
  716. }
  717. if (cases.length === 0) {
  718. throw new Error('there must be at least one case');
  719. }
  720. const constructors = [];
  721. // adt is mutated to add the individual cases
  722. const adt = {};
  723. each$e(cases, (acase, count) => {
  724. const keys$1 = keys(acase);
  725. // validation
  726. if (keys$1.length !== 1) {
  727. throw new Error('one and only one name per case');
  728. }
  729. const key = keys$1[0];
  730. const value = acase[key];
  731. // validation
  732. if (adt[key] !== undefined) {
  733. throw new Error('duplicate key detected:' + key);
  734. }
  735. else if (key === 'cata') {
  736. throw new Error('cannot have a case named cata (sorry)');
  737. }
  738. else if (!isArray$1(value)) {
  739. // this implicitly checks if acase is an object
  740. throw new Error('case arguments must be an array');
  741. }
  742. constructors.push(key);
  743. //
  744. // constructor for key
  745. //
  746. adt[key] = (...args) => {
  747. const argLength = args.length;
  748. // validation
  749. if (argLength !== value.length) {
  750. throw new Error('Wrong number of arguments to case ' + key + '. Expected ' + value.length + ' (' + value + '), got ' + argLength);
  751. }
  752. const match = (branches) => {
  753. const branchKeys = keys(branches);
  754. if (constructors.length !== branchKeys.length) {
  755. throw new Error('Wrong number of arguments to match. Expected: ' + constructors.join(',') + '\nActual: ' + branchKeys.join(','));
  756. }
  757. const allReqd = forall(constructors, (reqKey) => {
  758. return contains$2(branchKeys, reqKey);
  759. });
  760. if (!allReqd) {
  761. throw new Error('Not all branches were specified when using match. Specified: ' + branchKeys.join(', ') + '\nRequired: ' + constructors.join(', '));
  762. }
  763. return branches[key].apply(null, args);
  764. };
  765. //
  766. // the fold function for key
  767. //
  768. return {
  769. fold: (...foldArgs) => {
  770. // runtime validation
  771. if (foldArgs.length !== cases.length) {
  772. throw new Error('Wrong number of arguments to fold. Expected ' + cases.length + ', got ' + foldArgs.length);
  773. }
  774. const target = foldArgs[count];
  775. return target.apply(null, args);
  776. },
  777. match,
  778. // NOTE: Only for debugging.
  779. log: (label) => {
  780. // eslint-disable-next-line no-console
  781. console.log(label, {
  782. constructors,
  783. constructor: key,
  784. params: args
  785. });
  786. }
  787. };
  788. };
  789. });
  790. return adt;
  791. };
  792. const Adt = {
  793. generate: generate$2
  794. };
  795. const Cell = (initial) => {
  796. let value = initial;
  797. const get = () => {
  798. return value;
  799. };
  800. const set = (v) => {
  801. value = v;
  802. };
  803. return {
  804. get,
  805. set
  806. };
  807. };
  808. /**
  809. * Creates a new `Result<T, E>` that **does** contain a value.
  810. */
  811. const value$2 = (value) => {
  812. const applyHelper = (fn) => fn(value);
  813. const constHelper = constant(value);
  814. const outputHelper = () => output;
  815. const output = {
  816. // Debug info
  817. tag: true,
  818. inner: value,
  819. // Actual Result methods
  820. fold: (_onError, onValue) => onValue(value),
  821. isValue: always,
  822. isError: never,
  823. map: (mapper) => Result.value(mapper(value)),
  824. mapError: outputHelper,
  825. bind: applyHelper,
  826. exists: applyHelper,
  827. forall: applyHelper,
  828. getOr: constHelper,
  829. or: outputHelper,
  830. getOrThunk: constHelper,
  831. orThunk: outputHelper,
  832. getOrDie: constHelper,
  833. each: (fn) => {
  834. // Can't write the function inline because we don't want to return something by mistake
  835. fn(value);
  836. },
  837. toOptional: () => Optional.some(value),
  838. };
  839. return output;
  840. };
  841. /**
  842. * Creates a new `Result<T, E>` that **does not** contain a value, and therefore
  843. * contains an error.
  844. */
  845. const error = (error) => {
  846. const outputHelper = () => output;
  847. const output = {
  848. // Debug info
  849. tag: false,
  850. inner: error,
  851. // Actual Result methods
  852. fold: (onError, _onValue) => onError(error),
  853. isValue: never,
  854. isError: always,
  855. map: outputHelper,
  856. mapError: (mapper) => Result.error(mapper(error)),
  857. bind: outputHelper,
  858. exists: never,
  859. forall: always,
  860. getOr: identity,
  861. or: identity,
  862. getOrThunk: apply$1,
  863. orThunk: apply$1,
  864. getOrDie: die(String(error)),
  865. each: noop,
  866. toOptional: Optional.none,
  867. };
  868. return output;
  869. };
  870. /**
  871. * Creates a new `Result<T, E>` from an `Optional<T>` and an `E`. If the
  872. * `Optional` contains a value, so will the outputted `Result`. If it does not,
  873. * the outputted `Result` will contain an error (and that error will be the
  874. * error passed in).
  875. */
  876. const fromOption = (optional, err) => optional.fold(() => error(err), value$2);
  877. const Result = {
  878. value: value$2,
  879. error,
  880. fromOption
  881. };
  882. // Use window object as the global if it's available since CSP will block script evals
  883. // eslint-disable-next-line @typescript-eslint/no-implied-eval
  884. const Global = typeof window !== 'undefined' ? window : Function('return this;')();
  885. /* eslint-disable no-bitwise */
  886. const uuidV4Bytes = () => {
  887. const bytes = window.crypto.getRandomValues(new Uint8Array(16));
  888. // https://tools.ietf.org/html/rfc4122#section-4.1.3
  889. // This will first bit mask away the most significant 4 bits (version octet)
  890. // then mask in the v4 number we only care about v4 random version at this point so (byte & 0b00001111 | 0b01000000)
  891. bytes[6] = bytes[6] & 15 | 64;
  892. // https://tools.ietf.org/html/rfc4122#section-4.1.1
  893. // This will first bit mask away the highest two bits then masks in the highest bit so (byte & 0b00111111 | 0b10000000)
  894. // So it will set the Msb0=1 & Msb1=0 described by the "The variant specified in this document." row in the table
  895. bytes[8] = bytes[8] & 63 | 128;
  896. return bytes;
  897. };
  898. const uuidV4String = () => {
  899. const uuid = uuidV4Bytes();
  900. const getHexRange = (startIndex, endIndex) => {
  901. let buff = '';
  902. for (let i = startIndex; i <= endIndex; ++i) {
  903. const hexByte = uuid[i].toString(16).padStart(2, '0');
  904. buff += hexByte;
  905. }
  906. return buff;
  907. };
  908. // RFC 4122 UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  909. return `${getHexRange(0, 3)}-${getHexRange(4, 5)}-${getHexRange(6, 7)}-${getHexRange(8, 9)}-${getHexRange(10, 15)}`;
  910. };
  911. /**
  912. * Adds two numbers, and wrap to a range.
  913. * If the result overflows to the right, snap to the left.
  914. * If the result overflows to the left, snap to the right.
  915. */
  916. // ASSUMPTION: Max will always be larger than min
  917. const clamp$2 = (value, min, max) => Math.min(Math.max(value, min), max);
  918. // the division is meant to get a number between 0 and 1 for more information check this discussion: https://stackoverflow.com/questions/58285941/how-to-replace-math-random-with-crypto-getrandomvalues-and-keep-same-result
  919. const random = () => window.crypto.getRandomValues(new Uint32Array(1))[0] / 4294967295;
  920. /**
  921. * Generate a unique identifier.
  922. *
  923. * The unique portion of the identifier only contains an underscore
  924. * and digits, so that it may safely be used within HTML attributes.
  925. *
  926. * The chance of generating a non-unique identifier has been minimized
  927. * by combining the current time, a random number and a one-up counter.
  928. *
  929. * generate :: String -> String
  930. */
  931. let unique = 0;
  932. const generate$1 = (prefix) => {
  933. const date = new Date();
  934. const time = date.getTime();
  935. const random$1 = Math.floor(random() * 1000000000);
  936. unique++;
  937. return prefix + '_' + random$1 + unique + String(time);
  938. };
  939. /**
  940. * Generate a uuidv4 string
  941. * In accordance with RFC 4122 (https://datatracker.ietf.org/doc/html/rfc4122)
  942. */
  943. const uuidV4 = () => {
  944. if (window.isSecureContext) {
  945. return window.crypto.randomUUID();
  946. }
  947. else {
  948. return uuidV4String();
  949. }
  950. };
  951. const shallow$1 = (old, nu) => {
  952. return nu;
  953. };
  954. const deep$1 = (old, nu) => {
  955. const bothObjects = isPlainObject(old) && isPlainObject(nu);
  956. return bothObjects ? deepMerge(old, nu) : nu;
  957. };
  958. const baseMerge = (merger) => {
  959. return (...objects) => {
  960. if (objects.length === 0) {
  961. throw new Error(`Can't merge zero objects`);
  962. }
  963. const ret = {};
  964. for (let j = 0; j < objects.length; j++) {
  965. const curObject = objects[j];
  966. for (const key in curObject) {
  967. if (has$2(curObject, key)) {
  968. ret[key] = merger(ret[key], curObject[key]);
  969. }
  970. }
  971. }
  972. return ret;
  973. };
  974. };
  975. const deepMerge = baseMerge(deep$1);
  976. const merge$1 = baseMerge(shallow$1);
  977. /**
  978. * **Is** the value stored inside this Optional object equal to `rhs`?
  979. */
  980. const is$4 = (lhs, rhs, comparator = tripleEquals) => lhs.exists((left) => comparator(left, rhs));
  981. /**
  982. * Are these two Optional objects equal? Equality here means either they're both
  983. * `Some` (and the values are equal under the comparator) or they're both `None`.
  984. */
  985. const equals = (lhs, rhs, comparator = tripleEquals) => lift2(lhs, rhs, comparator).getOr(lhs.isNone() && rhs.isNone());
  986. const cat = (arr) => {
  987. const r = [];
  988. const push = (x) => {
  989. r.push(x);
  990. };
  991. for (let i = 0; i < arr.length; i++) {
  992. arr[i].each(push);
  993. }
  994. return r;
  995. };
  996. /*
  997. Notes on the lift functions:
  998. - We used to have a generic liftN, but we were concerned about its type-safety, and the below variants were faster in microbenchmarks.
  999. - The getOrDie calls are partial functions, but are checked beforehand. This is faster and more convenient (but less safe) than folds.
  1000. - && is used instead of a loop for simplicity and performance.
  1001. */
  1002. const lift2 = (oa, ob, f) => oa.isSome() && ob.isSome() ? Optional.some(f(oa.getOrDie(), ob.getOrDie())) : Optional.none();
  1003. const lift3 = (oa, ob, oc, f) => oa.isSome() && ob.isSome() && oc.isSome() ? Optional.some(f(oa.getOrDie(), ob.getOrDie(), oc.getOrDie())) : Optional.none();
  1004. // This can help with type inference, by specifying the type param on the none case, so the caller doesn't have to.
  1005. const someIf = (b, a) => b ? Optional.some(a) : Optional.none();
  1006. /** path :: ([String], JsObj?) -> JsObj */
  1007. const path = (parts, scope) => {
  1008. let o = scope !== undefined && scope !== null ? scope : Global;
  1009. for (let i = 0; i < parts.length && o !== undefined && o !== null; ++i) {
  1010. o = o[parts[i]];
  1011. }
  1012. return o;
  1013. };
  1014. /** resolve :: (String, JsObj?) -> JsObj */
  1015. const resolve$3 = (p, scope) => {
  1016. const parts = p.split('.');
  1017. return path(parts, scope);
  1018. };
  1019. Adt.generate([
  1020. { bothErrors: ['error1', 'error2'] },
  1021. { firstError: ['error1', 'value2'] },
  1022. { secondError: ['value1', 'error2'] },
  1023. { bothValues: ['value1', 'value2'] }
  1024. ]);
  1025. /** partition :: [Result a] -> { errors: [String], values: [a] } */
  1026. const partition$1 = (results) => {
  1027. const errors = [];
  1028. const values = [];
  1029. each$e(results, (result) => {
  1030. result.fold((err) => {
  1031. errors.push(err);
  1032. }, (value) => {
  1033. values.push(value);
  1034. });
  1035. });
  1036. return { errors, values };
  1037. };
  1038. const singleton = (doRevoke) => {
  1039. const subject = Cell(Optional.none());
  1040. const revoke = () => subject.get().each(doRevoke);
  1041. const clear = () => {
  1042. revoke();
  1043. subject.set(Optional.none());
  1044. };
  1045. const isSet = () => subject.get().isSome();
  1046. const get = () => subject.get();
  1047. const set = (s) => {
  1048. revoke();
  1049. subject.set(Optional.some(s));
  1050. };
  1051. return {
  1052. clear,
  1053. isSet,
  1054. get,
  1055. set
  1056. };
  1057. };
  1058. const repeatable = (delay) => {
  1059. const intervalId = Cell(Optional.none());
  1060. const revoke = () => intervalId.get().each((id) => clearInterval(id));
  1061. const clear = () => {
  1062. revoke();
  1063. intervalId.set(Optional.none());
  1064. };
  1065. const isSet = () => intervalId.get().isSome();
  1066. const get = () => intervalId.get();
  1067. const set = (functionToRepeat) => {
  1068. revoke();
  1069. intervalId.set(Optional.some(setInterval(functionToRepeat, delay)));
  1070. };
  1071. return {
  1072. clear,
  1073. isSet,
  1074. get,
  1075. set,
  1076. };
  1077. };
  1078. const value$1 = () => {
  1079. const subject = singleton(noop);
  1080. const on = (f) => subject.get().each(f);
  1081. return {
  1082. ...subject,
  1083. on
  1084. };
  1085. };
  1086. const removeFromStart = (str, numChars) => {
  1087. return str.substring(numChars);
  1088. };
  1089. const checkRange = (str, substr, start) => substr === '' || str.length >= substr.length && str.substr(start, start + substr.length) === substr;
  1090. const removeLeading = (str, prefix) => {
  1091. return startsWith(str, prefix) ? removeFromStart(str, prefix.length) : str;
  1092. };
  1093. const contains$1 = (str, substr, start = 0, end) => {
  1094. const idx = str.indexOf(substr, start);
  1095. if (idx !== -1) {
  1096. return isUndefined(end) ? true : idx + substr.length <= end;
  1097. }
  1098. else {
  1099. return false;
  1100. }
  1101. };
  1102. /** Does 'str' start with 'prefix'?
  1103. * Note: all strings start with the empty string.
  1104. * More formally, for all strings x, startsWith(x, "").
  1105. * This is so that for all strings x and y, startsWith(y + x, y)
  1106. */
  1107. const startsWith = (str, prefix) => {
  1108. return checkRange(str, prefix, 0);
  1109. };
  1110. /** Does 'str' end with 'suffix'?
  1111. * Note: all strings end with the empty string.
  1112. * More formally, for all strings x, endsWith(x, "").
  1113. * This is so that for all strings x and y, endsWith(x + y, y)
  1114. */
  1115. const endsWith = (str, suffix) => {
  1116. return checkRange(str, suffix, str.length - suffix.length);
  1117. };
  1118. const blank = (r) => (s) => s.replace(r, '');
  1119. /** removes all leading and trailing spaces */
  1120. const trim$4 = blank(/^\s+|\s+$/g);
  1121. const lTrim = blank(/^\s+/g);
  1122. const rTrim = blank(/\s+$/g);
  1123. const isNotEmpty = (s) => s.length > 0;
  1124. const isEmpty$5 = (s) => !isNotEmpty(s);
  1125. const repeat = (s, count) => count <= 0 ? '' : new Array(count + 1).join(s);
  1126. const toInt = (value, radix = 10) => {
  1127. const num = parseInt(value, radix);
  1128. return isNaN(num) ? Optional.none() : Optional.some(num);
  1129. };
  1130. // Run a function fn after rate ms. If another invocation occurs
  1131. // during the time it is waiting, ignore it completely.
  1132. const first$1 = (fn, rate) => {
  1133. let timer = null;
  1134. const cancel = () => {
  1135. if (!isNull(timer)) {
  1136. clearTimeout(timer);
  1137. timer = null;
  1138. }
  1139. };
  1140. const throttle = (...args) => {
  1141. if (isNull(timer)) {
  1142. timer = setTimeout(() => {
  1143. timer = null;
  1144. fn.apply(null, args);
  1145. }, rate);
  1146. }
  1147. };
  1148. return {
  1149. cancel,
  1150. throttle
  1151. };
  1152. };
  1153. // Run a function fn after rate ms. If another invocation occurs
  1154. // during the time it is waiting, reschedule the function again
  1155. // with the new arguments.
  1156. const last$1 = (fn, rate) => {
  1157. let timer = null;
  1158. const cancel = () => {
  1159. if (!isNull(timer)) {
  1160. clearTimeout(timer);
  1161. timer = null;
  1162. }
  1163. };
  1164. const throttle = (...args) => {
  1165. cancel();
  1166. timer = setTimeout(() => {
  1167. timer = null;
  1168. fn.apply(null, args);
  1169. }, rate);
  1170. };
  1171. return {
  1172. cancel,
  1173. throttle
  1174. };
  1175. };
  1176. const cached = (f) => {
  1177. let called = false;
  1178. let r;
  1179. return (...args) => {
  1180. if (!called) {
  1181. called = true;
  1182. r = f.apply(null, args);
  1183. }
  1184. return r;
  1185. };
  1186. };
  1187. const zeroWidth = '\uFEFF';
  1188. const nbsp = '\u00A0';
  1189. const ellipsis = '\u2026';
  1190. const isZwsp$2 = (char) => char === zeroWidth;
  1191. const removeZwsp = (s) => s.replace(/\uFEFF/g, '');
  1192. const stringArray = (a) => {
  1193. const all = {};
  1194. each$e(a, (key) => {
  1195. all[key] = {};
  1196. });
  1197. return keys(all);
  1198. };
  1199. const isArrayLike = (o) => o.length !== undefined;
  1200. const isArray = Array.isArray;
  1201. const toArray$1 = (obj) => {
  1202. if (!isArray(obj)) {
  1203. const array = [];
  1204. for (let i = 0, l = obj.length; i < l; i++) {
  1205. array[i] = obj[i];
  1206. }
  1207. return array;
  1208. }
  1209. else {
  1210. return obj;
  1211. }
  1212. };
  1213. const each$c = (o, cb, s) => {
  1214. if (!o) {
  1215. return false;
  1216. }
  1217. s = s || o;
  1218. if (isArrayLike(o)) {
  1219. // Indexed arrays, needed for Safari
  1220. for (let n = 0, l = o.length; n < l; n++) {
  1221. if (cb.call(s, o[n], n, o) === false) {
  1222. return false;
  1223. }
  1224. }
  1225. }
  1226. else {
  1227. // Hashtables
  1228. for (const n in o) {
  1229. if (has$2(o, n)) {
  1230. if (cb.call(s, o[n], n, o) === false) {
  1231. return false;
  1232. }
  1233. }
  1234. }
  1235. }
  1236. return true;
  1237. };
  1238. const map$1 = (array, callback) => {
  1239. const out = [];
  1240. each$c(array, (item, index) => {
  1241. out.push(callback(item, index, array));
  1242. });
  1243. return out;
  1244. };
  1245. const filter$3 = (a, f) => {
  1246. const o = [];
  1247. each$c(a, (v, index) => {
  1248. if (!f || f(v, index, a)) {
  1249. o.push(v);
  1250. }
  1251. });
  1252. return o;
  1253. };
  1254. const indexOf = (a, v) => {
  1255. if (a) {
  1256. for (let i = 0, l = a.length; i < l; i++) {
  1257. if (a[i] === v) {
  1258. return i;
  1259. }
  1260. }
  1261. }
  1262. return -1;
  1263. };
  1264. const reduce = (collection, iteratee, accumulator, thisArg) => {
  1265. let acc = isUndefined(accumulator) ? collection[0] : accumulator;
  1266. for (let i = 0; i < collection.length; i++) {
  1267. acc = iteratee.call(thisArg, acc, collection[i], i);
  1268. }
  1269. return acc;
  1270. };
  1271. const findIndex$1 = (array, predicate, thisArg) => {
  1272. for (let i = 0, l = array.length; i < l; i++) {
  1273. if (predicate.call(thisArg, array[i], i, array)) {
  1274. return i;
  1275. }
  1276. }
  1277. return -1;
  1278. };
  1279. const last = (collection) => collection[collection.length - 1];
  1280. const DeviceType = (os, browser, userAgent, mediaMatch) => {
  1281. const isiPad = os.isiOS() && /ipad/i.test(userAgent) === true;
  1282. const isiPhone = os.isiOS() && !isiPad;
  1283. const isMobile = os.isiOS() || os.isAndroid();
  1284. const isTouch = isMobile || mediaMatch('(pointer:coarse)');
  1285. const isTablet = isiPad || !isiPhone && isMobile && mediaMatch('(min-device-width:768px)');
  1286. const isPhone = isiPhone || isMobile && !isTablet;
  1287. const iOSwebview = browser.isSafari() && os.isiOS() && /safari/i.test(userAgent) === false;
  1288. const isDesktop = !isPhone && !isTablet && !iOSwebview;
  1289. return {
  1290. isiPad: constant(isiPad),
  1291. isiPhone: constant(isiPhone),
  1292. isTablet: constant(isTablet),
  1293. isPhone: constant(isPhone),
  1294. isTouch: constant(isTouch),
  1295. isAndroid: os.isAndroid,
  1296. isiOS: os.isiOS,
  1297. isWebView: constant(iOSwebview),
  1298. isDesktop: constant(isDesktop)
  1299. };
  1300. };
  1301. const firstMatch = (regexes, s) => {
  1302. for (let i = 0; i < regexes.length; i++) {
  1303. const x = regexes[i];
  1304. if (x.test(s)) {
  1305. return x;
  1306. }
  1307. }
  1308. return undefined;
  1309. };
  1310. const find$1 = (regexes, agent) => {
  1311. const r = firstMatch(regexes, agent);
  1312. if (!r) {
  1313. return { major: 0, minor: 0 };
  1314. }
  1315. const group = (i) => {
  1316. return Number(agent.replace(r, '$' + i));
  1317. };
  1318. return nu$3(group(1), group(2));
  1319. };
  1320. const detect$4 = (versionRegexes, agent) => {
  1321. const cleanedAgent = String(agent).toLowerCase();
  1322. if (versionRegexes.length === 0) {
  1323. return unknown$2();
  1324. }
  1325. return find$1(versionRegexes, cleanedAgent);
  1326. };
  1327. const unknown$2 = () => {
  1328. return nu$3(0, 0);
  1329. };
  1330. const nu$3 = (major, minor) => {
  1331. return { major, minor };
  1332. };
  1333. const Version = {
  1334. nu: nu$3,
  1335. detect: detect$4,
  1336. unknown: unknown$2
  1337. };
  1338. const detectBrowser$1 = (browsers, userAgentData) => {
  1339. return findMap(userAgentData.brands, (uaBrand) => {
  1340. const lcBrand = uaBrand.brand.toLowerCase();
  1341. return find$2(browsers, (browser) => { var _a; return lcBrand === ((_a = browser.brand) === null || _a === void 0 ? void 0 : _a.toLowerCase()); })
  1342. .map((info) => ({
  1343. current: info.name,
  1344. version: Version.nu(parseInt(uaBrand.version, 10), 0)
  1345. }));
  1346. });
  1347. };
  1348. const detect$3 = (candidates, userAgent) => {
  1349. const agent = String(userAgent).toLowerCase();
  1350. return find$2(candidates, (candidate) => {
  1351. return candidate.search(agent);
  1352. });
  1353. };
  1354. // They (browser and os) are the same at the moment, but they might
  1355. // not stay that way.
  1356. const detectBrowser = (browsers, userAgent) => {
  1357. return detect$3(browsers, userAgent).map((browser) => {
  1358. const version = Version.detect(browser.versionRegexes, userAgent);
  1359. return {
  1360. current: browser.name,
  1361. version
  1362. };
  1363. });
  1364. };
  1365. const detectOs = (oses, userAgent) => {
  1366. return detect$3(oses, userAgent).map((os) => {
  1367. const version = Version.detect(os.versionRegexes, userAgent);
  1368. return {
  1369. current: os.name,
  1370. version
  1371. };
  1372. });
  1373. };
  1374. const normalVersionRegex = /.*?version\/\ ?([0-9]+)\.([0-9]+).*/;
  1375. const checkContains = (target) => {
  1376. return (uastring) => {
  1377. return contains$1(uastring, target);
  1378. };
  1379. };
  1380. const browsers = [
  1381. // This is legacy Edge
  1382. {
  1383. name: 'Edge',
  1384. versionRegexes: [/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],
  1385. search: (uastring) => {
  1386. return contains$1(uastring, 'edge/') && contains$1(uastring, 'chrome') && contains$1(uastring, 'safari') && contains$1(uastring, 'applewebkit');
  1387. }
  1388. },
  1389. // This is Google Chrome and Chromium Edge
  1390. {
  1391. name: 'Chromium',
  1392. brand: 'Chromium',
  1393. versionRegexes: [/.*?chrome\/([0-9]+)\.([0-9]+).*/, normalVersionRegex],
  1394. search: (uastring) => {
  1395. return contains$1(uastring, 'chrome') && !contains$1(uastring, 'chromeframe');
  1396. }
  1397. },
  1398. {
  1399. name: 'IE',
  1400. versionRegexes: [/.*?msie\ ?([0-9]+)\.([0-9]+).*/, /.*?rv:([0-9]+)\.([0-9]+).*/],
  1401. search: (uastring) => {
  1402. return contains$1(uastring, 'msie') || contains$1(uastring, 'trident');
  1403. }
  1404. },
  1405. // INVESTIGATE: Is this still the Opera user agent?
  1406. {
  1407. name: 'Opera',
  1408. versionRegexes: [normalVersionRegex, /.*?opera\/([0-9]+)\.([0-9]+).*/],
  1409. search: checkContains('opera')
  1410. },
  1411. {
  1412. name: 'Firefox',
  1413. versionRegexes: [/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],
  1414. search: checkContains('firefox')
  1415. },
  1416. {
  1417. name: 'Safari',
  1418. versionRegexes: [normalVersionRegex, /.*?cpu os ([0-9]+)_([0-9]+).*/],
  1419. search: (uastring) => {
  1420. return (contains$1(uastring, 'safari') || contains$1(uastring, 'mobile/')) && contains$1(uastring, 'applewebkit');
  1421. }
  1422. }
  1423. ];
  1424. const oses = [
  1425. {
  1426. name: 'Windows',
  1427. search: checkContains('win'),
  1428. versionRegexes: [/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]
  1429. },
  1430. {
  1431. name: 'iOS',
  1432. search: (uastring) => {
  1433. return contains$1(uastring, 'iphone') || contains$1(uastring, 'ipad');
  1434. },
  1435. versionRegexes: [/.*?version\/\ ?([0-9]+)\.([0-9]+).*/, /.*cpu os ([0-9]+)_([0-9]+).*/, /.*cpu iphone os ([0-9]+)_([0-9]+).*/]
  1436. },
  1437. {
  1438. name: 'Android',
  1439. search: checkContains('android'),
  1440. versionRegexes: [/.*?android\ ?([0-9]+)\.([0-9]+).*/]
  1441. },
  1442. {
  1443. name: 'macOS',
  1444. search: checkContains('mac os x'),
  1445. versionRegexes: [/.*?mac\ os\ x\ ?([0-9]+)_([0-9]+).*/]
  1446. },
  1447. {
  1448. name: 'Linux',
  1449. search: checkContains('linux'),
  1450. versionRegexes: []
  1451. },
  1452. { name: 'Solaris',
  1453. search: checkContains('sunos'),
  1454. versionRegexes: []
  1455. },
  1456. {
  1457. name: 'FreeBSD',
  1458. search: checkContains('freebsd'),
  1459. versionRegexes: []
  1460. },
  1461. {
  1462. name: 'ChromeOS',
  1463. search: checkContains('cros'),
  1464. versionRegexes: [/.*?chrome\/([0-9]+)\.([0-9]+).*/]
  1465. }
  1466. ];
  1467. const PlatformInfo = {
  1468. browsers: constant(browsers),
  1469. oses: constant(oses)
  1470. };
  1471. const edge = 'Edge';
  1472. const chromium = 'Chromium';
  1473. const ie = 'IE';
  1474. const opera = 'Opera';
  1475. const firefox = 'Firefox';
  1476. const safari = 'Safari';
  1477. const unknown$1 = () => {
  1478. return nu$2({
  1479. current: undefined,
  1480. version: Version.unknown()
  1481. });
  1482. };
  1483. const nu$2 = (info) => {
  1484. const current = info.current;
  1485. const version = info.version;
  1486. const isBrowser = (name) => () => current === name;
  1487. return {
  1488. current,
  1489. version,
  1490. isEdge: isBrowser(edge),
  1491. isChromium: isBrowser(chromium),
  1492. // NOTE: isIe just looks too weird
  1493. isIE: isBrowser(ie),
  1494. isOpera: isBrowser(opera),
  1495. isFirefox: isBrowser(firefox),
  1496. isSafari: isBrowser(safari)
  1497. };
  1498. };
  1499. const Browser = {
  1500. unknown: unknown$1,
  1501. nu: nu$2,
  1502. edge: constant(edge),
  1503. chromium: constant(chromium),
  1504. ie: constant(ie),
  1505. opera: constant(opera),
  1506. firefox: constant(firefox),
  1507. safari: constant(safari)
  1508. };
  1509. const windows = 'Windows';
  1510. const ios = 'iOS';
  1511. const android = 'Android';
  1512. const linux = 'Linux';
  1513. const macos = 'macOS';
  1514. const solaris = 'Solaris';
  1515. const freebsd = 'FreeBSD';
  1516. const chromeos = 'ChromeOS';
  1517. // Though there is a bit of dupe with this and Browser, trying to
  1518. // reuse code makes it much harder to follow and change.
  1519. const unknown = () => {
  1520. return nu$1({
  1521. current: undefined,
  1522. version: Version.unknown()
  1523. });
  1524. };
  1525. const nu$1 = (info) => {
  1526. const current = info.current;
  1527. const version = info.version;
  1528. const isOS = (name) => () => current === name;
  1529. return {
  1530. current,
  1531. version,
  1532. isWindows: isOS(windows),
  1533. // TODO: Fix capitalisation
  1534. isiOS: isOS(ios),
  1535. isAndroid: isOS(android),
  1536. isMacOS: isOS(macos),
  1537. isLinux: isOS(linux),
  1538. isSolaris: isOS(solaris),
  1539. isFreeBSD: isOS(freebsd),
  1540. isChromeOS: isOS(chromeos)
  1541. };
  1542. };
  1543. const OperatingSystem = {
  1544. unknown,
  1545. nu: nu$1,
  1546. windows: constant(windows),
  1547. ios: constant(ios),
  1548. android: constant(android),
  1549. linux: constant(linux),
  1550. macos: constant(macos),
  1551. solaris: constant(solaris),
  1552. freebsd: constant(freebsd),
  1553. chromeos: constant(chromeos)
  1554. };
  1555. const detect$2 = (userAgent, userAgentDataOpt, mediaMatch) => {
  1556. const browsers = PlatformInfo.browsers();
  1557. const oses = PlatformInfo.oses();
  1558. const browser = userAgentDataOpt.bind((userAgentData) => detectBrowser$1(browsers, userAgentData))
  1559. .orThunk(() => detectBrowser(browsers, userAgent))
  1560. .fold(Browser.unknown, Browser.nu);
  1561. const os = detectOs(oses, userAgent).fold(OperatingSystem.unknown, OperatingSystem.nu);
  1562. const deviceType = DeviceType(os, browser, userAgent, mediaMatch);
  1563. return {
  1564. browser,
  1565. os,
  1566. deviceType
  1567. };
  1568. };
  1569. const PlatformDetection = {
  1570. detect: detect$2
  1571. };
  1572. const mediaMatch = (query) => window.matchMedia(query).matches;
  1573. // IMPORTANT: Must be in a thunk, otherwise rollup thinks calling this immediately
  1574. // causes side effects and won't tree shake this away
  1575. // Note: navigator.userAgentData is not part of the native typescript types yet
  1576. let platform$4 = cached(() => PlatformDetection.detect(window.navigator.userAgent, Optional.from((window.navigator.userAgentData)), mediaMatch));
  1577. const detect$1 = () => platform$4();
  1578. const unsafe = (name, scope) => {
  1579. return resolve$3(name, scope);
  1580. };
  1581. const getOrDie = (name, scope) => {
  1582. const actual = unsafe(name, scope);
  1583. if (actual === undefined || actual === null) {
  1584. throw new Error(name + ' not available on this browser');
  1585. }
  1586. return actual;
  1587. };
  1588. const getPrototypeOf$1 = Object.getPrototypeOf;
  1589. /*
  1590. * IE9 and above
  1591. *
  1592. * MDN no use on this one, but here's the link anyway:
  1593. * https://developer.mozilla.org/en/docs/Web/API/HTMLElement
  1594. */
  1595. const sandHTMLElement = (scope) => {
  1596. return getOrDie('HTMLElement', scope);
  1597. };
  1598. const isPrototypeOf = (x) => {
  1599. // use Resolve to get the window object for x and just return undefined if it can't find it.
  1600. // undefined scope later triggers using the global window.
  1601. const scope = resolve$3('ownerDocument.defaultView', x);
  1602. // TINY-7374: We can't rely on looking at the owner window HTMLElement as the element may have
  1603. // been constructed in a different window and then appended to the current window document.
  1604. return isObject(x) && (sandHTMLElement(scope).prototype.isPrototypeOf(x) || /^HTML\w*Element$/.test(getPrototypeOf$1(x).constructor.name));
  1605. };
  1606. /**
  1607. * This class contains various environment constants like browser versions etc.
  1608. * Normally you don't want to sniff specific browser versions but sometimes you have
  1609. * to when it's impossible to feature detect. So use this with care.
  1610. *
  1611. * @class tinymce.Env
  1612. * @static
  1613. */
  1614. const userAgent = window.navigator.userAgent;
  1615. const platform$3 = detect$1();
  1616. const browser$3 = platform$3.browser;
  1617. const os$1 = platform$3.os;
  1618. const deviceType = platform$3.deviceType;
  1619. const windowsPhone = userAgent.indexOf('Windows Phone') !== -1;
  1620. const Env = {
  1621. /**
  1622. * Transparent image data url.
  1623. *
  1624. * @property transparentSrc
  1625. * @type Boolean
  1626. * @final
  1627. */
  1628. transparentSrc: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
  1629. /**
  1630. * Returns the IE document mode. For non IE browsers, this will fake IE 10 document mode.
  1631. *
  1632. * @property documentMode
  1633. * @type Number
  1634. */
  1635. documentMode: browser$3.isIE() ? (document.documentMode || 7) : 10,
  1636. cacheSuffix: null,
  1637. container: null,
  1638. /**
  1639. * Constant if CSP mode is possible or not. Meaning we can't use script urls for the iframe.
  1640. */
  1641. canHaveCSP: !browser$3.isIE(),
  1642. windowsPhone,
  1643. /**
  1644. * @include ../../../../../tools/docs/tinymce.Env.js
  1645. */
  1646. browser: {
  1647. current: browser$3.current,
  1648. version: browser$3.version,
  1649. isChromium: browser$3.isChromium,
  1650. isEdge: browser$3.isEdge,
  1651. isFirefox: browser$3.isFirefox,
  1652. isIE: browser$3.isIE,
  1653. isOpera: browser$3.isOpera,
  1654. isSafari: browser$3.isSafari
  1655. },
  1656. os: {
  1657. current: os$1.current,
  1658. version: os$1.version,
  1659. isAndroid: os$1.isAndroid,
  1660. isChromeOS: os$1.isChromeOS,
  1661. isFreeBSD: os$1.isFreeBSD,
  1662. isiOS: os$1.isiOS,
  1663. isLinux: os$1.isLinux,
  1664. isMacOS: os$1.isMacOS,
  1665. isSolaris: os$1.isSolaris,
  1666. isWindows: os$1.isWindows
  1667. },
  1668. deviceType: {
  1669. isDesktop: deviceType.isDesktop,
  1670. isiPad: deviceType.isiPad,
  1671. isiPhone: deviceType.isiPhone,
  1672. isPhone: deviceType.isPhone,
  1673. isTablet: deviceType.isTablet,
  1674. isTouch: deviceType.isTouch,
  1675. isWebView: deviceType.isWebView
  1676. }
  1677. };
  1678. /**
  1679. * This class contains various utility functions. These are also exposed
  1680. * directly on the tinymce namespace.
  1681. *
  1682. * @class tinymce.util.Tools
  1683. */
  1684. /**
  1685. * Removes whitespace from the beginning and end of a string.
  1686. *
  1687. * @method trim
  1688. * @param {String} s String to remove whitespace from.
  1689. * @return {String} New string with removed whitespace.
  1690. */
  1691. const whiteSpaceRegExp$1 = /^\s*|\s*$/g;
  1692. const trim$3 = (str) => {
  1693. return isNullable(str) ? '' : ('' + str).replace(whiteSpaceRegExp$1, '');
  1694. };
  1695. /**
  1696. * Checks if a object is of a specific type for example an array.
  1697. *
  1698. * @method is
  1699. * @param {Object} obj Object to check type of.
  1700. * @param {String} type Optional type to check for.
  1701. * @return {Boolean} true/false if the object is of the specified type.
  1702. */
  1703. const is$3 = (obj, type) => {
  1704. if (!type) {
  1705. return obj !== undefined;
  1706. }
  1707. if (type === 'array' && isArray(obj)) {
  1708. return true;
  1709. }
  1710. return typeof obj === type;
  1711. };
  1712. /**
  1713. * Makes a name/object map out of an array with names.
  1714. *
  1715. * @method makeMap
  1716. * @param {Array/String} items Items to make map out of.
  1717. * @param {String} delim Optional delimiter to split string by.
  1718. * @param {Object} map Optional map to add items to.
  1719. * @return {Object} Name/value map of items.
  1720. */
  1721. const makeMap$4 = (items, delim, map = {}) => {
  1722. const resolvedItems = isString(items) ? items.split(delim || ',') : (items || []);
  1723. let i = resolvedItems.length;
  1724. while (i--) {
  1725. map[resolvedItems[i]] = {};
  1726. }
  1727. return map;
  1728. };
  1729. /**
  1730. * JavaScript does not protect hasOwnProperty method, so it is possible to overwrite it. This is
  1731. * an object independent version.
  1732. * Checks if the input object "<code>obj</code>" has the property "<code>prop</code>".
  1733. *
  1734. * @method hasOwnProperty
  1735. * @param {Object} obj Object to check if the property exists.
  1736. * @param {String} prop Name of a property on the object.
  1737. * @returns {Boolean} true if the object has the specified property.
  1738. */
  1739. const hasOwnProperty = has$2;
  1740. const extend$3 = (obj, ...exts) => {
  1741. for (let i = 0; i < exts.length; i++) {
  1742. const ext = exts[i];
  1743. for (const name in ext) {
  1744. if (has$2(ext, name)) {
  1745. const value = ext[name];
  1746. if (value !== undefined) {
  1747. obj[name] = value;
  1748. }
  1749. }
  1750. }
  1751. }
  1752. return obj;
  1753. };
  1754. /**
  1755. * Executed the specified function for each item in a object tree.
  1756. *
  1757. * @method walk
  1758. * @param {Object} o Object tree to walk though.
  1759. * @param {Function} f Function to call for each item.
  1760. * @param {String} n Optional name of collection inside the objects to walk for example childNodes.
  1761. * @param {String} s Optional scope to execute the function in.
  1762. */
  1763. const walk$4 = function (o, f, n, s) {
  1764. s = s || this;
  1765. if (o) {
  1766. if (n) {
  1767. o = o[n];
  1768. }
  1769. each$c(o, (o, i) => {
  1770. if (f.call(s, o, i, n) === false) {
  1771. return false;
  1772. }
  1773. else {
  1774. walk$4(o, f, n, s);
  1775. return true;
  1776. }
  1777. });
  1778. }
  1779. };
  1780. /**
  1781. * Resolves a string and returns the object from a specific structure.
  1782. *
  1783. * @method resolve
  1784. * @param {String} n Path to resolve for example a.b.c.d.
  1785. * @param {Object} o Optional object to search though, defaults to window.
  1786. * @return {Object} Last object in path or null if it couldn't be resolved.
  1787. * @example
  1788. * // Resolve a path into an object reference
  1789. * const obj = tinymce.resolve('a.b.c.d');
  1790. */
  1791. const resolve$2 = (n, o = window) => {
  1792. const path = n.split('.');
  1793. for (let i = 0, l = path.length; i < l; i++) {
  1794. o = o[path[i]];
  1795. if (!o) {
  1796. break;
  1797. }
  1798. }
  1799. return o;
  1800. };
  1801. /**
  1802. * Splits a string but removes the whitespace before and after each value.
  1803. *
  1804. * @method explode
  1805. * @param {String} s String to split.
  1806. * @param {String} d Delimiter to split by.
  1807. * @example
  1808. * // Split a string into an array with a,b,c
  1809. * const arr = tinymce.explode('a, b, c');
  1810. */
  1811. const explode$3 = (s, d) => {
  1812. if (isArray$1(s)) {
  1813. return s;
  1814. }
  1815. else if (s === '') {
  1816. return [];
  1817. }
  1818. else {
  1819. return map$1(s.split(d || ','), trim$3);
  1820. }
  1821. };
  1822. const _addCacheSuffix = (url) => {
  1823. const cacheSuffix = Env.cacheSuffix;
  1824. if (cacheSuffix) {
  1825. url += (url.indexOf('?') === -1 ? '?' : '&') + cacheSuffix;
  1826. }
  1827. return url;
  1828. };
  1829. const Tools = {
  1830. trim: trim$3,
  1831. /**
  1832. * Returns true/false if the object is an array or not.
  1833. *
  1834. * @method isArray
  1835. * @param {Object} obj Object to check.
  1836. * @return {Boolean} true/false state if the object is an array or not.
  1837. */
  1838. isArray: isArray,
  1839. is: is$3,
  1840. /**
  1841. * Converts the specified object into a real JavaScript array.
  1842. *
  1843. * @method toArray
  1844. * @param {Object} obj Object to convert into array.
  1845. * @return {Array} Array object based in input.
  1846. */
  1847. toArray: toArray$1,
  1848. makeMap: makeMap$4,
  1849. /**
  1850. * Performs an iteration of all items in a collection such as an object or array. This method will execute the
  1851. * callback function for each item in the collection, if the callback returns false the iteration will terminate.
  1852. * The callback has the following format: `cb(value, key_or_index)`.
  1853. *
  1854. * @method each
  1855. * @param {Object} o Collection to iterate.
  1856. * @param {Function} cb Callback function to execute for each item.
  1857. * @param {Object} s Optional scope to execute the callback in.
  1858. * @example
  1859. * // Iterate an array
  1860. * tinymce.each([ 1,2,3 ], (v, i) => {
  1861. * console.debug("Value: " + v + ", Index: " + i);
  1862. * });
  1863. *
  1864. * // Iterate an object
  1865. * tinymce.each({ a: 1, b: 2, c: 3 }, (v, k) => {
  1866. * console.debug("Value: " + v + ", Key: " + k);
  1867. * });
  1868. */
  1869. each: each$c,
  1870. /**
  1871. * Creates a new array by the return value of each iteration function call. This enables you to convert
  1872. * one array list into another.
  1873. *
  1874. * @method map
  1875. * @param {Array} array Array of items to iterate.
  1876. * @param {Function} callback Function to call for each item. It's return value will be the new value.
  1877. * @return {Array} Array with new values based on function return values.
  1878. */
  1879. map: map$1,
  1880. /**
  1881. * Filters out items from the input array by calling the specified function for each item.
  1882. * If the function returns false the item will be excluded if it returns true it will be included.
  1883. *
  1884. * @method grep
  1885. * @param {Array} a Array of items to loop though.
  1886. * @param {Function} f Function to call for each item. Include/exclude depends on it's return value.
  1887. * @return {Array} New array with values imported and filtered based in input.
  1888. * @example
  1889. * // Filter out some items, this will return an array with 4 and 5
  1890. * const items = tinymce.grep([ 1,2,3,4,5 ], (v) => v > 3);
  1891. */
  1892. grep: filter$3,
  1893. /**
  1894. * Returns an index of the item or -1 if item is not present in the array.
  1895. *
  1896. * @method inArray
  1897. * @param {any} item Item to search for.
  1898. * @param {Array} arr Array to search in.
  1899. * @return {Number} index of the item or -1 if item was not found.
  1900. */
  1901. inArray: indexOf,
  1902. hasOwn: hasOwnProperty,
  1903. extend: extend$3,
  1904. walk: walk$4,
  1905. resolve: resolve$2,
  1906. explode: explode$3,
  1907. _addCacheSuffix
  1908. };
  1909. const fromHtml$1 = (html, scope) => {
  1910. const doc = scope || document;
  1911. const div = doc.createElement('div');
  1912. div.innerHTML = html;
  1913. if (!div.hasChildNodes() || div.childNodes.length > 1) {
  1914. const message = 'HTML does not have a single root node';
  1915. // eslint-disable-next-line no-console
  1916. console.error(message, html);
  1917. throw new Error(message);
  1918. }
  1919. return fromDom$2(div.childNodes[0]);
  1920. };
  1921. const fromTag = (tag, scope) => {
  1922. const doc = scope || document;
  1923. const node = doc.createElement(tag);
  1924. return fromDom$2(node);
  1925. };
  1926. const fromText = (text, scope) => {
  1927. const doc = scope || document;
  1928. const node = doc.createTextNode(text);
  1929. return fromDom$2(node);
  1930. };
  1931. const fromDom$2 = (node) => {
  1932. // TODO: Consider removing this check, but left atm for safety
  1933. if (node === null || node === undefined) {
  1934. throw new Error('Node cannot be null or undefined');
  1935. }
  1936. return {
  1937. dom: node
  1938. };
  1939. };
  1940. const fromPoint$2 = (docElm, x, y) => Optional.from(docElm.dom.elementFromPoint(x, y)).map(fromDom$2);
  1941. // tslint:disable-next-line:variable-name
  1942. const SugarElement = {
  1943. fromHtml: fromHtml$1,
  1944. fromTag,
  1945. fromText,
  1946. fromDom: fromDom$2,
  1947. fromPoint: fromPoint$2
  1948. };
  1949. // NOTE: Mutates the range.
  1950. const setStart = (rng, situ) => {
  1951. situ.fold((e) => {
  1952. rng.setStartBefore(e.dom);
  1953. }, (e, o) => {
  1954. rng.setStart(e.dom, o);
  1955. }, (e) => {
  1956. rng.setStartAfter(e.dom);
  1957. });
  1958. };
  1959. const setFinish = (rng, situ) => {
  1960. situ.fold((e) => {
  1961. rng.setEndBefore(e.dom);
  1962. }, (e, o) => {
  1963. rng.setEnd(e.dom, o);
  1964. }, (e) => {
  1965. rng.setEndAfter(e.dom);
  1966. });
  1967. };
  1968. const relativeToNative = (win, startSitu, finishSitu) => {
  1969. const range = win.document.createRange();
  1970. setStart(range, startSitu);
  1971. setFinish(range, finishSitu);
  1972. return range;
  1973. };
  1974. const exactToNative = (win, start, soffset, finish, foffset) => {
  1975. const rng = win.document.createRange();
  1976. rng.setStart(start.dom, soffset);
  1977. rng.setEnd(finish.dom, foffset);
  1978. return rng;
  1979. };
  1980. const adt$3 = Adt.generate([
  1981. { ltr: ['start', 'soffset', 'finish', 'foffset'] },
  1982. { rtl: ['start', 'soffset', 'finish', 'foffset'] }
  1983. ]);
  1984. const fromRange = (win, type, range) => type(SugarElement.fromDom(range.startContainer), range.startOffset, SugarElement.fromDom(range.endContainer), range.endOffset);
  1985. const getRanges$1 = (win, selection) => selection.match({
  1986. domRange: (rng) => {
  1987. return {
  1988. ltr: constant(rng),
  1989. rtl: Optional.none
  1990. };
  1991. },
  1992. relative: (startSitu, finishSitu) => {
  1993. return {
  1994. ltr: cached(() => relativeToNative(win, startSitu, finishSitu)),
  1995. rtl: cached(() => Optional.some(relativeToNative(win, finishSitu, startSitu)))
  1996. };
  1997. },
  1998. exact: (start, soffset, finish, foffset) => {
  1999. return {
  2000. ltr: cached(() => exactToNative(win, start, soffset, finish, foffset)),
  2001. rtl: cached(() => Optional.some(exactToNative(win, finish, foffset, start, soffset)))
  2002. };
  2003. }
  2004. });
  2005. const doDiagnose = (win, ranges) => {
  2006. // If we cannot create a ranged selection from start > finish, it could be RTL
  2007. const rng = ranges.ltr();
  2008. if (rng.collapsed) {
  2009. // Let's check if it's RTL ... if it is, then reversing the direction will not be collapsed
  2010. const reversed = ranges.rtl().filter((rev) => rev.collapsed === false);
  2011. return reversed.map((rev) =>
  2012. // We need to use "reversed" here, because the original only has one point (collapsed)
  2013. adt$3.rtl(SugarElement.fromDom(rev.endContainer), rev.endOffset, SugarElement.fromDom(rev.startContainer), rev.startOffset)).getOrThunk(() => fromRange(win, adt$3.ltr, rng));
  2014. }
  2015. else {
  2016. return fromRange(win, adt$3.ltr, rng);
  2017. }
  2018. };
  2019. const diagnose = (win, selection) => {
  2020. const ranges = getRanges$1(win, selection);
  2021. return doDiagnose(win, ranges);
  2022. };
  2023. adt$3.ltr;
  2024. adt$3.rtl;
  2025. const COMMENT = 8;
  2026. const DOCUMENT = 9;
  2027. const DOCUMENT_FRAGMENT = 11;
  2028. const ELEMENT = 1;
  2029. const TEXT = 3;
  2030. const is$2 = (element, selector) => {
  2031. const dom = element.dom;
  2032. if (dom.nodeType !== ELEMENT) {
  2033. return false;
  2034. }
  2035. else {
  2036. const elem = dom;
  2037. if (elem.matches !== undefined) {
  2038. return elem.matches(selector);
  2039. }
  2040. else if (elem.msMatchesSelector !== undefined) {
  2041. return elem.msMatchesSelector(selector);
  2042. }
  2043. else if (elem.webkitMatchesSelector !== undefined) {
  2044. return elem.webkitMatchesSelector(selector);
  2045. }
  2046. else if (elem.mozMatchesSelector !== undefined) {
  2047. // cast to any as mozMatchesSelector doesn't exist in TS DOM lib
  2048. return elem.mozMatchesSelector(selector);
  2049. }
  2050. else {
  2051. throw new Error('Browser lacks native selectors');
  2052. } // unfortunately we can't throw this on startup :(
  2053. }
  2054. };
  2055. const bypassSelector = (dom) =>
  2056. // Only elements, documents and shadow roots support querySelector
  2057. // shadow root element type is DOCUMENT_FRAGMENT
  2058. dom.nodeType !== ELEMENT && dom.nodeType !== DOCUMENT && dom.nodeType !== DOCUMENT_FRAGMENT ||
  2059. // IE fix for complex queries on empty nodes: http://jsfiddle.net/spyder/fv9ptr5L/
  2060. dom.childElementCount === 0;
  2061. const all = (selector, scope) => {
  2062. const base = scope === undefined ? document : scope.dom;
  2063. return bypassSelector(base) ? [] : map$3(base.querySelectorAll(selector), SugarElement.fromDom);
  2064. };
  2065. const one = (selector, scope) => {
  2066. const base = scope === undefined ? document : scope.dom;
  2067. return bypassSelector(base) ? Optional.none() : Optional.from(base.querySelector(selector)).map(SugarElement.fromDom);
  2068. };
  2069. const eq = (e1, e2) => e1.dom === e2.dom;
  2070. // Returns: true if node e1 contains e2, otherwise false.
  2071. // (returns false if e1===e2: A node does not contain itself).
  2072. const contains = (e1, e2) => {
  2073. const d1 = e1.dom;
  2074. const d2 = e2.dom;
  2075. return d1 === d2 ? false : d1.contains(d2);
  2076. };
  2077. const is$1 = is$2;
  2078. /**
  2079. * Applies f repeatedly until it completes (by returning Optional.none()).
  2080. *
  2081. * Normally would just use recursion, but JavaScript lacks tail call optimisation.
  2082. *
  2083. * This is what recursion looks like when manually unravelled :)
  2084. */
  2085. const toArray = (target, f) => {
  2086. const r = [];
  2087. const recurse = (e) => {
  2088. r.push(e);
  2089. return f(e);
  2090. };
  2091. let cur = f(target);
  2092. do {
  2093. cur = cur.bind(recurse);
  2094. } while (cur.isSome());
  2095. return r;
  2096. };
  2097. const name = (element) => {
  2098. const r = element.dom.nodeName;
  2099. return r.toLowerCase();
  2100. };
  2101. const type$1 = (element) => element.dom.nodeType;
  2102. const isType = (t) => (element) => type$1(element) === t;
  2103. const isComment$1 = (element) => type$1(element) === COMMENT || name(element) === '#comment';
  2104. const isHTMLElement$1 = (element) => isElement$8(element) && isPrototypeOf(element.dom);
  2105. const isElement$8 = isType(ELEMENT);
  2106. const isText$c = isType(TEXT);
  2107. const isDocument$2 = isType(DOCUMENT);
  2108. const isDocumentFragment$1 = isType(DOCUMENT_FRAGMENT);
  2109. const isTag = (tag) => (e) => isElement$8(e) && name(e) === tag;
  2110. /**
  2111. * The document associated with the current element
  2112. * NOTE: this will throw if the owner is null.
  2113. */
  2114. const owner$1 = (element) => SugarElement.fromDom(element.dom.ownerDocument);
  2115. /**
  2116. * If the element is a document, return it. Otherwise, return its ownerDocument.
  2117. * @param dos
  2118. */
  2119. const documentOrOwner = (dos) => isDocument$2(dos) ? dos : owner$1(dos);
  2120. const documentElement = (element) => SugarElement.fromDom(documentOrOwner(element).dom.documentElement);
  2121. /**
  2122. * The window element associated with the element
  2123. * NOTE: this will throw if the defaultView is null.
  2124. */
  2125. const defaultView = (element) => SugarElement.fromDom(documentOrOwner(element).dom.defaultView);
  2126. const parent = (element) => Optional.from(element.dom.parentNode).map(SugarElement.fromDom);
  2127. const parentElement = (element) => Optional.from(element.dom.parentElement).map(SugarElement.fromDom);
  2128. const parents$1 = (element, isRoot) => {
  2129. const stop = isFunction(isRoot) ? isRoot : never;
  2130. // This is used a *lot* so it needs to be performant, not recursive
  2131. let dom = element.dom;
  2132. const ret = [];
  2133. while (dom.parentNode !== null && dom.parentNode !== undefined) {
  2134. const rawParent = dom.parentNode;
  2135. const p = SugarElement.fromDom(rawParent);
  2136. ret.push(p);
  2137. if (stop(p) === true) {
  2138. break;
  2139. }
  2140. else {
  2141. dom = rawParent;
  2142. }
  2143. }
  2144. return ret;
  2145. };
  2146. const siblings = (element) => {
  2147. // TODO: Refactor out children so we can just not add self instead of filtering afterwards
  2148. const filterSelf = (elements) => filter$5(elements, (x) => !eq(element, x));
  2149. return parent(element).map(children$1).map(filterSelf).getOr([]);
  2150. };
  2151. const prevSibling = (element) => Optional.from(element.dom.previousSibling).map(SugarElement.fromDom);
  2152. const nextSibling = (element) => Optional.from(element.dom.nextSibling).map(SugarElement.fromDom);
  2153. // This one needs to be reversed, so they're still in DOM order
  2154. const prevSiblings = (element) => reverse(toArray(element, prevSibling));
  2155. const nextSiblings = (element) => toArray(element, nextSibling);
  2156. const children$1 = (element) => map$3(element.dom.childNodes, SugarElement.fromDom);
  2157. const child$1 = (element, index) => {
  2158. const cs = element.dom.childNodes;
  2159. return Optional.from(cs[index]).map(SugarElement.fromDom);
  2160. };
  2161. const firstChild = (element) => child$1(element, 0);
  2162. const lastChild = (element) => child$1(element, element.dom.childNodes.length - 1);
  2163. const childNodesCount = (element) => element.dom.childNodes.length;
  2164. const getHead = (doc) => {
  2165. /*
  2166. * IE9 and above per
  2167. * https://developer.mozilla.org/en-US/docs/Web/API/Document/head
  2168. */
  2169. const b = doc.dom.head;
  2170. if (b === null || b === undefined) {
  2171. throw new Error('Head is not available yet');
  2172. }
  2173. return SugarElement.fromDom(b);
  2174. };
  2175. /**
  2176. * Is the element a ShadowRoot?
  2177. *
  2178. * Note: this is insufficient to test if any element is a shadow root, but it is sufficient to differentiate between
  2179. * a Document and a ShadowRoot.
  2180. */
  2181. const isShadowRoot = (dos) => isDocumentFragment$1(dos) && isNonNullable(dos.dom.host);
  2182. const getRootNode = (e) => SugarElement.fromDom(e.dom.getRootNode());
  2183. /** Where style tags need to go. ShadowRoot or document head */
  2184. const getStyleContainer = (dos) => isShadowRoot(dos) ? dos : getHead(documentOrOwner(dos));
  2185. /** Where content needs to go. ShadowRoot or document body */
  2186. const getContentContainer = (dos) =>
  2187. // Can't use SugarBody.body without causing a circular module reference (since SugarBody.inBody uses SugarShadowDom)
  2188. isShadowRoot(dos) ? dos : SugarElement.fromDom(documentOrOwner(dos).dom.body);
  2189. /** If this element is in a ShadowRoot, return it. */
  2190. const getShadowRoot = (e) => {
  2191. const r = getRootNode(e);
  2192. return isShadowRoot(r) ? Optional.some(r) : Optional.none();
  2193. };
  2194. /** Return the host of a ShadowRoot.
  2195. *
  2196. * This function will throw if Shadow DOM is unsupported in the browser, or if the host is null.
  2197. * If you actually have a ShadowRoot, this shouldn't happen.
  2198. */
  2199. const getShadowHost = (e) => SugarElement.fromDom(e.dom.host);
  2200. /**
  2201. * When Events bubble up through a ShadowRoot, the browser changes the target to be the shadow host.
  2202. * This function gets the "original" event target if possible.
  2203. * This only works if the shadow tree is open - if the shadow tree is closed, event.target is returned.
  2204. * See: https://developers.google.com/web/fundamentals/web-components/shadowdom#events
  2205. */
  2206. const getOriginalEventTarget = (event) => {
  2207. if (isNonNullable(event.target)) {
  2208. const el = SugarElement.fromDom(event.target);
  2209. if (isElement$8(el) && isOpenShadowHost(el)) {
  2210. // When target element is inside Shadow DOM we need to take first element from composedPath
  2211. // otherwise we'll get Shadow Root parent, not actual target element.
  2212. if (event.composed && event.composedPath) {
  2213. const composedPath = event.composedPath();
  2214. if (composedPath) {
  2215. return head(composedPath);
  2216. }
  2217. }
  2218. }
  2219. }
  2220. return Optional.from(event.target);
  2221. };
  2222. /** Return true if the element is a host of an open shadow root.
  2223. * Return false if the element is a host of a closed shadow root, or if the element is not a host.
  2224. */
  2225. const isOpenShadowHost = (element) => isNonNullable(element.dom.shadowRoot);
  2226. const mkEvent = (target, x, y, stop, prevent, kill, raw) => ({
  2227. target,
  2228. x,
  2229. y,
  2230. stop,
  2231. prevent,
  2232. kill,
  2233. raw
  2234. });
  2235. /** Wraps an Event in an EventArgs structure.
  2236. * The returned EventArgs structure has its target set to the "original" target if possible.
  2237. * See SugarShadowDom.getOriginalEventTarget
  2238. */
  2239. const fromRawEvent = (rawEvent) => {
  2240. const target = SugarElement.fromDom(getOriginalEventTarget(rawEvent).getOr(rawEvent.target));
  2241. const stop = () => rawEvent.stopPropagation();
  2242. const prevent = () => rawEvent.preventDefault();
  2243. const kill = compose(prevent, stop); // more of a sequence than a compose, but same effect
  2244. // FIX: Don't just expose the raw event. Need to identify what needs standardisation.
  2245. return mkEvent(target, rawEvent.clientX, rawEvent.clientY, stop, prevent, kill, rawEvent);
  2246. };
  2247. const handle$1 = (filter, handler) => (rawEvent) => {
  2248. if (filter(rawEvent)) {
  2249. handler(fromRawEvent(rawEvent));
  2250. }
  2251. };
  2252. const binder = (element, event, filter, handler, useCapture) => {
  2253. const wrapped = handle$1(filter, handler);
  2254. // IE9 minimum
  2255. element.dom.addEventListener(event, wrapped, useCapture);
  2256. return {
  2257. unbind: curry(unbind, element, event, wrapped, useCapture)
  2258. };
  2259. };
  2260. const bind$2 = (element, event, filter, handler) => binder(element, event, filter, handler, false);
  2261. const unbind = (element, event, handler, useCapture) => {
  2262. // IE9 minimum
  2263. element.dom.removeEventListener(event, handler, useCapture);
  2264. };
  2265. const filter$2 = always; // no filter on plain DomEvents
  2266. const bind$1 = (element, event, handler) => bind$2(element, event, filter$2, handler);
  2267. const getDocument = () => SugarElement.fromDom(document);
  2268. const focus$1 = (element, preventScroll = false) => element.dom.focus({ preventScroll });
  2269. const hasFocus$1 = (element) => {
  2270. const root = getRootNode(element).dom;
  2271. return element.dom === root.activeElement;
  2272. };
  2273. // Note: assuming that activeElement will always be a HTMLElement (maybe we should add a runtime check?)
  2274. const active = (root = getDocument()) => Optional.from(root.dom.activeElement).map(SugarElement.fromDom);
  2275. /**
  2276. * Return the descendant element that has focus.
  2277. * Use instead of SelectorFind.descendant(container, ':focus')
  2278. * because the :focus selector relies on keyboard focus.
  2279. */
  2280. const search = (element) => active(getRootNode(element))
  2281. .filter((e) => element.dom.contains(e.dom));
  2282. const before$4 = (marker, element) => {
  2283. const parent$1 = parent(marker);
  2284. parent$1.each((v) => {
  2285. v.dom.insertBefore(element.dom, marker.dom);
  2286. });
  2287. };
  2288. const after$4 = (marker, element) => {
  2289. const sibling = nextSibling(marker);
  2290. sibling.fold(() => {
  2291. const parent$1 = parent(marker);
  2292. parent$1.each((v) => {
  2293. append$1(v, element);
  2294. });
  2295. }, (v) => {
  2296. before$4(v, element);
  2297. });
  2298. };
  2299. const prepend = (parent, element) => {
  2300. const firstChild$1 = firstChild(parent);
  2301. firstChild$1.fold(() => {
  2302. append$1(parent, element);
  2303. }, (v) => {
  2304. parent.dom.insertBefore(element.dom, v.dom);
  2305. });
  2306. };
  2307. const append$1 = (parent, element) => {
  2308. parent.dom.appendChild(element.dom);
  2309. };
  2310. const wrap$2 = (element, wrapper) => {
  2311. before$4(element, wrapper);
  2312. append$1(wrapper, element);
  2313. };
  2314. const before$3 = (marker, elements) => {
  2315. each$e(elements, (x) => {
  2316. before$4(marker, x);
  2317. });
  2318. };
  2319. const after$3 = (marker, elements) => {
  2320. each$e(elements, (x, i) => {
  2321. const e = i === 0 ? marker : elements[i - 1];
  2322. after$4(e, x);
  2323. });
  2324. };
  2325. const append = (parent, elements) => {
  2326. each$e(elements, (x) => {
  2327. append$1(parent, x);
  2328. });
  2329. };
  2330. const rawSet = (dom, key, value) => {
  2331. /*
  2332. * JQuery coerced everything to a string, and silently did nothing on text node/null/undefined.
  2333. *
  2334. * We fail on those invalid cases, only allowing numbers and booleans.
  2335. */
  2336. if (isString(value) || isBoolean(value) || isNumber(value)) {
  2337. dom.setAttribute(key, value + '');
  2338. }
  2339. else {
  2340. // eslint-disable-next-line no-console
  2341. console.error('Invalid call to Attribute.set. Key ', key, ':: Value ', value, ':: Element ', dom);
  2342. throw new Error('Attribute value was not simple');
  2343. }
  2344. };
  2345. const set$4 = (element, key, value) => {
  2346. rawSet(element.dom, key, value);
  2347. };
  2348. const setAll$1 = (element, attrs) => {
  2349. const dom = element.dom;
  2350. each$d(attrs, (v, k) => {
  2351. rawSet(dom, k, v);
  2352. });
  2353. };
  2354. const get$9 = (element, key) => {
  2355. const v = element.dom.getAttribute(key);
  2356. // undefined is the more appropriate value for JS, and this matches JQuery
  2357. return v === null ? undefined : v;
  2358. };
  2359. const getOpt = (element, key) => Optional.from(get$9(element, key));
  2360. const has$1 = (element, key) => {
  2361. const dom = element.dom;
  2362. // return false for non-element nodes, no point in throwing an error
  2363. return dom && dom.hasAttribute ? dom.hasAttribute(key) : false;
  2364. };
  2365. const remove$9 = (element, key) => {
  2366. element.dom.removeAttribute(key);
  2367. };
  2368. const hasNone = (element) => {
  2369. const attrs = element.dom.attributes;
  2370. return attrs === undefined || attrs === null || attrs.length === 0;
  2371. };
  2372. const clone$4 = (element) => foldl(element.dom.attributes, (acc, attr) => {
  2373. acc[attr.name] = attr.value;
  2374. return acc;
  2375. }, {});
  2376. const empty = (element) => {
  2377. // shortcut "empty node" trick. Requires IE 9.
  2378. element.dom.textContent = '';
  2379. // If the contents was a single empty text node, the above doesn't remove it. But, it's still faster in general
  2380. // than removing every child node manually.
  2381. // The following is (probably) safe for performance as 99.9% of the time the trick works and
  2382. // Traverse.children will return an empty array.
  2383. each$e(children$1(element), (rogue) => {
  2384. remove$8(rogue);
  2385. });
  2386. };
  2387. const remove$8 = (element) => {
  2388. const dom = element.dom;
  2389. if (dom.parentNode !== null) {
  2390. dom.parentNode.removeChild(dom);
  2391. }
  2392. };
  2393. const unwrap = (wrapper) => {
  2394. const children = children$1(wrapper);
  2395. if (children.length > 0) {
  2396. after$3(wrapper, children);
  2397. }
  2398. remove$8(wrapper);
  2399. };
  2400. const clone$3 = (original, isDeep) => SugarElement.fromDom(original.dom.cloneNode(isDeep));
  2401. /** Shallow clone - just the tag, no children */
  2402. const shallow = (original) => clone$3(original, false);
  2403. /** Deep clone - everything copied including children */
  2404. const deep = (original) => clone$3(original, true);
  2405. /** Shallow clone, with a new tag */
  2406. const shallowAs = (original, tag) => {
  2407. const nu = SugarElement.fromTag(tag);
  2408. const attributes = clone$4(original);
  2409. setAll$1(nu, attributes);
  2410. return nu;
  2411. };
  2412. /** Change the tag name, but keep all children */
  2413. const mutate = (original, tag) => {
  2414. const nu = shallowAs(original, tag);
  2415. after$4(original, nu);
  2416. const children = children$1(original);
  2417. append(nu, children);
  2418. remove$8(original);
  2419. return nu;
  2420. };
  2421. const fromHtml = (html, scope) => {
  2422. const doc = scope || document;
  2423. const div = doc.createElement('div');
  2424. div.innerHTML = html;
  2425. return children$1(SugarElement.fromDom(div));
  2426. };
  2427. const fromDom$1 = (nodes) => map$3(nodes, SugarElement.fromDom);
  2428. const get$8 = (element) => element.dom.innerHTML;
  2429. const set$3 = (element, content) => {
  2430. const owner = owner$1(element);
  2431. const docDom = owner.dom;
  2432. // FireFox has *terrible* performance when using innerHTML = x
  2433. const fragment = SugarElement.fromDom(docDom.createDocumentFragment());
  2434. const contentElements = fromHtml(content, docDom);
  2435. append(fragment, contentElements);
  2436. empty(element);
  2437. append$1(element, fragment);
  2438. };
  2439. const getOuter = (element) => {
  2440. const container = SugarElement.fromTag('div');
  2441. const clone = SugarElement.fromDom(element.dom.cloneNode(true));
  2442. append$1(container, clone);
  2443. return get$8(container);
  2444. };
  2445. // some elements, such as mathml, don't have style attributes
  2446. // others, such as angular elements, have style attributes that aren't a CSSStyleDeclaration
  2447. const isSupported = (dom) => dom.style !== undefined && isFunction(dom.style.getPropertyValue);
  2448. // Node.contains() is very, very, very good performance
  2449. // http://jsperf.com/closest-vs-contains/5
  2450. const inBody = (element) => {
  2451. // Technically this is only required on IE, where contains() returns false for text nodes.
  2452. // But it's cheap enough to run everywhere and Sugar doesn't have platform detection (yet).
  2453. const dom = isText$c(element) ? element.dom.parentNode : element.dom;
  2454. // use ownerDocument.body to ensure this works inside iframes.
  2455. // Normally contains is bad because an element "contains" itself, but here we want that.
  2456. if (dom === undefined || dom === null || dom.ownerDocument === null) {
  2457. return false;
  2458. }
  2459. const doc = dom.ownerDocument;
  2460. return getShadowRoot(SugarElement.fromDom(dom)).fold(() => doc.body.contains(dom), compose1(inBody, getShadowHost));
  2461. };
  2462. const internalSet = (dom, property, value) => {
  2463. // This is going to hurt. Apologies.
  2464. // JQuery coerces numbers to pixels for certain property names, and other times lets numbers through.
  2465. // we're going to be explicit; strings only.
  2466. if (!isString(value)) {
  2467. // eslint-disable-next-line no-console
  2468. console.error('Invalid call to CSS.set. Property ', property, ':: Value ', value, ':: Element ', dom);
  2469. throw new Error('CSS value must be a string: ' + value);
  2470. }
  2471. // removed: support for dom().style[property] where prop is camel case instead of normal property name
  2472. if (isSupported(dom)) {
  2473. dom.style.setProperty(property, value);
  2474. }
  2475. };
  2476. const internalRemove = (dom, property) => {
  2477. /*
  2478. * IE9 and above - MDN doesn't have details, but here's a couple of random internet claims
  2479. *
  2480. * http://help.dottoro.com/ljopsjck.php
  2481. * http://stackoverflow.com/a/7901886/7546
  2482. */
  2483. if (isSupported(dom)) {
  2484. dom.style.removeProperty(property);
  2485. }
  2486. };
  2487. const set$2 = (element, property, value) => {
  2488. const dom = element.dom;
  2489. internalSet(dom, property, value);
  2490. };
  2491. const setAll = (element, css) => {
  2492. const dom = element.dom;
  2493. each$d(css, (v, k) => {
  2494. internalSet(dom, k, v);
  2495. });
  2496. };
  2497. /*
  2498. * NOTE: For certain properties, this returns the "used value" which is subtly different to the "computed value" (despite calling getComputedStyle).
  2499. * Blame CSS 2.0.
  2500. *
  2501. * https://developer.mozilla.org/en-US/docs/Web/CSS/used_value
  2502. */
  2503. const get$7 = (element, property) => {
  2504. const dom = element.dom;
  2505. /*
  2506. * IE9 and above per
  2507. * https://developer.mozilla.org/en/docs/Web/API/window.getComputedStyle
  2508. *
  2509. * Not in numerosity, because it doesn't memoize and looking this up dynamically in performance critical code would be horrendous.
  2510. *
  2511. * JQuery has some magic here for IE popups, but we don't really need that.
  2512. * It also uses element.ownerDocument.defaultView to handle iframes but that hasn't been required since FF 3.6.
  2513. */
  2514. const styles = window.getComputedStyle(dom);
  2515. const r = styles.getPropertyValue(property);
  2516. // jquery-ism: If r is an empty string, check that the element is not in a document. If it isn't, return the raw value.
  2517. // Turns out we do this a lot.
  2518. return (r === '' && !inBody(element)) ? getUnsafeProperty(dom, property) : r;
  2519. };
  2520. // removed: support for dom().style[property] where prop is camel case instead of normal property name
  2521. // empty string is what the browsers (IE11 and Chrome) return when the propertyValue doesn't exists.
  2522. const getUnsafeProperty = (dom, property) => isSupported(dom) ? dom.style.getPropertyValue(property) : '';
  2523. /*
  2524. * Gets the raw value from the style attribute. Useful for retrieving "used values" from the DOM:
  2525. * https://developer.mozilla.org/en-US/docs/Web/CSS/used_value
  2526. *
  2527. * Returns NONE if the property isn't set, or the value is an empty string.
  2528. */
  2529. const getRaw$1 = (element, property) => {
  2530. const dom = element.dom;
  2531. const raw = getUnsafeProperty(dom, property);
  2532. return Optional.from(raw).filter((r) => r.length > 0);
  2533. };
  2534. const getAllRaw = (element) => {
  2535. const css = {};
  2536. const dom = element.dom;
  2537. if (isSupported(dom)) {
  2538. for (let i = 0; i < dom.style.length; i++) {
  2539. const ruleName = dom.style.item(i);
  2540. css[ruleName] = dom.style[ruleName];
  2541. }
  2542. }
  2543. return css;
  2544. };
  2545. const remove$7 = (element, property) => {
  2546. const dom = element.dom;
  2547. internalRemove(dom, property);
  2548. if (is$4(getOpt(element, 'style').map(trim$4), '')) {
  2549. // No more styles left, remove the style attribute as well
  2550. remove$9(element, 'style');
  2551. }
  2552. };
  2553. /* NOTE: This function is here for the side effect it triggers.
  2554. The value itself is not used.
  2555. Be sure to not use the return value, and that it is not removed by a minifier.
  2556. */
  2557. const reflow = (e) => e.dom.offsetWidth;
  2558. const Dimension = (name, getOffset) => {
  2559. const set = (element, h) => {
  2560. if (!isNumber(h) && !h.match(/^[0-9]+$/)) {
  2561. throw new Error(name + '.set accepts only positive integer values. Value was ' + h);
  2562. }
  2563. const dom = element.dom;
  2564. if (isSupported(dom)) {
  2565. dom.style[name] = h + 'px';
  2566. }
  2567. };
  2568. /*
  2569. * jQuery supports querying width and height on the document and window objects.
  2570. *
  2571. * TBIO doesn't do this, so the code is removed to save space, but left here just in case.
  2572. */
  2573. /*
  2574. var getDocumentWidth = (element) => {
  2575. var dom = element.dom;
  2576. if (Node.isDocument(element)) {
  2577. var body = dom.body;
  2578. var doc = dom.documentElement;
  2579. return Math.max(
  2580. body.scrollHeight,
  2581. doc.scrollHeight,
  2582. body.offsetHeight,
  2583. doc.offsetHeight,
  2584. doc.clientHeight
  2585. );
  2586. }
  2587. };
  2588. var getWindowWidth = (element) => {
  2589. var dom = element.dom;
  2590. if (dom.window === dom) {
  2591. // There is no offsetHeight on a window, so use the clientHeight of the document
  2592. return dom.document.documentElement.clientHeight;
  2593. }
  2594. };
  2595. */
  2596. const get = (element) => {
  2597. const r = getOffset(element);
  2598. // zero or null means non-standard or disconnected, fall back to CSS
  2599. if (r <= 0 || r === null) {
  2600. const css = get$7(element, name);
  2601. // ugh this feels dirty, but it saves cycles
  2602. return parseFloat(css) || 0;
  2603. }
  2604. return r;
  2605. };
  2606. // in jQuery, getOuter replicates (or uses) box-sizing: border-box calculations
  2607. // although these calculations only seem relevant for quirks mode, and edge cases TBIO doesn't rely on
  2608. const getOuter = get;
  2609. const aggregate = (element, properties) => foldl(properties, (acc, property) => {
  2610. const val = get$7(element, property);
  2611. const value = val === undefined ? 0 : parseInt(val, 10);
  2612. return isNaN(value) ? acc : acc + value;
  2613. }, 0);
  2614. const max = (element, value, properties) => {
  2615. const cumulativeInclusions = aggregate(element, properties);
  2616. // if max-height is 100px and your cumulativeInclusions is 150px, there is no way max-height can be 100px, so we return 0.
  2617. const absoluteMax = value > cumulativeInclusions ? value - cumulativeInclusions : 0;
  2618. return absoluteMax;
  2619. };
  2620. return {
  2621. set,
  2622. get,
  2623. getOuter,
  2624. aggregate,
  2625. max
  2626. };
  2627. };
  2628. const api$1 = Dimension('height', (element) => {
  2629. // getBoundingClientRect gives better results than offsetHeight for tables with captions on Firefox
  2630. const dom = element.dom;
  2631. return inBody(element) ? dom.getBoundingClientRect().height : dom.offsetHeight;
  2632. });
  2633. const get$6 = (element) => api$1.get(element);
  2634. const r = (left, top) => {
  2635. const translate = (x, y) => r(left + x, top + y);
  2636. return {
  2637. left,
  2638. top,
  2639. translate
  2640. };
  2641. };
  2642. // tslint:disable-next-line:variable-name
  2643. const SugarPosition = r;
  2644. const boxPosition = (dom) => {
  2645. const box = dom.getBoundingClientRect();
  2646. return SugarPosition(box.left, box.top);
  2647. };
  2648. // Avoids falsy false fallthrough
  2649. const firstDefinedOrZero = (a, b) => {
  2650. if (a !== undefined) {
  2651. return a;
  2652. }
  2653. else {
  2654. return b !== undefined ? b : 0;
  2655. }
  2656. };
  2657. const absolute = (element) => {
  2658. const doc = element.dom.ownerDocument;
  2659. const body = doc.body;
  2660. const win = doc.defaultView;
  2661. const html = doc.documentElement;
  2662. if (body === element.dom) {
  2663. return SugarPosition(body.offsetLeft, body.offsetTop);
  2664. }
  2665. const scrollTop = firstDefinedOrZero(win === null || win === void 0 ? void 0 : win.pageYOffset, html.scrollTop);
  2666. const scrollLeft = firstDefinedOrZero(win === null || win === void 0 ? void 0 : win.pageXOffset, html.scrollLeft);
  2667. const clientTop = firstDefinedOrZero(html.clientTop, body.clientTop);
  2668. const clientLeft = firstDefinedOrZero(html.clientLeft, body.clientLeft);
  2669. return viewport(element).translate(scrollLeft - clientLeft, scrollTop - clientTop);
  2670. };
  2671. const viewport = (element) => {
  2672. const dom = element.dom;
  2673. const doc = dom.ownerDocument;
  2674. const body = doc.body;
  2675. if (body === dom) {
  2676. return SugarPosition(body.offsetLeft, body.offsetTop);
  2677. }
  2678. if (!inBody(element)) {
  2679. return SugarPosition(0, 0);
  2680. }
  2681. return boxPosition(dom);
  2682. };
  2683. // get scroll position (x,y) relative to document _doc (or global if not supplied)
  2684. const get$5 = (_DOC) => {
  2685. const doc = _DOC !== undefined ? _DOC.dom : document;
  2686. // ASSUMPTION: This is for cross-browser support, body works for Safari & EDGE, and when we have an iframe body scroller
  2687. const x = doc.body.scrollLeft || doc.documentElement.scrollLeft;
  2688. const y = doc.body.scrollTop || doc.documentElement.scrollTop;
  2689. return SugarPosition(x, y);
  2690. };
  2691. // Scroll content to (x,y) relative to document _doc (or global if not supplied)
  2692. const to = (x, y, _DOC) => {
  2693. const doc = _DOC !== undefined ? _DOC.dom : document;
  2694. const win = doc.defaultView;
  2695. if (win) {
  2696. win.scrollTo(x, y);
  2697. }
  2698. };
  2699. // TBIO-4472 Safari 10 - Scrolling typeahead with keyboard scrolls page
  2700. const intoView = (element, alignToTop) => {
  2701. const isSafari = detect$1().browser.isSafari();
  2702. // this method isn't in TypeScript
  2703. if (isSafari && isFunction(element.dom.scrollIntoViewIfNeeded)) {
  2704. element.dom.scrollIntoViewIfNeeded(false); // false=align to nearest edge
  2705. }
  2706. else {
  2707. element.dom.scrollIntoView(alignToTop); // true=to top, false=to bottom
  2708. }
  2709. };
  2710. const NodeValue = (is, name) => {
  2711. const get = (element) => {
  2712. if (!is(element)) {
  2713. throw new Error('Can only get ' + name + ' value of a ' + name + ' node');
  2714. }
  2715. return getOption(element).getOr('');
  2716. };
  2717. const getOption = (element) => is(element) ? Optional.from(element.dom.nodeValue) : Optional.none();
  2718. const set = (element, value) => {
  2719. if (!is(element)) {
  2720. throw new Error('Can only set raw ' + name + ' value of a ' + name + ' node');
  2721. }
  2722. element.dom.nodeValue = value;
  2723. };
  2724. return {
  2725. get,
  2726. getOption,
  2727. set
  2728. };
  2729. };
  2730. const fromElements = (elements, scope) => {
  2731. const doc = scope || document;
  2732. const fragment = doc.createDocumentFragment();
  2733. each$e(elements, (element) => {
  2734. fragment.appendChild(element.dom);
  2735. });
  2736. return SugarElement.fromDom(fragment);
  2737. };
  2738. const api = NodeValue(isText$c, 'text');
  2739. const get$4 = (element) => api.get(element);
  2740. const getOption = (element) => api.getOption(element);
  2741. const set$1 = (element, value) => api.set(element, value);
  2742. // Methods for handling attributes that contain a list of values <div foo="alpha beta theta">
  2743. const read$4 = (element, attr) => {
  2744. const value = get$9(element, attr);
  2745. return value === undefined || value === '' ? [] : value.split(' ');
  2746. };
  2747. const add$4 = (element, attr, id) => {
  2748. const old = read$4(element, attr);
  2749. const nu = old.concat([id]);
  2750. set$4(element, attr, nu.join(' '));
  2751. return true;
  2752. };
  2753. const remove$6 = (element, attr, id) => {
  2754. const nu = filter$5(read$4(element, attr), (v) => v !== id);
  2755. if (nu.length > 0) {
  2756. set$4(element, attr, nu.join(' '));
  2757. }
  2758. else {
  2759. remove$9(element, attr);
  2760. }
  2761. return false;
  2762. };
  2763. var ClosestOrAncestor = (is, ancestor, scope, a, isRoot) => {
  2764. if (is(scope, a)) {
  2765. return Optional.some(scope);
  2766. }
  2767. else if (isFunction(isRoot) && isRoot(scope)) {
  2768. return Optional.none();
  2769. }
  2770. else {
  2771. return ancestor(scope, a, isRoot);
  2772. }
  2773. };
  2774. const ancestor$5 = (scope, predicate, isRoot) => {
  2775. let element = scope.dom;
  2776. const stop = isFunction(isRoot) ? isRoot : never;
  2777. while (element.parentNode) {
  2778. element = element.parentNode;
  2779. const el = SugarElement.fromDom(element);
  2780. if (predicate(el)) {
  2781. return Optional.some(el);
  2782. }
  2783. else if (stop(el)) {
  2784. break;
  2785. }
  2786. }
  2787. return Optional.none();
  2788. };
  2789. const closest$4 = (scope, predicate, isRoot) => {
  2790. // This is required to avoid ClosestOrAncestor passing the predicate to itself
  2791. const is = (s, test) => test(s);
  2792. return ClosestOrAncestor(is, ancestor$5, scope, predicate, isRoot);
  2793. };
  2794. const sibling$1 = (scope, predicate) => {
  2795. const element = scope.dom;
  2796. if (!element.parentNode) {
  2797. return Optional.none();
  2798. }
  2799. return child(SugarElement.fromDom(element.parentNode), (x) => !eq(scope, x) && predicate(x));
  2800. };
  2801. const child = (scope, predicate) => {
  2802. const pred = (node) => predicate(SugarElement.fromDom(node));
  2803. const result = find$2(scope.dom.childNodes, pred);
  2804. return result.map(SugarElement.fromDom);
  2805. };
  2806. const descendant$2 = (scope, predicate) => {
  2807. const descend = (node) => {
  2808. // tslint:disable-next-line:prefer-for-of
  2809. for (let i = 0; i < node.childNodes.length; i++) {
  2810. const child = SugarElement.fromDom(node.childNodes[i]);
  2811. if (predicate(child)) {
  2812. return Optional.some(child);
  2813. }
  2814. const res = descend(node.childNodes[i]);
  2815. if (res.isSome()) {
  2816. return res;
  2817. }
  2818. }
  2819. return Optional.none();
  2820. };
  2821. return descend(scope.dom);
  2822. };
  2823. const ancestor$4 = (scope, selector, isRoot) => ancestor$5(scope, (e) => is$2(e, selector), isRoot);
  2824. const descendant$1 = (scope, selector) => one(selector, scope);
  2825. // Returns Some(closest ancestor element (sugared)) matching 'selector' up to isRoot, or None() otherwise
  2826. const closest$3 = (scope, selector, isRoot) => {
  2827. const is = (element, selector) => is$2(element, selector);
  2828. return ClosestOrAncestor(is, ancestor$4, scope, selector, isRoot);
  2829. };
  2830. // IE11 Can return undefined for a classList on elements such as math, so we make sure it's not undefined before attempting to use it.
  2831. const supports = (element) => element.dom.classList !== undefined;
  2832. const get$3 = (element) => read$4(element, 'class');
  2833. const add$3 = (element, clazz) => add$4(element, 'class', clazz);
  2834. const remove$5 = (element, clazz) => remove$6(element, 'class', clazz);
  2835. const toggle$2 = (element, clazz) => {
  2836. if (contains$2(get$3(element), clazz)) {
  2837. return remove$5(element, clazz);
  2838. }
  2839. else {
  2840. return add$3(element, clazz);
  2841. }
  2842. };
  2843. /*
  2844. * ClassList is IE10 minimum:
  2845. * https://developer.mozilla.org/en-US/docs/Web/API/Element.classList
  2846. *
  2847. * Note that IE doesn't support the second argument to toggle (at all).
  2848. * If it did, the toggler could be better.
  2849. */
  2850. const add$2 = (element, clazz) => {
  2851. if (supports(element)) {
  2852. element.dom.classList.add(clazz);
  2853. }
  2854. else {
  2855. add$3(element, clazz);
  2856. }
  2857. };
  2858. const cleanClass = (element) => {
  2859. const classList = supports(element) ? element.dom.classList : get$3(element);
  2860. // classList is a "live list", so this is up to date already
  2861. if (classList.length === 0) {
  2862. // No more classes left, remove the class attribute as well
  2863. remove$9(element, 'class');
  2864. }
  2865. };
  2866. const remove$4 = (element, clazz) => {
  2867. if (supports(element)) {
  2868. const classList = element.dom.classList;
  2869. classList.remove(clazz);
  2870. }
  2871. else {
  2872. remove$5(element, clazz);
  2873. }
  2874. cleanClass(element);
  2875. };
  2876. const toggle$1 = (element, clazz) => {
  2877. const result = supports(element) ? element.dom.classList.toggle(clazz) : toggle$2(element, clazz);
  2878. cleanClass(element);
  2879. return result;
  2880. };
  2881. const has = (element, clazz) => supports(element) && element.dom.classList.contains(clazz);
  2882. /*
  2883. * ClassList is IE10 minimum:
  2884. * https://developer.mozilla.org/en-US/docs/Web/API/Element.classList
  2885. */
  2886. const add$1 = (element, classes) => {
  2887. each$e(classes, (x) => {
  2888. add$2(element, x);
  2889. });
  2890. };
  2891. const remove$3 = (element, classes) => {
  2892. each$e(classes, (x) => {
  2893. remove$4(element, x);
  2894. });
  2895. };
  2896. const closest$2 = (target) => closest$3(target, '[contenteditable]');
  2897. const isEditable$2 = (element, assumeEditable = false) => {
  2898. if (inBody(element)) {
  2899. return element.dom.isContentEditable;
  2900. }
  2901. else {
  2902. // Find the closest contenteditable element and check if it's editable
  2903. return closest$2(element).fold(constant(assumeEditable), (editable) => getRaw(editable) === 'true');
  2904. }
  2905. };
  2906. const getRaw = (element) => element.dom.contentEditable;
  2907. const set = (element, editable) => {
  2908. element.dom.contentEditable = editable ? 'true' : 'false';
  2909. };
  2910. const ancestors$1 = (scope, predicate, isRoot) => filter$5(parents$1(scope, isRoot), predicate);
  2911. const children = (scope, predicate) => filter$5(children$1(scope), predicate);
  2912. const descendants$1 = (scope, predicate) => {
  2913. let result = [];
  2914. // Recurse.toArray() might help here
  2915. each$e(children$1(scope), (x) => {
  2916. if (predicate(x)) {
  2917. result = result.concat([x]);
  2918. }
  2919. result = result.concat(descendants$1(x, predicate));
  2920. });
  2921. return result;
  2922. };
  2923. // For all of the following:
  2924. //
  2925. // jQuery does siblings of firstChild. IE9+ supports scope.dom.children (similar to Traverse.children but elements only).
  2926. // Traverse should also do this (but probably not by default).
  2927. //
  2928. const ancestors = (scope, selector, isRoot) =>
  2929. // It may surprise you to learn this is exactly what JQuery does
  2930. // TODO: Avoid all this wrapping and unwrapping
  2931. ancestors$1(scope, (e) => is$2(e, selector), isRoot);
  2932. const descendants = (scope, selector) => all(selector, scope);
  2933. const ancestor$3 = (scope, predicate, isRoot) => ancestor$5(scope, predicate, isRoot).isSome();
  2934. const sibling = (scope, predicate) => sibling$1(scope, predicate).isSome();
  2935. const descendant = (scope, predicate) => descendant$2(scope, predicate).isSome();
  2936. const ancestor$2 = (element, target) => ancestor$3(element, curry(eq, target));
  2937. const ancestor$1 = (scope, selector, isRoot) => ancestor$4(scope, selector, isRoot).isSome();
  2938. const ensureIsRoot = (isRoot) => isFunction(isRoot) ? isRoot : never;
  2939. const ancestor = (scope, transform, isRoot) => {
  2940. let element = scope.dom;
  2941. const stop = ensureIsRoot(isRoot);
  2942. while (element.parentNode) {
  2943. element = element.parentNode;
  2944. const el = SugarElement.fromDom(element);
  2945. const transformed = transform(el);
  2946. if (transformed.isSome()) {
  2947. return transformed;
  2948. }
  2949. else if (stop(el)) {
  2950. break;
  2951. }
  2952. }
  2953. return Optional.none();
  2954. };
  2955. const closest$1 = (scope, transform, isRoot) => {
  2956. const current = transform(scope);
  2957. const stop = ensureIsRoot(isRoot);
  2958. return current.orThunk(() => stop(scope) ? Optional.none() : ancestor(scope, transform, stop));
  2959. };
  2960. const isTextNodeWithCursorPosition = (el) => getOption(el).filter((text) =>
  2961. // For the purposes of finding cursor positions only allow text nodes with content,
  2962. // but trim removes &nbsp; and that's allowed
  2963. text.trim().length !== 0 || text.indexOf(nbsp) > -1).isSome();
  2964. const isContentEditableFalse$b = (elem) => isHTMLElement$1(elem) && (get$9(elem, 'contenteditable') === 'false');
  2965. const elementsWithCursorPosition = ['img', 'br'];
  2966. const isCursorPosition = (elem) => {
  2967. const hasCursorPosition = isTextNodeWithCursorPosition(elem);
  2968. return hasCursorPosition || contains$2(elementsWithCursorPosition, name(elem)) || isContentEditableFalse$b(elem);
  2969. };
  2970. const first = (element) => descendant$2(element, isCursorPosition);
  2971. const create$c = (start, soffset, finish, foffset) => ({
  2972. start,
  2973. soffset,
  2974. finish,
  2975. foffset
  2976. });
  2977. // tslint:disable-next-line:variable-name
  2978. const SimRange = {
  2979. create: create$c
  2980. };
  2981. const adt$2 = Adt.generate([
  2982. { before: ['element'] },
  2983. { on: ['element', 'offset'] },
  2984. { after: ['element'] }
  2985. ]);
  2986. // Probably don't need this given that we now have "match"
  2987. const cata = (subject, onBefore, onOn, onAfter) => subject.fold(onBefore, onOn, onAfter);
  2988. const getStart$2 = (situ) => situ.fold(identity, identity, identity);
  2989. const before$2 = adt$2.before;
  2990. const on = adt$2.on;
  2991. const after$2 = adt$2.after;
  2992. // tslint:disable-next-line:variable-name
  2993. const Situ = {
  2994. before: before$2,
  2995. on,
  2996. after: after$2,
  2997. cata,
  2998. getStart: getStart$2
  2999. };
  3000. // Consider adding a type for "element"
  3001. const adt$1 = Adt.generate([
  3002. { domRange: ['rng'] },
  3003. { relative: ['startSitu', 'finishSitu'] },
  3004. { exact: ['start', 'soffset', 'finish', 'foffset'] }
  3005. ]);
  3006. const exactFromRange = (simRange) => adt$1.exact(simRange.start, simRange.soffset, simRange.finish, simRange.foffset);
  3007. const getStart$1 = (selection) => selection.match({
  3008. domRange: (rng) => SugarElement.fromDom(rng.startContainer),
  3009. relative: (startSitu, _finishSitu) => Situ.getStart(startSitu),
  3010. exact: (start, _soffset, _finish, _foffset) => start
  3011. });
  3012. const domRange = adt$1.domRange;
  3013. const relative = adt$1.relative;
  3014. const exact = adt$1.exact;
  3015. const getWin = (selection) => {
  3016. const start = getStart$1(selection);
  3017. return defaultView(start);
  3018. };
  3019. // This is out of place but it's API so I can't remove it
  3020. const range = SimRange.create;
  3021. // tslint:disable-next-line:variable-name
  3022. const SimSelection = {
  3023. domRange,
  3024. relative,
  3025. exact,
  3026. exactFromRange,
  3027. getWin,
  3028. range
  3029. };
  3030. const caretPositionFromPoint = (doc, x, y) => {
  3031. var _a;
  3032. return Optional.from((_a = doc.caretPositionFromPoint) === null || _a === void 0 ? void 0 : _a.call(doc, x, y))
  3033. .bind((pos) => {
  3034. // It turns out that Firefox can return null for pos.offsetNode
  3035. if (pos.offsetNode === null) {
  3036. return Optional.none();
  3037. }
  3038. const r = doc.createRange();
  3039. r.setStart(pos.offsetNode, pos.offset);
  3040. r.collapse();
  3041. return Optional.some(r);
  3042. });
  3043. };
  3044. const caretRangeFromPoint = (doc, x, y) => { var _a; return Optional.from((_a = doc.caretRangeFromPoint) === null || _a === void 0 ? void 0 : _a.call(doc, x, y)); };
  3045. const availableSearch = (doc, x, y) => {
  3046. if (doc.caretPositionFromPoint) {
  3047. return caretPositionFromPoint(doc, x, y); // defined standard, firefox only
  3048. }
  3049. else if (doc.caretRangeFromPoint) {
  3050. return caretRangeFromPoint(doc, x, y); // webkit/blink implementation
  3051. }
  3052. else {
  3053. return Optional.none(); // unsupported browser
  3054. }
  3055. };
  3056. const fromPoint$1 = (win, x, y) => {
  3057. const doc = win.document;
  3058. return availableSearch(doc, x, y).map((rng) => SimRange.create(SugarElement.fromDom(rng.startContainer), rng.startOffset, SugarElement.fromDom(rng.endContainer), rng.endOffset));
  3059. };
  3060. const beforeSpecial = (element, offset) => {
  3061. // From memory, we don't want to use <br> directly on Firefox because it locks the keyboard input.
  3062. // It turns out that <img> directly on IE locks the keyboard as well.
  3063. // If the offset is 0, use before. If the offset is 1, use after.
  3064. // TBIO-3889: Firefox Situ.on <input> results in a child of the <input>; Situ.before <input> results in platform inconsistencies
  3065. const name$1 = name(element);
  3066. if ('input' === name$1) {
  3067. return Situ.after(element);
  3068. }
  3069. else if (!contains$2(['br', 'img'], name$1)) {
  3070. return Situ.on(element, offset);
  3071. }
  3072. else {
  3073. return offset === 0 ? Situ.before(element) : Situ.after(element);
  3074. }
  3075. };
  3076. const preprocessRelative = (startSitu, finishSitu) => {
  3077. const start = startSitu.fold(Situ.before, beforeSpecial, Situ.after);
  3078. const finish = finishSitu.fold(Situ.before, beforeSpecial, Situ.after);
  3079. return SimSelection.relative(start, finish);
  3080. };
  3081. const preprocessExact = (start, soffset, finish, foffset) => {
  3082. const startSitu = beforeSpecial(start, soffset);
  3083. const finishSitu = beforeSpecial(finish, foffset);
  3084. return SimSelection.relative(startSitu, finishSitu);
  3085. };
  3086. const preprocess = (selection) => selection.match({
  3087. domRange: (rng) => {
  3088. const start = SugarElement.fromDom(rng.startContainer);
  3089. const finish = SugarElement.fromDom(rng.endContainer);
  3090. return preprocessExact(start, rng.startOffset, finish, rng.endOffset);
  3091. },
  3092. relative: preprocessRelative,
  3093. exact: preprocessExact
  3094. });
  3095. const toNative = (selection) => {
  3096. const win = SimSelection.getWin(selection).dom;
  3097. const getDomRange = (start, soffset, finish, foffset) => exactToNative(win, start, soffset, finish, foffset);
  3098. const filtered = preprocess(selection);
  3099. return diagnose(win, filtered).match({
  3100. ltr: getDomRange,
  3101. rtl: getDomRange
  3102. });
  3103. };
  3104. const getAtPoint = (win, x, y) => fromPoint$1(win, x, y);
  3105. const get$2 = (_win) => {
  3106. const win = _win === undefined ? window : _win;
  3107. if (detect$1().browser.isFirefox()) {
  3108. // TINY-7984: Firefox 91 is returning incorrect values for visualViewport.pageTop, so disable it for now
  3109. return Optional.none();
  3110. }
  3111. else {
  3112. return Optional.from(win.visualViewport);
  3113. }
  3114. };
  3115. const bounds = (x, y, width, height) => ({
  3116. x,
  3117. y,
  3118. width,
  3119. height,
  3120. right: x + width,
  3121. bottom: y + height
  3122. });
  3123. const getBounds = (_win) => {
  3124. const win = _win === undefined ? window : _win;
  3125. const doc = win.document;
  3126. const scroll = get$5(SugarElement.fromDom(doc));
  3127. return get$2(win).fold(() => {
  3128. const html = win.document.documentElement;
  3129. // Don't use window.innerWidth/innerHeight here, as we don't want to include scrollbars
  3130. // since the right/bottom position is based on the edge of the scrollbar not the window
  3131. const width = html.clientWidth;
  3132. const height = html.clientHeight;
  3133. return bounds(scroll.left, scroll.top, width, height);
  3134. }, (visualViewport) =>
  3135. // iOS doesn't update the pageTop/pageLeft when element.scrollIntoView() is called, so we need to fallback to the
  3136. // scroll position which will always be less than the page top/left values when page top/left are accurate/correct.
  3137. bounds(Math.max(visualViewport.pageLeft, scroll.left), Math.max(visualViewport.pageTop, scroll.top), visualViewport.width, visualViewport.height));
  3138. };
  3139. /**
  3140. * TreeWalker class enables you to walk the DOM in a linear manner.
  3141. *
  3142. * @class tinymce.dom.TreeWalker
  3143. * @example
  3144. * const walker = new tinymce.dom.TreeWalker(startNode);
  3145. *
  3146. * do {
  3147. * console.log(walker.current());
  3148. * } while (walker.next());
  3149. */
  3150. class DomTreeWalker {
  3151. constructor(startNode, rootNode) {
  3152. this.node = startNode;
  3153. this.rootNode = rootNode;
  3154. // This is a bit hacky but needed to ensure the 'this' variable
  3155. // always references the instance and not the caller scope
  3156. this.current = this.current.bind(this);
  3157. this.next = this.next.bind(this);
  3158. this.prev = this.prev.bind(this);
  3159. this.prev2 = this.prev2.bind(this);
  3160. }
  3161. /**
  3162. * Returns the current node.
  3163. *
  3164. * @method current
  3165. * @return {Node/undefined} Current node where the walker is, or undefined if the walker has reached the end.
  3166. */
  3167. current() {
  3168. return this.node;
  3169. }
  3170. /**
  3171. * Walks to the next node in tree.
  3172. *
  3173. * @method next
  3174. * @return {Node/undefined} Current node where the walker is after moving to the next node, or undefined if the walker has reached the end.
  3175. */
  3176. next(shallow) {
  3177. this.node = this.findSibling(this.node, 'firstChild', 'nextSibling', shallow);
  3178. return this.node;
  3179. }
  3180. /**
  3181. * Walks to the previous node in tree.
  3182. *
  3183. * @method prev
  3184. * @return {Node/undefined} Current node where the walker is after moving to the previous node, or undefined if the walker has reached the end.
  3185. */
  3186. prev(shallow) {
  3187. this.node = this.findSibling(this.node, 'lastChild', 'previousSibling', shallow);
  3188. return this.node;
  3189. }
  3190. prev2(shallow) {
  3191. this.node = this.findPreviousNode(this.node, shallow);
  3192. return this.node;
  3193. }
  3194. findSibling(node, startName, siblingName, shallow) {
  3195. if (node) {
  3196. // Walk into nodes if it has a start
  3197. if (!shallow && node[startName]) {
  3198. return node[startName];
  3199. }
  3200. // Return the sibling if it has one
  3201. if (node !== this.rootNode) {
  3202. let sibling = node[siblingName];
  3203. if (sibling) {
  3204. return sibling;
  3205. }
  3206. // Walk up the parents to look for siblings
  3207. for (let parent = node.parentNode; parent && parent !== this.rootNode; parent = parent.parentNode) {
  3208. sibling = parent[siblingName];
  3209. if (sibling) {
  3210. return sibling;
  3211. }
  3212. }
  3213. }
  3214. }
  3215. return undefined;
  3216. }
  3217. findPreviousNode(node, shallow) {
  3218. if (node) {
  3219. const sibling = node.previousSibling;
  3220. if (this.rootNode && sibling === this.rootNode) {
  3221. return;
  3222. }
  3223. if (sibling) {
  3224. if (!shallow) {
  3225. // Walk down to the most distant child
  3226. for (let child = sibling.lastChild; child; child = child.lastChild) {
  3227. if (!child.lastChild) {
  3228. return child;
  3229. }
  3230. }
  3231. }
  3232. return sibling;
  3233. }
  3234. const parent = node.parentNode;
  3235. if (parent && parent !== this.rootNode) {
  3236. return parent;
  3237. }
  3238. }
  3239. return undefined;
  3240. }
  3241. }
  3242. const whiteSpaceRegExp = /^[ \t\r\n]*$/;
  3243. const isWhitespaceText = (text) => whiteSpaceRegExp.test(text);
  3244. const isZwsp$1 = (text) => {
  3245. for (const c of text) {
  3246. if (!isZwsp$2(c)) {
  3247. return false;
  3248. }
  3249. }
  3250. return true;
  3251. };
  3252. // Don't compare other unicode spaces here, as we're only concerned about whitespace the browser would collapse
  3253. const isCollapsibleWhitespace$1 = (c) => ' \f\t\v'.indexOf(c) !== -1;
  3254. const isNewLineChar = (c) => c === '\n' || c === '\r';
  3255. const isNewline = (text, idx) => (idx < text.length && idx >= 0) ? isNewLineChar(text[idx]) : false;
  3256. // Converts duplicate whitespace to alternating space/nbsps and tabs to spaces
  3257. const normalize$4 = (text, tabSpaces = 4, isStartOfContent = true, isEndOfContent = true) => {
  3258. // Replace tabs with a variable amount of spaces
  3259. // Note: We don't use an actual tab character here, as it only works when in a "whitespace: pre" element,
  3260. // which will cause other issues, such as trying to type the content will also be treated as being in a pre.
  3261. const tabSpace = repeat(' ', tabSpaces);
  3262. const normalizedText = text.replace(/\t/g, tabSpace);
  3263. const result = foldl(normalizedText, (acc, c) => {
  3264. // Are we dealing with a char other than some collapsible whitespace or nbsp? if so then just use it as is
  3265. if (isCollapsibleWhitespace$1(c) || c === nbsp) {
  3266. // If the previous char is a space, we are at the start or end, or if the next char is a new line char, then we need
  3267. // to convert the space to a nbsp
  3268. if (acc.pcIsSpace || (acc.str === '' && isStartOfContent) || (acc.str.length === normalizedText.length - 1 && isEndOfContent) || isNewline(normalizedText, acc.str.length + 1)) {
  3269. return { pcIsSpace: false, str: acc.str + nbsp };
  3270. }
  3271. else {
  3272. return { pcIsSpace: true, str: acc.str + ' ' };
  3273. }
  3274. }
  3275. else {
  3276. // Treat newlines as being a space, since we'll need to convert any leading spaces to nsbps
  3277. return { pcIsSpace: isNewLineChar(c), str: acc.str + c };
  3278. }
  3279. }, { pcIsSpace: false, str: '' });
  3280. return result.str;
  3281. };
  3282. const isNodeType = (type) => {
  3283. return (node) => {
  3284. return !!node && node.nodeType === type;
  3285. };
  3286. };
  3287. // Firefox can allow you to get a selection on a restricted node, such as file/number inputs. These nodes
  3288. // won't implement the Object prototype, so Object.getPrototypeOf() will return null or something similar.
  3289. const isRestrictedNode = (node) => !!node && !Object.getPrototypeOf(node);
  3290. const isElement$7 = isNodeType(1);
  3291. const isHTMLElement = (node) => isElement$7(node) && isHTMLElement$1(SugarElement.fromDom(node));
  3292. const isSVGElement = (node) => isElement$7(node) && node.namespaceURI === 'http://www.w3.org/2000/svg';
  3293. const matchNodeName$1 = (name) => {
  3294. const lowerCasedName = name.toLowerCase();
  3295. return (node) => isNonNullable(node) && node.nodeName.toLowerCase() === lowerCasedName;
  3296. };
  3297. const matchNodeNames$1 = (names) => {
  3298. const lowerCasedNames = names.map((s) => s.toLowerCase());
  3299. return (node) => {
  3300. if (node && node.nodeName) {
  3301. const nodeName = node.nodeName.toLowerCase();
  3302. return contains$2(lowerCasedNames, nodeName);
  3303. }
  3304. return false;
  3305. };
  3306. };
  3307. const matchStyleValues = (name, values) => {
  3308. const items = values.toLowerCase().split(' ');
  3309. return (node) => {
  3310. if (isElement$7(node)) {
  3311. const win = node.ownerDocument.defaultView;
  3312. if (win) {
  3313. for (let i = 0; i < items.length; i++) {
  3314. const computed = win.getComputedStyle(node, null);
  3315. const cssValue = computed ? computed.getPropertyValue(name) : null;
  3316. if (cssValue === items[i]) {
  3317. return true;
  3318. }
  3319. }
  3320. }
  3321. }
  3322. return false;
  3323. };
  3324. };
  3325. const hasAttribute = (attrName) => {
  3326. return (node) => {
  3327. return isElement$7(node) && node.hasAttribute(attrName);
  3328. };
  3329. };
  3330. const isBogus$1 = (node) => isElement$7(node) && node.hasAttribute('data-mce-bogus');
  3331. const isBogusAll = (node) => isElement$7(node) && node.getAttribute('data-mce-bogus') === 'all';
  3332. const isTable$2 = (node) => isElement$7(node) && node.tagName === 'TABLE';
  3333. const hasContentEditableState = (value) => {
  3334. return (node) => {
  3335. if (isHTMLElement(node)) {
  3336. if (node.contentEditable === value) {
  3337. return true;
  3338. }
  3339. if (node.getAttribute('data-mce-contenteditable') === value) {
  3340. return true;
  3341. }
  3342. }
  3343. return false;
  3344. };
  3345. };
  3346. const isTextareaOrInput = matchNodeNames$1(['textarea', 'input']);
  3347. const isText$b = isNodeType(3);
  3348. const isCData = isNodeType(4);
  3349. const isPi = isNodeType(7);
  3350. const isComment = isNodeType(8);
  3351. const isDocument$1 = isNodeType(9);
  3352. const isDocumentFragment = isNodeType(11);
  3353. const isBr$7 = matchNodeName$1('br');
  3354. const isImg = matchNodeName$1('img');
  3355. const isAnchor = matchNodeName$1('a');
  3356. const isContentEditableTrue$3 = hasContentEditableState('true');
  3357. const isContentEditableFalse$a = hasContentEditableState('false');
  3358. const isEditingHost = (node) => isHTMLElement(node) && node.isContentEditable && isNonNullable(node.parentElement) && !node.parentElement.isContentEditable;
  3359. const isTableCell$3 = matchNodeNames$1(['td', 'th']);
  3360. const isTableCellOrCaption = matchNodeNames$1(['td', 'th', 'caption']);
  3361. const isTemplate = matchNodeName$1('template');
  3362. const isMedia$2 = matchNodeNames$1(['video', 'audio', 'object', 'embed']);
  3363. const isListItem$3 = matchNodeName$1('li');
  3364. const isDetails = matchNodeName$1('details');
  3365. const isSummary$1 = matchNodeName$1('summary');
  3366. const defaultOptionValues = {
  3367. skipBogus: true,
  3368. includeZwsp: false,
  3369. checkRootAsContent: false,
  3370. };
  3371. const hasWhitespacePreserveParent = (node, rootNode, schema) => {
  3372. const rootElement = SugarElement.fromDom(rootNode);
  3373. const startNode = SugarElement.fromDom(node);
  3374. const whitespaceElements = schema.getWhitespaceElements();
  3375. const predicate = (node) => has$2(whitespaceElements, name(node));
  3376. return ancestor$3(startNode, predicate, curry(eq, rootElement));
  3377. };
  3378. const isNamedAnchor = (node) => {
  3379. return isElement$7(node) && node.nodeName === 'A' && !node.hasAttribute('href') && (node.hasAttribute('name') || node.hasAttribute('id'));
  3380. };
  3381. const isNonEmptyElement$1 = (node, schema) => {
  3382. return isElement$7(node) && has$2(schema.getNonEmptyElements(), node.nodeName);
  3383. };
  3384. const isBookmark = hasAttribute('data-mce-bookmark');
  3385. const hasNonEditableParent = (node) => parentElement(SugarElement.fromDom(node)).exists((parent) => !isEditable$2(parent));
  3386. const isWhitespace$1 = (node, rootNode, schema) => isWhitespaceText(node.data)
  3387. && !hasWhitespacePreserveParent(node, rootNode, schema);
  3388. const isText$a = (node, rootNode, schema, options) => isText$b(node)
  3389. && !isWhitespace$1(node, rootNode, schema)
  3390. && (!options.includeZwsp || !isZwsp$1(node.data));
  3391. const isContentNode = (schema, node, rootNode, options) => {
  3392. return isFunction(options.isContent) && options.isContent(node)
  3393. || isNonEmptyElement$1(node, schema)
  3394. || isBookmark(node)
  3395. || isNamedAnchor(node)
  3396. || isText$a(node, rootNode, schema, options)
  3397. || isContentEditableFalse$a(node)
  3398. || isContentEditableTrue$3(node) && hasNonEditableParent(node);
  3399. };
  3400. const isEmptyNode = (schema, targetNode, opts) => {
  3401. const options = { ...defaultOptionValues, ...opts };
  3402. if (options.checkRootAsContent) {
  3403. if (isContentNode(schema, targetNode, targetNode, options)) {
  3404. return false;
  3405. }
  3406. }
  3407. let node = targetNode.firstChild;
  3408. let brCount = 0;
  3409. if (!node) {
  3410. return true;
  3411. }
  3412. const walker = new DomTreeWalker(node, targetNode);
  3413. do {
  3414. if (options.skipBogus && isElement$7(node)) {
  3415. const bogusValue = node.getAttribute('data-mce-bogus');
  3416. if (bogusValue) {
  3417. node = walker.next(bogusValue === 'all');
  3418. continue;
  3419. }
  3420. }
  3421. if (isComment(node)) {
  3422. node = walker.next(true);
  3423. continue;
  3424. }
  3425. if (isBr$7(node)) {
  3426. brCount++;
  3427. node = walker.next();
  3428. continue;
  3429. }
  3430. if (isContentNode(schema, node, targetNode, options)) {
  3431. return false;
  3432. }
  3433. node = walker.next();
  3434. } while (node);
  3435. return brCount <= 1;
  3436. };
  3437. const isEmpty$4 = (schema, elm, options) => {
  3438. return isEmptyNode(schema, elm.dom, { checkRootAsContent: true, ...options });
  3439. };
  3440. const isContent$1 = (schema, node, options) => {
  3441. return isContentNode(schema, node, node, { includeZwsp: defaultOptionValues.includeZwsp, ...options });
  3442. };
  3443. const nodeNameToNamespaceType = (name) => {
  3444. const lowerCaseName = name.toLowerCase();
  3445. if (lowerCaseName === 'svg') {
  3446. return 'svg';
  3447. }
  3448. else if (lowerCaseName === 'math') {
  3449. return 'math';
  3450. }
  3451. else {
  3452. return 'html';
  3453. }
  3454. };
  3455. const isNonHtmlElementRootName = (name) => nodeNameToNamespaceType(name) !== 'html';
  3456. const isNonHtmlElementRoot = (node) => isNonHtmlElementRootName(node.nodeName);
  3457. const toScopeType = (node) => nodeNameToNamespaceType(node.nodeName);
  3458. const namespaceElements = ['svg', 'math'];
  3459. const createNamespaceTracker = () => {
  3460. const currentScope = value$1();
  3461. const current = () => currentScope.get().map(toScopeType).getOr('html');
  3462. const track = (node) => {
  3463. if (isNonHtmlElementRoot(node)) {
  3464. currentScope.set(node);
  3465. }
  3466. else if (currentScope.get().exists((scopeNode) => !scopeNode.contains(node))) {
  3467. currentScope.clear();
  3468. }
  3469. return current();
  3470. };
  3471. const reset = () => {
  3472. currentScope.clear();
  3473. };
  3474. return {
  3475. track,
  3476. current,
  3477. reset
  3478. };
  3479. };
  3480. const transparentBlockAttr = 'data-mce-block';
  3481. // Returns the lowercase element names form a SchemaMap by excluding anyone that has uppercase letters.
  3482. // This method is to avoid having to specify all possible valid characters other than lowercase a-z such as '-' or ':' etc.
  3483. const elementNames = (map) => filter$5(keys(map), (key) => !/[A-Z]/.test(key));
  3484. const makeSelectorFromSchemaMap = (map) => map$3(elementNames(map), (name) => {
  3485. // Exclude namespace elements from processing
  3486. const escapedName = CSS.escape(name);
  3487. return `${escapedName}:` + map$3(namespaceElements, (ns) => `not(${ns} ${escapedName})`).join(':');
  3488. }).join(',');
  3489. const updateTransparent = (blocksSelector, transparent) => {
  3490. if (isNonNullable(transparent.querySelector(blocksSelector))) {
  3491. transparent.setAttribute(transparentBlockAttr, 'true');
  3492. if (transparent.getAttribute('data-mce-selected') === 'inline-boundary') {
  3493. transparent.removeAttribute('data-mce-selected');
  3494. }
  3495. return true;
  3496. }
  3497. else {
  3498. transparent.removeAttribute(transparentBlockAttr);
  3499. return false;
  3500. }
  3501. };
  3502. const updateBlockStateOnChildren = (schema, scope) => {
  3503. const transparentSelector = makeSelectorFromSchemaMap(schema.getTransparentElements());
  3504. const blocksSelector = makeSelectorFromSchemaMap(schema.getBlockElements());
  3505. return filter$5(scope.querySelectorAll(transparentSelector), (transparent) => updateTransparent(blocksSelector, transparent));
  3506. };
  3507. const trimEdge = (schema, el, leftSide) => {
  3508. var _a;
  3509. const childPropertyName = leftSide ? 'lastChild' : 'firstChild';
  3510. for (let child = el[childPropertyName]; child; child = child[childPropertyName]) {
  3511. if (isEmptyNode(schema, child, { checkRootAsContent: true })) {
  3512. (_a = child.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(child);
  3513. return;
  3514. }
  3515. }
  3516. };
  3517. const split$2 = (schema, parentElm, splitElm) => {
  3518. const range = document.createRange();
  3519. const parentNode = parentElm.parentNode;
  3520. if (parentNode) {
  3521. range.setStartBefore(parentElm);
  3522. range.setEndBefore(splitElm);
  3523. const beforeFragment = range.extractContents();
  3524. trimEdge(schema, beforeFragment, true);
  3525. range.setStartAfter(splitElm);
  3526. range.setEndAfter(parentElm);
  3527. const afterFragment = range.extractContents();
  3528. trimEdge(schema, afterFragment, false);
  3529. if (!isEmptyNode(schema, beforeFragment, { checkRootAsContent: true })) {
  3530. parentNode.insertBefore(beforeFragment, parentElm);
  3531. }
  3532. if (!isEmptyNode(schema, splitElm, { checkRootAsContent: true })) {
  3533. parentNode.insertBefore(splitElm, parentElm);
  3534. }
  3535. if (!isEmptyNode(schema, afterFragment, { checkRootAsContent: true })) {
  3536. parentNode.insertBefore(afterFragment, parentElm);
  3537. }
  3538. parentNode.removeChild(parentElm);
  3539. }
  3540. };
  3541. // This will find invalid blocks wrapped in anchors and split them out so for example
  3542. // <h1><a href="#"><h2>x</h2></a></h1> will find that h2 is invalid inside the H1 and split that out.
  3543. // This is a simplistic apporach so it's likely not covering all the cases but it's a start.
  3544. const splitInvalidChildren = (schema, scope, transparentBlocks) => {
  3545. const blocksElements = schema.getBlockElements();
  3546. const rootNode = SugarElement.fromDom(scope);
  3547. const isBlock = (el) => name(el) in blocksElements;
  3548. const isRoot = (el) => eq(el, rootNode);
  3549. each$e(fromDom$1(transparentBlocks), (transparentBlock) => {
  3550. ancestor$5(transparentBlock, isBlock, isRoot).each((parentBlock) => {
  3551. const invalidChildren = children(transparentBlock, (el) => isBlock(el) && !schema.isValidChild(name(parentBlock), name(el)));
  3552. if (invalidChildren.length > 0) {
  3553. const stateScope = parentElement(parentBlock);
  3554. each$e(invalidChildren, (child) => {
  3555. ancestor$5(child, isBlock, isRoot).each((parentBlock) => {
  3556. split$2(schema, parentBlock.dom, child.dom);
  3557. });
  3558. });
  3559. stateScope.each((scope) => updateBlockStateOnChildren(schema, scope.dom));
  3560. }
  3561. });
  3562. });
  3563. };
  3564. const unwrapInvalidChildren = (schema, scope, transparentBlocks) => {
  3565. each$e([...transparentBlocks, ...(isTransparentBlock(schema, scope) ? [scope] : [])], (block) => each$e(descendants(SugarElement.fromDom(block), block.nodeName.toLowerCase()), (elm) => {
  3566. if (isTransparentInline(schema, elm.dom)) {
  3567. unwrap(elm);
  3568. }
  3569. }));
  3570. };
  3571. const updateChildren = (schema, scope) => {
  3572. const transparentBlocks = updateBlockStateOnChildren(schema, scope);
  3573. splitInvalidChildren(schema, scope, transparentBlocks);
  3574. unwrapInvalidChildren(schema, scope, transparentBlocks);
  3575. };
  3576. const updateElement = (schema, target) => {
  3577. if (isTransparentElement(schema, target)) {
  3578. const blocksSelector = makeSelectorFromSchemaMap(schema.getBlockElements());
  3579. updateTransparent(blocksSelector, target);
  3580. }
  3581. };
  3582. const updateCaret = (schema, root, caretParent) => {
  3583. const isRoot = (el) => eq(el, SugarElement.fromDom(root));
  3584. const parents = parents$1(SugarElement.fromDom(caretParent), isRoot);
  3585. // Check the element just above below the root so in if caretParent is I in this
  3586. // case <body><p><b><i>|</i></b></p></body> it would use the P as the scope
  3587. get$b(parents, parents.length - 2).filter(isElement$8).fold(() => updateChildren(schema, root), (scope) => updateChildren(schema, scope.dom));
  3588. };
  3589. const hasBlockAttr = (el) => el.hasAttribute(transparentBlockAttr);
  3590. const isTransparentElementName = (schema, name) => has$2(schema.getTransparentElements(), name);
  3591. const isTransparentElement = (schema, node) => isElement$7(node) && isTransparentElementName(schema, node.nodeName);
  3592. const isTransparentBlock = (schema, node) => isTransparentElement(schema, node) && hasBlockAttr(node);
  3593. const isTransparentInline = (schema, node) => isTransparentElement(schema, node) && !hasBlockAttr(node);
  3594. const isTransparentAstBlock = (schema, node) => node.type === 1 && isTransparentElementName(schema, node.name) && isString(node.attr(transparentBlockAttr));
  3595. const browser$2 = detect$1().browser;
  3596. const firstElement = (nodes) => find$2(nodes, isElement$8);
  3597. // Firefox has a bug where caption height is not included correctly in offset calculations of tables
  3598. // this tries to compensate for that by detecting if that offsets are incorrect and then remove the height
  3599. const getTableCaptionDeltaY = (elm) => {
  3600. if (browser$2.isFirefox() && name(elm) === 'table') {
  3601. return firstElement(children$1(elm)).filter((elm) => {
  3602. return name(elm) === 'caption';
  3603. }).bind((caption) => {
  3604. return firstElement(nextSiblings(caption)).map((body) => {
  3605. const bodyTop = body.dom.offsetTop;
  3606. const captionTop = caption.dom.offsetTop;
  3607. const captionHeight = caption.dom.offsetHeight;
  3608. return bodyTop <= captionTop ? -captionHeight : 0;
  3609. });
  3610. }).getOr(0);
  3611. }
  3612. else {
  3613. return 0;
  3614. }
  3615. };
  3616. const hasChild = (elm, child) => elm.children && contains$2(elm.children, child);
  3617. const getPos = (body, elm, rootElm) => {
  3618. let x = 0, y = 0;
  3619. const doc = body.ownerDocument;
  3620. rootElm = rootElm ? rootElm : body;
  3621. if (elm) {
  3622. // Use getBoundingClientRect if it exists since it's faster than looping offset nodes
  3623. // Fallback to offsetParent calculations if the body isn't static better since it stops at the body root
  3624. if (rootElm === body && elm.getBoundingClientRect && get$7(SugarElement.fromDom(body), 'position') === 'static') {
  3625. const pos = elm.getBoundingClientRect();
  3626. // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit
  3627. // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position
  3628. x = pos.left + (doc.documentElement.scrollLeft || body.scrollLeft) - doc.documentElement.clientLeft;
  3629. y = pos.top + (doc.documentElement.scrollTop || body.scrollTop) - doc.documentElement.clientTop;
  3630. return { x, y };
  3631. }
  3632. let offsetParent = elm;
  3633. while (offsetParent && offsetParent !== rootElm && offsetParent.nodeType && !hasChild(offsetParent, rootElm)) {
  3634. const castOffsetParent = offsetParent;
  3635. x += castOffsetParent.offsetLeft || 0;
  3636. y += castOffsetParent.offsetTop || 0;
  3637. offsetParent = castOffsetParent.offsetParent;
  3638. }
  3639. offsetParent = elm.parentNode;
  3640. while (offsetParent && offsetParent !== rootElm && offsetParent.nodeType && !hasChild(offsetParent, rootElm)) {
  3641. x -= offsetParent.scrollLeft || 0;
  3642. y -= offsetParent.scrollTop || 0;
  3643. offsetParent = offsetParent.parentNode;
  3644. }
  3645. y += getTableCaptionDeltaY(SugarElement.fromDom(elm));
  3646. }
  3647. return { x, y };
  3648. };
  3649. const getCrossOrigin$1 = (url, settings) => {
  3650. const crossOriginFn = settings.crossOrigin;
  3651. if (settings.contentCssCors) {
  3652. return 'anonymous';
  3653. }
  3654. else if (isFunction(crossOriginFn)) {
  3655. return crossOriginFn(url);
  3656. }
  3657. else {
  3658. return undefined;
  3659. }
  3660. };
  3661. const StyleSheetLoader = (documentOrShadowRoot, settings = {}) => {
  3662. let idCount = 0;
  3663. const loadedStates = {};
  3664. const edos = SugarElement.fromDom(documentOrShadowRoot);
  3665. const doc = documentOrOwner(edos);
  3666. const _setReferrerPolicy = (referrerPolicy) => {
  3667. settings.referrerPolicy = referrerPolicy;
  3668. };
  3669. const _setContentCssCors = (contentCssCors) => {
  3670. settings.contentCssCors = contentCssCors;
  3671. };
  3672. const _setCrossOrigin = (crossOrigin) => {
  3673. settings.crossOrigin = crossOrigin;
  3674. };
  3675. const addStyle = (element) => {
  3676. append$1(getStyleContainer(edos), element);
  3677. };
  3678. const removeStyle = (id) => {
  3679. const styleContainer = getStyleContainer(edos);
  3680. descendant$1(styleContainer, '#' + id).each(remove$8);
  3681. };
  3682. const getOrCreateState = (url) => get$a(loadedStates, url).getOrThunk(() => ({
  3683. id: 'mce-u' + (idCount++),
  3684. passed: [],
  3685. failed: [],
  3686. count: 0
  3687. }));
  3688. /**
  3689. * Loads the specified CSS file and returns a Promise that will resolve when the stylesheet is loaded successfully or reject if it failed to load.
  3690. *
  3691. * @method load
  3692. * @param {String} url Url to be loaded.
  3693. * @return {Promise} A Promise that will resolve or reject when the stylesheet is loaded.
  3694. */
  3695. const load = (url) => new Promise((success, failure) => {
  3696. let link;
  3697. const urlWithSuffix = Tools._addCacheSuffix(url);
  3698. const state = getOrCreateState(urlWithSuffix);
  3699. loadedStates[urlWithSuffix] = state;
  3700. state.count++;
  3701. const resolve = (callbacks, status) => {
  3702. each$e(callbacks, call);
  3703. state.status = status;
  3704. state.passed = [];
  3705. state.failed = [];
  3706. if (link) {
  3707. link.onload = null;
  3708. link.onerror = null;
  3709. link = null;
  3710. }
  3711. };
  3712. const passed = () => resolve(state.passed, 2);
  3713. const failed = () => resolve(state.failed, 3);
  3714. if (success) {
  3715. state.passed.push(success);
  3716. }
  3717. if (failure) {
  3718. state.failed.push(failure);
  3719. }
  3720. // Is loading wait for it to pass
  3721. if (state.status === 1) {
  3722. return;
  3723. }
  3724. // Has finished loading and was success
  3725. if (state.status === 2) {
  3726. passed();
  3727. return;
  3728. }
  3729. // Has finished loading and was a failure
  3730. if (state.status === 3) {
  3731. failed();
  3732. return;
  3733. }
  3734. // Start loading
  3735. state.status = 1;
  3736. const linkElem = SugarElement.fromTag('link', doc.dom);
  3737. setAll$1(linkElem, {
  3738. rel: 'stylesheet',
  3739. type: 'text/css',
  3740. id: state.id
  3741. });
  3742. const crossorigin = getCrossOrigin$1(url, settings);
  3743. if (crossorigin !== undefined) {
  3744. set$4(linkElem, 'crossOrigin', crossorigin);
  3745. }
  3746. if (settings.referrerPolicy) {
  3747. // Note: Don't use link.referrerPolicy = ... here as it doesn't work on Safari
  3748. set$4(linkElem, 'referrerpolicy', settings.referrerPolicy);
  3749. }
  3750. link = linkElem.dom;
  3751. link.onload = passed;
  3752. link.onerror = failed;
  3753. addStyle(linkElem);
  3754. set$4(linkElem, 'href', urlWithSuffix);
  3755. });
  3756. /**
  3757. * Loads the specified css string in as a style element with an unique key.
  3758. *
  3759. * @method loadRawCss
  3760. * @param {String} key Unique key for the style element.
  3761. * @param {String} css Css style content to add.
  3762. */
  3763. const loadRawCss = (key, css) => {
  3764. const state = getOrCreateState(key);
  3765. loadedStates[key] = state;
  3766. state.count++;
  3767. // Start loading
  3768. const styleElem = SugarElement.fromTag('style', doc.dom);
  3769. setAll$1(styleElem, {
  3770. 'rel': 'stylesheet',
  3771. 'type': 'text/css',
  3772. 'id': state.id,
  3773. 'data-mce-key': key
  3774. });
  3775. styleElem.dom.innerHTML = css;
  3776. addStyle(styleElem);
  3777. };
  3778. /**
  3779. * Loads the specified CSS files and returns a Promise that is resolved when all stylesheets are loaded or rejected if any failed to load.
  3780. *
  3781. * @method loadAll
  3782. * @param {Array} urls URLs to be loaded.
  3783. * @return {Promise} A Promise that will resolve or reject when all stylesheets are loaded.
  3784. */
  3785. const loadAll = (urls) => {
  3786. const loadedUrls = Promise.allSettled(map$3(urls, (url) => load(url).then(constant(url))));
  3787. return loadedUrls.then((results) => {
  3788. const parts = partition$2(results, (r) => r.status === 'fulfilled');
  3789. if (parts.fail.length > 0) {
  3790. return Promise.reject(map$3(parts.fail, (result) => result.reason));
  3791. }
  3792. else {
  3793. return map$3(parts.pass, (result) => result.value);
  3794. }
  3795. });
  3796. };
  3797. /**
  3798. * Unloads the specified CSS file if no resources currently depend on it.
  3799. *
  3800. * @method unload
  3801. * @param {String} url URL to unload or remove.
  3802. */
  3803. const unload = (url) => {
  3804. const urlWithSuffix = Tools._addCacheSuffix(url);
  3805. get$a(loadedStates, urlWithSuffix).each((state) => {
  3806. const count = --state.count;
  3807. if (count === 0) {
  3808. delete loadedStates[urlWithSuffix];
  3809. removeStyle(state.id);
  3810. }
  3811. });
  3812. };
  3813. /**
  3814. * Unloads the specified CSS style element by key.
  3815. *
  3816. * @method unloadRawCss
  3817. * @param {String} key Key of CSS style resource to unload.
  3818. */
  3819. const unloadRawCss = (key) => {
  3820. get$a(loadedStates, key).each((state) => {
  3821. const count = --state.count;
  3822. if (count === 0) {
  3823. delete loadedStates[key];
  3824. removeStyle(state.id);
  3825. }
  3826. });
  3827. };
  3828. /**
  3829. * Unloads each specified CSS file if no resources currently depend on it.
  3830. *
  3831. * @method unloadAll
  3832. * @param {Array} urls URLs to unload or remove.
  3833. */
  3834. const unloadAll = (urls) => {
  3835. each$e(urls, (url) => {
  3836. unload(url);
  3837. });
  3838. };
  3839. return {
  3840. load,
  3841. loadRawCss,
  3842. loadAll,
  3843. unload,
  3844. unloadRawCss,
  3845. unloadAll,
  3846. _setReferrerPolicy,
  3847. _setContentCssCors,
  3848. _setCrossOrigin
  3849. };
  3850. };
  3851. /**
  3852. * This function is exported for testing purposes only - please use StyleSheetLoader.instance in production code.
  3853. */
  3854. const create$b = () => {
  3855. const map = new WeakMap();
  3856. const forElement = (referenceElement, settings) => {
  3857. const root = getRootNode(referenceElement);
  3858. const rootDom = root.dom;
  3859. return Optional.from(map.get(rootDom)).getOrThunk(() => {
  3860. const sl = StyleSheetLoader(rootDom, settings);
  3861. map.set(rootDom, sl);
  3862. return sl;
  3863. });
  3864. };
  3865. return {
  3866. forElement
  3867. };
  3868. };
  3869. const instance = create$b();
  3870. const isSpan = (node) => node.nodeName.toLowerCase() === 'span';
  3871. const isInlineContent = (node, schema) => isNonNullable(node) && (isContent$1(schema, node) || schema.isInline(node.nodeName.toLowerCase()));
  3872. const surroundedByInlineContent = (node, root, schema) => {
  3873. const prev = new DomTreeWalker(node, root).prev(false);
  3874. const next = new DomTreeWalker(node, root).next(false);
  3875. // Check if the next/previous is either inline content or the start/end (eg is undefined)
  3876. const prevIsInline = isUndefined(prev) || isInlineContent(prev, schema);
  3877. const nextIsInline = isUndefined(next) || isInlineContent(next, schema);
  3878. return prevIsInline && nextIsInline;
  3879. };
  3880. const isBookmarkNode$2 = (node) => isSpan(node) && node.getAttribute('data-mce-type') === 'bookmark';
  3881. // Keep text nodes with only spaces if surrounded by spans.
  3882. // eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b
  3883. const isKeepTextNode = (node, root, schema) => isText$b(node) && node.data.length > 0 && surroundedByInlineContent(node, root, schema);
  3884. // Keep elements as long as they have any children
  3885. const isKeepElement = (node) => isElement$7(node) ? node.childNodes.length > 0 : false;
  3886. const isDocument = (node) => isDocumentFragment(node) || isDocument$1(node);
  3887. // W3C valid browsers tend to leave empty nodes to the left/right side of the contents - this makes sense
  3888. // but we don't want that in our code since it serves no purpose for the end user
  3889. // For example splitting this html at the bold element:
  3890. // <p>text 1<span><b>CHOP</b></span>text 2</p>
  3891. // would produce:
  3892. // <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p>
  3893. // this function will then trim off empty edges and produce:
  3894. // <p>text 1</p><b>CHOP</b><p>text 2</p>
  3895. const trimNode = (dom, node, schema, root) => {
  3896. var _a;
  3897. const rootNode = root || node;
  3898. if (isElement$7(node) && isBookmarkNode$2(node)) {
  3899. return node;
  3900. }
  3901. const children = node.childNodes;
  3902. for (let i = children.length - 1; i >= 0; i--) {
  3903. trimNode(dom, children[i], schema, rootNode);
  3904. }
  3905. // If the only child is a bookmark then move it up
  3906. if (isElement$7(node)) {
  3907. const currentChildren = node.childNodes;
  3908. if (currentChildren.length === 1 && isBookmarkNode$2(currentChildren[0])) {
  3909. (_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(currentChildren[0], node);
  3910. }
  3911. }
  3912. // Remove any empty nodes
  3913. if (!isDocument(node) && !isContent$1(schema, node) && !isKeepElement(node) && !isKeepTextNode(node, rootNode, schema)) {
  3914. dom.remove(node);
  3915. }
  3916. return node;
  3917. };
  3918. /**
  3919. * Entity encoder class.
  3920. *
  3921. * @class tinymce.html.Entities
  3922. * @static
  3923. * @version 3.4
  3924. */
  3925. const makeMap$3 = Tools.makeMap;
  3926. const attrsCharsRegExp = /[&<>\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
  3927. const textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
  3928. const rawCharsRegExp = /[<>&\"\']/g;
  3929. const entityRegExp = /&#([a-z0-9]+);?|&([a-z0-9]+);/gi;
  3930. const asciiMap = {
  3931. 128: '\u20AC', 130: '\u201A', 131: '\u0192', 132: '\u201E', 133: '\u2026', 134: '\u2020',
  3932. 135: '\u2021', 136: '\u02C6', 137: '\u2030', 138: '\u0160', 139: '\u2039', 140: '\u0152',
  3933. 142: '\u017D', 145: '\u2018', 146: '\u2019', 147: '\u201C', 148: '\u201D', 149: '\u2022',
  3934. 150: '\u2013', 151: '\u2014', 152: '\u02DC', 153: '\u2122', 154: '\u0161', 155: '\u203A',
  3935. 156: '\u0153', 158: '\u017E', 159: '\u0178'
  3936. };
  3937. // Raw entities
  3938. const baseEntities = {
  3939. '\"': '&quot;', // Needs to be escaped since the YUI compressor would otherwise break the code
  3940. '\'': '&#39;',
  3941. '<': '&lt;',
  3942. '>': '&gt;',
  3943. '&': '&amp;',
  3944. '\u0060': '&#96;'
  3945. };
  3946. // Reverse lookup table for raw entities
  3947. const reverseEntities = {
  3948. '&lt;': '<',
  3949. '&gt;': '>',
  3950. '&amp;': '&',
  3951. '&quot;': '"',
  3952. '&apos;': `'`
  3953. };
  3954. // Decodes text by using the browser
  3955. const nativeDecode = (text) => {
  3956. const elm = SugarElement.fromTag('div').dom;
  3957. elm.innerHTML = text;
  3958. return elm.textContent || elm.innerText || text;
  3959. };
  3960. // Build a two way lookup table for the entities
  3961. const buildEntitiesLookup = (items, radix) => {
  3962. const lookup = {};
  3963. if (items) {
  3964. const itemList = items.split(',');
  3965. radix = radix || 10;
  3966. // Build entities lookup table
  3967. for (let i = 0; i < itemList.length; i += 2) {
  3968. const chr = String.fromCharCode(parseInt(itemList[i], radix));
  3969. // Only add non base entities
  3970. if (!baseEntities[chr]) {
  3971. const entity = '&' + itemList[i + 1] + ';';
  3972. lookup[chr] = entity;
  3973. lookup[entity] = chr;
  3974. }
  3975. }
  3976. return lookup;
  3977. }
  3978. else {
  3979. return undefined;
  3980. }
  3981. };
  3982. // Unpack entities lookup where the numbers are in radix 32 to reduce the size
  3983. const namedEntities = buildEntitiesLookup('50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' +
  3984. '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' +
  3985. '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' +
  3986. '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' +
  3987. '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' +
  3988. '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' +
  3989. '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' +
  3990. '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' +
  3991. '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' +
  3992. '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' +
  3993. 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' +
  3994. 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' +
  3995. 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' +
  3996. 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' +
  3997. 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' +
  3998. '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' +
  3999. '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' +
  4000. '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' +
  4001. '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' +
  4002. '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' +
  4003. 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' +
  4004. 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' +
  4005. 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' +
  4006. '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' +
  4007. '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32);
  4008. /**
  4009. * Encodes the specified string using raw entities. This means only the required XML base entities will be encoded.
  4010. *
  4011. * @method encodeRaw
  4012. * @param {String} text Text to encode.
  4013. * @param {Boolean} attr Optional flag to specify if the text is attribute contents.
  4014. * @return {String} Entity encoded text.
  4015. */
  4016. const encodeRaw = (text, attr) => text.replace(attr ? attrsCharsRegExp : textCharsRegExp, (chr) => {
  4017. return baseEntities[chr] || chr;
  4018. });
  4019. /**
  4020. * Encoded the specified text with both the attributes and text entities. This function will produce larger text contents
  4021. * since it doesn't know if the context is within an attribute or text node. This was added for compatibility
  4022. * and is exposed as the `DOMUtils.encode` function.
  4023. *
  4024. * @method encodeAllRaw
  4025. * @param {String} text Text to encode.
  4026. * @return {String} Entity encoded text.
  4027. */
  4028. const encodeAllRaw = (text) => ('' + text).replace(rawCharsRegExp, (chr) => {
  4029. return baseEntities[chr] || chr;
  4030. });
  4031. /**
  4032. * Encodes the specified string using numeric entities. The core entities will be
  4033. * encoded as named ones but all non lower ascii characters will be encoded into numeric entities.
  4034. *
  4035. * @method encodeNumeric
  4036. * @param {String} text Text to encode.
  4037. * @param {Boolean} attr Optional flag to specify if the text is attribute contents.
  4038. * @return {String} Entity encoded text.
  4039. */
  4040. const encodeNumeric = (text, attr) => text.replace(attr ? attrsCharsRegExp : textCharsRegExp, (chr) => {
  4041. // Multi byte sequence convert it to a single entity
  4042. if (chr.length > 1) {
  4043. return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';';
  4044. }
  4045. return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';';
  4046. });
  4047. /**
  4048. * Encodes the specified string using named entities. The core entities will be encoded
  4049. * as named ones but all non lower ascii characters will be encoded into named entities.
  4050. *
  4051. * @method encodeNamed
  4052. * @param {String} text Text to encode.
  4053. * @param {Boolean} attr Optional flag to specify if the text is attribute contents.
  4054. * @param {Object} entities Optional parameter with entities to use.
  4055. * @return {String} Entity encoded text.
  4056. */
  4057. const encodeNamed = (text, attr, entities) => {
  4058. const resolveEntities = entities || namedEntities;
  4059. return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, (chr) => {
  4060. return baseEntities[chr] || resolveEntities[chr] || chr;
  4061. });
  4062. };
  4063. /**
  4064. * Returns an encode function based on the name(s) and it's optional entities.
  4065. *
  4066. * @method getEncodeFunc
  4067. * @param {String} name Comma separated list of encoders for example named,numeric.
  4068. * @param {String} entities Optional parameter with entities to use instead of the built in set.
  4069. * @return {Function} Encode function to be used.
  4070. */
  4071. const getEncodeFunc = (name, entities) => {
  4072. const entitiesMap = buildEntitiesLookup(entities) || namedEntities;
  4073. const encodeNamedAndNumeric = (text, attr) => text.replace(attr ? attrsCharsRegExp : textCharsRegExp, (chr) => {
  4074. if (baseEntities[chr] !== undefined) {
  4075. return baseEntities[chr];
  4076. }
  4077. if (entitiesMap[chr] !== undefined) {
  4078. return entitiesMap[chr];
  4079. }
  4080. // Convert multi-byte sequences to a single entity.
  4081. if (chr.length > 1) {
  4082. return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';';
  4083. }
  4084. return '&#' + chr.charCodeAt(0) + ';';
  4085. });
  4086. const encodeCustomNamed = (text, attr) => {
  4087. return encodeNamed(text, attr, entitiesMap);
  4088. };
  4089. // Replace + with , to be compatible with previous TinyMCE versions
  4090. const nameMap = makeMap$3(name.replace(/\+/g, ','));
  4091. // Named and numeric encoder
  4092. if (nameMap.named && nameMap.numeric) {
  4093. return encodeNamedAndNumeric;
  4094. }
  4095. // Named encoder
  4096. if (nameMap.named) {
  4097. // Custom names
  4098. if (entities) {
  4099. return encodeCustomNamed;
  4100. }
  4101. return encodeNamed;
  4102. }
  4103. // Numeric
  4104. if (nameMap.numeric) {
  4105. return encodeNumeric;
  4106. }
  4107. // Raw encoder
  4108. return encodeRaw;
  4109. };
  4110. /**
  4111. * Decodes the specified string, this will replace entities with raw UTF characters.
  4112. *
  4113. * @method decode
  4114. * @param {String} text Text to entity decode.
  4115. * @return {String} Entity decoded string.
  4116. */
  4117. const decode = (text) => text.replace(entityRegExp, (all, numeric) => {
  4118. if (numeric) {
  4119. if (numeric.charAt(0).toLowerCase() === 'x') {
  4120. numeric = parseInt(numeric.substr(1), 16);
  4121. }
  4122. else {
  4123. numeric = parseInt(numeric, 10);
  4124. }
  4125. // Support upper UTF
  4126. if (numeric > 0xFFFF) {
  4127. numeric -= 0x10000;
  4128. // eslint-disable-next-line no-bitwise
  4129. return String.fromCharCode(0xD800 + (numeric >> 10), 0xDC00 + (numeric & 0x3FF));
  4130. }
  4131. return asciiMap[numeric] || String.fromCharCode(numeric);
  4132. }
  4133. return reverseEntities[all] || namedEntities[all] || nativeDecode(all);
  4134. });
  4135. const Entities = {
  4136. encodeRaw,
  4137. encodeAllRaw,
  4138. encodeNumeric,
  4139. encodeNamed,
  4140. getEncodeFunc,
  4141. decode
  4142. };
  4143. const split$1 = (items, delim) => {
  4144. items = Tools.trim(items);
  4145. return items ? items.split(delim || ' ') : [];
  4146. };
  4147. // Converts a wildcard expression string to a regexp for example *a will become /.*a/.
  4148. const patternToRegExp = (str) => new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$');
  4149. const isRegExp$1 = (obj) => isObject(obj) && obj.source && Object.prototype.toString.call(obj) === '[object RegExp]';
  4150. const deepCloneElementRule = (obj) => {
  4151. const helper = (value) => {
  4152. if (isArray$1(value)) {
  4153. return map$3(value, helper);
  4154. }
  4155. else if (isRegExp$1(value)) {
  4156. return new RegExp(value.source, value.flags);
  4157. }
  4158. else if (isObject(value)) {
  4159. return map$2(value, helper);
  4160. }
  4161. else {
  4162. return value;
  4163. }
  4164. };
  4165. return helper(obj);
  4166. };
  4167. const parseCustomElementsRules = (value) => {
  4168. const customElementRegExp = /^(~)?(.+)$/;
  4169. return bind$3(split$1(value, ','), (rule) => {
  4170. const matches = customElementRegExp.exec(rule);
  4171. if (matches) {
  4172. const inline = matches[1] === '~';
  4173. const cloneName = inline ? 'span' : 'div';
  4174. const name = matches[2];
  4175. return [{ cloneName, name }];
  4176. }
  4177. else {
  4178. return [];
  4179. }
  4180. });
  4181. };
  4182. const getGlobalAttributeSet = (type) => {
  4183. return Object.freeze([
  4184. // Present on all schema types
  4185. 'id',
  4186. 'accesskey',
  4187. 'class',
  4188. 'dir',
  4189. 'lang',
  4190. 'style',
  4191. 'tabindex',
  4192. 'title',
  4193. 'role',
  4194. // html5 and html5-strict extra attributes
  4195. ...(type !== 'html4' ? ['contenteditable', 'contextmenu', 'draggable', 'dropzone', 'hidden', 'spellcheck', 'translate', 'itemprop', 'itemscope', 'itemtype'] : []),
  4196. // html4 and html5 extra attributes
  4197. ...(type !== 'html5-strict' ? ['xml:lang'] : [])
  4198. ]);
  4199. };
  4200. // Missing elements in `phrasing` compared to HTML5 spec at 2024-01-30 (timestamped since spec is constantly evolving)
  4201. // area - required to be inside a map element so we should not add it to all elements.
  4202. // link - required to be in the body so we should not add it to all elements.
  4203. // math - currently not supported.
  4204. // meta - Only allowed if the `itemprop` attribute is set so very special.
  4205. // slot - We only want these to be accepted in registered custom components.
  4206. // Extra element in `phrasing`: command keygen
  4207. //
  4208. // Missing elements in `flow` compared to HTML5 spec at 2034-01-30 (timestamped since the spec is constantly evolving)
  4209. // search - Can be both in a block and inline position but is not a transparent element. So not supported at this time.
  4210. const getElementSetsAsStrings = (type) => {
  4211. let blockContent;
  4212. let phrasingContent;
  4213. // Block content elements
  4214. blockContent =
  4215. 'address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul';
  4216. // Phrasing content elements from the HTML5 spec (inline)
  4217. phrasingContent =
  4218. 'a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd ' +
  4219. 'label map noscript object q s samp script select small span strong sub sup ' +
  4220. 'textarea u var #text #comment';
  4221. // Add HTML5 items to globalAttributes, blockContent, phrasingContent
  4222. if (type !== 'html4') {
  4223. const transparentContent = 'a ins del canvas map';
  4224. blockContent += ' article aside details dialog figure main header footer hgroup section nav ' + transparentContent;
  4225. phrasingContent += ' audio canvas command data datalist mark meter output picture ' +
  4226. 'progress template time wbr video ruby bdi keygen svg';
  4227. }
  4228. // Add HTML4 elements unless it's html5-strict
  4229. if (type !== 'html5-strict') {
  4230. const html4PhrasingContent = 'acronym applet basefont big font strike tt';
  4231. phrasingContent = [phrasingContent, html4PhrasingContent].join(' ');
  4232. const html4BlockContent = 'center dir isindex noframes';
  4233. blockContent = [blockContent, html4BlockContent].join(' ');
  4234. }
  4235. // Flow content elements from the HTML5 spec (block+inline)
  4236. const flowContent = [blockContent, phrasingContent].join(' ');
  4237. return { blockContent, phrasingContent, flowContent };
  4238. };
  4239. const getElementSets = (type) => {
  4240. const { blockContent, phrasingContent, flowContent } = getElementSetsAsStrings(type);
  4241. const toArr = (value) => {
  4242. return Object.freeze(value.split(' '));
  4243. };
  4244. return Object.freeze({
  4245. blockContent: toArr(blockContent),
  4246. phrasingContent: toArr(phrasingContent),
  4247. flowContent: toArr(flowContent)
  4248. });
  4249. };
  4250. const cachedSets = {
  4251. 'html4': cached(() => getElementSets('html4')),
  4252. 'html5': cached(() => getElementSets('html5')),
  4253. 'html5-strict': cached(() => getElementSets('html5-strict'))
  4254. };
  4255. // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
  4256. const getElementsPreset = (type, name) => {
  4257. const { blockContent, phrasingContent, flowContent } = cachedSets[type]();
  4258. if (name === 'blocks') {
  4259. return Optional.some(blockContent);
  4260. }
  4261. else if (name === 'phrasing') {
  4262. return Optional.some(phrasingContent);
  4263. }
  4264. else if (name === 'flow') {
  4265. return Optional.some(flowContent);
  4266. }
  4267. else {
  4268. return Optional.none();
  4269. }
  4270. };
  4271. const makeSchema = (type) => {
  4272. const globalAttributes = getGlobalAttributeSet(type);
  4273. const { phrasingContent, flowContent } = getElementSetsAsStrings(type);
  4274. const schema = {};
  4275. const addElement = (name, attributes, children) => {
  4276. schema[name] = {
  4277. attributes: mapToObject(attributes, constant({})),
  4278. attributesOrder: attributes,
  4279. children: mapToObject(children, constant({}))
  4280. };
  4281. };
  4282. const add = (name, attributes = '', children = '') => {
  4283. const childNames = split$1(children);
  4284. const names = split$1(name);
  4285. let ni = names.length;
  4286. const allAttributes = [...globalAttributes, ...split$1(attributes)];
  4287. while (ni--) {
  4288. addElement(names[ni], allAttributes.slice(), childNames);
  4289. }
  4290. };
  4291. const addAttrs = (name, attributes) => {
  4292. const names = split$1(name);
  4293. const attrs = split$1(attributes);
  4294. let ni = names.length;
  4295. while (ni--) {
  4296. const schemaItem = schema[names[ni]];
  4297. for (let i = 0, l = attrs.length; i < l; i++) {
  4298. schemaItem.attributes[attrs[i]] = {};
  4299. schemaItem.attributesOrder.push(attrs[i]);
  4300. }
  4301. }
  4302. };
  4303. if (type !== 'html5-strict') {
  4304. const html4PhrasingContent = 'acronym applet basefont big font strike tt';
  4305. each$e(split$1(html4PhrasingContent), (name) => {
  4306. add(name, '', phrasingContent);
  4307. });
  4308. const html4BlockContent = 'center dir isindex noframes';
  4309. each$e(split$1(html4BlockContent), (name) => {
  4310. add(name, '', flowContent);
  4311. });
  4312. }
  4313. // HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement
  4314. // Schema items <element name>, <specific attributes>, <children ..>
  4315. add('html', 'manifest', 'head body');
  4316. add('head', '', 'base command link meta noscript script style title');
  4317. add('title hr noscript br');
  4318. add('base', 'href target');
  4319. add('link', 'href rel media hreflang type sizes hreflang');
  4320. add('meta', 'name http-equiv content charset');
  4321. add('style', 'media type scoped');
  4322. add('script', 'src async defer type charset');
  4323. add('body', 'onafterprint onbeforeprint onbeforeunload onblur onerror onfocus ' +
  4324. 'onhashchange onload onmessage onoffline ononline onpagehide onpageshow ' +
  4325. 'onpopstate onresize onscroll onstorage onunload', flowContent);
  4326. add('dd div', '', flowContent);
  4327. add('address dt caption', '', type === 'html4' ? phrasingContent : flowContent);
  4328. add('h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn', '', phrasingContent);
  4329. add('blockquote', 'cite', flowContent);
  4330. add('ol', 'reversed start type', 'li');
  4331. add('ul', '', 'li');
  4332. add('li', 'value', flowContent);
  4333. add('dl', '', 'dt dd');
  4334. add('a', 'href target rel media hreflang type', type === 'html4' ? phrasingContent : flowContent);
  4335. add('q', 'cite', phrasingContent);
  4336. add('ins del', 'cite datetime', flowContent);
  4337. add('img', 'src sizes srcset alt usemap ismap width height');
  4338. add('iframe', 'src name width height', flowContent);
  4339. add('embed', 'src type width height');
  4340. add('object', 'data type typemustmatch name usemap form width height', [flowContent, 'param'].join(' '));
  4341. add('param', 'name value');
  4342. add('map', 'name', [flowContent, 'area'].join(' '));
  4343. add('area', 'alt coords shape href target rel media hreflang type');
  4344. add('table', 'border', 'caption colgroup thead tfoot tbody tr' + (type === 'html4' ? ' col' : ''));
  4345. add('colgroup', 'span', 'col');
  4346. add('col', 'span');
  4347. add('tbody thead tfoot', '', 'tr');
  4348. add('tr', '', 'td th');
  4349. add('td', 'colspan rowspan headers', flowContent);
  4350. add('th', 'colspan rowspan headers scope abbr', flowContent);
  4351. add('form', 'accept-charset action autocomplete enctype method name novalidate target', flowContent);
  4352. add('fieldset', 'disabled form name', [flowContent, 'legend'].join(' '));
  4353. add('label', 'form for', phrasingContent);
  4354. add('input', 'accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate ' +
  4355. 'formtarget height list max maxlength min multiple name pattern readonly required size src step type value width');
  4356. add('button', 'disabled form formaction formenctype formmethod formnovalidate formtarget name type value', type === 'html4' ? flowContent : phrasingContent);
  4357. add('select', 'disabled form multiple name required size', 'option optgroup');
  4358. add('optgroup', 'disabled label', 'option');
  4359. add('option', 'disabled label selected value');
  4360. add('textarea', 'cols dirname disabled form maxlength name readonly required rows wrap');
  4361. add('menu', 'type label', [flowContent, 'li'].join(' '));
  4362. add('noscript', '', flowContent);
  4363. // Extend with HTML5 elements
  4364. if (type !== 'html4') {
  4365. add('wbr');
  4366. add('ruby', '', [phrasingContent, 'rt rp'].join(' '));
  4367. add('figcaption', '', flowContent);
  4368. add('mark rt rp bdi', '', phrasingContent);
  4369. add('summary', '', [phrasingContent, 'h1 h2 h3 h4 h5 h6'].join(' '));
  4370. add('canvas', 'width height', flowContent);
  4371. add('data', 'value', phrasingContent);
  4372. add('video', 'src crossorigin poster preload autoplay mediagroup loop ' +
  4373. 'muted controls width height buffered', [flowContent, 'track source'].join(' '));
  4374. add('audio', 'src crossorigin preload autoplay mediagroup loop muted controls ' +
  4375. 'buffered volume', [flowContent, 'track source'].join(' '));
  4376. add('picture', '', 'img source');
  4377. add('source', 'src srcset type media sizes');
  4378. add('track', 'kind src srclang label default');
  4379. add('datalist', '', [phrasingContent, 'option'].join(' '));
  4380. add('article section nav aside main header footer', '', flowContent);
  4381. add('hgroup', '', 'h1 h2 h3 h4 h5 h6');
  4382. add('figure', '', [flowContent, 'figcaption'].join(' '));
  4383. add('time', 'datetime', phrasingContent);
  4384. add('dialog', 'open', flowContent);
  4385. add('command', 'type label icon disabled checked radiogroup command');
  4386. add('output', 'for form name', phrasingContent);
  4387. add('progress', 'value max', phrasingContent);
  4388. add('meter', 'value min max low high optimum', phrasingContent);
  4389. add('details', 'open', [flowContent, 'summary'].join(' '));
  4390. add('keygen', 'autofocus challenge disabled form keytype name');
  4391. // SVGs only support a subset of the global attributes
  4392. addElement('svg', 'id tabindex lang xml:space class style x y width height viewBox preserveAspectRatio zoomAndPan transform'.split(' '), []);
  4393. }
  4394. // Extend with HTML4 attributes unless it's html5-strict
  4395. if (type !== 'html5-strict') {
  4396. addAttrs('script', 'language xml:space');
  4397. addAttrs('style', 'xml:space');
  4398. addAttrs('object', 'declare classid code codebase codetype archive standby align border hspace vspace');
  4399. addAttrs('embed', 'align name hspace vspace');
  4400. addAttrs('param', 'valuetype type');
  4401. addAttrs('a', 'charset name rev shape coords');
  4402. addAttrs('br', 'clear');
  4403. addAttrs('applet', 'codebase archive code object alt name width height align hspace vspace');
  4404. addAttrs('img', 'name longdesc align border hspace vspace');
  4405. addAttrs('iframe', 'longdesc frameborder marginwidth marginheight scrolling align');
  4406. addAttrs('font basefont', 'size color face');
  4407. addAttrs('input', 'usemap align');
  4408. addAttrs('select');
  4409. addAttrs('textarea');
  4410. addAttrs('h1 h2 h3 h4 h5 h6 div p legend caption', 'align');
  4411. addAttrs('ul', 'type compact');
  4412. addAttrs('li', 'type');
  4413. addAttrs('ol dl menu dir', 'compact');
  4414. addAttrs('pre', 'width xml:space');
  4415. addAttrs('hr', 'align noshade size width');
  4416. addAttrs('isindex', 'prompt');
  4417. addAttrs('table', 'summary width frame rules cellspacing cellpadding align bgcolor');
  4418. addAttrs('col', 'width align char charoff valign');
  4419. addAttrs('colgroup', 'width align char charoff valign');
  4420. addAttrs('thead', 'align char charoff valign');
  4421. addAttrs('tr', 'align char charoff valign bgcolor');
  4422. addAttrs('th', 'axis align char charoff valign nowrap bgcolor width height');
  4423. addAttrs('form', 'accept');
  4424. addAttrs('td', 'abbr axis scope align char charoff valign nowrap bgcolor width height');
  4425. addAttrs('tfoot', 'align char charoff valign');
  4426. addAttrs('tbody', 'align char charoff valign');
  4427. addAttrs('area', 'nohref');
  4428. addAttrs('body', 'background bgcolor text link vlink alink');
  4429. }
  4430. // Extend with HTML5 attributes unless it's html4
  4431. if (type !== 'html4') {
  4432. addAttrs('input button select textarea', 'autofocus');
  4433. addAttrs('input textarea', 'placeholder');
  4434. addAttrs('a', 'download');
  4435. addAttrs('link script img', 'crossorigin');
  4436. addAttrs('img', 'loading');
  4437. addAttrs('iframe', 'sandbox seamless allow allowfullscreen loading referrerpolicy'); // Excluded: srcdoc
  4438. }
  4439. // Special: iframe, ruby, video, audio, label
  4440. if (type !== 'html4') {
  4441. // Video/audio elements cannot have nested children
  4442. each$e([schema.video, schema.audio], (item) => {
  4443. delete item.children.audio;
  4444. delete item.children.video;
  4445. });
  4446. }
  4447. // Delete children of the same name from it's parent
  4448. // For example: form can't have a child of the name form
  4449. each$e(split$1('a form meter progress dfn'), (name) => {
  4450. if (schema[name]) {
  4451. delete schema[name].children[name];
  4452. }
  4453. });
  4454. // Delete header, footer, sectioning and heading content descendants
  4455. /* each('dt th address', function(name) {
  4456. delete schema[name].children[name];
  4457. });*/
  4458. // Caption can't have tables
  4459. delete schema.caption.children.table;
  4460. // Delete scripts by default due to possible XSS
  4461. delete schema.script;
  4462. // TODO: LI:s can only have value if parent is OL
  4463. return schema;
  4464. };
  4465. const prefixToOperation = (prefix) => prefix === '-' ? 'remove' : 'add';
  4466. const parseValidChild = (name) => {
  4467. // see: https://html.spec.whatwg.org/#valid-custom-element-name
  4468. const validChildRegExp = /^(@?)([A-Za-z0-9_\-.\u00b7\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u037d\u037f-\u1fff\u200c-\u200d\u203f-\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]+)$/;
  4469. return Optional.from(validChildRegExp.exec(name)).map((matches) => ({
  4470. preset: matches[1] === '@',
  4471. name: matches[2]
  4472. }));
  4473. };
  4474. const parseValidChildrenRules = (value) => {
  4475. // see: https://html.spec.whatwg.org/#valid-custom-element-name
  4476. const childRuleRegExp = /^([+\-]?)([A-Za-z0-9_\-.\u00b7\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u037d\u037f-\u1fff\u200c-\u200d\u203f-\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]+)\[([^\]]+)]$/; // from w3c's custom grammar (above)
  4477. return bind$3(split$1(value, ','), (rule) => {
  4478. const matches = childRuleRegExp.exec(rule);
  4479. if (matches) {
  4480. const prefix = matches[1];
  4481. const operation = prefix ? prefixToOperation(prefix) : 'replace';
  4482. const name = matches[2];
  4483. const validChildren = bind$3(split$1(matches[3], '|'), (validChild) => parseValidChild(validChild).toArray());
  4484. return [{ operation, name, validChildren }];
  4485. }
  4486. else {
  4487. return [];
  4488. }
  4489. });
  4490. };
  4491. const parseValidElementsAttrDataIntoElement = (attrData, targetElement) => {
  4492. const attrRuleRegExp = /^([!\-])?(\w+[\\:]:\w+|[^=~<]+)?(?:([=~<])(.*))?$/;
  4493. const hasPatternsRegExp = /[*?+]/;
  4494. const { attributes, attributesOrder } = targetElement;
  4495. return each$e(split$1(attrData, '|'), (rule) => {
  4496. const matches = attrRuleRegExp.exec(rule);
  4497. if (matches) {
  4498. const attr = {};
  4499. const attrType = matches[1];
  4500. const attrName = matches[2].replace(/[\\:]:/g, ':');
  4501. const attrPrefix = matches[3];
  4502. const value = matches[4];
  4503. // Required
  4504. if (attrType === '!') {
  4505. targetElement.attributesRequired = targetElement.attributesRequired || [];
  4506. targetElement.attributesRequired.push(attrName);
  4507. attr.required = true;
  4508. }
  4509. // Denied from global
  4510. if (attrType === '-') {
  4511. delete attributes[attrName];
  4512. attributesOrder.splice(Tools.inArray(attributesOrder, attrName), 1);
  4513. return;
  4514. }
  4515. // Default value
  4516. if (attrPrefix) {
  4517. if (attrPrefix === '=') { // Default value
  4518. targetElement.attributesDefault = targetElement.attributesDefault || [];
  4519. targetElement.attributesDefault.push({ name: attrName, value });
  4520. attr.defaultValue = value;
  4521. }
  4522. else if (attrPrefix === '~') { // Forced value
  4523. targetElement.attributesForced = targetElement.attributesForced || [];
  4524. targetElement.attributesForced.push({ name: attrName, value });
  4525. attr.forcedValue = value;
  4526. }
  4527. else if (attrPrefix === '<') { // Required values
  4528. attr.validValues = Tools.makeMap(value, '?');
  4529. }
  4530. }
  4531. // Check for attribute patterns
  4532. if (hasPatternsRegExp.test(attrName)) {
  4533. const attrPattern = attr;
  4534. targetElement.attributePatterns = targetElement.attributePatterns || [];
  4535. attrPattern.pattern = patternToRegExp(attrName);
  4536. targetElement.attributePatterns.push(attrPattern);
  4537. }
  4538. else {
  4539. // Add attribute to order list if it doesn't already exist
  4540. if (!attributes[attrName]) {
  4541. attributesOrder.push(attrName);
  4542. }
  4543. attributes[attrName] = attr;
  4544. }
  4545. }
  4546. });
  4547. };
  4548. const cloneAttributesInto = (from, to) => {
  4549. each$d(from.attributes, (value, key) => {
  4550. to.attributes[key] = value;
  4551. });
  4552. to.attributesOrder.push(...from.attributesOrder);
  4553. };
  4554. const parseValidElementsRules = (globalElement, validElements) => {
  4555. const elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)])?$/;
  4556. return bind$3(split$1(validElements, ','), (rule) => {
  4557. const matches = elementRuleRegExp.exec(rule);
  4558. if (matches) {
  4559. const prefix = matches[1];
  4560. const elementName = matches[2];
  4561. const outputName = matches[3];
  4562. const attrsPrefix = matches[4];
  4563. const attrData = matches[5];
  4564. const element = {
  4565. attributes: {},
  4566. attributesOrder: []
  4567. };
  4568. globalElement.each((el) => cloneAttributesInto(el, element));
  4569. if (prefix === '#') {
  4570. element.paddEmpty = true;
  4571. }
  4572. else if (prefix === '-') {
  4573. element.removeEmpty = true;
  4574. }
  4575. if (attrsPrefix === '!') {
  4576. element.removeEmptyAttrs = true;
  4577. }
  4578. if (attrData) {
  4579. parseValidElementsAttrDataIntoElement(attrData, element);
  4580. }
  4581. // Handle substitute elements such as b/strong
  4582. if (outputName) {
  4583. element.outputName = elementName;
  4584. }
  4585. // Mutate the local globalElement option state if we find a global @ rule
  4586. if (elementName === '@') {
  4587. // We only care about the first one
  4588. if (globalElement.isNone()) {
  4589. globalElement = Optional.some(element);
  4590. }
  4591. else {
  4592. return [];
  4593. }
  4594. }
  4595. return [outputName ? { name: elementName, element, aliasName: outputName } : { name: elementName, element }];
  4596. }
  4597. else {
  4598. return [];
  4599. }
  4600. });
  4601. };
  4602. /**
  4603. * Schema validator class.
  4604. *
  4605. * @class tinymce.html.Schema
  4606. * @version 3.4
  4607. * @example
  4608. * if (tinymce.activeEditor.schema.isValidChild('p', 'span')) {
  4609. * alert('span is valid child of p.');
  4610. * }
  4611. *
  4612. * if (tinymce.activeEditor.schema.getElementRule('p')) {
  4613. * alert('P is a valid element.');
  4614. * }
  4615. */
  4616. const mapCache = {};
  4617. const makeMap$2 = Tools.makeMap, each$b = Tools.each, extend$2 = Tools.extend, explode$2 = Tools.explode;
  4618. const createMap = (defaultValue, extendWith = {}) => {
  4619. const value = makeMap$2(defaultValue, ' ', makeMap$2(defaultValue.toUpperCase(), ' '));
  4620. return extend$2(value, extendWith);
  4621. };
  4622. // A curated list using the textBlockElements map and parts of the blockElements map from the schema
  4623. // TODO: TINY-8728 Investigate if the extras can be added directly to the default text block elements
  4624. const getTextRootBlockElements = (schema) => createMap('td th li dt dd figcaption caption details summary', schema.getTextBlockElements());
  4625. const compileElementMap = (value, mode) => {
  4626. if (value) {
  4627. const styles = {};
  4628. if (isString(value)) {
  4629. value = {
  4630. '*': value
  4631. };
  4632. }
  4633. // Convert styles into a rule list
  4634. each$b(value, (value, key) => {
  4635. styles[key] = styles[key.toUpperCase()] = mode === 'map' ? makeMap$2(value, /[, ]/) : explode$2(value, /[, ]/);
  4636. });
  4637. return styles;
  4638. }
  4639. else {
  4640. return undefined;
  4641. }
  4642. };
  4643. const Schema = (settings = {}) => {
  4644. var _a;
  4645. const elements = {};
  4646. const children = {};
  4647. let patternElements = [];
  4648. const customElementsMap = {};
  4649. const specialElements = {};
  4650. // Creates an lookup table map object for the specified option or the default value
  4651. const createLookupTable = (option, defaultValue, extendWith) => {
  4652. const value = settings[option];
  4653. if (!value) {
  4654. // Get cached default map or make it if needed
  4655. let newValue = mapCache[option];
  4656. if (!newValue) {
  4657. newValue = createMap(defaultValue, extendWith);
  4658. mapCache[option] = newValue;
  4659. }
  4660. return newValue;
  4661. }
  4662. else {
  4663. // Create custom map
  4664. return makeMap$2(value, /[, ]/, makeMap$2(value.toUpperCase(), /[, ]/));
  4665. }
  4666. };
  4667. const schemaType = (_a = settings.schema) !== null && _a !== void 0 ? _a : 'html5';
  4668. const schemaItems = makeSchema(schemaType);
  4669. // Allow all elements and attributes if verify_html is set to false
  4670. if (settings.verify_html === false) {
  4671. settings.valid_elements = '*[*]';
  4672. }
  4673. const validStyles = compileElementMap(settings.valid_styles);
  4674. const invalidStyles = compileElementMap(settings.invalid_styles, 'map');
  4675. const validClasses = compileElementMap(settings.valid_classes, 'map');
  4676. // Setup map objects
  4677. const whitespaceElementsMap = createLookupTable('whitespace_elements', 'pre script noscript style textarea video audio iframe object code');
  4678. const selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr');
  4679. const voidElementsMap = createLookupTable('void_elements', 'area base basefont br col frame hr img input isindex link ' +
  4680. 'meta param embed source wbr track');
  4681. const boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize ' +
  4682. 'noshade nowrap readonly selected autoplay loop controls allowfullscreen');
  4683. const nonEmptyOrMoveCaretBeforeOnEnter = 'td th iframe video audio object script code';
  4684. const nonEmptyElementsMap = createLookupTable('non_empty_elements', nonEmptyOrMoveCaretBeforeOnEnter + ' pre svg textarea summary', voidElementsMap);
  4685. const moveCaretBeforeOnEnterElementsMap = createLookupTable('move_caret_before_on_enter_elements', nonEmptyOrMoveCaretBeforeOnEnter + ' table', voidElementsMap);
  4686. const headings = 'h1 h2 h3 h4 h5 h6';
  4687. const textBlockElementsMap = createLookupTable('text_block_elements', headings + ' p div address pre form ' +
  4688. 'blockquote center dir fieldset header footer article section hgroup aside main nav figure');
  4689. const blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' +
  4690. 'th tr td li ol ul caption dl dt dd noscript menu isindex option ' +
  4691. 'datalist select optgroup figcaption details summary html body multicol listing colgroup col', textBlockElementsMap);
  4692. const textInlineElementsMap = createLookupTable('text_inline_elements', 'span strong b em i font s strike u var cite ' +
  4693. 'dfn code mark q sup sub samp');
  4694. const transparentElementsMap = createLookupTable('transparent_elements', 'a ins del canvas map');
  4695. const wrapBlockElementsMap = createLookupTable('wrap_block_elements', 'pre ' + headings);
  4696. // See https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments
  4697. each$b(('script noscript iframe noframes noembed title style textarea xmp plaintext').split(' '), (name) => {
  4698. specialElements[name] = new RegExp('<\/' + name + '[^>]*>', 'gi');
  4699. });
  4700. // Parses the specified valid_elements string and adds to the current rules
  4701. const addValidElements = (validElements) => {
  4702. const globalElement = Optional.from(elements['@']);
  4703. const hasPatternsRegExp = /[*?+]/;
  4704. each$e(parseValidElementsRules(globalElement, validElements !== null && validElements !== void 0 ? validElements : ''), ({ name, element, aliasName }) => {
  4705. if (aliasName) {
  4706. elements[aliasName] = element;
  4707. }
  4708. // Add pattern or exact element
  4709. if (hasPatternsRegExp.test(name)) {
  4710. const patternElement = element;
  4711. patternElement.pattern = patternToRegExp(name);
  4712. patternElements.push(patternElement);
  4713. }
  4714. else {
  4715. elements[name] = element;
  4716. }
  4717. });
  4718. };
  4719. const setValidElements = (validElements) => {
  4720. // Clear any existing rules. Note that since `elements` is exposed we can't
  4721. // overwrite it, so instead we delete all the properties
  4722. patternElements = [];
  4723. each$e(keys(elements), (name) => {
  4724. delete elements[name];
  4725. });
  4726. addValidElements(validElements);
  4727. };
  4728. const addCustomElement = (name, spec) => {
  4729. var _a, _b;
  4730. // Flush cached items since we are altering the default maps
  4731. delete mapCache.text_block_elements;
  4732. delete mapCache.block_elements;
  4733. const inline = spec.extends ? !isBlock(spec.extends) : false;
  4734. const cloneName = spec.extends;
  4735. children[name] = cloneName ? children[cloneName] : {};
  4736. customElementsMap[name] = cloneName !== null && cloneName !== void 0 ? cloneName : name;
  4737. // Treat all custom elements as being non-empty by default
  4738. nonEmptyElementsMap[name.toUpperCase()] = {};
  4739. nonEmptyElementsMap[name] = {};
  4740. // If it's not marked as inline then add it to valid block elements
  4741. if (!inline) {
  4742. blockElementsMap[name.toUpperCase()] = {};
  4743. blockElementsMap[name] = {};
  4744. }
  4745. // Add elements clone if needed
  4746. if (cloneName && !elements[name] && elements[cloneName]) {
  4747. const customRule = deepCloneElementRule(elements[cloneName]);
  4748. delete customRule.removeEmptyAttrs;
  4749. delete customRule.removeEmpty;
  4750. elements[name] = customRule;
  4751. }
  4752. else {
  4753. elements[name] = { attributesOrder: [], attributes: {} };
  4754. }
  4755. // Add custom attributes
  4756. if (isArray$1(spec.attributes)) {
  4757. const processAttrName = (name) => {
  4758. customRule.attributesOrder.push(name);
  4759. customRule.attributes[name] = {};
  4760. };
  4761. const customRule = (_a = elements[name]) !== null && _a !== void 0 ? _a : {};
  4762. delete customRule.attributesDefault;
  4763. delete customRule.attributesForced;
  4764. delete customRule.attributePatterns;
  4765. delete customRule.attributesRequired;
  4766. customRule.attributesOrder = [];
  4767. customRule.attributes = {};
  4768. each$e(spec.attributes, (attrName) => {
  4769. const globalAttrs = getGlobalAttributeSet(schemaType);
  4770. parseValidChild(attrName).each(({ preset, name }) => {
  4771. if (preset) {
  4772. if (name === 'global') {
  4773. each$e(globalAttrs, processAttrName);
  4774. }
  4775. }
  4776. else {
  4777. processAttrName(name);
  4778. }
  4779. });
  4780. });
  4781. elements[name] = customRule;
  4782. }
  4783. // Add custom pad empty rule
  4784. if (isBoolean(spec.padEmpty)) {
  4785. const customRule = (_b = elements[name]) !== null && _b !== void 0 ? _b : {};
  4786. customRule.paddEmpty = spec.padEmpty;
  4787. elements[name] = customRule;
  4788. }
  4789. // Add custom children
  4790. if (isArray$1(spec.children)) {
  4791. const customElementChildren = {};
  4792. const processNodeName = (name) => {
  4793. customElementChildren[name] = {};
  4794. };
  4795. const processPreset = (name) => {
  4796. getElementsPreset(schemaType, name).each((names) => {
  4797. each$e(names, processNodeName);
  4798. });
  4799. };
  4800. each$e(spec.children, (child) => {
  4801. parseValidChild(child).each(({ preset, name }) => {
  4802. if (preset) {
  4803. processPreset(name);
  4804. }
  4805. else {
  4806. processNodeName(name);
  4807. }
  4808. });
  4809. });
  4810. children[name] = customElementChildren;
  4811. }
  4812. // Add custom elements at extends positions
  4813. if (cloneName) {
  4814. each$d(children, (element, elmName) => {
  4815. if (element[cloneName]) {
  4816. children[elmName] = element = extend$2({}, children[elmName]);
  4817. element[name] = element[cloneName];
  4818. }
  4819. });
  4820. }
  4821. };
  4822. const addCustomElementsFromString = (customElements) => {
  4823. each$e(parseCustomElementsRules(customElements !== null && customElements !== void 0 ? customElements : ''), ({ name, cloneName }) => {
  4824. addCustomElement(name, { extends: cloneName });
  4825. });
  4826. };
  4827. const addCustomElements = (customElements) => {
  4828. if (isObject(customElements)) {
  4829. each$d(customElements, (spec, name) => addCustomElement(name, spec));
  4830. }
  4831. else if (isString(customElements)) {
  4832. addCustomElementsFromString(customElements);
  4833. }
  4834. };
  4835. // Adds valid children to the schema object
  4836. const addValidChildren = (validChildren) => {
  4837. each$e(parseValidChildrenRules(validChildren !== null && validChildren !== void 0 ? validChildren : ''), ({ operation, name, validChildren }) => {
  4838. const parent = operation === 'replace' ? { '#comment': {} } : children[name];
  4839. const processNodeName = (name) => {
  4840. if (operation === 'remove') {
  4841. delete parent[name];
  4842. }
  4843. else {
  4844. parent[name] = {};
  4845. }
  4846. };
  4847. const processPreset = (name) => {
  4848. getElementsPreset(schemaType, name).each((names) => {
  4849. each$e(names, processNodeName);
  4850. });
  4851. };
  4852. each$e(validChildren, ({ preset, name }) => {
  4853. if (preset) {
  4854. processPreset(name);
  4855. }
  4856. else {
  4857. processNodeName(name);
  4858. }
  4859. });
  4860. children[name] = parent;
  4861. });
  4862. };
  4863. const getElementRule = (name) => {
  4864. const element = elements[name];
  4865. // Exact match found
  4866. if (element) {
  4867. return element;
  4868. }
  4869. // No exact match then try the patterns
  4870. let i = patternElements.length;
  4871. while (i--) {
  4872. const patternElement = patternElements[i];
  4873. if (patternElement.pattern.test(name)) {
  4874. return patternElement;
  4875. }
  4876. }
  4877. return undefined;
  4878. };
  4879. const setup = () => {
  4880. if (!settings.valid_elements) {
  4881. // No valid elements defined then clone the elements from the schema spec
  4882. each$b(schemaItems, (element, name) => {
  4883. elements[name] = {
  4884. attributes: element.attributes,
  4885. attributesOrder: element.attributesOrder
  4886. };
  4887. children[name] = element.children;
  4888. });
  4889. // Prefer strong/em over b/i
  4890. each$b(split$1('strong/b em/i'), (item) => {
  4891. const items = split$1(item, '/');
  4892. elements[items[1]].outputName = items[0];
  4893. });
  4894. // Add default alt attribute for images, removed since alt="" is treated as presentational.
  4895. // elements.img.attributesDefault = [{name: 'alt', value: ''}];
  4896. // By default,
  4897. // - padd the text inline element if it is empty and also a child of an empty root block
  4898. // - in all other cases, remove the text inline element if it is empty
  4899. each$b(textInlineElementsMap, (_val, name) => {
  4900. if (elements[name]) {
  4901. if (settings.padd_empty_block_inline_children) {
  4902. elements[name].paddInEmptyBlock = true;
  4903. }
  4904. elements[name].removeEmpty = true;
  4905. }
  4906. });
  4907. // Remove these if they are empty by default
  4908. each$b(split$1('ol ul blockquote a table tbody'), (name) => {
  4909. if (elements[name]) {
  4910. elements[name].removeEmpty = true;
  4911. }
  4912. });
  4913. // Padd these by default
  4914. each$b(split$1('p h1 h2 h3 h4 h5 h6 th td pre div address caption li summary'), (name) => {
  4915. if (elements[name]) {
  4916. elements[name].paddEmpty = true;
  4917. }
  4918. });
  4919. // Remove these if they have no attributes
  4920. each$b(split$1('span'), (name) => {
  4921. elements[name].removeEmptyAttrs = true;
  4922. });
  4923. // Remove these by default
  4924. // TODO: Reenable in 4.1
  4925. /* each(split('script style'), function(name) {
  4926. delete elements[name];
  4927. });*/
  4928. }
  4929. else {
  4930. setValidElements(settings.valid_elements);
  4931. each$b(schemaItems, (element, name) => {
  4932. children[name] = element.children;
  4933. });
  4934. }
  4935. // Opt in is done with options like `extended_valid_elements`
  4936. delete elements.svg;
  4937. addCustomElements(settings.custom_elements);
  4938. addValidChildren(settings.valid_children);
  4939. addValidElements(settings.extended_valid_elements);
  4940. // Todo: Remove this when we fix list handling to be valid
  4941. addValidChildren('+ol[ul|ol],+ul[ul|ol]');
  4942. // Some elements are not valid by themselves - require parents
  4943. each$b({
  4944. dd: 'dl',
  4945. dt: 'dl',
  4946. li: 'ul ol',
  4947. td: 'tr',
  4948. th: 'tr',
  4949. tr: 'tbody thead tfoot',
  4950. tbody: 'table',
  4951. thead: 'table',
  4952. tfoot: 'table',
  4953. legend: 'fieldset',
  4954. area: 'map',
  4955. param: 'video audio object'
  4956. }, (parents, item) => {
  4957. if (elements[item]) {
  4958. elements[item].parentsRequired = split$1(parents);
  4959. }
  4960. });
  4961. // Delete invalid elements
  4962. if (settings.invalid_elements) {
  4963. each$b(explode$2(settings.invalid_elements), (item) => {
  4964. if (elements[item]) {
  4965. delete elements[item];
  4966. }
  4967. });
  4968. }
  4969. // If the user didn't allow span only allow internal spans
  4970. if (!getElementRule('span')) {
  4971. addValidElements('span[!data-mce-type|*]');
  4972. }
  4973. };
  4974. /**
  4975. * Name/value map object with valid parents and children to those parents.
  4976. *
  4977. * @field children
  4978. * @type Object
  4979. * @example
  4980. * children = {
  4981. * div: { p:{}, h1:{} }
  4982. * };
  4983. */
  4984. /**
  4985. * Name/value map object with valid styles for each element.
  4986. *
  4987. * @method getValidStyles
  4988. * @type Object
  4989. */
  4990. const getValidStyles = constant(validStyles);
  4991. /**
  4992. * Name/value map object with valid styles for each element.
  4993. *
  4994. * @method getInvalidStyles
  4995. * @type Object
  4996. */
  4997. const getInvalidStyles = constant(invalidStyles);
  4998. /**
  4999. * Name/value map object with valid classes for each element.
  5000. *
  5001. * @method getValidClasses
  5002. * @type Object
  5003. */
  5004. const getValidClasses = constant(validClasses);
  5005. /**
  5006. * Returns a map with boolean attributes.
  5007. *
  5008. * @method getBoolAttrs
  5009. * @return {Object} Name/value lookup map for boolean attributes.
  5010. */
  5011. const getBoolAttrs = constant(boolAttrMap);
  5012. /**
  5013. * Returns a map with block elements.
  5014. *
  5015. * @method getBlockElements
  5016. * @return {Object} Name/value lookup map for block elements.
  5017. */
  5018. const getBlockElements = constant(blockElementsMap);
  5019. /**
  5020. * Returns a map with text block elements. For example: <code>&#60;p&#62;</code>, <code>&#60;h1&#62;</code> to <code>&#60;h6&#62;</code>, <code>&#60;div&#62;</code> or <code>&#60;address&#62;</code>.
  5021. *
  5022. * @method getTextBlockElements
  5023. * @return {Object} Name/value lookup map for block elements.
  5024. */
  5025. const getTextBlockElements = constant(textBlockElementsMap);
  5026. /**
  5027. * Returns a map of inline text format nodes. For example: <code>&#60;strong&#62;</code>, <code>&#60;span&#62;</code> or <code>&#60;ins&#62;</code>.
  5028. *
  5029. * @method getTextInlineElements
  5030. * @return {Object} Name/value lookup map for text format elements.
  5031. */
  5032. const getTextInlineElements = constant(textInlineElementsMap);
  5033. /**
  5034. * Returns a map with void elements. For example: <code>&#60;br&#62;</code> or <code>&#60;img&#62;</code>.
  5035. *
  5036. * @method getVoidElements
  5037. * @return {Object} Name/value lookup map for void elements.
  5038. */
  5039. const getVoidElements = constant(Object.seal(voidElementsMap));
  5040. /**
  5041. * Returns a map with self closing tags. For example: <code>&#60;li&#62;</code>.
  5042. *
  5043. * @method getSelfClosingElements
  5044. * @return {Object} Name/value lookup map for self closing tags elements.
  5045. */
  5046. const getSelfClosingElements = constant(selfClosingElementsMap);
  5047. /**
  5048. * Returns a map with elements that should be treated as contents regardless if it has text
  5049. * content in them or not. For example: <code>&#60;td&#62;</code>, <code>&#60;video&#62;</code> or <code>&#60;img&#62;</code>.
  5050. *
  5051. * @method getNonEmptyElements
  5052. * @return {Object} Name/value lookup map for non empty elements.
  5053. */
  5054. const getNonEmptyElements = constant(nonEmptyElementsMap);
  5055. /**
  5056. * Returns a map with elements that the caret should be moved in front of after enter is
  5057. * pressed.
  5058. *
  5059. * @method getMoveCaretBeforeOnEnterElements
  5060. * @return {Object} Name/value lookup map for elements to place the caret in front of.
  5061. */
  5062. const getMoveCaretBeforeOnEnterElements = constant(moveCaretBeforeOnEnterElementsMap);
  5063. /**
  5064. * Returns a map with elements where white space is to be preserved. For example: <code>&#60;pre&#62;</code> or <code>&#60;script&#62;</code>.
  5065. *
  5066. * @method getWhitespaceElements
  5067. * @return {Object} Name/value lookup map for white space elements.
  5068. */
  5069. const getWhitespaceElements = constant(whitespaceElementsMap);
  5070. /**
  5071. * Returns a map with elements that should be treated as transparent.
  5072. *
  5073. * @method getTransparentElements
  5074. * @return {Object} Name/value lookup map for special elements.
  5075. */
  5076. const getTransparentElements = constant(transparentElementsMap);
  5077. const getWrapBlockElements = constant(wrapBlockElementsMap);
  5078. /**
  5079. * Returns a map with special elements. These are elements that needs to be parsed
  5080. * in a special way such as script, style, textarea etc. The map object values
  5081. * are regexps used to find the end of the element.
  5082. *
  5083. * @method getSpecialElements
  5084. * @return {Object} Name/value lookup map for special elements.
  5085. */
  5086. const getSpecialElements = constant(Object.seal(specialElements));
  5087. /**
  5088. * Returns true/false if the specified element and it's child is valid or not
  5089. * according to the schema.
  5090. *
  5091. * @method isValidChild
  5092. * @param {String} name Element name to check for.
  5093. * @param {String} child Element child to verify.
  5094. * @return {Boolean} True/false if the element is a valid child of the specified parent.
  5095. */
  5096. const isValidChild = (name, child) => {
  5097. const parent = children[name.toLowerCase()];
  5098. return !!(parent && parent[child.toLowerCase()]);
  5099. };
  5100. /**
  5101. * Returns true/false if the specified element name and optional attribute is
  5102. * valid according to the schema.
  5103. *
  5104. * @method isValid
  5105. * @param {String} name Name of element to check.
  5106. * @param {String} attr Optional attribute name to check for.
  5107. * @return {Boolean} True/false if the element and attribute is valid.
  5108. */
  5109. const isValid = (name, attr) => {
  5110. const rule = getElementRule(name);
  5111. // Check if it's a valid element
  5112. if (rule) {
  5113. if (attr) {
  5114. // Check if attribute name exists
  5115. if (rule.attributes[attr]) {
  5116. return true;
  5117. }
  5118. // Check if attribute matches a regexp pattern
  5119. const attrPatterns = rule.attributePatterns;
  5120. if (attrPatterns) {
  5121. let i = attrPatterns.length;
  5122. while (i--) {
  5123. if (attrPatterns[i].pattern.test(attr)) {
  5124. return true;
  5125. }
  5126. }
  5127. }
  5128. }
  5129. else {
  5130. return true;
  5131. }
  5132. }
  5133. // No match
  5134. return false;
  5135. };
  5136. const isBlock = (name) => has$2(getBlockElements(), name);
  5137. // Check if name starts with # to detect non-element node names like #text and #comment
  5138. const isInline = (name) => !startsWith(name, '#') && isValid(name) && !isBlock(name);
  5139. const isWrapper = (name) => has$2(getWrapBlockElements(), name) || isInline(name);
  5140. /**
  5141. * Returns true/false if the specified element is valid or not
  5142. * according to the schema.
  5143. *
  5144. * @method getElementRule
  5145. * @param {String} name Element name to check for.
  5146. * @return {Object} Element object or undefined if the element isn't valid.
  5147. */
  5148. /**
  5149. * Returns an map object of all custom elements.
  5150. *
  5151. * @method getCustomElements
  5152. * @return {Object} Name/value map object of all custom elements.
  5153. */
  5154. const getCustomElements = constant(customElementsMap);
  5155. /**
  5156. * Parses a valid elements string and adds it to the schema. The valid elements
  5157. * format is for example <code>element[attr=default|otherattr]</code>.
  5158. * Existing rules will be replaced with the ones specified, so this extends the schema.
  5159. *
  5160. * @method addValidElements
  5161. * @param {String} valid_elements String in the valid elements format to be parsed.
  5162. */
  5163. /**
  5164. * Parses a valid elements string and sets it to the schema. The valid elements
  5165. * format is for example <code>element[attr=default|otherattr]</code>.
  5166. * Existing rules will be replaced with the ones specified, so this extends the schema.
  5167. *
  5168. * @method setValidElements
  5169. * @param {String} valid_elements String in the valid elements format to be parsed.
  5170. */
  5171. /**
  5172. * Adds custom non-HTML elements to the schema. For more information about adding custom elements see:
  5173. * <a href="https://www.tiny.cloud/docs/tinymce/latest/content-filtering/#custom_elements">custom_elements</a>
  5174. *
  5175. * @method addCustomElements
  5176. * @param {String/Object} custom_elements Comma separated list or record of custom elements to add.
  5177. */
  5178. /**
  5179. * Parses a valid children string and adds them to the schema structure. The valid children
  5180. * format is for example <code>element[child1|child2]</code>.
  5181. *
  5182. * @method addValidChildren
  5183. * @param {String} valid_children Valid children elements string to parse
  5184. */
  5185. setup();
  5186. return {
  5187. type: schemaType,
  5188. children,
  5189. elements,
  5190. getValidStyles,
  5191. getValidClasses,
  5192. getBlockElements,
  5193. getInvalidStyles,
  5194. getVoidElements,
  5195. getTextBlockElements,
  5196. getTextInlineElements,
  5197. getBoolAttrs,
  5198. getElementRule,
  5199. getSelfClosingElements,
  5200. getNonEmptyElements,
  5201. getMoveCaretBeforeOnEnterElements,
  5202. getWhitespaceElements,
  5203. getTransparentElements,
  5204. getSpecialElements,
  5205. isValidChild,
  5206. isValid,
  5207. isBlock,
  5208. isInline,
  5209. isWrapper,
  5210. getCustomElements,
  5211. addValidElements,
  5212. setValidElements,
  5213. addCustomElements,
  5214. addValidChildren
  5215. };
  5216. };
  5217. const hexColour = (value) => ({
  5218. value: normalizeHex(value)
  5219. });
  5220. const normalizeHex = (hex) => removeLeading(hex, '#').toUpperCase();
  5221. const toHex = (component) => {
  5222. const hex = component.toString(16);
  5223. return (hex.length === 1 ? '0' + hex : hex).toUpperCase();
  5224. };
  5225. const fromRgba = (rgbaColour) => {
  5226. const value = toHex(rgbaColour.red) + toHex(rgbaColour.green) + toHex(rgbaColour.blue);
  5227. return hexColour(value);
  5228. };
  5229. const rgbRegex = /^\s*rgb\s*\(\s*(\d+)\s*[,\s]\s*(\d+)\s*[,\s]\s*(\d+)\s*\)\s*$/i;
  5230. // This regex will match rgba(0, 0, 0, 0.5) or rgba(0, 0, 0, 50%) , or without commas
  5231. const rgbaRegex = /^\s*rgba\s*\(\s*(\d+)\s*[,\s]\s*(\d+)\s*[,\s]\s*(\d+)\s*[,\s]\s*((?:\d?\.\d+|\d+)%?)\s*\)\s*$/i;
  5232. const rgbaColour = (red, green, blue, alpha) => ({
  5233. red,
  5234. green,
  5235. blue,
  5236. alpha
  5237. });
  5238. const fromStringValues = (red, green, blue, alpha) => {
  5239. const r = parseInt(red, 10);
  5240. const g = parseInt(green, 10);
  5241. const b = parseInt(blue, 10);
  5242. const a = parseFloat(alpha);
  5243. return rgbaColour(r, g, b, a);
  5244. };
  5245. const getColorFormat = (colorString) => {
  5246. if (rgbRegex.test(colorString)) {
  5247. return 'rgb';
  5248. }
  5249. else if (rgbaRegex.test(colorString)) {
  5250. return 'rgba';
  5251. }
  5252. return 'other';
  5253. };
  5254. const fromString = (rgbaString) => {
  5255. const rgbMatch = rgbRegex.exec(rgbaString);
  5256. if (rgbMatch !== null) {
  5257. return Optional.some(fromStringValues(rgbMatch[1], rgbMatch[2], rgbMatch[3], '1'));
  5258. }
  5259. const rgbaMatch = rgbaRegex.exec(rgbaString);
  5260. if (rgbaMatch !== null) {
  5261. return Optional.some(fromStringValues(rgbaMatch[1], rgbaMatch[2], rgbaMatch[3], rgbaMatch[4]));
  5262. }
  5263. return Optional.none();
  5264. };
  5265. const toString = (rgba) => `rgba(${rgba.red},${rgba.green},${rgba.blue},${rgba.alpha})`;
  5266. const rgbaToHexString = (color) => fromString(color)
  5267. .map(fromRgba)
  5268. .map((h) => '#' + h.value)
  5269. .getOr(color);
  5270. /**
  5271. * This class is used to parse CSS styles. It also compresses styles to reduce the output size.
  5272. *
  5273. * @class tinymce.html.Styles
  5274. * @version 3.4
  5275. * @example
  5276. * const Styles = tinymce.html.Styles({
  5277. * url_converter: (url) => {
  5278. * return url;
  5279. * }
  5280. * });
  5281. *
  5282. * styles = Styles.parse('border: 1px solid red');
  5283. * styles.color = 'red';
  5284. *
  5285. * console.log(tinymce.html.Styles().serialize(styles));
  5286. */
  5287. const Styles = (settings = {}, schema) => {
  5288. /* jshint maxlen:255 */
  5289. /* eslint max-len:0 */
  5290. const urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi;
  5291. const styleRegExp = /\s*([^:]+):\s*([^;]+);?/g;
  5292. const trimRightRegExp = /\s+$/;
  5293. const encodingLookup = {};
  5294. let validStyles;
  5295. let invalidStyles;
  5296. const invisibleChar = zeroWidth;
  5297. if (schema) {
  5298. validStyles = schema.getValidStyles();
  5299. invalidStyles = schema.getInvalidStyles();
  5300. }
  5301. const encodingItems = (`\\" \\' \\; \\: ; : ` + invisibleChar).split(' ');
  5302. for (let i = 0; i < encodingItems.length; i++) {
  5303. encodingLookup[encodingItems[i]] = invisibleChar + i;
  5304. encodingLookup[invisibleChar + i] = encodingItems[i];
  5305. }
  5306. // eslint-disable-next-line consistent-this
  5307. const self = {
  5308. /**
  5309. * Parses the specified style value into an object collection. This parser will also
  5310. * merge and remove any redundant items that browsers might have added. URLs inside
  5311. * the styles will also be converted to absolute/relative based on the settings.
  5312. *
  5313. * @method parse
  5314. * @param {String} css Style value to parse. For example: `border:1px solid red;`
  5315. * @return {Object} Object representation of that style. For example: `{ border: '1px solid red' }`
  5316. */
  5317. parse: (css) => {
  5318. const styles = {};
  5319. let isEncoded = false;
  5320. const urlConverter = settings.url_converter;
  5321. const urlConverterScope = settings.url_converter_scope || self;
  5322. const compress = (prefix, suffix, noJoin) => {
  5323. const top = styles[prefix + '-top' + suffix];
  5324. if (!top) {
  5325. return;
  5326. }
  5327. const right = styles[prefix + '-right' + suffix];
  5328. if (!right) {
  5329. return;
  5330. }
  5331. const bottom = styles[prefix + '-bottom' + suffix];
  5332. if (!bottom) {
  5333. return;
  5334. }
  5335. const left = styles[prefix + '-left' + suffix];
  5336. if (!left) {
  5337. return;
  5338. }
  5339. const box = [top, right, bottom, left];
  5340. let i = box.length - 1;
  5341. while (i--) {
  5342. if (box[i] !== box[i + 1]) {
  5343. break;
  5344. }
  5345. }
  5346. if (i > -1 && noJoin) {
  5347. return;
  5348. }
  5349. styles[prefix + suffix] = i === -1 ? box[0] : box.join(' ');
  5350. delete styles[prefix + '-top' + suffix];
  5351. delete styles[prefix + '-right' + suffix];
  5352. delete styles[prefix + '-bottom' + suffix];
  5353. delete styles[prefix + '-left' + suffix];
  5354. };
  5355. /**
  5356. * Checks if the specific style can be compressed in other words if all border-width are equal.
  5357. */
  5358. const canCompress = (key) => {
  5359. const value = styles[key];
  5360. if (!value) {
  5361. return;
  5362. }
  5363. // Make sure not to split values like 'rgb(100, 50, 100);
  5364. const values = value.indexOf(',') > -1 ? [value] : value.split(' ');
  5365. let i = values.length;
  5366. while (i--) {
  5367. if (values[i] !== values[0]) {
  5368. return false;
  5369. }
  5370. }
  5371. styles[key] = values[0];
  5372. return true;
  5373. };
  5374. /**
  5375. * Compresses multiple styles into one style.
  5376. */
  5377. const compress2 = (target, a, b, c) => {
  5378. if (!canCompress(a)) {
  5379. return;
  5380. }
  5381. if (!canCompress(b)) {
  5382. return;
  5383. }
  5384. if (!canCompress(c)) {
  5385. return;
  5386. }
  5387. // Compress
  5388. styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c];
  5389. delete styles[a];
  5390. delete styles[b];
  5391. delete styles[c];
  5392. };
  5393. // Encodes the specified string by replacing all \" \' ; : with _<num>
  5394. const encode = (str) => {
  5395. isEncoded = true;
  5396. return encodingLookup[str];
  5397. };
  5398. // Decodes the specified string by replacing all _<num> with it's original value \" \' etc
  5399. // It will also decode the \" \' if keepSlashes is set to false or omitted
  5400. const decode = (str, keepSlashes) => {
  5401. if (isEncoded) {
  5402. str = str.replace(/\uFEFF[0-9]/g, (str) => {
  5403. return encodingLookup[str];
  5404. });
  5405. }
  5406. if (!keepSlashes) {
  5407. str = str.replace(/\\([\'\";:])/g, '$1');
  5408. }
  5409. return str;
  5410. };
  5411. const decodeSingleHexSequence = (escSeq) => {
  5412. return String.fromCharCode(parseInt(escSeq.slice(1), 16));
  5413. };
  5414. const decodeHexSequences = (value) => {
  5415. return value.replace(/\\[0-9a-f]+/gi, decodeSingleHexSequence);
  5416. };
  5417. const processUrl = (match, url, url2, url3, str, str2) => {
  5418. str = str || str2;
  5419. if (str) {
  5420. str = decode(str);
  5421. // Force strings into single quote format
  5422. return `'` + str.replace(/\'/g, `\\'`) + `'`;
  5423. }
  5424. url = decode(url || url2 || url3 || '');
  5425. if (!settings.allow_script_urls) {
  5426. const scriptUrl = url.replace(/[\s\r\n]+/g, '');
  5427. if (/(java|vb)script:/i.test(scriptUrl)) {
  5428. return '';
  5429. }
  5430. if (!settings.allow_svg_data_urls && /^data:image\/svg/i.test(scriptUrl)) {
  5431. return '';
  5432. }
  5433. }
  5434. // Convert the URL to relative/absolute depending on config
  5435. if (urlConverter) {
  5436. url = urlConverter.call(urlConverterScope, url, 'style');
  5437. }
  5438. // Output new URL format
  5439. return `url('` + url.replace(/\'/g, `\\'`) + `')`;
  5440. };
  5441. if (css) {
  5442. css = css.replace(/[\u0000-\u001F]/g, '');
  5443. // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing
  5444. css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, (str) => {
  5445. return str.replace(/[;:]/g, encode);
  5446. });
  5447. // Parse styles
  5448. let matches;
  5449. while ((matches = styleRegExp.exec(css))) {
  5450. styleRegExp.lastIndex = matches.index + matches[0].length;
  5451. let name = matches[1].replace(trimRightRegExp, '').toLowerCase();
  5452. let value = matches[2].replace(trimRightRegExp, '');
  5453. if (name && value) {
  5454. // Decode escaped sequences like \65 -> e
  5455. name = decodeHexSequences(name);
  5456. value = decodeHexSequences(value);
  5457. // Skip properties with double quotes and sequences like \" \' in their names
  5458. // See 'mXSS Attacks: Attacking well-secured Web-Applications by using innerHTML Mutations'
  5459. // https://cure53.de/fp170.pdf
  5460. if (name.indexOf(invisibleChar) !== -1 || name.indexOf('"') !== -1) {
  5461. continue;
  5462. }
  5463. // Don't allow behavior name or expression/comments within the values
  5464. if (!settings.allow_script_urls && (name === 'behavior' || /expression\s*\(|\/\*|\*\//.test(value))) {
  5465. continue;
  5466. }
  5467. // Opera will produce 700 instead of bold in their style values
  5468. if (name === 'font-weight' && value === '700') {
  5469. value = 'bold';
  5470. }
  5471. else if (name === 'color' || name === 'background-color') { // Lowercase colors like RED
  5472. value = value.toLowerCase();
  5473. }
  5474. // Convert RGB colors to HEX
  5475. if (getColorFormat(value) === 'rgb') {
  5476. fromString(value).each((rgba) => {
  5477. value = rgbaToHexString(toString(rgba)).toLowerCase();
  5478. });
  5479. }
  5480. // Convert URLs and force them into url('value') format
  5481. value = value.replace(urlOrStrRegExp, processUrl);
  5482. styles[name] = isEncoded ? decode(value, true) : value;
  5483. }
  5484. }
  5485. // Compress the styles to reduce it's size for example IE will expand styles
  5486. compress('border', '', true);
  5487. compress('border', '-width');
  5488. compress('border', '-color');
  5489. compress('border', '-style');
  5490. compress('padding', '');
  5491. compress('margin', '');
  5492. compress2('border', 'border-width', 'border-style', 'border-color');
  5493. // Remove pointless border, IE produces these
  5494. if (styles.border === 'medium none') {
  5495. delete styles.border;
  5496. }
  5497. // IE 11 will produce a border-image: none when getting the style attribute from <p style="border: 1px solid red"></p>
  5498. // So let us assume it shouldn't be there
  5499. if (styles['border-image'] === 'none') {
  5500. delete styles['border-image'];
  5501. }
  5502. }
  5503. return styles;
  5504. },
  5505. /**
  5506. * Serializes the specified style object into a string.
  5507. *
  5508. * @method serialize
  5509. * @param {Object} styles Object to serialize as string. For example: `{ border: '1px solid red' }`
  5510. * @param {String} elementName Optional element name, if specified only the styles that matches the schema will be serialized.
  5511. * @return {String} String representation of the style object. For example: `border: 1px solid red`
  5512. */
  5513. serialize: (styles, elementName) => {
  5514. let css = '';
  5515. const serializeStyles = (elemName, validStyleList) => {
  5516. const styleList = validStyleList[elemName];
  5517. if (styleList) {
  5518. for (let i = 0, l = styleList.length; i < l; i++) {
  5519. const name = styleList[i];
  5520. const value = styles[name];
  5521. if (value) {
  5522. css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
  5523. }
  5524. }
  5525. }
  5526. };
  5527. const isValid = (name, elemName) => {
  5528. if (!invalidStyles || !elemName) {
  5529. return true;
  5530. }
  5531. let styleMap = invalidStyles['*'];
  5532. if (styleMap && styleMap[name]) {
  5533. return false;
  5534. }
  5535. styleMap = invalidStyles[elemName];
  5536. return !(styleMap && styleMap[name]);
  5537. };
  5538. // Serialize styles according to schema
  5539. if (elementName && validStyles) {
  5540. // Serialize global styles and element specific styles
  5541. serializeStyles('*', validStyles);
  5542. serializeStyles(elementName, validStyles);
  5543. }
  5544. else {
  5545. // Output the styles in the order they are inside the object
  5546. each$d(styles, (value, name) => {
  5547. if (value && isValid(name, elementName)) {
  5548. css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
  5549. }
  5550. });
  5551. }
  5552. return css;
  5553. }
  5554. };
  5555. return self;
  5556. };
  5557. // Note: The values here aren't used. This is just used as a hash map to see if the key exists
  5558. const deprecated = {
  5559. keyLocation: true,
  5560. layerX: true,
  5561. layerY: true,
  5562. returnValue: true,
  5563. webkitMovementX: true,
  5564. webkitMovementY: true,
  5565. keyIdentifier: true,
  5566. mozPressure: true
  5567. };
  5568. // Note: We can't rely on `instanceof` here as it won't work if the event was fired from another window.
  5569. // Additionally, the constructor name might be `MouseEvent` or similar so we can't rely on the constructor name.
  5570. const isNativeEvent = (event) => event instanceof Event || isFunction(event.initEvent);
  5571. // Checks if it is our own isDefaultPrevented function
  5572. const hasIsDefaultPrevented = (event) => event.isDefaultPrevented === always || event.isDefaultPrevented === never;
  5573. // An event needs normalizing if it doesn't have the prevent default function or if it's a native event
  5574. const needsNormalizing = (event) => isNullable(event.preventDefault) || isNativeEvent(event);
  5575. const clone$2 = (originalEvent, data) => {
  5576. const event = data !== null && data !== void 0 ? data : {};
  5577. // Copy all properties from the original event
  5578. for (const name in originalEvent) {
  5579. // Some properties are deprecated and produces a warning so don't include them
  5580. if (!has$2(deprecated, name)) {
  5581. event[name] = originalEvent[name];
  5582. }
  5583. }
  5584. // The composed path can't be cloned, so delegate instead
  5585. if (isNonNullable(originalEvent.composedPath)) {
  5586. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  5587. event.composedPath = () => originalEvent.composedPath();
  5588. }
  5589. // The getModifierState won't work when cloned, so delegate instead
  5590. if (isNonNullable(originalEvent.getModifierState)) {
  5591. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  5592. event.getModifierState = (keyArg) => originalEvent.getModifierState(keyArg);
  5593. }
  5594. // The getTargetRanges won't work when cloned, so delegate instead
  5595. if (isNonNullable(originalEvent.getTargetRanges)) {
  5596. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  5597. event.getTargetRanges = () => originalEvent.getTargetRanges();
  5598. }
  5599. return event;
  5600. };
  5601. const normalize$3 = (type, originalEvent, fallbackTarget, data) => {
  5602. var _a;
  5603. const event = clone$2(originalEvent, data);
  5604. event.type = type;
  5605. // Normalize target IE uses srcElement
  5606. if (isNullable(event.target)) {
  5607. event.target = (_a = event.srcElement) !== null && _a !== void 0 ? _a : fallbackTarget;
  5608. }
  5609. if (needsNormalizing(originalEvent)) {
  5610. // Add preventDefault method
  5611. event.preventDefault = () => {
  5612. event.defaultPrevented = true;
  5613. event.isDefaultPrevented = always;
  5614. // Execute preventDefault on the original event object
  5615. if (isFunction(originalEvent.preventDefault)) {
  5616. originalEvent.preventDefault();
  5617. }
  5618. };
  5619. // Add stopPropagation
  5620. event.stopPropagation = () => {
  5621. event.cancelBubble = true;
  5622. event.isPropagationStopped = always;
  5623. // Execute stopPropagation on the original event object
  5624. if (isFunction(originalEvent.stopPropagation)) {
  5625. originalEvent.stopPropagation();
  5626. }
  5627. };
  5628. // Add stopImmediatePropagation
  5629. event.stopImmediatePropagation = () => {
  5630. event.isImmediatePropagationStopped = always;
  5631. event.stopPropagation();
  5632. };
  5633. // Add event delegation states
  5634. if (!hasIsDefaultPrevented(event)) {
  5635. event.isDefaultPrevented = event.defaultPrevented === true ? always : never;
  5636. event.isPropagationStopped = event.cancelBubble === true ? always : never;
  5637. event.isImmediatePropagationStopped = never;
  5638. }
  5639. }
  5640. return event;
  5641. };
  5642. /**
  5643. * This class wraps the browsers native event logic with more convenient methods.
  5644. *
  5645. * @class tinymce.dom.EventUtils
  5646. */
  5647. const eventExpandoPrefix = 'mce-data-';
  5648. const mouseEventRe = /^(?:mouse|contextmenu)|click/;
  5649. /**
  5650. * Binds a native event to a callback on the speified target.
  5651. */
  5652. const addEvent = (target, name, callback, capture) => {
  5653. target.addEventListener(name, callback, capture || false);
  5654. };
  5655. /**
  5656. * Unbinds a native event callback on the specified target.
  5657. */
  5658. const removeEvent = (target, name, callback, capture) => {
  5659. target.removeEventListener(name, callback, capture || false);
  5660. };
  5661. const isMouseEvent = (event) => isNonNullable(event) && mouseEventRe.test(event.type);
  5662. /**
  5663. * Normalizes a native event object or just adds the event specific methods on a custom event.
  5664. */
  5665. const fix = (originalEvent, data) => {
  5666. const event = normalize$3(originalEvent.type, originalEvent, document, data);
  5667. // Calculate pageX/Y if missing and clientX/Y available
  5668. if (isMouseEvent(originalEvent) && isUndefined(originalEvent.pageX) && !isUndefined(originalEvent.clientX)) {
  5669. const eventDoc = event.target.ownerDocument || document;
  5670. const doc = eventDoc.documentElement;
  5671. const body = eventDoc.body;
  5672. const mouseEvent = event;
  5673. mouseEvent.pageX = originalEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
  5674. (doc && doc.clientLeft || body && body.clientLeft || 0);
  5675. mouseEvent.pageY = originalEvent.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) -
  5676. (doc && doc.clientTop || body && body.clientTop || 0);
  5677. }
  5678. return event;
  5679. };
  5680. /**
  5681. * Bind a DOMContentLoaded event across browsers and executes the callback once the page DOM is initialized.
  5682. * It will also set/check the domLoaded state of the event_utils instance so ready isn't called multiple times.
  5683. */
  5684. const bindOnReady = (win, callback, eventUtils) => {
  5685. const doc = win.document, event = { type: 'ready' };
  5686. if (eventUtils.domLoaded) {
  5687. callback(event);
  5688. return;
  5689. }
  5690. const isDocReady = () => {
  5691. // Check complete or interactive state if there is a body
  5692. // element on some iframes IE 8 will produce a null body
  5693. return doc.readyState === 'complete' || (doc.readyState === 'interactive' && doc.body);
  5694. };
  5695. // Gets called when the DOM is ready
  5696. const readyHandler = () => {
  5697. removeEvent(win, 'DOMContentLoaded', readyHandler);
  5698. removeEvent(win, 'load', readyHandler);
  5699. if (!eventUtils.domLoaded) {
  5700. eventUtils.domLoaded = true;
  5701. callback(event);
  5702. }
  5703. // Clean memory for IE
  5704. win = null;
  5705. };
  5706. if (isDocReady()) {
  5707. readyHandler();
  5708. }
  5709. else {
  5710. addEvent(win, 'DOMContentLoaded', readyHandler);
  5711. }
  5712. // Fallback if any of the above methods should fail for some odd reason
  5713. if (!eventUtils.domLoaded) {
  5714. addEvent(win, 'load', readyHandler);
  5715. }
  5716. };
  5717. /**
  5718. * This class enables you to bind/unbind native events to elements and normalize it's behavior across browsers.
  5719. */
  5720. class EventUtils {
  5721. constructor() {
  5722. // State if the DOMContentLoaded was executed or not
  5723. this.domLoaded = false;
  5724. this.events = {};
  5725. this.count = 1;
  5726. this.expando = eventExpandoPrefix + (+new Date()).toString(32);
  5727. this.hasFocusIn = 'onfocusin' in document.documentElement;
  5728. this.count = 1;
  5729. }
  5730. bind(target, names, callback, scope) {
  5731. const self = this;
  5732. let callbackList;
  5733. const win = window;
  5734. // Native event handler function patches the event and executes the callbacks for the expando
  5735. const defaultNativeHandler = (evt) => {
  5736. self.executeHandlers(fix(evt || win.event), id);
  5737. };
  5738. // Don't bind to text nodes or comments
  5739. if (!target || isText$b(target) || isComment(target)) {
  5740. return callback;
  5741. }
  5742. // Create or get events id for the target
  5743. let id;
  5744. if (!target[self.expando]) {
  5745. id = self.count++;
  5746. target[self.expando] = id;
  5747. self.events[id] = {};
  5748. }
  5749. else {
  5750. id = target[self.expando];
  5751. }
  5752. // Setup the specified scope or use the target as a default
  5753. scope = scope || target;
  5754. // Split names and bind each event, enables you to bind multiple events with one call
  5755. const namesList = names.split(' ');
  5756. let i = namesList.length;
  5757. while (i--) {
  5758. let name = namesList[i];
  5759. let nativeHandler = defaultNativeHandler;
  5760. let capture = false;
  5761. let fakeName = false;
  5762. // Use ready instead of DOMContentLoaded
  5763. if (name === 'DOMContentLoaded') {
  5764. name = 'ready';
  5765. }
  5766. // DOM is already ready
  5767. if (self.domLoaded && name === 'ready' && target.readyState === 'complete') {
  5768. callback.call(scope, fix({ type: name }));
  5769. continue;
  5770. }
  5771. // Fake bubbling of focusin/focusout
  5772. if (!self.hasFocusIn && (name === 'focusin' || name === 'focusout')) {
  5773. capture = true;
  5774. fakeName = name === 'focusin' ? 'focus' : 'blur';
  5775. nativeHandler = (evt) => {
  5776. const event = fix(evt || win.event);
  5777. event.type = event.type === 'focus' ? 'focusin' : 'focusout';
  5778. self.executeHandlers(event, id);
  5779. };
  5780. }
  5781. // Setup callback list and bind native event
  5782. callbackList = self.events[id][name];
  5783. if (!callbackList) {
  5784. self.events[id][name] = callbackList = [{ func: callback, scope }];
  5785. callbackList.fakeName = fakeName;
  5786. callbackList.capture = capture;
  5787. // callbackList.callback = callback;
  5788. // Add the nativeHandler to the callback list so that we can later unbind it
  5789. callbackList.nativeHandler = nativeHandler;
  5790. // Check if the target has native events support
  5791. if (name === 'ready') {
  5792. bindOnReady(target, nativeHandler, self);
  5793. }
  5794. else {
  5795. addEvent(target, fakeName || name, nativeHandler, capture);
  5796. }
  5797. }
  5798. else {
  5799. if (name === 'ready' && self.domLoaded) {
  5800. callback(fix({ type: name }));
  5801. }
  5802. else {
  5803. // If it already has an native handler then just push the callback
  5804. callbackList.push({ func: callback, scope });
  5805. }
  5806. }
  5807. }
  5808. target = callbackList = null; // Clean memory for IE
  5809. return callback;
  5810. }
  5811. unbind(target, names, callback) {
  5812. // Don't bind to text nodes or comments
  5813. if (!target || isText$b(target) || isComment(target)) {
  5814. return this;
  5815. }
  5816. // Unbind event or events if the target has the expando
  5817. const id = target[this.expando];
  5818. if (id) {
  5819. let eventMap = this.events[id];
  5820. // Specific callback
  5821. if (names) {
  5822. const namesList = names.split(' ');
  5823. let i = namesList.length;
  5824. while (i--) {
  5825. const name = namesList[i];
  5826. const callbackList = eventMap[name];
  5827. // Unbind the event if it exists in the map
  5828. if (callbackList) {
  5829. // Remove specified callback
  5830. if (callback) {
  5831. let ci = callbackList.length;
  5832. while (ci--) {
  5833. if (callbackList[ci].func === callback) {
  5834. const nativeHandler = callbackList.nativeHandler;
  5835. const fakeName = callbackList.fakeName, capture = callbackList.capture;
  5836. // Clone callbackList since unbind inside a callback would otherwise break the handlers loop
  5837. const newCallbackList = callbackList.slice(0, ci).concat(callbackList.slice(ci + 1));
  5838. newCallbackList.nativeHandler = nativeHandler;
  5839. newCallbackList.fakeName = fakeName;
  5840. newCallbackList.capture = capture;
  5841. eventMap[name] = newCallbackList;
  5842. }
  5843. }
  5844. }
  5845. // Remove all callbacks if there isn't a specified callback or there is no callbacks left
  5846. if (!callback || callbackList.length === 0) {
  5847. delete eventMap[name];
  5848. removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture);
  5849. }
  5850. }
  5851. }
  5852. }
  5853. else {
  5854. // All events for a specific element
  5855. each$d(eventMap, (callbackList, name) => {
  5856. removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture);
  5857. });
  5858. eventMap = {};
  5859. }
  5860. // Check if object is empty, if it isn't then we won't remove the expando map
  5861. for (const name in eventMap) {
  5862. if (has$2(eventMap, name)) {
  5863. return this;
  5864. }
  5865. }
  5866. // Delete event object
  5867. delete this.events[id];
  5868. // Remove expando from target
  5869. try {
  5870. // IE will fail here since it can't delete properties from window
  5871. delete target[this.expando];
  5872. }
  5873. catch (_a) {
  5874. // IE will set it to null
  5875. target[this.expando] = null;
  5876. }
  5877. }
  5878. return this;
  5879. }
  5880. /**
  5881. * Fires the specified event on the specified target.
  5882. * <br>
  5883. * <em>Deprecated in TinyMCE 6.0 and has been marked for removal in TinyMCE 7.0. Use <code>dispatch</code> instead.</em>
  5884. *
  5885. * @method fire
  5886. * @param {Object} target Target node/window or custom object.
  5887. * @param {String} name Event name to fire.
  5888. * @param {Object} args Optional arguments to send to the observers.
  5889. * @return {EventUtils} Event utils instance.
  5890. * @deprecated Use dispatch() instead
  5891. */
  5892. fire(target, name, args) {
  5893. return this.dispatch(target, name, args);
  5894. }
  5895. /**
  5896. * Dispatches the specified event on the specified target.
  5897. *
  5898. * @method dispatch
  5899. * @param {Node/window} target Target node/window or custom object.
  5900. * @param {String} name Event name to dispatch.
  5901. * @param {Object} args Optional arguments to send to the observers.
  5902. * @return {EventUtils} Event utils instance.
  5903. */
  5904. dispatch(target, name, args) {
  5905. // Don't bind to text nodes or comments
  5906. if (!target || isText$b(target) || isComment(target)) {
  5907. return this;
  5908. }
  5909. // Build event object by patching the args
  5910. const event = fix({ type: name, target }, args);
  5911. do {
  5912. // Found an expando that means there is listeners to execute
  5913. const id = target[this.expando];
  5914. if (id) {
  5915. this.executeHandlers(event, id);
  5916. }
  5917. // Walk up the DOM
  5918. target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow;
  5919. } while (target && !event.isPropagationStopped());
  5920. return this;
  5921. }
  5922. /**
  5923. * Removes all bound event listeners for the specified target. This will also remove any bound
  5924. * listeners to child nodes within that target.
  5925. *
  5926. * @method clean
  5927. * @param {Object} target Target node/window object.
  5928. * @return {EventUtils} Event utils instance.
  5929. */
  5930. clean(target) {
  5931. // Don't bind to text nodes or comments
  5932. if (!target || isText$b(target) || isComment(target)) {
  5933. return this;
  5934. }
  5935. // Unbind any element on the specified target
  5936. if (target[this.expando]) {
  5937. this.unbind(target);
  5938. }
  5939. // Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children
  5940. if (!target.getElementsByTagName) {
  5941. target = target.document;
  5942. }
  5943. // Remove events from each child element
  5944. if (target && target.getElementsByTagName) {
  5945. this.unbind(target);
  5946. const children = target.getElementsByTagName('*');
  5947. let i = children.length;
  5948. while (i--) {
  5949. target = children[i];
  5950. if (target[this.expando]) {
  5951. this.unbind(target);
  5952. }
  5953. }
  5954. }
  5955. return this;
  5956. }
  5957. /**
  5958. * Destroys the event object. Call this to remove memory leaks.
  5959. */
  5960. destroy() {
  5961. this.events = {};
  5962. }
  5963. // Legacy function for canceling events
  5964. cancel(e) {
  5965. if (e) {
  5966. e.preventDefault();
  5967. e.stopImmediatePropagation();
  5968. }
  5969. return false;
  5970. }
  5971. /**
  5972. * Executes all event handler callbacks for a specific event.
  5973. *
  5974. * @private
  5975. * @param {Event} evt Event object.
  5976. * @param {String} id Expando id value to look for.
  5977. */
  5978. executeHandlers(evt, id) {
  5979. const container = this.events[id];
  5980. const callbackList = container && container[evt.type];
  5981. if (callbackList) {
  5982. for (let i = 0, l = callbackList.length; i < l; i++) {
  5983. const callback = callbackList[i];
  5984. // Check if callback exists might be removed if a unbind is called inside the callback
  5985. if (callback && callback.func.call(callback.scope, evt) === false) {
  5986. evt.preventDefault();
  5987. }
  5988. // Should we stop propagation to immediate listeners
  5989. if (evt.isImmediatePropagationStopped()) {
  5990. return;
  5991. }
  5992. }
  5993. }
  5994. }
  5995. }
  5996. EventUtils.Event = new EventUtils();
  5997. /**
  5998. * Utility class for various DOM manipulation and retrieval functions.
  5999. *
  6000. * @class tinymce.dom.DOMUtils
  6001. * @example
  6002. * // Add a class to an element by id in the page
  6003. * tinymce.DOM.addClass('someid', 'someclass');
  6004. *
  6005. * // Add a class to an element by id inside the editor
  6006. * tinymce.activeEditor.dom.addClass('someid', 'someclass');
  6007. */
  6008. // Shorten names
  6009. const each$a = Tools.each;
  6010. const grep = Tools.grep;
  6011. const internalStyleName = 'data-mce-style';
  6012. const numericalCssMap = Tools.makeMap('fill-opacity font-weight line-height opacity orphans widows z-index zoom', ' ');
  6013. const legacySetAttribute = (elm, name, value) => {
  6014. if (isNullable(value) || value === '') {
  6015. remove$9(elm, name);
  6016. }
  6017. else {
  6018. set$4(elm, name, value);
  6019. }
  6020. };
  6021. // Convert camel cased names back to hyphenated names
  6022. const camelCaseToHyphens = (name) => name.replace(/[A-Z]/g, (v) => '-' + v.toLowerCase());
  6023. const findNodeIndex = (node, normalized) => {
  6024. let idx = 0;
  6025. if (node) {
  6026. for (let lastNodeType = node.nodeType, tempNode = node.previousSibling; tempNode; tempNode = tempNode.previousSibling) {
  6027. const nodeType = tempNode.nodeType;
  6028. // Normalize text nodes
  6029. if (normalized && isText$b(tempNode)) {
  6030. if (nodeType === lastNodeType || !tempNode.data.length) {
  6031. continue;
  6032. }
  6033. }
  6034. idx++;
  6035. lastNodeType = nodeType;
  6036. }
  6037. }
  6038. return idx;
  6039. };
  6040. const updateInternalStyleAttr = (styles, elm) => {
  6041. const rawValue = get$9(elm, 'style');
  6042. const value = styles.serialize(styles.parse(rawValue), name(elm));
  6043. legacySetAttribute(elm, internalStyleName, value);
  6044. };
  6045. const convertStyleToString = (cssValue, cssName) => {
  6046. if (isNumber(cssValue)) {
  6047. return has$2(numericalCssMap, cssName) ? cssValue + '' : cssValue + 'px';
  6048. }
  6049. else {
  6050. return cssValue;
  6051. }
  6052. };
  6053. const applyStyle$1 = ($elm, cssName, cssValue) => {
  6054. const normalizedName = camelCaseToHyphens(cssName);
  6055. if (isNullable(cssValue) || cssValue === '') {
  6056. remove$7($elm, normalizedName);
  6057. }
  6058. else {
  6059. set$2($elm, normalizedName, convertStyleToString(cssValue, normalizedName));
  6060. }
  6061. };
  6062. const setupAttrHooks = (styles, settings, getContext) => {
  6063. const keepValues = settings.keep_values;
  6064. const keepUrlHook = {
  6065. set: (elm, value, name) => {
  6066. const sugarElm = SugarElement.fromDom(elm);
  6067. if (isFunction(settings.url_converter) && isNonNullable(value)) {
  6068. value = settings.url_converter.call(settings.url_converter_scope || getContext(), String(value), name, elm);
  6069. }
  6070. const internalName = 'data-mce-' + name;
  6071. legacySetAttribute(sugarElm, internalName, value);
  6072. legacySetAttribute(sugarElm, name, value);
  6073. },
  6074. get: (elm, name) => {
  6075. const sugarElm = SugarElement.fromDom(elm);
  6076. return get$9(sugarElm, 'data-mce-' + name) || get$9(sugarElm, name);
  6077. }
  6078. };
  6079. const attrHooks = {
  6080. style: {
  6081. set: (elm, value) => {
  6082. const sugarElm = SugarElement.fromDom(elm);
  6083. if (keepValues) {
  6084. legacySetAttribute(sugarElm, internalStyleName, value);
  6085. }
  6086. remove$9(sugarElm, 'style');
  6087. // If setting a style then delegate to the css api, otherwise
  6088. // this will cause issues when using a content security policy
  6089. if (isString(value)) {
  6090. setAll(sugarElm, styles.parse(value));
  6091. }
  6092. },
  6093. get: (elm) => {
  6094. const sugarElm = SugarElement.fromDom(elm);
  6095. const value = get$9(sugarElm, internalStyleName) || get$9(sugarElm, 'style');
  6096. return styles.serialize(styles.parse(value), name(sugarElm));
  6097. }
  6098. }
  6099. };
  6100. if (keepValues) {
  6101. attrHooks.href = attrHooks.src = keepUrlHook;
  6102. }
  6103. return attrHooks;
  6104. };
  6105. /**
  6106. * Constructs a new DOMUtils instance. Consult the TinyMCE Documentation for more details on settings etc for this class.
  6107. *
  6108. * @private
  6109. * @constructor
  6110. * @method DOMUtils
  6111. * @param {Document} doc Document reference to bind the utility class to.
  6112. * @param {settings} settings Optional settings collection.
  6113. */
  6114. const DOMUtils = (doc, settings = {}) => {
  6115. const addedStyles = {};
  6116. const win = window;
  6117. const files = {};
  6118. let counter = 0;
  6119. const stdMode = true;
  6120. const boxModel = true;
  6121. const styleSheetLoader = instance.forElement(SugarElement.fromDom(doc), {
  6122. contentCssCors: settings.contentCssCors,
  6123. referrerPolicy: settings.referrerPolicy,
  6124. crossOrigin: (url) => {
  6125. const crossOrigin = settings.crossOrigin;
  6126. if (isFunction(crossOrigin)) {
  6127. return crossOrigin(url, 'stylesheet');
  6128. }
  6129. else {
  6130. return undefined;
  6131. }
  6132. }
  6133. });
  6134. const boundEvents = [];
  6135. const schema = settings.schema ? settings.schema : Schema({});
  6136. const styles = Styles({
  6137. url_converter: settings.url_converter,
  6138. url_converter_scope: settings.url_converter_scope,
  6139. }, settings.schema);
  6140. const events = settings.ownEvents ? new EventUtils() : EventUtils.Event;
  6141. const blockElementsMap = schema.getBlockElements();
  6142. /**
  6143. * Returns true/false if the specified element is a block element or not.
  6144. *
  6145. * @method isBlock
  6146. * @param {Node/String} node Element/Node to check.
  6147. * @return {Boolean} True/False state if the node is a block element or not.
  6148. */
  6149. const isBlock = (node) => {
  6150. if (isString(node)) {
  6151. return has$2(blockElementsMap, node);
  6152. }
  6153. else {
  6154. return isElement$7(node) && (has$2(blockElementsMap, node.nodeName) || isTransparentBlock(schema, node));
  6155. }
  6156. };
  6157. const get = (elm) => elm && doc && isString(elm)
  6158. ? doc.getElementById(elm)
  6159. : elm;
  6160. const _get = (elm) => {
  6161. const value = get(elm);
  6162. return isNonNullable(value) ? SugarElement.fromDom(value) : null;
  6163. };
  6164. const getAttrib = (elm, name, defaultVal = '') => {
  6165. let value;
  6166. const $elm = _get(elm);
  6167. if (isNonNullable($elm) && isElement$8($elm)) {
  6168. const hook = attrHooks[name];
  6169. if (hook && hook.get) {
  6170. value = hook.get($elm.dom, name);
  6171. }
  6172. else {
  6173. value = get$9($elm, name);
  6174. }
  6175. }
  6176. return isNonNullable(value) ? value : defaultVal;
  6177. };
  6178. const getAttribs = (elm) => {
  6179. const node = get(elm);
  6180. return isNullable(node) ? [] : node.attributes;
  6181. };
  6182. const setAttrib = (elm, name, value) => {
  6183. run(elm, (e) => {
  6184. if (isElement$7(e)) {
  6185. const $elm = SugarElement.fromDom(e);
  6186. const val = value === '' ? null : value;
  6187. const originalValue = get$9($elm, name);
  6188. const hook = attrHooks[name];
  6189. if (hook && hook.set) {
  6190. hook.set($elm.dom, val, name);
  6191. }
  6192. else {
  6193. legacySetAttribute($elm, name, val);
  6194. }
  6195. if (originalValue !== val && settings.onSetAttrib) {
  6196. settings.onSetAttrib({
  6197. attrElm: $elm.dom, // We lie here to not break backwards compatibility
  6198. attrName: name,
  6199. attrValue: val
  6200. });
  6201. }
  6202. }
  6203. });
  6204. };
  6205. const clone = (node, deep) => {
  6206. return node.cloneNode(deep);
  6207. };
  6208. const getRoot = () => settings.root_element || doc.body;
  6209. const getViewPort = (argWin) => {
  6210. const vp = getBounds(argWin);
  6211. // Returns viewport size excluding scrollbars
  6212. return {
  6213. x: vp.x,
  6214. y: vp.y,
  6215. w: vp.width,
  6216. h: vp.height
  6217. };
  6218. };
  6219. const getPos$1 = (elm, rootElm) => getPos(doc.body, get(elm), rootElm);
  6220. const setStyle = (elm, name, value) => {
  6221. run(elm, (e) => {
  6222. const $elm = SugarElement.fromDom(e);
  6223. applyStyle$1($elm, name, value);
  6224. if (settings.update_styles) {
  6225. updateInternalStyleAttr(styles, $elm);
  6226. }
  6227. });
  6228. };
  6229. const setStyles = (elm, stylesArg) => {
  6230. run(elm, (e) => {
  6231. const $elm = SugarElement.fromDom(e);
  6232. each$d(stylesArg, (v, n) => {
  6233. applyStyle$1($elm, n, v);
  6234. });
  6235. if (settings.update_styles) {
  6236. updateInternalStyleAttr(styles, $elm);
  6237. }
  6238. });
  6239. };
  6240. const getStyle = (elm, name, computed) => {
  6241. const $elm = get(elm);
  6242. if (isNullable($elm) || (!isHTMLElement($elm) && !isSVGElement($elm))) {
  6243. return undefined;
  6244. }
  6245. if (computed) {
  6246. return get$7(SugarElement.fromDom($elm), camelCaseToHyphens(name));
  6247. }
  6248. else {
  6249. // Camelcase it, if needed
  6250. name = name.replace(/-(\D)/g, (a, b) => b.toUpperCase());
  6251. if (name === 'float') {
  6252. name = 'cssFloat';
  6253. }
  6254. return $elm.style ? $elm.style[name] : undefined;
  6255. }
  6256. };
  6257. const getSize = (elm) => {
  6258. const $elm = get(elm);
  6259. if (!$elm) {
  6260. return { w: 0, h: 0 };
  6261. }
  6262. let w = getStyle($elm, 'width');
  6263. let h = getStyle($elm, 'height');
  6264. // Non pixel value, then force offset/clientWidth
  6265. if (!w || w.indexOf('px') === -1) {
  6266. w = '0';
  6267. }
  6268. // Non pixel value, then force offset/clientWidth
  6269. if (!h || h.indexOf('px') === -1) {
  6270. h = '0';
  6271. }
  6272. return {
  6273. w: parseInt(w, 10) || $elm.offsetWidth || $elm.clientWidth,
  6274. h: parseInt(h, 10) || $elm.offsetHeight || $elm.clientHeight
  6275. };
  6276. };
  6277. const getRect = (elm) => {
  6278. const $elm = get(elm);
  6279. const pos = getPos$1($elm);
  6280. const size = getSize($elm);
  6281. return {
  6282. x: pos.x, y: pos.y,
  6283. w: size.w, h: size.h
  6284. };
  6285. };
  6286. const is = (elm, selector) => {
  6287. if (!elm) {
  6288. return false;
  6289. }
  6290. const elms = isArray$1(elm) ? elm : [elm];
  6291. return exists(elms, (e) => {
  6292. return is$2(SugarElement.fromDom(e), selector);
  6293. });
  6294. };
  6295. const getParents = (elm, selector, root, collect) => {
  6296. const result = [];
  6297. let node = get(elm);
  6298. collect = collect === undefined;
  6299. // Default root on inline mode
  6300. const resolvedRoot = root || (getRoot().nodeName !== 'BODY' ? getRoot().parentNode : null);
  6301. // Wrap node name as func
  6302. if (isString(selector)) {
  6303. if (selector === '*') {
  6304. selector = isElement$7;
  6305. }
  6306. else {
  6307. const selectorVal = selector;
  6308. selector = (node) => is(node, selectorVal);
  6309. }
  6310. }
  6311. while (node) {
  6312. // TODO: Remove nullable check once TINY-6599 is complete
  6313. if (node === resolvedRoot || isNullable(node.nodeType) || isDocument$1(node) || isDocumentFragment(node)) {
  6314. break;
  6315. }
  6316. if (!selector || selector(node)) {
  6317. if (collect) {
  6318. result.push(node);
  6319. }
  6320. else {
  6321. return [node];
  6322. }
  6323. }
  6324. node = node.parentNode;
  6325. }
  6326. return collect ? result : null;
  6327. };
  6328. const getParent = (node, selector, root) => {
  6329. const parents = getParents(node, selector, root, false);
  6330. return parents && parents.length > 0 ? parents[0] : null;
  6331. };
  6332. const _findSib = (node, selector, name) => {
  6333. let func = selector;
  6334. if (node) {
  6335. // If expression make a function of it using is
  6336. if (isString(selector)) {
  6337. func = (node) => {
  6338. return is(node, selector);
  6339. };
  6340. }
  6341. // Loop all siblings
  6342. for (let tempNode = node[name]; tempNode; tempNode = tempNode[name]) {
  6343. if (isFunction(func) && func(tempNode)) {
  6344. return tempNode;
  6345. }
  6346. }
  6347. }
  6348. return null;
  6349. };
  6350. const getNext = (node, selector) => _findSib(node, selector, 'nextSibling');
  6351. const getPrev = (node, selector) => _findSib(node, selector, 'previousSibling');
  6352. const isParentNode = (node) => isFunction(node.querySelectorAll);
  6353. const select = (selector, scope) => {
  6354. var _a, _b;
  6355. const elm = (_b = (_a = get(scope)) !== null && _a !== void 0 ? _a : settings.root_element) !== null && _b !== void 0 ? _b : doc;
  6356. return isParentNode(elm) ? from(elm.querySelectorAll(selector)) : [];
  6357. };
  6358. const run = function (elm, func, scope) {
  6359. const context = scope !== null && scope !== void 0 ? scope : this;
  6360. if (isArray$1(elm)) {
  6361. const result = [];
  6362. each$a(elm, (e, i) => {
  6363. const node = get(e);
  6364. if (node) {
  6365. result.push(func.call(context, node, i));
  6366. }
  6367. });
  6368. return result;
  6369. }
  6370. else {
  6371. const node = get(elm);
  6372. return !node ? false : func.call(context, node);
  6373. }
  6374. };
  6375. const setAttribs = (elm, attrs) => {
  6376. run(elm, ($elm) => {
  6377. each$d(attrs, (value, name) => {
  6378. setAttrib($elm, name, value);
  6379. });
  6380. });
  6381. };
  6382. const setHTML = (elm, html) => {
  6383. run(elm, (e) => {
  6384. const $elm = SugarElement.fromDom(e);
  6385. set$3($elm, html);
  6386. });
  6387. };
  6388. const add = (parentElm, name, attrs, html, create) => run(parentElm, (parentElm) => {
  6389. const newElm = isString(name) ? doc.createElement(name) : name;
  6390. if (isNonNullable(attrs)) {
  6391. setAttribs(newElm, attrs);
  6392. }
  6393. if (html) {
  6394. if (!isString(html) && html.nodeType) {
  6395. newElm.appendChild(html);
  6396. }
  6397. else if (isString(html)) {
  6398. setHTML(newElm, html);
  6399. }
  6400. }
  6401. return !create ? parentElm.appendChild(newElm) : newElm;
  6402. });
  6403. const create = (name, attrs, html) => add(doc.createElement(name), name, attrs, html, true);
  6404. const decode = Entities.decode;
  6405. const encode = Entities.encodeAllRaw;
  6406. const createHTML = (name, attrs, html = '') => {
  6407. let outHtml = '<' + name;
  6408. for (const key in attrs) {
  6409. if (hasNonNullableKey(attrs, key)) {
  6410. outHtml += ' ' + key + '="' + encode(attrs[key]) + '"';
  6411. }
  6412. }
  6413. if (isEmpty$5(html) && has$2(schema.getVoidElements(), name)) {
  6414. return outHtml + ' />';
  6415. }
  6416. else {
  6417. return outHtml + '>' + html + '</' + name + '>';
  6418. }
  6419. };
  6420. const createFragment = (html) => {
  6421. const container = doc.createElement('div');
  6422. const frag = doc.createDocumentFragment();
  6423. // Append the container to the fragment so as to remove it from
  6424. // the current document context
  6425. frag.appendChild(container);
  6426. if (html) {
  6427. container.innerHTML = html;
  6428. }
  6429. let node;
  6430. while ((node = container.firstChild)) {
  6431. frag.appendChild(node);
  6432. }
  6433. // Remove the container now that all the children have been transferred
  6434. frag.removeChild(container);
  6435. return frag;
  6436. };
  6437. const remove = (node, keepChildren) => {
  6438. return run(node, (n) => {
  6439. const $node = SugarElement.fromDom(n);
  6440. if (keepChildren) {
  6441. // Unwrap but don't keep any empty text nodes
  6442. each$e(children$1($node), (child) => {
  6443. if (isText$c(child) && child.dom.length === 0) {
  6444. remove$8(child);
  6445. }
  6446. else {
  6447. before$4($node, child);
  6448. }
  6449. });
  6450. }
  6451. remove$8($node);
  6452. return $node.dom;
  6453. });
  6454. };
  6455. const removeAllAttribs = (e) => run(e, (e) => {
  6456. const attrs = e.attributes;
  6457. for (let i = attrs.length - 1; i >= 0; i--) {
  6458. e.removeAttributeNode(attrs.item(i));
  6459. }
  6460. });
  6461. const parseStyle = (cssText) => styles.parse(cssText);
  6462. const serializeStyle = (stylesArg, name) => styles.serialize(stylesArg, name);
  6463. const addStyle = (cssText) => {
  6464. // Prevent inline from loading the same styles twice
  6465. if (self !== DOMUtils.DOM && doc === document) {
  6466. if (addedStyles[cssText]) {
  6467. return;
  6468. }
  6469. addedStyles[cssText] = true;
  6470. }
  6471. // Create style element if needed
  6472. let styleElm = doc.getElementById('mceDefaultStyles');
  6473. if (!styleElm) {
  6474. styleElm = doc.createElement('style');
  6475. styleElm.id = 'mceDefaultStyles';
  6476. styleElm.type = 'text/css';
  6477. const head = doc.head;
  6478. if (head.firstChild) {
  6479. head.insertBefore(styleElm, head.firstChild);
  6480. }
  6481. else {
  6482. head.appendChild(styleElm);
  6483. }
  6484. }
  6485. // Append style data to old or new style element
  6486. if (styleElm.styleSheet) {
  6487. styleElm.styleSheet.cssText += cssText;
  6488. }
  6489. else {
  6490. styleElm.appendChild(doc.createTextNode(cssText));
  6491. }
  6492. };
  6493. const loadCSS = (urls) => {
  6494. if (!urls) {
  6495. urls = '';
  6496. }
  6497. each$e(urls.split(','), (url) => {
  6498. files[url] = true;
  6499. styleSheetLoader.load(url).catch(noop);
  6500. });
  6501. };
  6502. const toggleClass = (elm, cls, state) => {
  6503. run(elm, (e) => {
  6504. if (isElement$7(e)) {
  6505. const $elm = SugarElement.fromDom(e);
  6506. // TINY-4520: DomQuery used to handle specifying multiple classes and the
  6507. // formatter relies on it due to the changes made for TINY-7227
  6508. const classes = cls.split(' ');
  6509. each$e(classes, (c) => {
  6510. if (isNonNullable(state)) {
  6511. const fn = state ? add$2 : remove$4;
  6512. fn($elm, c);
  6513. }
  6514. else {
  6515. toggle$1($elm, c);
  6516. }
  6517. });
  6518. }
  6519. });
  6520. };
  6521. const addClass = (elm, cls) => {
  6522. toggleClass(elm, cls, true);
  6523. };
  6524. const removeClass = (elm, cls) => {
  6525. toggleClass(elm, cls, false);
  6526. };
  6527. const hasClass = (elm, cls) => {
  6528. const $elm = _get(elm);
  6529. // TINY-4520: DomQuery used to handle specifying multiple classes and the
  6530. // formatter relies on it due to the changes made for TINY-7227
  6531. const classes = cls.split(' ');
  6532. return isNonNullable($elm) && forall(classes, (c) => has($elm, c));
  6533. };
  6534. const show = (elm) => {
  6535. run(elm, (e) => remove$7(SugarElement.fromDom(e), 'display'));
  6536. };
  6537. const hide = (elm) => {
  6538. run(elm, (e) => set$2(SugarElement.fromDom(e), 'display', 'none'));
  6539. };
  6540. const isHidden = (elm) => {
  6541. const $elm = _get(elm);
  6542. return isNonNullable($elm) && is$4(getRaw$1($elm, 'display'), 'none');
  6543. };
  6544. const uniqueId = (prefix) => (!prefix ? 'mce_' : prefix) + (counter++);
  6545. const getOuterHTML = (elm) => {
  6546. const $elm = _get(elm);
  6547. if (isNonNullable($elm)) {
  6548. return isElement$7($elm.dom) ? $elm.dom.outerHTML : getOuter($elm);
  6549. }
  6550. else {
  6551. return '';
  6552. }
  6553. };
  6554. const setOuterHTML = (elm, html) => {
  6555. run(elm, ($elm) => {
  6556. if (isElement$7($elm)) {
  6557. $elm.outerHTML = html;
  6558. }
  6559. });
  6560. };
  6561. const insertAfter = (node, reference) => {
  6562. const referenceNode = get(reference);
  6563. return run(node, (node) => {
  6564. const parent = referenceNode === null || referenceNode === void 0 ? void 0 : referenceNode.parentNode;
  6565. const nextSibling = referenceNode === null || referenceNode === void 0 ? void 0 : referenceNode.nextSibling;
  6566. if (parent) {
  6567. if (nextSibling) {
  6568. parent.insertBefore(node, nextSibling);
  6569. }
  6570. else {
  6571. parent.appendChild(node);
  6572. }
  6573. }
  6574. return node;
  6575. });
  6576. };
  6577. const replace = (newElm, oldElm, keepChildren) => run(oldElm, (elm) => {
  6578. var _a;
  6579. const replacee = isArray$1(oldElm) ? newElm.cloneNode(true) : newElm;
  6580. if (keepChildren) {
  6581. each$a(grep(elm.childNodes), (node) => {
  6582. replacee.appendChild(node);
  6583. });
  6584. }
  6585. (_a = elm.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(replacee, elm);
  6586. return elm;
  6587. });
  6588. const rename = (elm, name) => {
  6589. if (elm.nodeName !== name.toUpperCase()) {
  6590. // Rename block element
  6591. const newElm = create(name);
  6592. // Copy attribs to new block
  6593. each$a(getAttribs(elm), (attrNode) => {
  6594. setAttrib(newElm, attrNode.nodeName, getAttrib(elm, attrNode.nodeName));
  6595. });
  6596. // Replace block
  6597. replace(newElm, elm, true);
  6598. return newElm;
  6599. }
  6600. else {
  6601. return elm;
  6602. }
  6603. };
  6604. const findCommonAncestor = (a, b) => {
  6605. let ps = a;
  6606. while (ps) {
  6607. let pe = b;
  6608. while (pe && ps !== pe) {
  6609. pe = pe.parentNode;
  6610. }
  6611. if (ps === pe) {
  6612. break;
  6613. }
  6614. ps = ps.parentNode;
  6615. }
  6616. if (!ps && a.ownerDocument) {
  6617. return a.ownerDocument.documentElement;
  6618. }
  6619. else {
  6620. return ps;
  6621. }
  6622. };
  6623. const isEmpty = (node, elements, options) => {
  6624. if (isPlainObject(elements)) {
  6625. const isContent = (node) => {
  6626. const name = node.nodeName.toLowerCase();
  6627. return Boolean(elements[name]);
  6628. };
  6629. return isEmptyNode(schema, node, { ...options, isContent });
  6630. }
  6631. else {
  6632. return isEmptyNode(schema, node, options);
  6633. }
  6634. };
  6635. const createRng = () => doc.createRange();
  6636. const split = (parentElm, splitElm, replacementElm) => {
  6637. let range = createRng();
  6638. let beforeFragment;
  6639. let afterFragment;
  6640. if (parentElm && splitElm && parentElm.parentNode && splitElm.parentNode) {
  6641. const parentNode = parentElm.parentNode;
  6642. // Get before chunk
  6643. range.setStart(parentNode, findNodeIndex(parentElm));
  6644. range.setEnd(splitElm.parentNode, findNodeIndex(splitElm));
  6645. beforeFragment = range.extractContents();
  6646. // Get after chunk
  6647. range = createRng();
  6648. range.setStart(splitElm.parentNode, findNodeIndex(splitElm) + 1);
  6649. range.setEnd(parentNode, findNodeIndex(parentElm) + 1);
  6650. afterFragment = range.extractContents();
  6651. // Insert before chunk
  6652. parentNode.insertBefore(trimNode(self, beforeFragment, schema), parentElm);
  6653. // Insert middle chunk
  6654. if (replacementElm) {
  6655. parentNode.insertBefore(replacementElm, parentElm);
  6656. // pa.replaceChild(replacementElm, splitElm);
  6657. }
  6658. else {
  6659. parentNode.insertBefore(splitElm, parentElm);
  6660. }
  6661. // Insert after chunk
  6662. parentNode.insertBefore(trimNode(self, afterFragment, schema), parentElm);
  6663. remove(parentElm);
  6664. return replacementElm || splitElm;
  6665. }
  6666. else {
  6667. return undefined;
  6668. }
  6669. };
  6670. const bind = (target, name, func, scope) => {
  6671. if (isArray$1(target)) {
  6672. let i = target.length;
  6673. const rv = [];
  6674. while (i--) {
  6675. rv[i] = bind(target[i], name, func, scope);
  6676. }
  6677. return rv;
  6678. }
  6679. else {
  6680. // Collect all window/document events bound by editor instance
  6681. if (settings.collect && (target === doc || target === win)) {
  6682. boundEvents.push([target, name, func, scope]);
  6683. }
  6684. return events.bind(target, name, func, scope || self);
  6685. }
  6686. };
  6687. const unbind = (target, name, func) => {
  6688. if (isArray$1(target)) {
  6689. let i = target.length;
  6690. const rv = [];
  6691. while (i--) {
  6692. rv[i] = unbind(target[i], name, func);
  6693. }
  6694. return rv;
  6695. }
  6696. else {
  6697. // Remove any bound events matching the input
  6698. if (boundEvents.length > 0 && (target === doc || target === win)) {
  6699. let i = boundEvents.length;
  6700. while (i--) {
  6701. const [boundTarget, boundName, boundFunc] = boundEvents[i];
  6702. if (target === boundTarget && (!name || name === boundName) && (!func || func === boundFunc)) {
  6703. events.unbind(boundTarget, boundName, boundFunc);
  6704. }
  6705. }
  6706. }
  6707. return events.unbind(target, name, func);
  6708. }
  6709. };
  6710. const dispatch = (target, name, evt) => events.dispatch(target, name, evt);
  6711. const fire = (target, name, evt) => events.dispatch(target, name, evt);
  6712. const getContentEditable = (node) => {
  6713. if (node && isHTMLElement(node)) {
  6714. // Check for fake content editable
  6715. const contentEditable = node.getAttribute('data-mce-contenteditable');
  6716. if (contentEditable && contentEditable !== 'inherit') {
  6717. return contentEditable;
  6718. }
  6719. // Check for real content editable
  6720. return node.contentEditable !== 'inherit' ? node.contentEditable : null;
  6721. }
  6722. else {
  6723. return null;
  6724. }
  6725. };
  6726. const getContentEditableParent = (node) => {
  6727. const root = getRoot();
  6728. let state = null;
  6729. for (let tempNode = node; tempNode && tempNode !== root; tempNode = tempNode.parentNode) {
  6730. state = getContentEditable(tempNode);
  6731. if (state !== null) {
  6732. break;
  6733. }
  6734. }
  6735. return state;
  6736. };
  6737. const isEditable = (node) => {
  6738. if (isNonNullable(node)) {
  6739. const scope = isElement$7(node) ? node : node.parentElement;
  6740. return isNonNullable(scope) && isHTMLElement(scope) && isEditable$2(SugarElement.fromDom(scope));
  6741. }
  6742. else {
  6743. return false;
  6744. }
  6745. };
  6746. const destroy = () => {
  6747. // Unbind all events bound to window/document by editor instance
  6748. if (boundEvents.length > 0) {
  6749. let i = boundEvents.length;
  6750. while (i--) {
  6751. const [boundTarget, boundName, boundFunc] = boundEvents[i];
  6752. events.unbind(boundTarget, boundName, boundFunc);
  6753. }
  6754. }
  6755. // Remove CSS files added to the dom
  6756. each$d(files, (_, url) => {
  6757. styleSheetLoader.unload(url);
  6758. delete files[url];
  6759. });
  6760. };
  6761. const isChildOf = (node, parent) => {
  6762. return node === parent || parent.contains(node);
  6763. };
  6764. const dumpRng = (r) => ('startContainer: ' + r.startContainer.nodeName +
  6765. ', startOffset: ' + r.startOffset +
  6766. ', endContainer: ' + r.endContainer.nodeName +
  6767. ', endOffset: ' + r.endOffset);
  6768. // eslint-disable-next-line consistent-this
  6769. const self = {
  6770. doc,
  6771. settings,
  6772. win,
  6773. files,
  6774. stdMode,
  6775. boxModel,
  6776. styleSheetLoader,
  6777. boundEvents,
  6778. styles,
  6779. schema,
  6780. events,
  6781. isBlock: isBlock,
  6782. root: null,
  6783. clone,
  6784. /**
  6785. * Returns the root node of the document. This is normally the body but might be a DIV. Parents like getParent will not
  6786. * go above the point of this root node.
  6787. *
  6788. * @method getRoot
  6789. * @return {Element} Root element for the utility class.
  6790. */
  6791. getRoot,
  6792. /**
  6793. * Returns the viewport of the window.
  6794. *
  6795. * @method getViewPort
  6796. * @param {Window} win Optional window to get viewport of.
  6797. * @return {Object} Viewport object with fields x, y, w and h.
  6798. */
  6799. getViewPort,
  6800. /**
  6801. * Returns the rectangle for a specific element.
  6802. *
  6803. * @method getRect
  6804. * @param {Element/String} elm Element object or element ID to get rectangle from.
  6805. * @return {Object} Rectangle for specified element object with x, y, w, h fields.
  6806. */
  6807. getRect,
  6808. /**
  6809. * Returns the size dimensions of the specified element.
  6810. *
  6811. * @method getSize
  6812. * @param {Element/String} elm Element object or element ID to get rectangle from.
  6813. * @return {Object} Rectangle for specified element object with w, h fields.
  6814. */
  6815. getSize,
  6816. /**
  6817. * Returns a node by the specified selector function. This function will
  6818. * loop through all parent nodes and call the specified function for each node.
  6819. * If the function then returns true indicating that it has found what it was looking for, the loop execution will then end
  6820. * and the node it found will be returned.
  6821. *
  6822. * @method getParent
  6823. * @param {Node/String} node DOM node to search parents on or ID string.
  6824. * @param {Function} selector Selection function or CSS selector to execute on each node.
  6825. * @param {Node} root Optional root element, never go beyond this point.
  6826. * @return {Node} DOM Node or null if it wasn't found.
  6827. */
  6828. getParent,
  6829. /**
  6830. * Returns a node list of all parents matching the specified selector function or pattern.
  6831. * If the function then returns true indicating that it has found what it was looking for and that node will be collected.
  6832. *
  6833. * @method getParents
  6834. * @param {Node/String} node DOM node to search parents on or ID string.
  6835. * @param {Function} selector Selection function to execute on each node or CSS pattern.
  6836. * @param {Node} root Optional root element, never go beyond this point.
  6837. * @return {Array} Array of nodes or null if it wasn't found.
  6838. */
  6839. getParents: getParents,
  6840. /**
  6841. * Returns the specified element by ID or the input element if it isn't a string.
  6842. *
  6843. * @method get
  6844. * @param {String/Element} n Element id to look for or element to just pass though.
  6845. * @return {Element} Element matching the specified id or null if it wasn't found.
  6846. */
  6847. get,
  6848. /**
  6849. * Returns the next node that matches selector or function
  6850. *
  6851. * @method getNext
  6852. * @param {Node} node Node to find siblings from.
  6853. * @param {String/function} selector Selector CSS expression or function.
  6854. * @return {Node} Next node item matching the selector or null if it wasn't found.
  6855. */
  6856. getNext,
  6857. /**
  6858. * Returns the previous node that matches selector or function
  6859. *
  6860. * @method getPrev
  6861. * @param {Node} node Node to find siblings from.
  6862. * @param {String/function} selector Selector CSS expression or function.
  6863. * @return {Node} Previous node item matching the selector or null if it wasn't found.
  6864. */
  6865. getPrev,
  6866. // #ifndef jquery
  6867. /**
  6868. * Returns a list of the elements specified by the given CSS selector. For example: `div#a1 p.test`
  6869. *
  6870. * @method select
  6871. * @param {String} selector Target CSS selector.
  6872. * @param {Object} scope Optional root element/scope element to search in.
  6873. * @return {Array} Array with all matched elements.
  6874. * @example
  6875. * // Adds a class to all paragraphs in the currently active editor
  6876. * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass');
  6877. *
  6878. * // Adds a class to all spans that have the test class in the currently active editor
  6879. * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('span.test'), 'someclass')
  6880. */
  6881. select,
  6882. /**
  6883. * Returns true/false if the specified element matches the specified css pattern.
  6884. *
  6885. * @method is
  6886. * @param {Node/NodeList} elm DOM node to match or an array of nodes to match.
  6887. * @param {String} selector CSS pattern to match the element against.
  6888. */
  6889. is,
  6890. // #endif
  6891. /**
  6892. * Adds the specified element to another element or elements.
  6893. *
  6894. * @method add
  6895. * @param {String/Element/Array} parentElm Element id string, DOM node element or array of ids or elements to add to.
  6896. * @param {String/Element} name Name of new element to add or existing element to add.
  6897. * @param {Object} attrs Optional object collection with arguments to add to the new element(s).
  6898. * @param {String} html Optional inner HTML contents to add for each element.
  6899. * @param {Boolean} create Optional flag if the element should be created or added.
  6900. * @return {Element/Array} Element that got created, or an array of created elements if multiple input elements
  6901. * were passed in.
  6902. * @example
  6903. * // Adds a new paragraph to the end of the active editor
  6904. * tinymce.activeEditor.dom.add(tinymce.activeEditor.getBody(), 'p', { title: 'my title' }, 'Some content');
  6905. */
  6906. add,
  6907. /**
  6908. * Creates a new element.
  6909. *
  6910. * @method create
  6911. * @param {String} name Name of new element.
  6912. * @param {Object} attrs Optional object name/value collection with element attributes.
  6913. * @param {String} html Optional HTML string to set as inner HTML of the element.
  6914. * @return {Element} HTML DOM node element that got created.
  6915. * @example
  6916. * // Adds an element where the caret/selection is in the active editor
  6917. * var el = tinymce.activeEditor.dom.create('div', { id: 'test', 'class': 'myclass' }, 'some content');
  6918. * tinymce.activeEditor.selection.setNode(el);
  6919. */
  6920. create,
  6921. /**
  6922. * Creates HTML string for element. The element will be closed unless an empty inner HTML string is passed in.
  6923. *
  6924. * @method createHTML
  6925. * @param {String} name Name of new element.
  6926. * @param {Object} attrs Optional object name/value collection with element attributes.
  6927. * @param {String} html Optional HTML string to set as inner HTML of the element.
  6928. * @return {String} String with new HTML element, for example: <a href="#">test</a>.
  6929. * @example
  6930. * // Creates a html chunk and inserts it at the current selection/caret location
  6931. * tinymce.activeEditor.insertContent(tinymce.activeEditor.dom.createHTML('a', { href: 'test.html' }, 'some line'));
  6932. */
  6933. createHTML,
  6934. /**
  6935. * Creates a document fragment out of the specified HTML string.
  6936. *
  6937. * @method createFragment
  6938. * @param {String} html Html string to create fragment from.
  6939. * @return {DocumentFragment} Document fragment node.
  6940. */
  6941. createFragment,
  6942. /**
  6943. * Removes/deletes the specified element(s) from the DOM.
  6944. *
  6945. * @method remove
  6946. * @param {String/Element/Array} node ID of element or DOM element object or array containing multiple elements/ids.
  6947. * @param {Boolean} keepChildren Optional state to keep children or not. If set to true all children will be
  6948. * placed at the location of the removed element.
  6949. * @return {Element/Array} HTML DOM element that got removed, or an array of removed elements if multiple input elements
  6950. * were passed in.
  6951. * @example
  6952. * // Removes all paragraphs in the active editor
  6953. * tinymce.activeEditor.dom.remove(tinymce.activeEditor.dom.select('p'));
  6954. *
  6955. * // Removes an element by id in the document
  6956. * tinymce.DOM.remove('mydiv');
  6957. */
  6958. remove,
  6959. /**
  6960. * Sets the CSS style value on a HTML element. The name can be a camelcase string
  6961. * or the CSS style name like background-color.
  6962. *
  6963. * @method setStyle
  6964. * @param {String/Element/Array} elm HTML element/Array of elements to set CSS style value on.
  6965. * @param {String} name Name of the style value to set.
  6966. * @param {String} value Value to set on the style.
  6967. * @example
  6968. * // Sets a style value on all paragraphs in the currently active editor
  6969. * tinymce.activeEditor.dom.setStyle(tinymce.activeEditor.dom.select('p'), 'background-color', 'red');
  6970. *
  6971. * // Sets a style value to an element by id in the current document
  6972. * tinymce.DOM.setStyle('mydiv', 'background-color', 'red');
  6973. */
  6974. setStyle,
  6975. /**
  6976. * Returns the current style or runtime/computed value of an element.
  6977. *
  6978. * @method getStyle
  6979. * @param {String/Element} elm HTML element or element id string to get style from.
  6980. * @param {String} name Style name to return.
  6981. * @param {Boolean} computed Computed style.
  6982. * @return {String} Current style or computed style value of an element.
  6983. */
  6984. getStyle: getStyle,
  6985. /**
  6986. * Sets multiple styles on the specified element(s).
  6987. *
  6988. * @method setStyles
  6989. * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set styles on.
  6990. * @param {Object} styles Name/Value collection of style items to add to the element(s).
  6991. * @example
  6992. * // Sets styles on all paragraphs in the currently active editor
  6993. * tinymce.activeEditor.dom.setStyles(tinymce.activeEditor.dom.select('p'), { 'background-color': 'red', 'color': 'green' });
  6994. *
  6995. * // Sets styles to an element by id in the current document
  6996. * tinymce.DOM.setStyles('mydiv', { 'background-color': 'red', 'color': 'green' });
  6997. */
  6998. setStyles,
  6999. /**
  7000. * Removes all attributes from an element or elements.
  7001. *
  7002. * @method removeAllAttribs
  7003. * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to remove attributes from.
  7004. */
  7005. removeAllAttribs,
  7006. /**
  7007. * Sets the specified attribute of an element or elements.
  7008. *
  7009. * @method setAttrib
  7010. * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attribute on.
  7011. * @param {String} name Name of attribute to set.
  7012. * @param {String} value Value to set on the attribute - if this value is falsy like null, 0 or '' it will remove
  7013. * the attribute instead.
  7014. * @example
  7015. * // Sets class attribute on all paragraphs in the active editor
  7016. * tinymce.activeEditor.dom.setAttrib(tinymce.activeEditor.dom.select('p'), 'class', 'myclass');
  7017. *
  7018. * // Sets class attribute on a specific element in the current page
  7019. * tinymce.dom.setAttrib('mydiv', 'class', 'myclass');
  7020. */
  7021. setAttrib,
  7022. /**
  7023. * Sets two or more specified attributes of an element or elements.
  7024. *
  7025. * @method setAttribs
  7026. * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attributes on.
  7027. * @param {Object} attrs Name/Value collection of attribute items to add to the element(s).
  7028. * @example
  7029. * // Sets class and title attributes on all paragraphs in the active editor
  7030. * tinymce.activeEditor.dom.setAttribs(tinymce.activeEditor.dom.select('p'), { 'class': 'myclass', title: 'some title' });
  7031. *
  7032. * // Sets class and title attributes on a specific element in the current page
  7033. * tinymce.DOM.setAttribs('mydiv', { 'class': 'myclass', title: 'some title' });
  7034. */
  7035. setAttribs,
  7036. /**
  7037. * Returns the specified attribute by name.
  7038. *
  7039. * @method getAttrib
  7040. * @param {String/Element} elm Element string id or DOM element to get attribute from.
  7041. * @param {String} name Name of attribute to get.
  7042. * @param {String} defaultVal Optional default value to return if the attribute didn't exist.
  7043. * @return {String} Attribute value string, default value or null if the attribute wasn't found.
  7044. */
  7045. getAttrib,
  7046. /**
  7047. * Returns the absolute x, y position of a node. The position will be returned in an object with x, y fields.
  7048. *
  7049. * @method getPos
  7050. * @param {Element/String} elm HTML element or element id to get x, y position from.
  7051. * @param {Element} rootElm Optional root element to stop calculations at.
  7052. * @return {Object} Absolute position of the specified element object with x, y fields.
  7053. */
  7054. getPos: getPos$1,
  7055. /**
  7056. * Parses the specified style value into an object collection. This parser will also
  7057. * merge and remove any redundant items that browsers might have added. It will also convert non-hex
  7058. * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings.
  7059. *
  7060. * @method parseStyle
  7061. * @param {String} cssText Style value to parse, for example: border:1px solid red;.
  7062. * @return {Object} Object representation of that style, for example: {border: '1px solid red'}
  7063. */
  7064. parseStyle,
  7065. /**
  7066. * Serializes the specified style object into a string.
  7067. *
  7068. * @method serializeStyle
  7069. * @param {Object} styles Object to serialize as string, for example: {border: '1px solid red'}
  7070. * @param {String} name Optional element name.
  7071. * @return {String} String representation of the style object, for example: border: 1px solid red.
  7072. */
  7073. serializeStyle,
  7074. /**
  7075. * Adds a style element at the top of the document with the specified cssText content.
  7076. *
  7077. * @method addStyle
  7078. * @param {String} cssText CSS Text style to add to top of head of document.
  7079. */
  7080. addStyle,
  7081. /**
  7082. * Imports/loads the specified CSS file into the document bound to the class.
  7083. *
  7084. * @method loadCSS
  7085. * @param {String} url URL to CSS file to load.
  7086. * @example
  7087. * // Loads a CSS file dynamically into the current document
  7088. * tinymce.DOM.loadCSS('somepath/some.css');
  7089. *
  7090. * // Loads a CSS file into the currently active editor instance
  7091. * tinymce.activeEditor.dom.loadCSS('somepath/some.css');
  7092. *
  7093. * // Loads a CSS file into an editor instance by id
  7094. * tinymce.get('someid').dom.loadCSS('somepath/some.css');
  7095. *
  7096. * // Loads multiple CSS files into the current document
  7097. * tinymce.DOM.loadCSS('somepath/some.css,somepath/someother.css');
  7098. */
  7099. loadCSS,
  7100. /**
  7101. * Adds a class to the specified element or elements.
  7102. *
  7103. * @method addClass
  7104. * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs.
  7105. * @param {String} cls Class name to add to each element.
  7106. * @return {String/Array} String with new class value or array with new class values for all elements.
  7107. * @example
  7108. * // Adds a class to all paragraphs in the active editor
  7109. * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'myclass');
  7110. *
  7111. * // Adds a class to a specific element in the current page
  7112. * tinymce.DOM.addClass('mydiv', 'myclass');
  7113. */
  7114. addClass,
  7115. /**
  7116. * Removes a class from the specified element or elements.
  7117. *
  7118. * @method removeClass
  7119. * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs.
  7120. * @param {String} cls Class name to remove from each element.
  7121. * @return {String/Array} String of remaining class name(s), or an array of strings if multiple input elements
  7122. * were passed in.
  7123. * @example
  7124. * // Removes a class from all paragraphs in the active editor
  7125. * tinymce.activeEditor.dom.removeClass(tinymce.activeEditor.dom.select('p'), 'myclass');
  7126. *
  7127. * // Removes a class from a specific element in the current page
  7128. * tinymce.DOM.removeClass('mydiv', 'myclass');
  7129. */
  7130. removeClass,
  7131. /**
  7132. * Returns true if the specified element has the specified class.
  7133. *
  7134. * @method hasClass
  7135. * @param {String/Element} elm HTML element or element id string to check CSS class on.
  7136. * @param {String} cls CSS class to check for.
  7137. * @return {Boolean} true/false if the specified element has the specified class.
  7138. */
  7139. hasClass,
  7140. /**
  7141. * Toggles the specified class on/off.
  7142. *
  7143. * @method toggleClass
  7144. * @param {Element} elm Element to toggle class on.
  7145. * @param {String} cls Class to toggle on/off.
  7146. * @param {Boolean} state Optional state to set.
  7147. */
  7148. toggleClass,
  7149. /**
  7150. * Shows the specified element(s) by ID by setting the "display" style.
  7151. *
  7152. * @method show
  7153. * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to show.
  7154. */
  7155. show,
  7156. /**
  7157. * Hides the specified element(s) by ID by setting the "display" style.
  7158. *
  7159. * @method hide
  7160. * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to hide.
  7161. * @example
  7162. * // Hides an element by id in the document
  7163. * tinymce.DOM.hide('myid');
  7164. */
  7165. hide,
  7166. /**
  7167. * Returns true/false if the element is hidden or not by checking the "display" style.
  7168. *
  7169. * @method isHidden
  7170. * @param {String/Element} elm Id or element to check display state on.
  7171. * @return {Boolean} true/false if the element is hidden or not.
  7172. */
  7173. isHidden,
  7174. /**
  7175. * Returns a unique id. This can be useful when generating elements on the fly.
  7176. * This method will not check if the element already exists.
  7177. *
  7178. * @method uniqueId
  7179. * @param {String} prefix Optional prefix to add in front of all ids - defaults to "mce_".
  7180. * @return {String} Unique id.
  7181. */
  7182. uniqueId,
  7183. /**
  7184. * Sets the specified HTML content inside the element or elements. The HTML will first be processed. This means
  7185. * URLs will get converted, hex color values fixed etc. Check processHTML for details.
  7186. *
  7187. * @method setHTML
  7188. * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set HTML inside of.
  7189. * @param {String} html HTML content to set as inner HTML of the element.
  7190. * @example
  7191. * // Sets the inner HTML of all paragraphs in the active editor
  7192. * tinymce.activeEditor.dom.setHTML(tinymce.activeEditor.dom.select('p'), 'some inner html');
  7193. *
  7194. * // Sets the inner HTML of an element by id in the document
  7195. * tinymce.DOM.setHTML('mydiv', 'some inner html');
  7196. */
  7197. setHTML,
  7198. /**
  7199. * Returns the outer HTML of an element.
  7200. *
  7201. * @method getOuterHTML
  7202. * @param {String/Element} elm Element ID or element object to get outer HTML from.
  7203. * @return {String} Outer HTML string.
  7204. * @example
  7205. * tinymce.DOM.getOuterHTML(editorElement);
  7206. * tinymce.activeEditor.getOuterHTML(tinymce.activeEditor.getBody());
  7207. */
  7208. getOuterHTML,
  7209. /**
  7210. * Sets the specified outer HTML on an element or elements.
  7211. *
  7212. * @method setOuterHTML
  7213. * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set outer HTML on.
  7214. * @param {Object} html HTML code to set as outer value for the element.
  7215. * @example
  7216. * // Sets the outer HTML of all paragraphs in the active editor
  7217. * tinymce.activeEditor.dom.setOuterHTML(tinymce.activeEditor.dom.select('p'), '<div>some html</div>');
  7218. *
  7219. * // Sets the outer HTML of an element by id in the document
  7220. * tinymce.DOM.setOuterHTML('mydiv', '<div>some html</div>');
  7221. */
  7222. setOuterHTML,
  7223. /**
  7224. * Entity decodes a string. This method decodes any HTML entities, such as `&amp;aring;`.
  7225. *
  7226. * @method decode
  7227. * @param {String} s String to decode entities on.
  7228. * @return {String} Entity decoded string.
  7229. */
  7230. decode,
  7231. /**
  7232. * Entity encodes a string. This method encodes the most common entities, such as `<`, `>`, `"` and `&`.
  7233. *
  7234. * @method encode
  7235. * @param {String} text String to encode with entities.
  7236. * @return {String} Entity encoded string.
  7237. */
  7238. encode,
  7239. /**
  7240. * Inserts an element after the reference element.
  7241. *
  7242. * @method insertAfter
  7243. * @param {Element} node Element to insert after the reference.
  7244. * @param {Element/String/Array} referenceNode Reference element, element id or array of elements to insert after.
  7245. * @return {Element/Array} Element that got added or an array with elements.
  7246. */
  7247. insertAfter,
  7248. /**
  7249. * Replaces the specified element or elements with the new element specified. The new element will
  7250. * be cloned if multiple input elements are passed in.
  7251. *
  7252. * @method replace
  7253. * @param {Element} newElm New element to replace old ones with.
  7254. * @param {Element/String/Array} oldElm Element DOM node, element id or array of elements or ids to replace.
  7255. * @param {Boolean} keepChildren Optional keep children state, if set to true child nodes from the old object will be added
  7256. * to new ones.
  7257. */
  7258. replace,
  7259. /**
  7260. * Renames the specified element and keeps its attributes and children.
  7261. *
  7262. * @method rename
  7263. * @param {Element} elm Element to rename.
  7264. * @param {String} name Name of the new element.
  7265. * @return {Element} New element or the old element if it needed renaming.
  7266. */
  7267. rename,
  7268. /**
  7269. * Find the common ancestor of two elements. This is a shorter method than using the DOM Range logic.
  7270. *
  7271. * @method findCommonAncestor
  7272. * @param {Element} a Element to find common ancestor of.
  7273. * @param {Element} b Element to find common ancestor of.
  7274. * @return {Element} Common ancestor element of the two input elements.
  7275. */
  7276. findCommonAncestor,
  7277. /**
  7278. * Executes the specified function on the element by id or dom element node or array of elements/id.
  7279. *
  7280. * @method run
  7281. * @param {String/Element/Array} elm ID or DOM element object or array with ids or elements.
  7282. * @param {Function} func Function to execute for each item.
  7283. * @param {Object} scope Optional scope to execute the function in.
  7284. * @return {Object/Array} Single object, or an array of objects if multiple input elements were passed in.
  7285. */
  7286. run,
  7287. /**
  7288. * Returns a NodeList with attributes for the element.
  7289. *
  7290. * @method getAttribs
  7291. * @param {HTMLElement/string} elm Element node or string id to get attributes from.
  7292. * @return {NodeList} NodeList with attributes.
  7293. */
  7294. getAttribs,
  7295. /**
  7296. * Returns true/false if the specified node is to be considered empty or not.
  7297. *
  7298. * @method isEmpty
  7299. * @param {Node} node The target node to check if it's empty.
  7300. * @param {Object} elements Optional name/value object with elements that are automatically treated as non-empty elements.
  7301. * @return {Boolean} true/false if the node is empty or not.
  7302. * @example
  7303. * tinymce.DOM.isEmpty(node, { img: true });
  7304. */
  7305. isEmpty,
  7306. /**
  7307. * Creates a new DOM Range object. This will use the native DOM Range API if it's
  7308. * available. If it's not, it will fall back to the custom TinyMCE implementation.
  7309. *
  7310. * @method createRng
  7311. * @return {DOMRange} DOM Range object.
  7312. * @example
  7313. * const rng = tinymce.DOM.createRng();
  7314. * alert(rng.startContainer + "," + rng.startOffset);
  7315. */
  7316. createRng,
  7317. /**
  7318. * Returns the index of the specified node within its parent.
  7319. *
  7320. * @method nodeIndex
  7321. * @param {Node} node Node to look for.
  7322. * @param {Boolean} normalized Optional true/false state if the index is what it would be after a normalization.
  7323. * @return {Number} Index of the specified node.
  7324. */
  7325. nodeIndex: findNodeIndex,
  7326. /**
  7327. * Splits an element into two new elements and places the specified split
  7328. * element or elements between the new ones. For example splitting the paragraph at the bold element in
  7329. * this example `<p>abc<b>abc</b>123</p>` would produce `<p>abc</p><b>abc</b><p>123</p>`.
  7330. *
  7331. * @method split
  7332. * @param {Element} parentElm Parent element to split.
  7333. * @param {Element} splitElm Element to split at.
  7334. * @param {Element} replacementElm Optional replacement element to replace the split element with.
  7335. * @return {Element} Returns the split element or the replacement element if that is specified.
  7336. */
  7337. split,
  7338. /**
  7339. * Adds an event handler to the specified object.
  7340. *
  7341. * @method bind
  7342. * @param {Element/Document/Window/Array} target Target element to bind events to.
  7343. * handler to or an array of elements/ids/documents.
  7344. * @param {String} name Name of event handler to add, for example: click.
  7345. * @param {Function} func Function to execute when the event occurs.
  7346. * @param {Object} scope Optional scope to execute the function in.
  7347. * @return {Function} Function callback handler the same as the one passed in.
  7348. */
  7349. bind: bind,
  7350. /**
  7351. * Removes the specified event handler by name and function from an element or collection of elements.
  7352. *
  7353. * @method unbind
  7354. * @param {Element/Document/Window/Array} target Target element to unbind events on.
  7355. * @param {String} name Event handler name, for example: "click"
  7356. * @param {Function} func Function to remove.
  7357. * @return {Boolean/Array} Bool state of true if the handler was removed, or an array of states if multiple input elements
  7358. * were passed in.
  7359. */
  7360. unbind: unbind,
  7361. /**
  7362. * Fires the specified event name and optional object on the specified target.
  7363. * <br>
  7364. * <em>Deprecated in TinyMCE 6.0 and has been marked for removal in TinyMCE 7.0. Use <code>dispatch</code> instead.</em>
  7365. *
  7366. * @method fire
  7367. * @param {Node/Document/Window} target Target element or object to fire event on.
  7368. * @param {String} name Event name to fire.
  7369. * @param {Object} evt Event object to send.
  7370. * @return {Event} Event object.
  7371. * @deprecated Use dispatch() instead
  7372. */
  7373. fire,
  7374. /**
  7375. * Dispatches the specified event name and optional object on the specified target.
  7376. *
  7377. * @method dispatch
  7378. * @param {Node/Document/Window} target Target element or object to dispatch event on.
  7379. * @param {String} name Name of the event to fire.
  7380. * @param {Object} evt Event object to send.
  7381. * @return {Event} Event object.
  7382. */
  7383. dispatch,
  7384. // Returns the content editable state of a node
  7385. getContentEditable,
  7386. getContentEditableParent,
  7387. /**
  7388. * Checks if the specified node is editable within the given context of its parents.
  7389. *
  7390. * @method isEditable
  7391. * @param {Node} node Node to check if it's editable.
  7392. * @return {Boolean} Will be true if the node is editable and false if it's not editable.
  7393. */
  7394. isEditable,
  7395. /**
  7396. * Destroys all internal references to the DOM to solve memory leak issues.
  7397. *
  7398. * @method destroy
  7399. */
  7400. destroy,
  7401. isChildOf,
  7402. dumpRng
  7403. };
  7404. const attrHooks = setupAttrHooks(styles, settings, constant(self));
  7405. return self;
  7406. };
  7407. /**
  7408. * Instance of DOMUtils for the current document.
  7409. *
  7410. * @static
  7411. * @property DOM
  7412. * @type tinymce.dom.DOMUtils
  7413. * @example
  7414. * // Example of how to add a class to some element by id
  7415. * tinymce.DOM.addClass('someid', 'someclass');
  7416. */
  7417. DOMUtils.DOM = DOMUtils(document);
  7418. DOMUtils.nodeIndex = findNodeIndex;
  7419. /**
  7420. * This class handles asynchronous/synchronous loading of JavaScript files it will execute callbacks
  7421. * when various items gets loaded. This class is useful to load external JavaScript files.
  7422. *
  7423. * @class tinymce.dom.ScriptLoader
  7424. * @example
  7425. * // Load a script from a specific URL using the global script loader
  7426. * tinymce.ScriptLoader.load('somescript.js');
  7427. *
  7428. * // Load a script using a unique instance of the script loader
  7429. * const scriptLoader = new tinymce.dom.ScriptLoader();
  7430. *
  7431. * scriptLoader.load('somescript.js');
  7432. *
  7433. * // Load multiple scripts
  7434. * scriptLoader.add('somescript1.js');
  7435. * scriptLoader.add('somescript2.js');
  7436. * scriptLoader.add('somescript3.js');
  7437. *
  7438. * scriptLoader.loadQueue().then(() => {
  7439. * alert('All scripts are now loaded.');
  7440. * });
  7441. */
  7442. const DOM$f = DOMUtils.DOM;
  7443. const QUEUED = 0;
  7444. const LOADING = 1;
  7445. const LOADED = 2;
  7446. const FAILED = 3;
  7447. class ScriptLoader {
  7448. constructor(settings = {}) {
  7449. this.states = {};
  7450. this.queue = [];
  7451. this.scriptLoadedCallbacks = {};
  7452. this.queueLoadedCallbacks = [];
  7453. this.loading = false;
  7454. this.settings = settings;
  7455. }
  7456. _setReferrerPolicy(referrerPolicy) {
  7457. this.settings.referrerPolicy = referrerPolicy;
  7458. }
  7459. _setCrossOrigin(crossOrigin) {
  7460. this.settings.crossOrigin = crossOrigin;
  7461. }
  7462. /**
  7463. * Loads a specific script directly without adding it to the load queue.
  7464. *
  7465. * @method loadScript
  7466. * @param {String} url Absolute URL to script to add.
  7467. * @return {Promise} A promise that will resolve when the script loaded successfully or reject if it failed to load.
  7468. */
  7469. loadScript(url) {
  7470. return new Promise((resolve, reject) => {
  7471. const dom = DOM$f;
  7472. let elm;
  7473. const cleanup = () => {
  7474. dom.remove(id);
  7475. if (elm) {
  7476. elm.onerror = elm.onload = elm = null;
  7477. }
  7478. };
  7479. // Execute callback when script is loaded
  7480. const done = () => {
  7481. cleanup();
  7482. resolve();
  7483. };
  7484. const error = () => {
  7485. // We can't mark it as done if there is a load error since
  7486. // A) We don't want to produce 404 errors on the server and
  7487. // B) the onerror event won't fire on all browsers.
  7488. cleanup();
  7489. reject('Failed to load script: ' + url);
  7490. };
  7491. const id = dom.uniqueId();
  7492. // Create new script element
  7493. elm = document.createElement('script');
  7494. elm.id = id;
  7495. elm.type = 'text/javascript';
  7496. elm.src = Tools._addCacheSuffix(url);
  7497. if (this.settings.referrerPolicy) {
  7498. // Note: Don't use elm.referrerPolicy = ... here as it doesn't work on Safari
  7499. dom.setAttrib(elm, 'referrerpolicy', this.settings.referrerPolicy);
  7500. }
  7501. const crossOrigin = this.settings.crossOrigin;
  7502. if (isFunction(crossOrigin)) {
  7503. const resultCrossOrigin = crossOrigin(url);
  7504. if (resultCrossOrigin !== undefined) {
  7505. dom.setAttrib(elm, 'crossorigin', resultCrossOrigin);
  7506. }
  7507. }
  7508. elm.onload = done;
  7509. // Add onerror event will get fired on some browsers but not all of them
  7510. elm.onerror = error;
  7511. // Add script to document
  7512. (document.getElementsByTagName('head')[0] || document.body).appendChild(elm);
  7513. });
  7514. }
  7515. /**
  7516. * Returns true/false if a script has been loaded or not.
  7517. *
  7518. * @method isDone
  7519. * @param {String} url URL to check for.
  7520. * @return {Boolean} true/false if the URL is loaded.
  7521. */
  7522. isDone(url) {
  7523. return this.states[url] === LOADED;
  7524. }
  7525. /**
  7526. * Marks a specific script to be loaded. This can be useful if a script got loaded outside
  7527. * the script loader or to skip it from loading some script.
  7528. *
  7529. * @method markDone
  7530. * @param {String} url Absolute URL to the script to mark as loaded.
  7531. */
  7532. markDone(url) {
  7533. this.states[url] = LOADED;
  7534. }
  7535. /**
  7536. * Adds a specific script to the load queue of the script loader.
  7537. *
  7538. * @method add
  7539. * @param {String} url Absolute URL to script to add.
  7540. * @return {Promise} A promise that will resolve when the script loaded successfully or reject if it failed to load.
  7541. */
  7542. add(url) {
  7543. const self = this;
  7544. self.queue.push(url);
  7545. // Add url to load queue
  7546. const state = self.states[url];
  7547. if (state === undefined) {
  7548. self.states[url] = QUEUED;
  7549. }
  7550. return new Promise((resolve, reject) => {
  7551. // Store away callback for later execution
  7552. if (!self.scriptLoadedCallbacks[url]) {
  7553. self.scriptLoadedCallbacks[url] = [];
  7554. }
  7555. self.scriptLoadedCallbacks[url].push({
  7556. resolve,
  7557. reject
  7558. });
  7559. });
  7560. }
  7561. load(url) {
  7562. return this.add(url);
  7563. }
  7564. remove(url) {
  7565. delete this.states[url];
  7566. delete this.scriptLoadedCallbacks[url];
  7567. }
  7568. /**
  7569. * Starts the loading of the queue.
  7570. *
  7571. * @method loadQueue
  7572. * @return {Promise} A promise that is resolved when all queued items are loaded or rejected with the script urls that failed to load.
  7573. */
  7574. loadQueue() {
  7575. const queue = this.queue;
  7576. this.queue = [];
  7577. return this.loadScripts(queue);
  7578. }
  7579. /**
  7580. * Loads the specified queue of files and executes the callback ones they are loaded.
  7581. * This method is generally not used outside this class but it might be useful in some scenarios.
  7582. *
  7583. * @method loadScripts
  7584. * @param {Array} scripts Array of queue items to load.
  7585. * @return {Promise} A promise that is resolved when all scripts are loaded or rejected with the script urls that failed to load.
  7586. */
  7587. loadScripts(scripts) {
  7588. const self = this;
  7589. const execCallbacks = (name, url) => {
  7590. // Execute URL callback functions
  7591. get$a(self.scriptLoadedCallbacks, url).each((callbacks) => {
  7592. each$e(callbacks, (callback) => callback[name](url));
  7593. });
  7594. delete self.scriptLoadedCallbacks[url];
  7595. };
  7596. const processResults = (results) => {
  7597. const failures = filter$5(results, (result) => result.status === 'rejected');
  7598. if (failures.length > 0) {
  7599. return Promise.reject(bind$3(failures, ({ reason }) => isArray$1(reason) ? reason : [reason]));
  7600. }
  7601. else {
  7602. return Promise.resolve();
  7603. }
  7604. };
  7605. const load = (urls) => Promise.allSettled(map$3(urls, (url) => {
  7606. // Script is already loaded then execute script callbacks directly
  7607. if (self.states[url] === LOADED) {
  7608. execCallbacks('resolve', url);
  7609. return Promise.resolve();
  7610. }
  7611. else if (self.states[url] === FAILED) {
  7612. execCallbacks('reject', url);
  7613. return Promise.reject(url);
  7614. }
  7615. else {
  7616. // Script is not already loaded, so load it
  7617. self.states[url] = LOADING;
  7618. return self.loadScript(url).then(() => {
  7619. self.states[url] = LOADED;
  7620. execCallbacks('resolve', url);
  7621. // Immediately load additional scripts if any were added to the queue while loading this script
  7622. const queue = self.queue;
  7623. if (queue.length > 0) {
  7624. self.queue = [];
  7625. return load(queue).then(processResults);
  7626. }
  7627. else {
  7628. return Promise.resolve();
  7629. }
  7630. }, () => {
  7631. self.states[url] = FAILED;
  7632. execCallbacks('reject', url);
  7633. return Promise.reject(url);
  7634. });
  7635. }
  7636. }));
  7637. const processQueue = (urls) => {
  7638. self.loading = true;
  7639. return load(urls).then((results) => {
  7640. self.loading = false;
  7641. // Start loading the next queued item
  7642. const nextQueuedItem = self.queueLoadedCallbacks.shift();
  7643. Optional.from(nextQueuedItem).each(call);
  7644. return processResults(results);
  7645. });
  7646. };
  7647. // Wait for any other scripts to finish loading first, otherwise load immediately
  7648. const uniqueScripts = stringArray(scripts);
  7649. if (self.loading) {
  7650. return new Promise((resolve, reject) => {
  7651. self.queueLoadedCallbacks.push(() => {
  7652. processQueue(uniqueScripts).then(resolve, reject);
  7653. });
  7654. });
  7655. }
  7656. else {
  7657. return processQueue(uniqueScripts);
  7658. }
  7659. }
  7660. }
  7661. ScriptLoader.ScriptLoader = new ScriptLoader();
  7662. const isDuplicated = (items, item) => {
  7663. const firstIndex = items.indexOf(item);
  7664. return firstIndex !== -1 && items.indexOf(item, firstIndex + 1) > firstIndex;
  7665. };
  7666. const isRaw = (str) => isObject(str) && has$2(str, 'raw');
  7667. const isTokenised = (str) => isArray$1(str) && str.length > 1;
  7668. const data = {};
  7669. const currentCode = Cell('en');
  7670. const getLanguageData = () => get$a(data, currentCode.get());
  7671. const getData$1 = () => map$2(data, (value) => ({ ...value }));
  7672. /**
  7673. * Sets the current language code.
  7674. *
  7675. * @method setCode
  7676. * @param {String} newCode Current language code.
  7677. */
  7678. const setCode = (newCode) => {
  7679. if (newCode) {
  7680. currentCode.set(newCode);
  7681. }
  7682. };
  7683. /**
  7684. * Returns the current language code.
  7685. *
  7686. * @method getCode
  7687. * @return {String} Current language code.
  7688. */
  7689. const getCode = () => currentCode.get();
  7690. /**
  7691. * Adds translations for a specific language code.
  7692. * Translation keys are set to be case insensitive.
  7693. *
  7694. * @method add
  7695. * @param {String} code Language code like sv_SE.
  7696. * @param {Object} items Name/value object where key is english and value is the translation.
  7697. */
  7698. const add = (code, items) => {
  7699. let langData = data[code];
  7700. if (!langData) {
  7701. data[code] = langData = {};
  7702. }
  7703. const lcNames = map$3(keys(items), (name) => name.toLowerCase());
  7704. each$d(items, (translation, name) => {
  7705. const lcName = name.toLowerCase();
  7706. if (lcName !== name && isDuplicated(lcNames, lcName)) {
  7707. if (!has$2(items, lcName)) {
  7708. langData[lcName] = translation;
  7709. }
  7710. langData[name] = translation;
  7711. }
  7712. else {
  7713. langData[lcName] = translation;
  7714. }
  7715. });
  7716. };
  7717. /**
  7718. * Translates the specified text.
  7719. *
  7720. * It has a few formats:
  7721. * I18n.translate("Text");
  7722. * I18n.translate(["Text {0}/{1}", 0, 1]);
  7723. * I18n.translate({raw: "Raw string"});
  7724. *
  7725. * @method translate
  7726. * @param {String/Object/Array} text Text to translate.
  7727. * @return {String} String that got translated.
  7728. */
  7729. const translate = (text) => {
  7730. const langData = getLanguageData().getOr({});
  7731. /*
  7732. * number - string
  7733. * null, undefined and empty string - empty string
  7734. * array - comma-delimited string
  7735. * object - in [object Object]
  7736. * function - in [object Function]
  7737. */
  7738. const toString = (obj) => {
  7739. if (isFunction(obj)) {
  7740. return Object.prototype.toString.call(obj);
  7741. }
  7742. return !isEmpty(obj) ? '' + obj : '';
  7743. };
  7744. const isEmpty = (text) => text === '' || text === null || text === undefined;
  7745. const getLangData = (text) => {
  7746. // make sure we work on a string and return a string
  7747. const textStr = toString(text);
  7748. return has$2(langData, textStr)
  7749. ? toString(langData[textStr])
  7750. : get$a(langData, textStr.toLowerCase()).map(toString).getOr(textStr);
  7751. };
  7752. const removeContext = (str) => str.replace(/{context:\w+}$/, '');
  7753. const replaceWithEllipsisChar = (text) => text.replaceAll('...', ellipsis);
  7754. // empty strings
  7755. if (isEmpty(text)) {
  7756. return '';
  7757. }
  7758. // Raw, already translated
  7759. if (isRaw(text)) {
  7760. return replaceWithEllipsisChar(toString(text.raw));
  7761. }
  7762. // Tokenised {translations}
  7763. if (isTokenised(text)) {
  7764. const values = text.slice(1);
  7765. const substitued = getLangData(text[0]).replace(/\{([0-9]+)\}/g, ($1, $2) => has$2(values, $2) ? toString(values[$2]) : $1);
  7766. return replaceWithEllipsisChar(removeContext(substitued));
  7767. }
  7768. // straight forward translation mapping
  7769. return replaceWithEllipsisChar(removeContext(getLangData(text)));
  7770. };
  7771. /**
  7772. * Returns true/false if the currently active language pack is rtl or not.
  7773. *
  7774. * @method isRtl
  7775. * @return {Boolean} True if the current language pack is rtl.
  7776. */
  7777. const isRtl$1 = () => getLanguageData()
  7778. .bind((items) => get$a(items, '_dir'))
  7779. .exists((dir) => dir === 'rtl');
  7780. /**
  7781. * Returns true/false if specified language pack exists.
  7782. *
  7783. * @method hasCode
  7784. * @param {String} code Code to check for.
  7785. * @return {Boolean} True if the current language pack for the specified code exists.
  7786. */
  7787. const hasCode = (code) => has$2(data, code);
  7788. const I18n = {
  7789. getData: getData$1,
  7790. setCode,
  7791. getCode,
  7792. add,
  7793. translate,
  7794. isRtl: isRtl$1,
  7795. hasCode
  7796. };
  7797. const AddOnManager = () => {
  7798. const items = [];
  7799. const urls = {};
  7800. const lookup = {};
  7801. const _listeners = [];
  7802. const runListeners = (name, state) => {
  7803. const matchedListeners = filter$5(_listeners, (listener) => listener.name === name && listener.state === state);
  7804. each$e(matchedListeners, (listener) => listener.resolve());
  7805. };
  7806. const isLoaded = (name) => has$2(urls, name);
  7807. const isAdded = (name) => has$2(lookup, name);
  7808. const get = (name) => {
  7809. if (lookup[name]) {
  7810. return lookup[name].instance;
  7811. }
  7812. return undefined;
  7813. };
  7814. const loadLanguagePack = (name, languages) => {
  7815. const language = I18n.getCode();
  7816. const wrappedLanguages = ',' + (languages || '') + ',';
  7817. if (!language || languages && wrappedLanguages.indexOf(',' + language + ',') === -1) {
  7818. return;
  7819. }
  7820. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  7821. ScriptLoader.ScriptLoader.add(urls[name] + '/langs/' + language + '.js');
  7822. };
  7823. const requireLangPack = (name, languages) => {
  7824. if (AddOnManager.languageLoad !== false) {
  7825. if (isLoaded(name)) {
  7826. loadLanguagePack(name, languages);
  7827. }
  7828. else {
  7829. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  7830. waitFor(name, 'loaded').then(() => loadLanguagePack(name, languages));
  7831. }
  7832. }
  7833. };
  7834. const add = (id, addOn) => {
  7835. items.push(addOn);
  7836. lookup[id] = { instance: addOn };
  7837. runListeners(id, 'added');
  7838. return addOn;
  7839. };
  7840. const remove = (name) => {
  7841. delete urls[name];
  7842. delete lookup[name];
  7843. };
  7844. const createUrl = (baseUrl, dep) => {
  7845. if (isString(dep)) {
  7846. return isString(baseUrl) ?
  7847. { prefix: '', resource: dep, suffix: '' } :
  7848. { prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix };
  7849. }
  7850. else {
  7851. return dep;
  7852. }
  7853. };
  7854. const load = (name, addOnUrl) => {
  7855. if (urls[name]) {
  7856. return Promise.resolve();
  7857. }
  7858. let urlString = isString(addOnUrl) ? addOnUrl : addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix;
  7859. if (urlString.indexOf('/') !== 0 && urlString.indexOf('://') === -1) {
  7860. urlString = AddOnManager.baseURL + '/' + urlString;
  7861. }
  7862. urls[name] = urlString.substring(0, urlString.lastIndexOf('/'));
  7863. const done = () => {
  7864. runListeners(name, 'loaded');
  7865. return Promise.resolve();
  7866. };
  7867. if (lookup[name]) {
  7868. return done();
  7869. }
  7870. else {
  7871. return ScriptLoader.ScriptLoader.add(urlString).then(done);
  7872. }
  7873. };
  7874. const waitFor = (name, state = 'added') => {
  7875. if (state === 'added' && isAdded(name)) {
  7876. return Promise.resolve();
  7877. }
  7878. else if (state === 'loaded' && isLoaded(name)) {
  7879. return Promise.resolve();
  7880. }
  7881. else {
  7882. return new Promise((resolve) => {
  7883. _listeners.push({ name, state, resolve });
  7884. });
  7885. }
  7886. };
  7887. return {
  7888. items,
  7889. urls,
  7890. lookup,
  7891. /**
  7892. * Returns the specified add on by the short name.
  7893. *
  7894. * @method get
  7895. * @param {String} name Add-on to look for.
  7896. * @return {tinymce.Theme/tinymce.Plugin} Theme or plugin add-on instance or undefined.
  7897. */
  7898. get,
  7899. /**
  7900. * Loads a language pack for the specified add-on.
  7901. *
  7902. * @method requireLangPack
  7903. * @param {String} name Short name of the add-on.
  7904. * @param {String} languages Optional comma or space separated list of languages to check if it matches the name.
  7905. */
  7906. requireLangPack,
  7907. /**
  7908. * Adds a instance of the add-on by it's short name.
  7909. *
  7910. * @method add
  7911. * @param {String} id Short name/id for the add-on.
  7912. * @param {tinymce.Theme/tinymce.Plugin} addOn Theme or plugin to add.
  7913. * @return {tinymce.Theme/tinymce.Plugin} The same theme or plugin instance that got passed in.
  7914. * @example
  7915. * // Create a simple plugin
  7916. * const TestPlugin = (ed, url) => {
  7917. * ed.on('click', (e) => {
  7918. * ed.windowManager.alert('Hello World!');
  7919. * });
  7920. * };
  7921. *
  7922. * // Register plugin using the add method
  7923. * tinymce.PluginManager.add('test', TestPlugin);
  7924. *
  7925. * // Initialize TinyMCE
  7926. * tinymce.init({
  7927. * ...
  7928. * plugins: '-test' // Init the plugin but don't try to load it
  7929. * });
  7930. */
  7931. add,
  7932. remove,
  7933. createUrl,
  7934. /**
  7935. * Loads an add-on from a specific url.
  7936. *
  7937. * @method load
  7938. * @param {String} name Short name of the add-on that gets loaded.
  7939. * @param {String} addOnUrl URL to the add-on that will get loaded.
  7940. * @return {Promise} A promise that will resolve when the add-on is loaded successfully or reject if it failed to load.
  7941. * @example
  7942. * // Loads a plugin from an external URL
  7943. * tinymce.PluginManager.load('myplugin', '/some/dir/someplugin/plugin.js');
  7944. *
  7945. * // Initialize TinyMCE
  7946. * tinymce.init({
  7947. * ...
  7948. * plugins: '-myplugin' // Don't try to load it again
  7949. * });
  7950. */
  7951. load,
  7952. waitFor
  7953. };
  7954. };
  7955. AddOnManager.languageLoad = true;
  7956. AddOnManager.baseURL = '';
  7957. AddOnManager.PluginManager = AddOnManager();
  7958. AddOnManager.ThemeManager = AddOnManager();
  7959. AddOnManager.ModelManager = AddOnManager();
  7960. const annotation = constant('mce-annotation');
  7961. const dataAnnotation = constant('data-mce-annotation');
  7962. const dataAnnotationId = constant('data-mce-annotation-uid');
  7963. const dataAnnotationActive = constant('data-mce-annotation-active');
  7964. const dataAnnotationClasses = constant('data-mce-annotation-classes');
  7965. const dataAnnotationAttributes = constant('data-mce-annotation-attrs');
  7966. const isRoot$1 = (root) => (node) => eq(node, root);
  7967. // Given the current editor selection, identify the uid of any current
  7968. // annotation
  7969. const identify = (editor, annotationName) => {
  7970. const rng = editor.selection.getRng();
  7971. const start = SugarElement.fromDom(rng.startContainer);
  7972. const root = SugarElement.fromDom(editor.getBody());
  7973. const selector = annotationName.fold(() => '.' + annotation(), (an) => `[${dataAnnotation()}="${an}"]`);
  7974. const newStart = child$1(start, rng.startOffset).getOr(start);
  7975. const closest = closest$3(newStart, selector, isRoot$1(root));
  7976. return closest.bind((c) => getOpt(c, `${dataAnnotationId()}`).bind((uid) => getOpt(c, `${dataAnnotation()}`).map((name) => {
  7977. const elements = findMarkers(editor, uid);
  7978. return {
  7979. uid,
  7980. name,
  7981. elements
  7982. };
  7983. })));
  7984. };
  7985. const isAnnotation = (elem) => isElement$8(elem) && has(elem, annotation());
  7986. const isBogusElement = (elem, root) => has$1(elem, 'data-mce-bogus') || ancestor$1(elem, '[data-mce-bogus="all"]', isRoot$1(root));
  7987. const findMarkers = (editor, uid) => {
  7988. const body = SugarElement.fromDom(editor.getBody());
  7989. const descendants$1 = descendants(body, `[${dataAnnotationId()}="${uid}"]`);
  7990. return filter$5(descendants$1, (descendant) => !isBogusElement(descendant, body));
  7991. };
  7992. const findAll = (editor, name) => {
  7993. const body = SugarElement.fromDom(editor.getBody());
  7994. const markers = descendants(body, `[${dataAnnotation()}="${name}"]`);
  7995. const directory = {};
  7996. each$e(markers, (m) => {
  7997. if (!isBogusElement(m, body)) {
  7998. const uid = get$9(m, dataAnnotationId());
  7999. const nodesAlready = get$a(directory, uid).getOr([]);
  8000. directory[uid] = nodesAlready.concat([m]);
  8001. }
  8002. });
  8003. return directory;
  8004. };
  8005. const setup$E = (editor, registry) => {
  8006. const changeCallbacks = Cell({});
  8007. const initData = () => ({
  8008. listeners: [],
  8009. previous: value$1()
  8010. });
  8011. const withCallbacks = (name, f) => {
  8012. updateCallbacks(name, (data) => {
  8013. f(data);
  8014. return data;
  8015. });
  8016. };
  8017. const updateCallbacks = (name, f) => {
  8018. const callbackMap = changeCallbacks.get();
  8019. const data = get$a(callbackMap, name).getOrThunk(initData);
  8020. const outputData = f(data);
  8021. callbackMap[name] = outputData;
  8022. changeCallbacks.set(callbackMap);
  8023. };
  8024. const fireCallbacks = (name, uid, elements) => {
  8025. withCallbacks(name, (data) => {
  8026. each$e(data.listeners, (f) => f(true, name, {
  8027. uid,
  8028. nodes: map$3(elements, (elem) => elem.dom)
  8029. }));
  8030. });
  8031. };
  8032. const fireNoAnnotation = (name) => {
  8033. withCallbacks(name, (data) => {
  8034. each$e(data.listeners, (f) => f(false, name));
  8035. });
  8036. };
  8037. const toggleActiveAttr = (uid, state) => {
  8038. each$e(findMarkers(editor, uid), (elem) => {
  8039. if (state) {
  8040. set$4(elem, dataAnnotationActive(), 'true');
  8041. }
  8042. else {
  8043. remove$9(elem, dataAnnotationActive());
  8044. }
  8045. });
  8046. };
  8047. // NOTE: Runs in alphabetical order.
  8048. const onNodeChange = last$1(() => {
  8049. const annotations = sort(registry.getNames());
  8050. each$e(annotations, (name) => {
  8051. updateCallbacks(name, (data) => {
  8052. const prev = data.previous.get();
  8053. identify(editor, Optional.some(name)).fold(() => {
  8054. prev.each((uid) => {
  8055. // Changed from something to nothing.
  8056. fireNoAnnotation(name);
  8057. data.previous.clear();
  8058. toggleActiveAttr(uid, false);
  8059. });
  8060. }, ({ uid, name, elements }) => {
  8061. // Changed from a different annotation (or nothing)
  8062. if (!is$4(prev, uid)) {
  8063. prev.each((uid) => toggleActiveAttr(uid, false));
  8064. fireCallbacks(name, uid, elements);
  8065. data.previous.set(uid);
  8066. toggleActiveAttr(uid, true);
  8067. }
  8068. });
  8069. return {
  8070. previous: data.previous,
  8071. listeners: data.listeners
  8072. };
  8073. });
  8074. });
  8075. }, 30);
  8076. editor.on('remove', () => {
  8077. onNodeChange.cancel();
  8078. });
  8079. editor.on('NodeChange', () => {
  8080. onNodeChange.throttle();
  8081. });
  8082. const addListener = (name, f) => {
  8083. updateCallbacks(name, (data) => ({
  8084. previous: data.previous,
  8085. listeners: data.listeners.concat([f])
  8086. }));
  8087. };
  8088. return {
  8089. addListener
  8090. };
  8091. };
  8092. const setup$D = (editor, registry) => {
  8093. const dataAnnotation$1 = dataAnnotation();
  8094. const identifyParserNode = (node) => Optional.from(node.attr(dataAnnotation$1)).bind(registry.lookup);
  8095. const removeDirectAnnotation = (node) => {
  8096. var _a, _b;
  8097. node.attr(dataAnnotationId(), null);
  8098. node.attr(dataAnnotation(), null);
  8099. node.attr(dataAnnotationActive(), null);
  8100. const customAttrNames = Optional.from(node.attr(dataAnnotationAttributes())).map((names) => names.split(',')).getOr([]);
  8101. const customClasses = Optional.from(node.attr(dataAnnotationClasses())).map((names) => names.split(',')).getOr([]);
  8102. each$e(customAttrNames, (name) => node.attr(name, null));
  8103. const classList = (_b = (_a = node.attr('class')) === null || _a === void 0 ? void 0 : _a.split(' ')) !== null && _b !== void 0 ? _b : [];
  8104. const newClassList = difference(classList, [annotation()].concat(customClasses));
  8105. node.attr('class', newClassList.length > 0 ? newClassList.join(' ') : null);
  8106. node.attr(dataAnnotationClasses(), null);
  8107. node.attr(dataAnnotationAttributes(), null);
  8108. };
  8109. editor.serializer.addTempAttr(dataAnnotationActive());
  8110. editor.serializer.addAttributeFilter(dataAnnotation$1, (nodes) => {
  8111. for (const node of nodes) {
  8112. identifyParserNode(node).each((settings) => {
  8113. if (settings.persistent === false) {
  8114. if (node.name === 'span') {
  8115. node.unwrap();
  8116. }
  8117. else {
  8118. removeDirectAnnotation(node);
  8119. }
  8120. }
  8121. });
  8122. }
  8123. });
  8124. };
  8125. const create$a = () => {
  8126. const annotations = {};
  8127. const register = (name, settings) => {
  8128. annotations[name] = {
  8129. name,
  8130. settings
  8131. };
  8132. };
  8133. const lookup = (name) => get$a(annotations, name).map((a) => a.settings);
  8134. const getNames = () => keys(annotations);
  8135. return {
  8136. register,
  8137. lookup,
  8138. getNames
  8139. };
  8140. };
  8141. const TextWalker = (startNode, rootNode, isBoundary = never) => {
  8142. const walker = new DomTreeWalker(startNode, rootNode);
  8143. const walk = (direction) => {
  8144. let next;
  8145. do {
  8146. next = walker[direction]();
  8147. } while (next && !isText$b(next) && !isBoundary(next));
  8148. return Optional.from(next).filter(isText$b);
  8149. };
  8150. return {
  8151. current: () => Optional.from(walker.current()).filter(isText$b),
  8152. next: () => walk('next'),
  8153. prev: () => walk('prev'),
  8154. prev2: () => walk('prev2')
  8155. };
  8156. };
  8157. /**
  8158. * The TextSeeker class enables you to seek for a specific point in text across the DOM.
  8159. *
  8160. * @class tinymce.dom.TextSeeker
  8161. * @example
  8162. * const seeker = tinymce.dom.TextSeeker(editor.dom);
  8163. * const startOfWord = seeker.backwards(startNode, startOffset, (textNode, offset, text) => {
  8164. * const lastSpaceCharIndex = text.lastIndexOf(' ');
  8165. * if (lastSpaceCharIndex !== -1) {
  8166. * return lastSpaceCharIndex + 1;
  8167. * } else {
  8168. * // No space found so continue searching
  8169. * return -1;
  8170. * }
  8171. * });
  8172. */
  8173. /**
  8174. * Constructs a new TextSeeker instance.
  8175. *
  8176. * @constructor
  8177. * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference.
  8178. * @param {Function} isBoundary Optional function to determine if the seeker should continue to walk past the node provided. The default is to search until a block or <code>br</code> element is found.
  8179. */
  8180. const TextSeeker = (dom, isBoundary) => {
  8181. const isBlockBoundary = isBoundary ? isBoundary : (node) => dom.isBlock(node) || isBr$7(node) || isContentEditableFalse$a(node);
  8182. const walk = (node, offset, walker, process) => {
  8183. if (isText$b(node)) {
  8184. const newOffset = process(node, offset, node.data);
  8185. if (newOffset !== -1) {
  8186. return Optional.some({ container: node, offset: newOffset });
  8187. }
  8188. }
  8189. return walker().bind((next) => walk(next.container, next.offset, walker, process));
  8190. };
  8191. /**
  8192. * Search backwards through text nodes until a match, boundary, or root node has been found.
  8193. *
  8194. * @method backwards
  8195. * @param {Node} node The node to start searching from.
  8196. * @param {Number} offset The offset of the node to start searching from.
  8197. * @param {Function} process A function that's passed the current text node, the current offset and the text content of the node. It should return the offset of the match or -1 to continue searching.
  8198. * @param {Node} root An optional root node to constrain the search to.
  8199. * @return {Object} An object containing the matched text node and offset. If no match is found, null will be returned.
  8200. */
  8201. const backwards = (node, offset, process, root) => {
  8202. const walker = TextWalker(node, root !== null && root !== void 0 ? root : dom.getRoot(), isBlockBoundary);
  8203. return walk(node, offset, () => walker.prev().map((prev) => ({ container: prev, offset: prev.length })), process).getOrNull();
  8204. };
  8205. /**
  8206. * Search forwards through text nodes until a match, boundary, or root node has been found.
  8207. *
  8208. * @method forwards
  8209. * @param {Node} node The node to start searching from.
  8210. * @param {Number} offset The offset of the node to start searching from.
  8211. * @param {Function} process A function that's passed the current text node, the current offset and the text content of the node. It should return the offset of the match or -1 to continue searching.
  8212. * @param {Node} root An optional root node to constrain the search to.
  8213. * @return {Object} An object containing the matched text node and offset. If no match is found, null will be returned.
  8214. */
  8215. const forwards = (node, offset, process, root) => {
  8216. const walker = TextWalker(node, root !== null && root !== void 0 ? root : dom.getRoot(), isBlockBoundary);
  8217. return walk(node, offset, () => walker.next().map((next) => ({ container: next, offset: 0 })), process).getOrNull();
  8218. };
  8219. return {
  8220. backwards,
  8221. forwards
  8222. };
  8223. };
  8224. const tableCells = ['td', 'th'];
  8225. const tableSections = ['thead', 'tbody', 'tfoot'];
  8226. const textBlocks = [
  8227. 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'address', 'pre', 'form',
  8228. 'blockquote', 'center', 'dir', 'fieldset', 'header', 'footer', 'article',
  8229. 'section', 'hgroup', 'aside', 'nav', 'figure'
  8230. ];
  8231. const listItems$1 = ['li', 'dd', 'dt'];
  8232. const lists = ['ul', 'ol', 'dl'];
  8233. const wsElements = ['pre', 'script', 'textarea', 'style'];
  8234. const lazyLookup = (items) => {
  8235. let lookup;
  8236. return (node) => {
  8237. lookup = lookup ? lookup : mapToObject(items, always);
  8238. return has$2(lookup, name(node));
  8239. };
  8240. };
  8241. // WARNING: don't add anything to this file, the intention is to move these checks into the Schema
  8242. const isTable$1 = (node) => name(node) === 'table';
  8243. const isBr$6 = (node) => isElement$8(node) && name(node) === 'br';
  8244. const isTextBlock$3 = lazyLookup(textBlocks);
  8245. const isList$1 = lazyLookup(lists);
  8246. const isListItem$2 = lazyLookup(listItems$1);
  8247. const isTableSection = lazyLookup(tableSections);
  8248. const isTableCell$2 = lazyLookup(tableCells);
  8249. const isWsPreserveElement = lazyLookup(wsElements);
  8250. const getLastChildren$1 = (elm) => {
  8251. const children = [];
  8252. let rawNode = elm.dom;
  8253. while (rawNode) {
  8254. children.push(SugarElement.fromDom(rawNode));
  8255. rawNode = rawNode.lastChild;
  8256. }
  8257. return children;
  8258. };
  8259. const removeTrailingBr = (elm) => {
  8260. const allBrs = descendants(elm, 'br');
  8261. const brs = filter$5(getLastChildren$1(elm).slice(-1), isBr$6);
  8262. if (allBrs.length === brs.length) {
  8263. each$e(brs, remove$8);
  8264. }
  8265. };
  8266. const createPaddingBr = () => {
  8267. const br = SugarElement.fromTag('br');
  8268. set$4(br, 'data-mce-bogus', '1');
  8269. return br;
  8270. };
  8271. const fillWithPaddingBr = (elm) => {
  8272. empty(elm);
  8273. append$1(elm, createPaddingBr());
  8274. };
  8275. const trimBlockTrailingBr = (elm, schema) => {
  8276. lastChild(elm).each((lastChild) => {
  8277. prevSibling(lastChild).each((lastChildPrevSibling) => {
  8278. if (schema.isBlock(name(elm)) && isBr$6(lastChild) && schema.isBlock(name(lastChildPrevSibling))) {
  8279. remove$8(lastChild);
  8280. }
  8281. });
  8282. });
  8283. };
  8284. /**
  8285. * Utility functions for working with zero width space
  8286. * characters used as character containers etc.
  8287. *
  8288. * @private
  8289. * @class tinymce.text.Zwsp
  8290. * @example
  8291. * const isZwsp = Zwsp.isZwsp('\uFEFF');
  8292. * const abc = Zwsp.trim('a\uFEFFc');
  8293. */
  8294. // This is technically not a ZWSP but a ZWNBSP or a BYTE ORDER MARK it used to be a ZWSP
  8295. const ZWSP$1 = zeroWidth;
  8296. const isZwsp = isZwsp$2;
  8297. const trim$2 = removeZwsp;
  8298. const insert$5 = (editor) => editor.insertContent(ZWSP$1, { preserve_zwsp: true });
  8299. /**
  8300. * This module handles caret containers. A caret container is a node that
  8301. * holds the caret for positional purposes.
  8302. *
  8303. * @private
  8304. * @class tinymce.caret.CaretContainer
  8305. */
  8306. const isElement$6 = isElement$7;
  8307. const isText$9 = isText$b;
  8308. const isCaretContainerBlock$1 = (node) => {
  8309. if (isText$9(node)) {
  8310. node = node.parentNode;
  8311. }
  8312. return isElement$6(node) && node.hasAttribute('data-mce-caret');
  8313. };
  8314. const isCaretContainerInline = (node) => isText$9(node) && isZwsp(node.data);
  8315. const isCaretContainer$2 = (node) => isCaretContainerBlock$1(node) || isCaretContainerInline(node);
  8316. const hasContent = (node) => node.firstChild !== node.lastChild || !isBr$7(node.firstChild);
  8317. const insertInline$1 = (node, before) => {
  8318. var _a;
  8319. const doc = (_a = node.ownerDocument) !== null && _a !== void 0 ? _a : document;
  8320. const textNode = doc.createTextNode(ZWSP$1);
  8321. const parentNode = node.parentNode;
  8322. if (!before) {
  8323. const sibling = node.nextSibling;
  8324. if (isText$9(sibling)) {
  8325. if (isCaretContainer$2(sibling)) {
  8326. return sibling;
  8327. }
  8328. if (startsWithCaretContainer$1(sibling)) {
  8329. sibling.splitText(1);
  8330. return sibling;
  8331. }
  8332. }
  8333. if (node.nextSibling) {
  8334. parentNode === null || parentNode === void 0 ? void 0 : parentNode.insertBefore(textNode, node.nextSibling);
  8335. }
  8336. else {
  8337. parentNode === null || parentNode === void 0 ? void 0 : parentNode.appendChild(textNode);
  8338. }
  8339. }
  8340. else {
  8341. const sibling = node.previousSibling;
  8342. if (isText$9(sibling)) {
  8343. if (isCaretContainer$2(sibling)) {
  8344. return sibling;
  8345. }
  8346. if (endsWithCaretContainer$1(sibling)) {
  8347. return sibling.splitText(sibling.data.length - 1);
  8348. }
  8349. }
  8350. parentNode === null || parentNode === void 0 ? void 0 : parentNode.insertBefore(textNode, node);
  8351. }
  8352. return textNode;
  8353. };
  8354. const isBeforeInline = (pos) => {
  8355. const container = pos.container();
  8356. if (!isText$b(container)) {
  8357. return false;
  8358. }
  8359. // The text nodes may not be normalized, so check the current node and the previous one
  8360. return container.data.charAt(pos.offset()) === ZWSP$1 || pos.isAtStart() && isCaretContainerInline(container.previousSibling);
  8361. };
  8362. const isAfterInline = (pos) => {
  8363. const container = pos.container();
  8364. if (!isText$b(container)) {
  8365. return false;
  8366. }
  8367. // The text nodes may not be normalized, so check the current node and the next one
  8368. return container.data.charAt(pos.offset() - 1) === ZWSP$1 || pos.isAtEnd() && isCaretContainerInline(container.nextSibling);
  8369. };
  8370. const insertBlock = (blockName, node, before) => {
  8371. var _a;
  8372. const doc = (_a = node.ownerDocument) !== null && _a !== void 0 ? _a : document;
  8373. const blockNode = doc.createElement(blockName);
  8374. blockNode.setAttribute('data-mce-caret', before ? 'before' : 'after');
  8375. blockNode.setAttribute('data-mce-bogus', 'all');
  8376. blockNode.appendChild(createPaddingBr().dom);
  8377. const parentNode = node.parentNode;
  8378. if (!before) {
  8379. if (node.nextSibling) {
  8380. parentNode === null || parentNode === void 0 ? void 0 : parentNode.insertBefore(blockNode, node.nextSibling);
  8381. }
  8382. else {
  8383. parentNode === null || parentNode === void 0 ? void 0 : parentNode.appendChild(blockNode);
  8384. }
  8385. }
  8386. else {
  8387. parentNode === null || parentNode === void 0 ? void 0 : parentNode.insertBefore(blockNode, node);
  8388. }
  8389. return blockNode;
  8390. };
  8391. const startsWithCaretContainer$1 = (node) => isText$9(node) && node.data[0] === ZWSP$1;
  8392. const endsWithCaretContainer$1 = (node) => isText$9(node) && node.data[node.data.length - 1] === ZWSP$1;
  8393. const trimBogusBr = (elm) => {
  8394. var _a;
  8395. const brs = elm.getElementsByTagName('br');
  8396. const lastBr = brs[brs.length - 1];
  8397. if (isBogus$1(lastBr)) {
  8398. (_a = lastBr.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(lastBr);
  8399. }
  8400. };
  8401. const showCaretContainerBlock = (caretContainer) => {
  8402. if (caretContainer && caretContainer.hasAttribute('data-mce-caret')) {
  8403. trimBogusBr(caretContainer);
  8404. caretContainer.removeAttribute('data-mce-caret');
  8405. caretContainer.removeAttribute('data-mce-bogus');
  8406. caretContainer.removeAttribute('style');
  8407. caretContainer.removeAttribute('data-mce-style');
  8408. caretContainer.removeAttribute('_moz_abspos');
  8409. return caretContainer;
  8410. }
  8411. return null;
  8412. };
  8413. const isRangeInCaretContainerBlock = (range) => isCaretContainerBlock$1(range.startContainer);
  8414. const round$2 = Math.round;
  8415. const clone$1 = (rect) => {
  8416. if (!rect) {
  8417. return { left: 0, top: 0, bottom: 0, right: 0, width: 0, height: 0 };
  8418. }
  8419. return {
  8420. left: round$2(rect.left),
  8421. top: round$2(rect.top),
  8422. bottom: round$2(rect.bottom),
  8423. right: round$2(rect.right),
  8424. width: round$2(rect.width),
  8425. height: round$2(rect.height)
  8426. };
  8427. };
  8428. const collapse = (rect, toStart) => {
  8429. rect = clone$1(rect);
  8430. if (toStart) {
  8431. rect.right = rect.left;
  8432. }
  8433. else {
  8434. rect.left = rect.left + rect.width;
  8435. rect.right = rect.left;
  8436. }
  8437. rect.width = 0;
  8438. return rect;
  8439. };
  8440. const isEqual = (rect1, rect2) => (rect1.left === rect2.left &&
  8441. rect1.top === rect2.top &&
  8442. rect1.bottom === rect2.bottom &&
  8443. rect1.right === rect2.right);
  8444. const isValidOverflow = (overflowY, rect1, rect2) => overflowY >= 0 && overflowY <= Math.min(rect1.height, rect2.height) / 2;
  8445. const isAbove$1 = (rect1, rect2) => {
  8446. const halfHeight = Math.min(rect2.height / 2, rect1.height / 2);
  8447. if ((rect1.bottom - halfHeight) < rect2.top) {
  8448. return true;
  8449. }
  8450. if (rect1.top > rect2.bottom) {
  8451. return false;
  8452. }
  8453. return isValidOverflow(rect2.top - rect1.bottom, rect1, rect2);
  8454. };
  8455. const isBelow$1 = (rect1, rect2) => {
  8456. if (rect1.top > rect2.bottom) {
  8457. return true;
  8458. }
  8459. if (rect1.bottom < rect2.top) {
  8460. return false;
  8461. }
  8462. return isValidOverflow(rect2.bottom - rect1.top, rect1, rect2);
  8463. };
  8464. const containsXY = (rect, clientX, clientY) => (clientX >= rect.left &&
  8465. clientX <= rect.right &&
  8466. clientY >= rect.top &&
  8467. clientY <= rect.bottom);
  8468. const boundingClientRectFromRects = (rects) => {
  8469. return foldl(rects, (acc, rect) => {
  8470. return acc.fold(() => Optional.some(rect), (prevRect) => {
  8471. const left = Math.min(rect.left, prevRect.left);
  8472. const top = Math.min(rect.top, prevRect.top);
  8473. const right = Math.max(rect.right, prevRect.right);
  8474. const bottom = Math.max(rect.bottom, prevRect.bottom);
  8475. return Optional.some({
  8476. top,
  8477. right,
  8478. bottom,
  8479. left,
  8480. width: right - left,
  8481. height: bottom - top
  8482. });
  8483. });
  8484. }, Optional.none());
  8485. };
  8486. const distanceToRectEdgeFromXY = (rect, x, y) => {
  8487. const cx = Math.max(Math.min(x, rect.left + rect.width), rect.left);
  8488. const cy = Math.max(Math.min(y, rect.top + rect.height), rect.top);
  8489. return Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy));
  8490. };
  8491. const overlapY = (r1, r2) => Math.max(0, Math.min(r1.bottom, r2.bottom) - Math.max(r1.top, r2.top));
  8492. const getSelectedNode = (range) => {
  8493. const startContainer = range.startContainer, startOffset = range.startOffset;
  8494. if (startContainer === range.endContainer && startContainer.hasChildNodes() && range.endOffset === startOffset + 1) {
  8495. return startContainer.childNodes[startOffset];
  8496. }
  8497. return null;
  8498. };
  8499. const getNode$1 = (container, offset) => {
  8500. if (isElement$7(container) && container.hasChildNodes()) {
  8501. const childNodes = container.childNodes;
  8502. const safeOffset = clamp$2(offset, 0, childNodes.length - 1);
  8503. return childNodes[safeOffset];
  8504. }
  8505. else {
  8506. return container;
  8507. }
  8508. };
  8509. /** @deprecated Use getNode instead */
  8510. const getNodeUnsafe = (container, offset) => {
  8511. // If a negative offset is used on an element then `undefined` should be returned
  8512. if (offset < 0 && isElement$7(container) && container.hasChildNodes()) {
  8513. return undefined;
  8514. }
  8515. else {
  8516. return getNode$1(container, offset);
  8517. }
  8518. };
  8519. /**
  8520. * This class contains logic for detecting extending characters.
  8521. *
  8522. * @private
  8523. * @class tinymce.text.ExtendingChar
  8524. * @example
  8525. * const isExtending = ExtendingChar.isExtendingChar('a');
  8526. */
  8527. // Generated from: http://www.unicode.org/Public/UNIDATA/DerivedCoreProperties.txt
  8528. // Only includes the characters in that fit into UCS-2 16 bit
  8529. const extendingChars = new RegExp('[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A' +
  8530. '\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0' +
  8531. '\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08E3-\u0902\u093A\u093C' +
  8532. '\u0941-\u0948\u094D\u0951-\u0957\u0962-\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09CD\u09D7\u09E2-\u09E3' +
  8533. '\u0A01-\u0A02\u0A3C\u0A41-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A51\u0A70-\u0A71\u0A75\u0A81-\u0A82\u0ABC' +
  8534. '\u0AC1-\u0AC5\u0AC7-\u0AC8\u0ACD\u0AE2-\u0AE3\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B4D\u0B56\u0B57' +
  8535. '\u0B62-\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56' +
  8536. '\u0C62-\u0C63\u0C81\u0CBC\u0CBF\u0CC2\u0CC6\u0CCC-\u0CCD\u0CD5-\u0CD6\u0CE2-\u0CE3\u0D01\u0D3E\u0D41-\u0D44' +
  8537. '\u0D4D\u0D57\u0D62-\u0D63\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9' +
  8538. '\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86-\u0F87\u0F8D-\u0F97' +
  8539. '\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039-\u103A\u103D-\u103E\u1058-\u1059\u105E-\u1060\u1071-\u1074' +
  8540. '\u1082\u1085-\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B4-\u17B5' +
  8541. '\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u18A9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193B\u1A17-\u1A18' +
  8542. '\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABD\u1ABE\u1B00-\u1B03\u1B34' +
  8543. '\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80-\u1B81\u1BA2-\u1BA5\u1BA8-\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8-\u1BE9' +
  8544. '\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8-\u1CF9' +
  8545. '\u1DC0-\u1DF5\u1DFC-\u1DFF\u200C-\u200D\u20D0-\u20DC\u20DD-\u20E0\u20E1\u20E2-\u20E4\u20E5-\u20F0\u2CEF-\u2CF1' +
  8546. '\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u302E-\u302F\u3099-\u309A\uA66F\uA670-\uA672\uA674-\uA67D\uA69E-\uA69F\uA6F0-\uA6F1' +
  8547. '\uA802\uA806\uA80B\uA825-\uA826\uA8C4\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC' +
  8548. '\uA9E5\uAA29-\uAA2E\uAA31-\uAA32\uAA35-\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7-\uAAB8\uAABE-\uAABF\uAAC1' +
  8549. '\uAAEC-\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E-\uFF9F]');
  8550. const isExtendingChar = (ch) => isString(ch) && ch.charCodeAt(0) >= 768 && extendingChars.test(ch);
  8551. const or = (...args) => {
  8552. return (x) => {
  8553. for (let i = 0; i < args.length; i++) {
  8554. if (args[i](x)) {
  8555. return true;
  8556. }
  8557. }
  8558. return false;
  8559. };
  8560. };
  8561. const and = (...args) => {
  8562. return (x) => {
  8563. for (let i = 0; i < args.length; i++) {
  8564. if (!args[i](x)) {
  8565. return false;
  8566. }
  8567. }
  8568. return true;
  8569. };
  8570. };
  8571. /**
  8572. * This module contains logic for handling caret candidates. A caret candidate is
  8573. * for example text nodes, images, input elements, cE=false elements etc.
  8574. *
  8575. * @private
  8576. * @class tinymce.caret.CaretCandidate
  8577. */
  8578. const isContentEditableTrue$2 = isContentEditableTrue$3;
  8579. const isContentEditableFalse$9 = isContentEditableFalse$a;
  8580. const isBr$5 = isBr$7;
  8581. const isText$8 = isText$b;
  8582. const isInvalidTextElement = matchNodeNames$1(['script', 'style', 'textarea']);
  8583. const isAtomicInline = matchNodeNames$1(['img', 'input', 'textarea', 'hr', 'iframe', 'video', 'audio', 'object', 'embed']);
  8584. const isTable = matchNodeNames$1(['table']);
  8585. const isCaretContainer$1 = isCaretContainer$2;
  8586. const isCaretCandidate$3 = (node) => {
  8587. if (isCaretContainer$1(node)) {
  8588. return false;
  8589. }
  8590. if (isText$8(node)) {
  8591. return !isInvalidTextElement(node.parentNode);
  8592. }
  8593. return isAtomicInline(node) || isBr$5(node) || isTable(node) || isNonUiContentEditableFalse(node);
  8594. };
  8595. // UI components on IE is marked with contenteditable=false and unselectable=true so lets not handle those as real content editables
  8596. const isUnselectable = (node) => isElement$7(node) && node.getAttribute('unselectable') === 'true';
  8597. const isNonUiContentEditableFalse = (node) => !isUnselectable(node) && isContentEditableFalse$9(node);
  8598. const isInEditable = (node, root) => {
  8599. for (let tempNode = node.parentNode; tempNode && tempNode !== root; tempNode = tempNode.parentNode) {
  8600. if (isNonUiContentEditableFalse(tempNode)) {
  8601. return false;
  8602. }
  8603. if (isContentEditableTrue$2(tempNode)) {
  8604. return true;
  8605. }
  8606. }
  8607. return true;
  8608. };
  8609. const isAtomicContentEditableFalse = (node) => {
  8610. if (!isNonUiContentEditableFalse(node)) {
  8611. return false;
  8612. }
  8613. return !foldl(from(node.getElementsByTagName('*')), (result, elm) => {
  8614. return result || isContentEditableTrue$2(elm);
  8615. }, false);
  8616. };
  8617. const isAtomic$1 = (node) => isAtomicInline(node) || isAtomicContentEditableFalse(node);
  8618. const isEditableCaretCandidate$1 = (node, root) => isCaretCandidate$3(node) && isInEditable(node, root);
  8619. /**
  8620. * This module contains logic for creating caret positions within a document a caretposition
  8621. * is similar to a DOMRange object but it doesn't have two endpoints and is also more lightweight
  8622. * since it's now updated live when the DOM changes.
  8623. *
  8624. * @private
  8625. * @class tinymce.caret.CaretPosition
  8626. * @example
  8627. * const caretPos1 = CaretPosition(container, offset);
  8628. * const caretPos2 = CaretPosition.fromRangeStart(someRange);
  8629. */
  8630. const isElement$5 = isElement$7;
  8631. const isCaretCandidate$2 = isCaretCandidate$3;
  8632. const isBlock$3 = matchStyleValues('display', 'block table');
  8633. const isFloated = matchStyleValues('float', 'left right');
  8634. const isValidElementCaretCandidate = and(isElement$5, isCaretCandidate$2, not(isFloated));
  8635. const isNotPre = not(matchStyleValues('white-space', 'pre pre-line pre-wrap'));
  8636. const isText$7 = isText$b;
  8637. const isBr$4 = isBr$7;
  8638. const nodeIndex$1 = DOMUtils.nodeIndex;
  8639. const resolveIndex$1 = getNodeUnsafe;
  8640. const createRange$1 = (doc) => doc ? doc.createRange() : DOMUtils.DOM.createRng();
  8641. const isWhiteSpace$1 = (chr) => isString(chr) && /[\r\n\t ]/.test(chr);
  8642. const isRange = (rng) => !!rng.setStart && !!rng.setEnd;
  8643. const isHiddenWhiteSpaceRange = (range) => {
  8644. const container = range.startContainer;
  8645. const offset = range.startOffset;
  8646. if (isWhiteSpace$1(range.toString()) && isNotPre(container.parentNode) && isText$b(container)) {
  8647. const text = container.data;
  8648. if (isWhiteSpace$1(text[offset - 1]) || isWhiteSpace$1(text[offset + 1])) {
  8649. return true;
  8650. }
  8651. }
  8652. return false;
  8653. };
  8654. // Hack for older WebKit versions that doesn't
  8655. // support getBoundingClientRect on BR elements
  8656. const getBrClientRect = (brNode) => {
  8657. const doc = brNode.ownerDocument;
  8658. const rng = createRange$1(doc);
  8659. const nbsp$1 = doc.createTextNode(nbsp);
  8660. const parentNode = brNode.parentNode;
  8661. parentNode.insertBefore(nbsp$1, brNode);
  8662. rng.setStart(nbsp$1, 0);
  8663. rng.setEnd(nbsp$1, 1);
  8664. const clientRect = clone$1(rng.getBoundingClientRect());
  8665. parentNode.removeChild(nbsp$1);
  8666. return clientRect;
  8667. };
  8668. // Safari will not return a rect for <p>a<br>|b</p> for some odd reason
  8669. const getBoundingClientRectWebKitText = (rng) => {
  8670. const sc = rng.startContainer;
  8671. const ec = rng.endContainer;
  8672. const so = rng.startOffset;
  8673. const eo = rng.endOffset;
  8674. if (sc === ec && isText$b(ec) && so === 0 && eo === 1) {
  8675. const newRng = rng.cloneRange();
  8676. newRng.setEndAfter(ec);
  8677. return getBoundingClientRect$1(newRng);
  8678. }
  8679. else {
  8680. return null;
  8681. }
  8682. };
  8683. const isZeroRect = (r) => r.left === 0 && r.right === 0 && r.top === 0 && r.bottom === 0;
  8684. const getBoundingClientRect$1 = (item) => {
  8685. var _a;
  8686. let clientRect;
  8687. const clientRects = item.getClientRects();
  8688. if (clientRects.length > 0) {
  8689. clientRect = clone$1(clientRects[0]);
  8690. }
  8691. else {
  8692. clientRect = clone$1(item.getBoundingClientRect());
  8693. }
  8694. if (!isRange(item) && isBr$4(item) && isZeroRect(clientRect)) {
  8695. return getBrClientRect(item);
  8696. }
  8697. if (isZeroRect(clientRect) && isRange(item)) {
  8698. return (_a = getBoundingClientRectWebKitText(item)) !== null && _a !== void 0 ? _a : clientRect;
  8699. }
  8700. return clientRect;
  8701. };
  8702. const collapseAndInflateWidth = (clientRect, toStart) => {
  8703. const newClientRect = collapse(clientRect, toStart);
  8704. newClientRect.width = 1;
  8705. newClientRect.right = newClientRect.left + 1;
  8706. return newClientRect;
  8707. };
  8708. const getCaretPositionClientRects = (caretPosition) => {
  8709. const clientRects = [];
  8710. const addUniqueAndValidRect = (clientRect) => {
  8711. if (clientRect.height === 0) {
  8712. return;
  8713. }
  8714. if (clientRects.length > 0) {
  8715. if (isEqual(clientRect, clientRects[clientRects.length - 1])) {
  8716. return;
  8717. }
  8718. }
  8719. clientRects.push(clientRect);
  8720. };
  8721. const addCharacterOffset = (container, offset) => {
  8722. const range = createRange$1(container.ownerDocument);
  8723. if (offset < container.data.length) {
  8724. if (isExtendingChar(container.data[offset])) {
  8725. return;
  8726. }
  8727. // WebKit returns two client rects for a position after an extending
  8728. // character a\uxxx|b so expand on "b" and collapse to start of "b" box
  8729. if (isExtendingChar(container.data[offset - 1])) {
  8730. range.setStart(container, offset);
  8731. range.setEnd(container, offset + 1);
  8732. if (!isHiddenWhiteSpaceRange(range)) {
  8733. addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect$1(range), false));
  8734. return;
  8735. }
  8736. }
  8737. }
  8738. if (offset > 0) {
  8739. range.setStart(container, offset - 1);
  8740. range.setEnd(container, offset);
  8741. if (!isHiddenWhiteSpaceRange(range)) {
  8742. addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect$1(range), false));
  8743. }
  8744. }
  8745. if (offset < container.data.length) {
  8746. range.setStart(container, offset);
  8747. range.setEnd(container, offset + 1);
  8748. if (!isHiddenWhiteSpaceRange(range)) {
  8749. addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect$1(range), true));
  8750. }
  8751. }
  8752. };
  8753. const container = caretPosition.container();
  8754. const offset = caretPosition.offset();
  8755. if (isText$7(container)) {
  8756. addCharacterOffset(container, offset);
  8757. return clientRects;
  8758. }
  8759. if (isElement$5(container)) {
  8760. if (caretPosition.isAtEnd()) {
  8761. const node = resolveIndex$1(container, offset);
  8762. if (isText$7(node)) {
  8763. addCharacterOffset(node, node.data.length);
  8764. }
  8765. if (isValidElementCaretCandidate(node) && !isBr$4(node)) {
  8766. addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect$1(node), false));
  8767. }
  8768. }
  8769. else {
  8770. const node = resolveIndex$1(container, offset);
  8771. if (isText$7(node)) {
  8772. addCharacterOffset(node, 0);
  8773. }
  8774. if (isValidElementCaretCandidate(node) && caretPosition.isAtEnd()) {
  8775. addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect$1(node), false));
  8776. return clientRects;
  8777. }
  8778. const beforeNode = resolveIndex$1(caretPosition.container(), caretPosition.offset() - 1);
  8779. if (isValidElementCaretCandidate(beforeNode) && !isBr$4(beforeNode)) {
  8780. if (isBlock$3(beforeNode) || isBlock$3(node) || !isValidElementCaretCandidate(node)) {
  8781. addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect$1(beforeNode), false));
  8782. }
  8783. }
  8784. if (isValidElementCaretCandidate(node)) {
  8785. addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect$1(node), true));
  8786. }
  8787. }
  8788. }
  8789. return clientRects;
  8790. };
  8791. /**
  8792. * Represents a location within the document by a container and an offset.
  8793. *
  8794. * @constructor
  8795. * @param {Node} container Container node.
  8796. * @param {Number} offset Offset within that container node.
  8797. * @param {Array} clientRects Optional client rects array for the position.
  8798. */
  8799. const CaretPosition = (container, offset, clientRects) => {
  8800. const isAtStart = () => {
  8801. if (isText$7(container)) {
  8802. return offset === 0;
  8803. }
  8804. return offset === 0;
  8805. };
  8806. const isAtEnd = () => {
  8807. if (isText$7(container)) {
  8808. return offset >= container.data.length;
  8809. }
  8810. return offset >= container.childNodes.length;
  8811. };
  8812. const toRange = () => {
  8813. const range = createRange$1(container.ownerDocument);
  8814. range.setStart(container, offset);
  8815. range.setEnd(container, offset);
  8816. return range;
  8817. };
  8818. const getClientRects = () => {
  8819. if (!clientRects) {
  8820. clientRects = getCaretPositionClientRects(CaretPosition(container, offset));
  8821. }
  8822. return clientRects;
  8823. };
  8824. const isVisible = () => getClientRects().length > 0;
  8825. const isEqual = (caretPosition) => caretPosition && container === caretPosition.container() && offset === caretPosition.offset();
  8826. const getNode = (before) => resolveIndex$1(container, before ? offset - 1 : offset);
  8827. return {
  8828. /**
  8829. * Returns the container node.
  8830. *
  8831. * @method container
  8832. * @return {Node} Container node.
  8833. */
  8834. container: constant(container),
  8835. /**
  8836. * Returns the offset within the container node.
  8837. *
  8838. * @method offset
  8839. * @return {Number} Offset within the container node.
  8840. */
  8841. offset: constant(offset),
  8842. /**
  8843. * Returns a range out of a the caret position.
  8844. *
  8845. * @method toRange
  8846. * @return {DOMRange} range for the caret position.
  8847. */
  8848. toRange,
  8849. /**
  8850. * Returns the client rects for the caret position. Might be multiple rects between
  8851. * block elements.
  8852. *
  8853. * @method getClientRects
  8854. * @return {Array} Array of client rects.
  8855. */
  8856. getClientRects,
  8857. /**
  8858. * Returns true if the caret location is visible/displayed on screen.
  8859. *
  8860. * @method isVisible
  8861. * @return {Boolean} true/false if the position is visible or not.
  8862. */
  8863. isVisible,
  8864. /**
  8865. * Returns true if the caret location is at the beginning of text node or container.
  8866. *
  8867. * @method isVisible
  8868. * @return {Boolean} true/false if the position is at the beginning.
  8869. */
  8870. isAtStart,
  8871. /**
  8872. * Returns true if the caret location is at the end of text node or container.
  8873. *
  8874. * @method isVisible
  8875. * @return {Boolean} true/false if the position is at the end.
  8876. */
  8877. isAtEnd,
  8878. /**
  8879. * Compares the caret position to another caret position. This will only compare the
  8880. * container and offset not it's visual position.
  8881. *
  8882. * @method isEqual
  8883. * @param {tinymce.caret.CaretPosition} caretPosition Caret position to compare with.
  8884. * @return {Boolean} true if the caret positions are equal.
  8885. */
  8886. isEqual,
  8887. /**
  8888. * Returns the closest resolved node from a node index. That means if you have an offset after the
  8889. * last node in a container it will return that last node.
  8890. *
  8891. * @method getNode
  8892. * @return {Node} Node that is closest to the index.
  8893. */
  8894. getNode
  8895. };
  8896. };
  8897. /**
  8898. * Creates a caret position from the start of a range.
  8899. *
  8900. * @method fromRangeStart
  8901. * @param {DOMRange} range DOM Range to create caret position from.
  8902. * @return {tinymce.caret.CaretPosition} Caret position from the start of DOM range.
  8903. */
  8904. CaretPosition.fromRangeStart = (range) => CaretPosition(range.startContainer, range.startOffset);
  8905. /**
  8906. * Creates a caret position from the end of a range.
  8907. *
  8908. * @method fromRangeEnd
  8909. * @param {DOMRange} range DOM Range to create caret position from.
  8910. * @return {tinymce.caret.CaretPosition} Caret position from the end of DOM range.
  8911. */
  8912. CaretPosition.fromRangeEnd = (range) => CaretPosition(range.endContainer, range.endOffset);
  8913. /**
  8914. * Creates a caret position from a node and places the offset after it.
  8915. *
  8916. * @method after
  8917. * @param {Node} node Node to get caret position from.
  8918. * @return {tinymce.caret.CaretPosition} Caret position from the node.
  8919. */
  8920. // TODO: TINY-8865 - This may not be safe to cast as Node and alternative solutions need to be looked into
  8921. CaretPosition.after = (node) => CaretPosition(node.parentNode, nodeIndex$1(node) + 1);
  8922. /**
  8923. * Creates a caret position from a node and places the offset before it.
  8924. *
  8925. * @method before
  8926. * @param {Node} node Node to get caret position from.
  8927. * @return {tinymce.caret.CaretPosition} Caret position from the node.
  8928. */
  8929. // TODO: TINY-8865 - This may not be safe to cast as Node and alternative solutions need to be looked into
  8930. CaretPosition.before = (node) => CaretPosition(node.parentNode, nodeIndex$1(node));
  8931. CaretPosition.isAbove = (pos1, pos2) => lift2(head(pos2.getClientRects()), last$2(pos1.getClientRects()), isAbove$1).getOr(false);
  8932. CaretPosition.isBelow = (pos1, pos2) => lift2(last$2(pos2.getClientRects()), head(pos1.getClientRects()), isBelow$1).getOr(false);
  8933. CaretPosition.isAtStart = (pos) => pos ? pos.isAtStart() : false;
  8934. CaretPosition.isAtEnd = (pos) => pos ? pos.isAtEnd() : false;
  8935. CaretPosition.isTextPosition = (pos) => pos ? isText$b(pos.container()) : false;
  8936. CaretPosition.isElementPosition = (pos) => !CaretPosition.isTextPosition(pos);
  8937. const trimEmptyTextNode$1 = (dom, node) => {
  8938. if (isText$b(node) && node.data.length === 0) {
  8939. dom.remove(node);
  8940. }
  8941. };
  8942. const insertNode = (dom, rng, node) => {
  8943. rng.insertNode(node);
  8944. trimEmptyTextNode$1(dom, node.previousSibling);
  8945. trimEmptyTextNode$1(dom, node.nextSibling);
  8946. };
  8947. const insertFragment = (dom, rng, frag) => {
  8948. const firstChild = Optional.from(frag.firstChild);
  8949. const lastChild = Optional.from(frag.lastChild);
  8950. rng.insertNode(frag);
  8951. firstChild.each((child) => trimEmptyTextNode$1(dom, child.previousSibling));
  8952. lastChild.each((child) => trimEmptyTextNode$1(dom, child.nextSibling));
  8953. };
  8954. // Wrapper to Range.insertNode which removes any empty text nodes created in the process.
  8955. // Doesn't merge adjacent text nodes - this is according to the DOM spec.
  8956. const rangeInsertNode = (dom, rng, node) => {
  8957. if (isDocumentFragment(node)) {
  8958. insertFragment(dom, rng, node);
  8959. }
  8960. else {
  8961. insertNode(dom, rng, node);
  8962. }
  8963. };
  8964. /**
  8965. * This module creates or resolves xpath like string representation of a CaretPositions.
  8966. *
  8967. * The format is a / separated list of chunks with:
  8968. * <element|text()>[index|after|before]
  8969. *
  8970. * For example:
  8971. * p[0]/b[0]/text()[0],1 = <p><b>a|c</b></p>
  8972. * p[0]/img[0],before = <p>|<img></p>
  8973. * p[0]/img[0],after = <p><img>|</p>
  8974. *
  8975. * @private
  8976. * @static
  8977. * @class tinymce.caret.CaretBookmark
  8978. * @example
  8979. * const bookmark = CaretBookmark.create(rootElm, CaretPosition.before(rootElm.firstChild));
  8980. * const caretPosition = CaretBookmark.resolve(bookmark);
  8981. */
  8982. const isText$6 = isText$b;
  8983. const isBogus = isBogus$1;
  8984. const nodeIndex = DOMUtils.nodeIndex;
  8985. const normalizedParent = (node) => {
  8986. const parentNode = node.parentNode;
  8987. if (isBogus(parentNode)) {
  8988. return normalizedParent(parentNode);
  8989. }
  8990. return parentNode;
  8991. };
  8992. const getChildNodes = (node) => {
  8993. if (!node) {
  8994. return [];
  8995. }
  8996. return reduce(node.childNodes, (result, node) => {
  8997. if (isBogus(node) && node.nodeName !== 'BR') {
  8998. result = result.concat(getChildNodes(node));
  8999. }
  9000. else {
  9001. result.push(node);
  9002. }
  9003. return result;
  9004. }, []);
  9005. };
  9006. const normalizedTextOffset = (node, offset) => {
  9007. let tempNode = node;
  9008. while ((tempNode = tempNode.previousSibling)) {
  9009. if (!isText$6(tempNode)) {
  9010. break;
  9011. }
  9012. offset += tempNode.data.length;
  9013. }
  9014. return offset;
  9015. };
  9016. const equal = (a) => (b) => a === b;
  9017. const normalizedNodeIndex = (node) => {
  9018. let nodes, index;
  9019. nodes = getChildNodes(normalizedParent(node));
  9020. index = findIndex$1(nodes, equal(node), node);
  9021. nodes = nodes.slice(0, index + 1);
  9022. const numTextFragments = reduce(nodes, (result, node, i) => {
  9023. if (isText$6(node) && isText$6(nodes[i - 1])) {
  9024. result++;
  9025. }
  9026. return result;
  9027. }, 0);
  9028. nodes = filter$3(nodes, matchNodeNames$1([node.nodeName]));
  9029. index = findIndex$1(nodes, equal(node), node);
  9030. return index - numTextFragments;
  9031. };
  9032. const createPathItem = (node) => {
  9033. const name = isText$6(node) ? 'text()' : node.nodeName.toLowerCase();
  9034. return name + '[' + normalizedNodeIndex(node) + ']';
  9035. };
  9036. const parentsUntil$1 = (root, node, predicate) => {
  9037. const parents = [];
  9038. for (let tempNode = node.parentNode; tempNode && tempNode !== root; tempNode = tempNode.parentNode) {
  9039. if (predicate && predicate(tempNode)) {
  9040. break;
  9041. }
  9042. parents.push(tempNode);
  9043. }
  9044. return parents;
  9045. };
  9046. const create$9 = (root, caretPosition) => {
  9047. let path = [];
  9048. let container = caretPosition.container();
  9049. let offset = caretPosition.offset();
  9050. let outputOffset;
  9051. if (isText$6(container)) {
  9052. outputOffset = normalizedTextOffset(container, offset);
  9053. }
  9054. else {
  9055. const childNodes = container.childNodes;
  9056. if (offset >= childNodes.length) {
  9057. outputOffset = 'after';
  9058. offset = childNodes.length - 1;
  9059. }
  9060. else {
  9061. outputOffset = 'before';
  9062. }
  9063. container = childNodes[offset];
  9064. }
  9065. path.push(createPathItem(container));
  9066. let parents = parentsUntil$1(root, container);
  9067. parents = filter$3(parents, not(isBogus$1));
  9068. path = path.concat(map$1(parents, (node) => {
  9069. return createPathItem(node);
  9070. }));
  9071. return path.reverse().join('/') + ',' + outputOffset;
  9072. };
  9073. const resolvePathItem = (node, name, index) => {
  9074. let nodes = getChildNodes(node);
  9075. nodes = filter$3(nodes, (node, index) => {
  9076. return !isText$6(node) || !isText$6(nodes[index - 1]);
  9077. });
  9078. nodes = filter$3(nodes, matchNodeNames$1([name]));
  9079. return nodes[index];
  9080. };
  9081. const findTextPosition = (container, offset) => {
  9082. let node = container;
  9083. let targetOffset = 0;
  9084. while (isText$6(node)) {
  9085. const dataLen = node.data.length;
  9086. if (offset >= targetOffset && offset <= targetOffset + dataLen) {
  9087. container = node;
  9088. offset = offset - targetOffset;
  9089. break;
  9090. }
  9091. if (!isText$6(node.nextSibling)) {
  9092. container = node;
  9093. offset = dataLen;
  9094. break;
  9095. }
  9096. targetOffset += dataLen;
  9097. node = node.nextSibling;
  9098. }
  9099. if (isText$6(container) && offset > container.data.length) {
  9100. offset = container.data.length;
  9101. }
  9102. return CaretPosition(container, offset);
  9103. };
  9104. const resolve$1 = (root, path) => {
  9105. if (!path) {
  9106. return null;
  9107. }
  9108. const parts = path.split(',');
  9109. const paths = parts[0].split('/');
  9110. const offset = parts.length > 1 ? parts[1] : 'before';
  9111. const container = reduce(paths, (result, value) => {
  9112. const match = /([\w\-\(\)]+)\[([0-9]+)\]/.exec(value);
  9113. if (!match) {
  9114. return null;
  9115. }
  9116. if (match[1] === 'text()') {
  9117. match[1] = '#text';
  9118. }
  9119. return resolvePathItem(result, match[1], parseInt(match[2], 10));
  9120. }, root);
  9121. if (!container) {
  9122. return null;
  9123. }
  9124. if (!isText$6(container) && container.parentNode) {
  9125. let nodeOffset;
  9126. if (offset === 'after') {
  9127. nodeOffset = nodeIndex(container) + 1;
  9128. }
  9129. else {
  9130. nodeOffset = nodeIndex(container);
  9131. }
  9132. return CaretPosition(container.parentNode, nodeOffset);
  9133. }
  9134. return findTextPosition(container, parseInt(offset, 10));
  9135. };
  9136. const isContentEditableFalse$8 = isContentEditableFalse$a;
  9137. const getNormalizedTextOffset$1 = (trim, container, offset) => {
  9138. let trimmedOffset = trim(container.data.slice(0, offset)).length;
  9139. for (let node = container.previousSibling; node && isText$b(node); node = node.previousSibling) {
  9140. trimmedOffset += trim(node.data).length;
  9141. }
  9142. return trimmedOffset;
  9143. };
  9144. const getPoint = (dom, trim, normalized, rng, start) => {
  9145. const container = start ? rng.startContainer : rng.endContainer;
  9146. let offset = start ? rng.startOffset : rng.endOffset;
  9147. const point = [];
  9148. const root = dom.getRoot();
  9149. if (isText$b(container)) {
  9150. point.push(normalized ? getNormalizedTextOffset$1(trim, container, offset) : offset);
  9151. }
  9152. else {
  9153. let after = 0;
  9154. const childNodes = container.childNodes;
  9155. if (offset >= childNodes.length && childNodes.length) {
  9156. after = 1;
  9157. offset = Math.max(0, childNodes.length - 1);
  9158. }
  9159. point.push(dom.nodeIndex(childNodes[offset], normalized) + after);
  9160. }
  9161. for (let node = container; node && node !== root; node = node.parentNode) {
  9162. point.push(dom.nodeIndex(node, normalized));
  9163. }
  9164. return point;
  9165. };
  9166. const getLocation = (trim, selection, normalized, rng) => {
  9167. const dom = selection.dom;
  9168. const start = getPoint(dom, trim, normalized, rng, true);
  9169. const forward = selection.isForward();
  9170. const fakeCaret = isRangeInCaretContainerBlock(rng) ? { isFakeCaret: true } : {};
  9171. if (!selection.isCollapsed()) {
  9172. const end = getPoint(dom, trim, normalized, rng, false);
  9173. return { start, end, forward, ...fakeCaret };
  9174. }
  9175. else {
  9176. return { start, forward, ...fakeCaret };
  9177. }
  9178. };
  9179. const findIndex = (dom, name, element) => {
  9180. let count = 0;
  9181. Tools.each(dom.select(name), (node) => {
  9182. if (node.getAttribute('data-mce-bogus') === 'all') {
  9183. return;
  9184. }
  9185. else if (node === element) {
  9186. return false;
  9187. }
  9188. else {
  9189. count++;
  9190. return;
  9191. }
  9192. });
  9193. return count;
  9194. };
  9195. const moveEndPoint$1 = (rng, start) => {
  9196. let container = start ? rng.startContainer : rng.endContainer;
  9197. let offset = start ? rng.startOffset : rng.endOffset;
  9198. // normalize Table Cell selection
  9199. if (isElement$7(container) && container.nodeName === 'TR') {
  9200. const childNodes = container.childNodes;
  9201. container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)];
  9202. if (container) {
  9203. offset = start ? 0 : container.childNodes.length;
  9204. if (start) {
  9205. rng.setStart(container, offset);
  9206. }
  9207. else {
  9208. rng.setEnd(container, offset);
  9209. }
  9210. }
  9211. }
  9212. };
  9213. const normalizeTableCellSelection = (rng) => {
  9214. moveEndPoint$1(rng, true);
  9215. moveEndPoint$1(rng, false);
  9216. return rng;
  9217. };
  9218. const findSibling = (node, offset) => {
  9219. if (isElement$7(node)) {
  9220. node = getNode$1(node, offset);
  9221. if (isContentEditableFalse$8(node)) {
  9222. return node;
  9223. }
  9224. }
  9225. if (isCaretContainer$2(node)) {
  9226. if (isText$b(node) && isCaretContainerBlock$1(node)) {
  9227. node = node.parentNode;
  9228. }
  9229. let sibling = node.previousSibling;
  9230. if (isContentEditableFalse$8(sibling)) {
  9231. return sibling;
  9232. }
  9233. sibling = node.nextSibling;
  9234. if (isContentEditableFalse$8(sibling)) {
  9235. return sibling;
  9236. }
  9237. }
  9238. return undefined;
  9239. };
  9240. const findAdjacentContentEditableFalseElm = (rng) => {
  9241. return findSibling(rng.startContainer, rng.startOffset) || findSibling(rng.endContainer, rng.endOffset);
  9242. };
  9243. const getOffsetBookmark = (trim, normalized, selection) => {
  9244. const element = selection.getNode();
  9245. const rng = selection.getRng();
  9246. if (element.nodeName === 'IMG' || isContentEditableFalse$8(element)) {
  9247. const name = element.nodeName;
  9248. return { name, index: findIndex(selection.dom, name, element) };
  9249. }
  9250. const sibling = findAdjacentContentEditableFalseElm(rng);
  9251. if (sibling) {
  9252. const name = sibling.tagName;
  9253. return { name, index: findIndex(selection.dom, name, sibling) };
  9254. }
  9255. return getLocation(trim, selection, normalized, rng);
  9256. };
  9257. const getCaretBookmark = (selection) => {
  9258. const rng = selection.getRng();
  9259. return {
  9260. start: create$9(selection.dom.getRoot(), CaretPosition.fromRangeStart(rng)),
  9261. end: create$9(selection.dom.getRoot(), CaretPosition.fromRangeEnd(rng)),
  9262. forward: selection.isForward()
  9263. };
  9264. };
  9265. const getRangeBookmark = (selection) => {
  9266. return { rng: selection.getRng(), forward: selection.isForward() };
  9267. };
  9268. const createBookmarkSpan = (dom, id, filled) => {
  9269. const args = { 'data-mce-type': 'bookmark', id, 'style': 'overflow:hidden;line-height:0px' };
  9270. return filled ? dom.create('span', args, '&#xFEFF;') : dom.create('span', args);
  9271. };
  9272. const getPersistentBookmark = (selection, filled) => {
  9273. const dom = selection.dom;
  9274. let rng = selection.getRng();
  9275. const id = dom.uniqueId();
  9276. const collapsed = selection.isCollapsed();
  9277. const element = selection.getNode();
  9278. const name = element.nodeName;
  9279. const forward = selection.isForward();
  9280. if (name === 'IMG') {
  9281. return { name, index: findIndex(dom, name, element) };
  9282. }
  9283. // W3C method
  9284. const rng2 = normalizeTableCellSelection(rng.cloneRange());
  9285. // Insert end marker
  9286. if (!collapsed) {
  9287. rng2.collapse(false);
  9288. const endBookmarkNode = createBookmarkSpan(dom, id + '_end', filled);
  9289. rangeInsertNode(dom, rng2, endBookmarkNode);
  9290. }
  9291. rng = normalizeTableCellSelection(rng);
  9292. rng.collapse(true);
  9293. const startBookmarkNode = createBookmarkSpan(dom, id + '_start', filled);
  9294. rangeInsertNode(dom, rng, startBookmarkNode);
  9295. selection.moveToBookmark({ id, keep: true, forward });
  9296. return { id, forward };
  9297. };
  9298. const getBookmark$3 = (selection, type, normalized = false) => {
  9299. if (type === 2) {
  9300. return getOffsetBookmark(trim$2, normalized, selection);
  9301. }
  9302. else if (type === 3) {
  9303. return getCaretBookmark(selection);
  9304. }
  9305. else if (type) {
  9306. return getRangeBookmark(selection);
  9307. }
  9308. else {
  9309. return getPersistentBookmark(selection, false);
  9310. }
  9311. };
  9312. const getUndoBookmark = curry(getOffsetBookmark, identity, true);
  9313. const isInlinePattern = (pattern) => pattern.type === 'inline-command' || pattern.type === 'inline-format';
  9314. const isBlockPattern = (pattern) => pattern.type === 'block-command' || pattern.type === 'block-format';
  9315. const hasBlockTrigger = (pattern, trigger) => (pattern.type === 'block-command' || pattern.type === 'block-format') && pattern.trigger === trigger;
  9316. const normalizePattern = (pattern) => {
  9317. var _a;
  9318. const err = (message) => Result.error({ message, pattern });
  9319. const formatOrCmd = (name, onFormat, onCommand) => {
  9320. if (pattern.format !== undefined) {
  9321. let formats;
  9322. if (isArray$1(pattern.format)) {
  9323. if (!forall(pattern.format, isString)) {
  9324. return err(name + ' pattern has non-string items in the `format` array');
  9325. }
  9326. formats = pattern.format;
  9327. }
  9328. else if (isString(pattern.format)) {
  9329. formats = [pattern.format];
  9330. }
  9331. else {
  9332. return err(name + ' pattern has non-string `format` parameter');
  9333. }
  9334. return Result.value(onFormat(formats));
  9335. }
  9336. else if (pattern.cmd !== undefined) {
  9337. if (!isString(pattern.cmd)) {
  9338. return err(name + ' pattern has non-string `cmd` parameter');
  9339. }
  9340. return Result.value(onCommand(pattern.cmd, pattern.value));
  9341. }
  9342. else {
  9343. return err(name + ' pattern is missing both `format` and `cmd` parameters');
  9344. }
  9345. };
  9346. if (!isObject(pattern)) {
  9347. return err('Raw pattern is not an object');
  9348. }
  9349. if (!isString(pattern.start)) {
  9350. return err('Raw pattern is missing `start` parameter');
  9351. }
  9352. if (pattern.end !== undefined) {
  9353. // inline pattern
  9354. if (!isString(pattern.end)) {
  9355. return err('Inline pattern has non-string `end` parameter');
  9356. }
  9357. if (pattern.start.length === 0 && pattern.end.length === 0) {
  9358. return err('Inline pattern has empty `start` and `end` parameters');
  9359. }
  9360. let start = pattern.start;
  9361. let end = pattern.end;
  9362. // when the end is empty swap with start as it is more efficient
  9363. if (end.length === 0) {
  9364. end = start;
  9365. start = '';
  9366. }
  9367. return formatOrCmd('Inline', (format) => ({ type: 'inline-format', start, end, format }), (cmd, value) => ({ type: 'inline-command', start, end, cmd, value }));
  9368. }
  9369. else if (pattern.replacement !== undefined) {
  9370. // replacement pattern
  9371. if (!isString(pattern.replacement)) {
  9372. return err('Replacement pattern has non-string `replacement` parameter');
  9373. }
  9374. if (pattern.start.length === 0) {
  9375. return err('Replacement pattern has empty `start` parameter');
  9376. }
  9377. return Result.value({
  9378. type: 'inline-command',
  9379. start: '',
  9380. end: pattern.start,
  9381. cmd: 'mceInsertContent',
  9382. value: pattern.replacement
  9383. });
  9384. }
  9385. else {
  9386. // block pattern
  9387. const trigger = (_a = pattern.trigger) !== null && _a !== void 0 ? _a : 'space';
  9388. if (pattern.start.length === 0) {
  9389. return err('Block pattern has empty `start` parameter');
  9390. }
  9391. return formatOrCmd('Block', (formats) => ({
  9392. type: 'block-format',
  9393. start: pattern.start,
  9394. format: formats[0],
  9395. trigger
  9396. }), (command, commandValue) => ({
  9397. type: 'block-command',
  9398. start: pattern.start,
  9399. cmd: command,
  9400. value: commandValue,
  9401. trigger
  9402. }));
  9403. }
  9404. };
  9405. const getBlockPatterns = (patterns) => filter$5(patterns, isBlockPattern);
  9406. const getInlinePatterns = (patterns) => filter$5(patterns, isInlinePattern);
  9407. const createPatternSet = (patterns, dynamicPatternsLookup) => ({
  9408. inlinePatterns: getInlinePatterns(patterns),
  9409. blockPatterns: getBlockPatterns(patterns),
  9410. dynamicPatternsLookup
  9411. });
  9412. const filterByTrigger = (patterns, trigger) => {
  9413. return {
  9414. ...patterns,
  9415. blockPatterns: filter$5(patterns.blockPatterns, (pattern) => hasBlockTrigger(pattern, trigger))
  9416. };
  9417. };
  9418. const fromRawPatterns = (patterns) => {
  9419. const normalized = partition$1(map$3(patterns, normalizePattern));
  9420. // eslint-disable-next-line no-console
  9421. each$e(normalized.errors, (err) => console.error(err.message, err.pattern));
  9422. return normalized.values;
  9423. };
  9424. const fromRawPatternsLookup = (lookupFn) => {
  9425. return (ctx) => {
  9426. const rawPatterns = lookupFn(ctx);
  9427. return fromRawPatterns(rawPatterns);
  9428. };
  9429. };
  9430. const firePreProcess = (editor, args) => editor.dispatch('PreProcess', args);
  9431. const firePostProcess = (editor, args) => editor.dispatch('PostProcess', args);
  9432. const fireRemove = (editor) => {
  9433. editor.dispatch('remove');
  9434. };
  9435. const fireDetach = (editor) => {
  9436. editor.dispatch('detach');
  9437. };
  9438. const fireSwitchMode = (editor, mode) => {
  9439. editor.dispatch('SwitchMode', { mode });
  9440. };
  9441. const fireObjectResizeStart = (editor, target, width, height, origin) => {
  9442. editor.dispatch('ObjectResizeStart', { target, width, height, origin });
  9443. };
  9444. const fireObjectResized = (editor, target, width, height, origin) => {
  9445. editor.dispatch('ObjectResized', { target, width, height, origin });
  9446. };
  9447. const firePreInit = (editor) => {
  9448. editor.dispatch('PreInit');
  9449. };
  9450. const firePostRender = (editor) => {
  9451. editor.dispatch('PostRender');
  9452. };
  9453. const fireInit = (editor) => {
  9454. editor.dispatch('Init');
  9455. };
  9456. const firePlaceholderToggle = (editor, state) => {
  9457. editor.dispatch('PlaceholderToggle', { state });
  9458. };
  9459. const fireError = (editor, errorType, error) => {
  9460. editor.dispatch(errorType, error);
  9461. };
  9462. const fireFormatApply = (editor, format, node, vars) => {
  9463. editor.dispatch('FormatApply', { format, node, vars });
  9464. };
  9465. const fireFormatRemove = (editor, format, node, vars) => {
  9466. editor.dispatch('FormatRemove', { format, node, vars });
  9467. };
  9468. const fireBeforeSetContent = (editor, args) => editor.dispatch('BeforeSetContent', args);
  9469. const fireSetContent = (editor, args) => editor.dispatch('SetContent', args);
  9470. const fireBeforeGetContent = (editor, args) => editor.dispatch('BeforeGetContent', args);
  9471. const fireGetContent = (editor, args) => editor.dispatch('GetContent', args);
  9472. const fireAutocompleterStart = (editor, args) => {
  9473. editor.dispatch('AutocompleterStart', args);
  9474. };
  9475. const fireAutocompleterUpdate = (editor, args) => {
  9476. editor.dispatch('AutocompleterUpdate', args);
  9477. };
  9478. const fireAutocompleterUpdateActiveRange = (editor, args) => {
  9479. editor.dispatch('AutocompleterUpdateActiveRange', args);
  9480. };
  9481. const fireAutocompleterEnd = (editor) => {
  9482. editor.dispatch('AutocompleterEnd');
  9483. };
  9484. const firePastePreProcess = (editor, html, internal) => editor.dispatch('PastePreProcess', { content: html, internal });
  9485. const firePastePostProcess = (editor, node, internal) => editor.dispatch('PastePostProcess', { node, internal });
  9486. const firePastePlainTextToggle = (editor, state) => editor.dispatch('PastePlainTextToggle', { state });
  9487. const fireEditableRootStateChange = (editor, state) => editor.dispatch('EditableRootStateChange', { state });
  9488. const fireDisabledStateChange = (editor, state) => editor.dispatch('DisabledStateChange', { state });
  9489. const fireCloseTooltips = (editor) => editor.dispatch('CloseActiveTooltips');
  9490. const deviceDetection$1 = detect$1().deviceType;
  9491. const isTouch = deviceDetection$1.isTouch();
  9492. const DOM$e = DOMUtils.DOM;
  9493. const getHash = (value) => {
  9494. const items = value.indexOf('=') > 0 ? value.split(/[;,](?![^=;,]*(?:[;,]|$))/) : value.split(',');
  9495. return foldl(items, (output, item) => {
  9496. const arr = item.split('=');
  9497. const key = arr[0];
  9498. const val = arr.length > 1 ? arr[1] : key;
  9499. output[trim$4(key)] = trim$4(val);
  9500. return output;
  9501. }, {});
  9502. };
  9503. const isRegExp = (x) => is$5(x, RegExp);
  9504. const option$1 = (name) => (editor) => editor.options.get(name);
  9505. const stringOrObjectProcessor = (value) => isString(value) || isObject(value);
  9506. const bodyOptionProcessor = (editor, defaultValue = '') => (value) => {
  9507. const valid = isString(value);
  9508. if (valid) {
  9509. if (value.indexOf('=') !== -1) {
  9510. const bodyObj = getHash(value);
  9511. return { value: get$a(bodyObj, editor.id).getOr(defaultValue), valid };
  9512. }
  9513. else {
  9514. return { value, valid };
  9515. }
  9516. }
  9517. else {
  9518. return { valid: false, message: 'Must be a string.' };
  9519. }
  9520. };
  9521. const register$7 = (editor) => {
  9522. const registerOption = editor.options.register;
  9523. registerOption('id', {
  9524. processor: 'string',
  9525. default: editor.id
  9526. });
  9527. registerOption('selector', {
  9528. processor: 'string'
  9529. });
  9530. registerOption('target', {
  9531. processor: 'object'
  9532. });
  9533. registerOption('suffix', {
  9534. processor: 'string'
  9535. });
  9536. registerOption('cache_suffix', {
  9537. processor: 'string'
  9538. });
  9539. registerOption('base_url', {
  9540. processor: 'string'
  9541. });
  9542. registerOption('referrer_policy', {
  9543. processor: 'string',
  9544. default: ''
  9545. });
  9546. registerOption('crossorigin', {
  9547. processor: 'function',
  9548. default: constant(undefined)
  9549. });
  9550. registerOption('language_load', {
  9551. processor: 'boolean',
  9552. default: true
  9553. });
  9554. registerOption('inline', {
  9555. processor: 'boolean',
  9556. default: false
  9557. });
  9558. registerOption('iframe_attrs', {
  9559. processor: 'object',
  9560. default: {}
  9561. });
  9562. registerOption('doctype', {
  9563. processor: 'string',
  9564. default: '<!DOCTYPE html>'
  9565. });
  9566. registerOption('document_base_url', {
  9567. processor: 'string',
  9568. default: editor.editorManager.documentBaseURL
  9569. });
  9570. registerOption('body_id', {
  9571. processor: bodyOptionProcessor(editor, 'tinymce'),
  9572. default: 'tinymce'
  9573. });
  9574. registerOption('body_class', {
  9575. processor: bodyOptionProcessor(editor),
  9576. default: ''
  9577. });
  9578. registerOption('content_security_policy', {
  9579. processor: 'string',
  9580. default: ''
  9581. });
  9582. registerOption('br_in_pre', {
  9583. processor: 'boolean',
  9584. default: true
  9585. });
  9586. registerOption('forced_root_block', {
  9587. processor: (value) => {
  9588. const valid = isString(value) && isNotEmpty(value);
  9589. if (valid) {
  9590. return { value, valid };
  9591. }
  9592. else {
  9593. return { valid: false, message: 'Must be a non-empty string.' };
  9594. }
  9595. },
  9596. default: 'p'
  9597. });
  9598. registerOption('forced_root_block_attrs', {
  9599. processor: 'object',
  9600. default: {}
  9601. });
  9602. registerOption('newline_behavior', {
  9603. processor: (value) => {
  9604. const valid = contains$2(['block', 'linebreak', 'invert', 'default'], value);
  9605. return valid ? { value, valid } : { valid: false, message: 'Must be one of: block, linebreak, invert or default.' };
  9606. },
  9607. default: 'default'
  9608. });
  9609. registerOption('br_newline_selector', {
  9610. processor: 'string',
  9611. default: '.mce-toc h2,figcaption,caption'
  9612. });
  9613. registerOption('no_newline_selector', {
  9614. processor: 'string',
  9615. default: ''
  9616. });
  9617. registerOption('keep_styles', {
  9618. processor: 'boolean',
  9619. default: true
  9620. });
  9621. registerOption('end_container_on_empty_block', {
  9622. processor: (value) => {
  9623. if (isBoolean(value)) {
  9624. return { valid: true, value };
  9625. }
  9626. else if (isString(value)) {
  9627. return { valid: true, value };
  9628. }
  9629. else {
  9630. return { valid: false, message: 'Must be boolean or a string' };
  9631. }
  9632. },
  9633. default: 'blockquote'
  9634. });
  9635. registerOption('font_size_style_values', {
  9636. processor: 'string',
  9637. default: 'xx-small,x-small,small,medium,large,x-large,xx-large'
  9638. });
  9639. registerOption('font_size_legacy_values', {
  9640. processor: 'string',
  9641. // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size
  9642. default: 'xx-small,small,medium,large,x-large,xx-large,300%'
  9643. });
  9644. registerOption('font_size_classes', {
  9645. processor: 'string',
  9646. default: ''
  9647. });
  9648. registerOption('automatic_uploads', {
  9649. processor: 'boolean',
  9650. default: true
  9651. });
  9652. registerOption('images_reuse_filename', {
  9653. processor: 'boolean',
  9654. default: false
  9655. });
  9656. registerOption('images_replace_blob_uris', {
  9657. processor: 'boolean',
  9658. default: true
  9659. });
  9660. registerOption('icons', {
  9661. processor: 'string',
  9662. default: ''
  9663. });
  9664. registerOption('icons_url', {
  9665. processor: 'string',
  9666. default: ''
  9667. });
  9668. registerOption('images_upload_url', {
  9669. processor: 'string',
  9670. default: ''
  9671. });
  9672. registerOption('images_upload_base_path', {
  9673. processor: 'string',
  9674. default: ''
  9675. });
  9676. registerOption('images_upload_credentials', {
  9677. processor: 'boolean',
  9678. default: false
  9679. });
  9680. registerOption('images_upload_handler', {
  9681. processor: 'function'
  9682. });
  9683. registerOption('language', {
  9684. processor: 'string',
  9685. default: 'en'
  9686. });
  9687. registerOption('language_url', {
  9688. processor: 'string',
  9689. default: ''
  9690. });
  9691. registerOption('entity_encoding', {
  9692. processor: 'string',
  9693. default: 'named'
  9694. });
  9695. registerOption('indent', {
  9696. processor: 'boolean',
  9697. default: true
  9698. });
  9699. registerOption('indent_before', {
  9700. processor: 'string',
  9701. default: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' +
  9702. 'tfoot,tbody,tr,section,details,summary,article,hgroup,aside,figure,figcaption,option,optgroup,datalist'
  9703. });
  9704. registerOption('indent_after', {
  9705. processor: 'string',
  9706. default: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' +
  9707. 'tfoot,tbody,tr,section,details,summary,article,hgroup,aside,figure,figcaption,option,optgroup,datalist'
  9708. });
  9709. registerOption('indent_use_margin', {
  9710. processor: 'boolean',
  9711. default: false
  9712. });
  9713. registerOption('indentation', {
  9714. processor: 'string',
  9715. default: '40px'
  9716. });
  9717. registerOption('content_css', {
  9718. processor: (value) => {
  9719. const valid = value === false || isString(value) || isArrayOf(value, isString);
  9720. if (valid) {
  9721. if (isString(value)) {
  9722. return { value: map$3(value.split(','), trim$4), valid };
  9723. }
  9724. else if (isArray$1(value)) {
  9725. return { value, valid };
  9726. }
  9727. else if (value === false) {
  9728. return { value: [], valid };
  9729. }
  9730. else {
  9731. return { value, valid };
  9732. }
  9733. }
  9734. else {
  9735. return { valid: false, message: 'Must be false, a string or an array of strings.' };
  9736. }
  9737. },
  9738. default: isInline$2(editor) ? [] : ['default']
  9739. });
  9740. registerOption('content_style', {
  9741. processor: 'string'
  9742. });
  9743. registerOption('content_css_cors', {
  9744. processor: 'boolean',
  9745. default: false
  9746. });
  9747. registerOption('font_css', {
  9748. processor: (value) => {
  9749. const valid = isString(value) || isArrayOf(value, isString);
  9750. if (valid) {
  9751. const newValue = isArray$1(value) ? value : map$3(value.split(','), trim$4);
  9752. return { value: newValue, valid };
  9753. }
  9754. else {
  9755. return { valid: false, message: 'Must be a string or an array of strings.' };
  9756. }
  9757. },
  9758. default: []
  9759. });
  9760. registerOption('extended_mathml_attributes', {
  9761. processor: 'string[]'
  9762. });
  9763. registerOption('extended_mathml_elements', {
  9764. processor: 'string[]'
  9765. });
  9766. registerOption('inline_boundaries', {
  9767. processor: 'boolean',
  9768. default: true
  9769. });
  9770. registerOption('inline_boundaries_selector', {
  9771. processor: 'string',
  9772. default: 'a[href],code,span.mce-annotation'
  9773. });
  9774. registerOption('object_resizing', {
  9775. processor: (value) => {
  9776. const valid = isBoolean(value) || isString(value);
  9777. if (valid) {
  9778. if (value === false || deviceDetection$1.isiPhone() || deviceDetection$1.isiPad()) {
  9779. return { value: '', valid };
  9780. }
  9781. else {
  9782. return { value: value === true ? 'table,img,figure.image,div,video,iframe' : value, valid };
  9783. }
  9784. }
  9785. else {
  9786. return { valid: false, message: 'Must be boolean or a string' };
  9787. }
  9788. },
  9789. // No nice way to do object resizing on touch devices at this stage
  9790. default: !isTouch
  9791. });
  9792. registerOption('resize_img_proportional', {
  9793. processor: 'boolean',
  9794. default: true
  9795. });
  9796. registerOption('event_root', {
  9797. processor: 'string'
  9798. });
  9799. registerOption('service_message', {
  9800. processor: 'string'
  9801. });
  9802. registerOption('onboarding', {
  9803. processor: 'boolean',
  9804. default: true
  9805. });
  9806. registerOption('tiny_cloud_entry_url', {
  9807. processor: 'string'
  9808. });
  9809. registerOption('theme', {
  9810. processor: (value) => value === false || isString(value) || isFunction(value),
  9811. default: 'silver'
  9812. });
  9813. registerOption('theme_url', {
  9814. processor: 'string'
  9815. });
  9816. registerOption('formats', {
  9817. processor: 'object'
  9818. });
  9819. registerOption('format_empty_lines', {
  9820. processor: 'boolean',
  9821. default: false
  9822. });
  9823. registerOption('format_noneditable_selector', {
  9824. processor: 'string',
  9825. default: ''
  9826. });
  9827. registerOption('preview_styles', {
  9828. processor: (value) => {
  9829. const valid = value === false || isString(value);
  9830. if (valid) {
  9831. return { value: value === false ? '' : value, valid };
  9832. }
  9833. else {
  9834. return { valid: false, message: 'Must be false or a string' };
  9835. }
  9836. },
  9837. default: 'font-family font-size font-weight font-style text-decoration text-transform color background-color border border-radius outline text-shadow'
  9838. });
  9839. registerOption('custom_ui_selector', {
  9840. processor: 'string',
  9841. default: ''
  9842. });
  9843. registerOption('hidden_input', {
  9844. processor: 'boolean',
  9845. default: true
  9846. });
  9847. registerOption('submit_patch', {
  9848. processor: 'boolean',
  9849. default: true
  9850. });
  9851. registerOption('encoding', {
  9852. processor: 'string'
  9853. });
  9854. registerOption('add_form_submit_trigger', {
  9855. processor: 'boolean',
  9856. default: true
  9857. });
  9858. registerOption('add_unload_trigger', {
  9859. processor: 'boolean',
  9860. default: true
  9861. });
  9862. registerOption('custom_undo_redo_levels', {
  9863. processor: 'number',
  9864. default: 0
  9865. });
  9866. registerOption('disable_nodechange', {
  9867. processor: 'boolean',
  9868. default: false
  9869. });
  9870. registerOption('disabled', {
  9871. processor: (value) => {
  9872. if (isBoolean(value)) {
  9873. if (editor.initialized && isDisabled$1(editor) !== value) {
  9874. // Schedules the callback to run in the next microtask queue once the option is updated
  9875. // TODO: TINY-11586 - Implement `onChange` callback when the value of an option changes
  9876. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  9877. Promise.resolve().then(() => {
  9878. fireDisabledStateChange(editor, value);
  9879. });
  9880. }
  9881. return { valid: true, value };
  9882. }
  9883. return { valid: false, message: 'The value must be a boolean.' };
  9884. },
  9885. default: false
  9886. });
  9887. registerOption('readonly', {
  9888. processor: 'boolean',
  9889. default: false
  9890. });
  9891. registerOption('editable_root', {
  9892. processor: 'boolean',
  9893. default: true
  9894. });
  9895. registerOption('plugins', {
  9896. processor: 'string[]',
  9897. default: []
  9898. });
  9899. registerOption('external_plugins', {
  9900. processor: 'object'
  9901. });
  9902. registerOption('forced_plugins', {
  9903. processor: 'string[]'
  9904. });
  9905. registerOption('model', {
  9906. processor: 'string',
  9907. default: editor.hasPlugin('rtc') ? 'plugin' : 'dom'
  9908. });
  9909. registerOption('model_url', {
  9910. processor: 'string'
  9911. });
  9912. registerOption('block_unsupported_drop', {
  9913. processor: 'boolean',
  9914. default: true
  9915. });
  9916. registerOption('visual', {
  9917. processor: 'boolean',
  9918. default: true
  9919. });
  9920. registerOption('visual_table_class', {
  9921. processor: 'string',
  9922. default: 'mce-item-table'
  9923. });
  9924. registerOption('visual_anchor_class', {
  9925. processor: 'string',
  9926. default: 'mce-item-anchor'
  9927. });
  9928. registerOption('iframe_aria_text', {
  9929. processor: 'string',
  9930. default: 'Rich Text Area'.concat(editor.hasPlugin('help') ? '. Press ALT-0 for help.' : '')
  9931. });
  9932. registerOption('setup', {
  9933. processor: 'function'
  9934. });
  9935. registerOption('init_instance_callback', {
  9936. processor: 'function'
  9937. });
  9938. registerOption('url_converter', {
  9939. processor: 'function',
  9940. // Note: Don't bind here, as the binding is handled via the `url_converter_scope`
  9941. // eslint-disable-next-line @typescript-eslint/unbound-method
  9942. default: editor.convertURL
  9943. });
  9944. registerOption('url_converter_scope', {
  9945. processor: 'object',
  9946. default: editor
  9947. });
  9948. registerOption('urlconverter_callback', {
  9949. processor: 'function'
  9950. });
  9951. registerOption('allow_conditional_comments', {
  9952. processor: 'boolean',
  9953. default: false
  9954. });
  9955. registerOption('allow_html_data_urls', {
  9956. processor: 'boolean',
  9957. default: false
  9958. });
  9959. registerOption('allow_svg_data_urls', {
  9960. processor: 'boolean'
  9961. });
  9962. registerOption('allow_html_in_named_anchor', {
  9963. processor: 'boolean',
  9964. default: false
  9965. });
  9966. registerOption('allow_html_in_comments', {
  9967. processor: 'boolean',
  9968. default: false
  9969. });
  9970. registerOption('allow_script_urls', {
  9971. processor: 'boolean',
  9972. default: false
  9973. });
  9974. registerOption('allow_unsafe_link_target', {
  9975. processor: 'boolean',
  9976. default: false
  9977. });
  9978. registerOption('allow_mathml_annotation_encodings', {
  9979. processor: (value) => {
  9980. const valid = isArrayOf(value, isString);
  9981. return valid ? { value, valid } : { valid: false, message: 'Must be an array of strings.' };
  9982. },
  9983. default: []
  9984. });
  9985. registerOption('convert_fonts_to_spans', {
  9986. processor: 'boolean',
  9987. default: true,
  9988. deprecated: true
  9989. });
  9990. registerOption('fix_list_elements', {
  9991. processor: 'boolean',
  9992. default: false
  9993. });
  9994. registerOption('preserve_cdata', {
  9995. processor: 'boolean',
  9996. default: false
  9997. });
  9998. registerOption('remove_trailing_brs', {
  9999. processor: 'boolean',
  10000. default: true
  10001. });
  10002. registerOption('pad_empty_with_br', {
  10003. processor: 'boolean',
  10004. default: false,
  10005. });
  10006. registerOption('inline_styles', {
  10007. processor: 'boolean',
  10008. default: true,
  10009. deprecated: true
  10010. });
  10011. registerOption('element_format', {
  10012. processor: 'string',
  10013. default: 'html'
  10014. });
  10015. registerOption('entities', {
  10016. processor: 'string'
  10017. });
  10018. registerOption('schema', {
  10019. processor: 'string',
  10020. default: 'html5'
  10021. });
  10022. registerOption('convert_urls', {
  10023. processor: 'boolean',
  10024. default: true
  10025. });
  10026. registerOption('relative_urls', {
  10027. processor: 'boolean',
  10028. default: true
  10029. });
  10030. registerOption('remove_script_host', {
  10031. processor: 'boolean',
  10032. default: true
  10033. });
  10034. registerOption('custom_elements', {
  10035. processor: stringOrObjectProcessor
  10036. });
  10037. registerOption('extended_valid_elements', {
  10038. processor: 'string'
  10039. });
  10040. registerOption('invalid_elements', {
  10041. processor: 'string'
  10042. });
  10043. registerOption('invalid_styles', {
  10044. processor: stringOrObjectProcessor
  10045. });
  10046. registerOption('valid_children', {
  10047. processor: 'string'
  10048. });
  10049. registerOption('valid_classes', {
  10050. processor: stringOrObjectProcessor
  10051. });
  10052. registerOption('valid_elements', {
  10053. processor: 'string'
  10054. });
  10055. registerOption('valid_styles', {
  10056. processor: stringOrObjectProcessor
  10057. });
  10058. registerOption('verify_html', {
  10059. processor: 'boolean',
  10060. default: true
  10061. });
  10062. registerOption('auto_focus', {
  10063. processor: (value) => isString(value) || value === true
  10064. });
  10065. registerOption('browser_spellcheck', {
  10066. processor: 'boolean',
  10067. default: false
  10068. });
  10069. registerOption('protect', {
  10070. processor: 'array'
  10071. });
  10072. registerOption('images_file_types', {
  10073. processor: 'string',
  10074. default: 'jpeg,jpg,jpe,jfi,jif,jfif,png,gif,bmp,webp'
  10075. });
  10076. registerOption('deprecation_warnings', {
  10077. processor: 'boolean',
  10078. default: true
  10079. });
  10080. registerOption('a11y_advanced_options', {
  10081. processor: 'boolean',
  10082. default: false
  10083. });
  10084. registerOption('api_key', {
  10085. processor: 'string'
  10086. });
  10087. registerOption('license_key', {
  10088. processor: 'string'
  10089. });
  10090. registerOption('paste_block_drop', {
  10091. processor: 'boolean',
  10092. default: false
  10093. });
  10094. registerOption('paste_data_images', {
  10095. processor: 'boolean',
  10096. default: true
  10097. });
  10098. registerOption('paste_preprocess', {
  10099. processor: 'function'
  10100. });
  10101. registerOption('paste_postprocess', {
  10102. processor: 'function'
  10103. });
  10104. registerOption('paste_webkit_styles', {
  10105. processor: 'string',
  10106. default: 'none'
  10107. });
  10108. registerOption('paste_remove_styles_if_webkit', {
  10109. processor: 'boolean',
  10110. default: true
  10111. });
  10112. registerOption('paste_merge_formats', {
  10113. processor: 'boolean',
  10114. default: true
  10115. });
  10116. registerOption('smart_paste', {
  10117. processor: 'boolean',
  10118. default: true
  10119. });
  10120. registerOption('paste_as_text', {
  10121. processor: 'boolean',
  10122. default: false
  10123. });
  10124. registerOption('paste_tab_spaces', {
  10125. processor: 'number',
  10126. default: 4
  10127. });
  10128. registerOption('text_patterns', {
  10129. processor: (value) => {
  10130. if (isArrayOf(value, isObject) || value === false) {
  10131. const patterns = value === false ? [] : value;
  10132. return { value: fromRawPatterns(patterns), valid: true };
  10133. }
  10134. else {
  10135. return { valid: false, message: 'Must be an array of objects or false.' };
  10136. }
  10137. },
  10138. default: [
  10139. { start: '*', end: '*', format: 'italic' },
  10140. { start: '**', end: '**', format: 'bold' },
  10141. { start: '#', format: 'h1', trigger: 'space' },
  10142. { start: '##', format: 'h2', trigger: 'space' },
  10143. { start: '###', format: 'h3', trigger: 'space' },
  10144. { start: '####', format: 'h4', trigger: 'space' },
  10145. { start: '#####', format: 'h5', trigger: 'space' },
  10146. { start: '######', format: 'h6', trigger: 'space' },
  10147. { start: '1.', cmd: 'InsertOrderedList', trigger: 'space' },
  10148. { start: '*', cmd: 'InsertUnorderedList', trigger: 'space' },
  10149. { start: '-', cmd: 'InsertUnorderedList', trigger: 'space' },
  10150. { start: '>', cmd: 'mceBlockQuote', trigger: 'space' },
  10151. { start: '---', cmd: 'InsertHorizontalRule', trigger: 'space' },
  10152. ]
  10153. });
  10154. registerOption('text_patterns_lookup', {
  10155. processor: (value) => {
  10156. if (isFunction(value)) {
  10157. return {
  10158. value: fromRawPatternsLookup(value),
  10159. valid: true,
  10160. };
  10161. }
  10162. else {
  10163. return { valid: false, message: 'Must be a single function' };
  10164. }
  10165. },
  10166. default: (_ctx) => []
  10167. });
  10168. registerOption('noneditable_class', {
  10169. processor: 'string',
  10170. default: 'mceNonEditable'
  10171. });
  10172. registerOption('editable_class', {
  10173. processor: 'string',
  10174. default: 'mceEditable'
  10175. });
  10176. registerOption('noneditable_regexp', {
  10177. processor: (value) => {
  10178. if (isArrayOf(value, isRegExp)) {
  10179. return { value, valid: true };
  10180. }
  10181. else if (isRegExp(value)) {
  10182. return { value: [value], valid: true };
  10183. }
  10184. else {
  10185. return { valid: false, message: 'Must be a RegExp or an array of RegExp.' };
  10186. }
  10187. },
  10188. default: []
  10189. });
  10190. registerOption('table_tab_navigation', {
  10191. processor: 'boolean',
  10192. default: true
  10193. });
  10194. registerOption('highlight_on_focus', {
  10195. processor: 'boolean',
  10196. default: true
  10197. });
  10198. registerOption('xss_sanitization', {
  10199. processor: 'boolean',
  10200. default: true
  10201. });
  10202. registerOption('details_initial_state', {
  10203. processor: (value) => {
  10204. const valid = contains$2(['inherited', 'collapsed', 'expanded'], value);
  10205. return valid ? { value, valid } : { valid: false, message: 'Must be one of: inherited, collapsed, or expanded.' };
  10206. },
  10207. default: 'inherited'
  10208. });
  10209. registerOption('details_serialized_state', {
  10210. processor: (value) => {
  10211. const valid = contains$2(['inherited', 'collapsed', 'expanded'], value);
  10212. return valid ? { value, valid } : { valid: false, message: 'Must be one of: inherited, collapsed, or expanded.' };
  10213. },
  10214. default: 'inherited'
  10215. });
  10216. registerOption('init_content_sync', {
  10217. processor: 'boolean',
  10218. default: false
  10219. });
  10220. registerOption('newdocument_content', {
  10221. processor: 'string',
  10222. default: ''
  10223. });
  10224. registerOption('sandbox_iframes', {
  10225. processor: 'boolean',
  10226. default: true
  10227. });
  10228. registerOption('sandbox_iframes_exclusions', {
  10229. processor: 'string[]',
  10230. default: [
  10231. 'youtube.com',
  10232. 'youtu.be',
  10233. 'vimeo.com',
  10234. 'player.vimeo.com',
  10235. 'dailymotion.com',
  10236. 'embed.music.apple.com',
  10237. 'open.spotify.com',
  10238. 'giphy.com',
  10239. 'dai.ly',
  10240. 'codepen.io',
  10241. ]
  10242. });
  10243. registerOption('convert_unsafe_embeds', {
  10244. processor: 'boolean',
  10245. default: true
  10246. });
  10247. registerOption('user_id', {
  10248. processor: 'string',
  10249. default: 'Anonymous'
  10250. });
  10251. registerOption('fetch_users', {
  10252. processor: (value) => {
  10253. if (value === undefined) {
  10254. return { valid: true, value: undefined };
  10255. }
  10256. if (isFunction(value)) {
  10257. return { valid: true, value };
  10258. }
  10259. return {
  10260. valid: false,
  10261. message: 'fetch_users must be a function that returns a Promise<ExpectedUser[]>'
  10262. };
  10263. }
  10264. });
  10265. // These options must be registered later in the init sequence due to their default values
  10266. editor.on('ScriptsLoaded', () => {
  10267. registerOption('directionality', {
  10268. processor: 'string',
  10269. default: I18n.isRtl() ? 'rtl' : undefined
  10270. });
  10271. registerOption('placeholder', {
  10272. processor: 'string',
  10273. // Fallback to the original elements placeholder if not set in the settings
  10274. default: DOM$e.getAttrib(editor.getElement(), 'placeholder')
  10275. });
  10276. });
  10277. registerOption('lists_indent_on_tab', {
  10278. processor: 'boolean',
  10279. default: true
  10280. });
  10281. registerOption('list_max_depth', {
  10282. processor: (value) => {
  10283. const valid = isNumber(value);
  10284. if (valid) {
  10285. if (value < 0) {
  10286. throw new Error('list_max_depth cannot be set to lower than 0');
  10287. }
  10288. return { value, valid };
  10289. }
  10290. else {
  10291. return { valid: false, message: 'Must be a number' };
  10292. }
  10293. },
  10294. });
  10295. };
  10296. const getIframeAttrs = option$1('iframe_attrs');
  10297. const getDocType = option$1('doctype');
  10298. const getDocumentBaseUrl = option$1('document_base_url');
  10299. const getBodyId = option$1('body_id');
  10300. const getBodyClass = option$1('body_class');
  10301. const getContentSecurityPolicy = option$1('content_security_policy');
  10302. const shouldPutBrInPre$1 = option$1('br_in_pre');
  10303. const getForcedRootBlock = option$1('forced_root_block');
  10304. const getForcedRootBlockAttrs = option$1('forced_root_block_attrs');
  10305. const getNewlineBehavior = option$1('newline_behavior');
  10306. const getBrNewLineSelector = option$1('br_newline_selector');
  10307. const getNoNewLineSelector = option$1('no_newline_selector');
  10308. const shouldKeepStyles = option$1('keep_styles');
  10309. const shouldEndContainerOnEmptyBlock = option$1('end_container_on_empty_block');
  10310. const isAutomaticUploadsEnabled = option$1('automatic_uploads');
  10311. const shouldReuseFileName = option$1('images_reuse_filename');
  10312. const shouldReplaceBlobUris = option$1('images_replace_blob_uris');
  10313. const getIconPackName = option$1('icons');
  10314. const getIconsUrl = option$1('icons_url');
  10315. const getImageUploadUrl = option$1('images_upload_url');
  10316. const getImageUploadBasePath = option$1('images_upload_base_path');
  10317. const getImagesUploadCredentials = option$1('images_upload_credentials');
  10318. const getImagesUploadHandler = option$1('images_upload_handler');
  10319. const shouldUseContentCssCors = option$1('content_css_cors');
  10320. const getReferrerPolicy = option$1('referrer_policy');
  10321. const getCrossOrigin = option$1('crossorigin');
  10322. const getLanguageCode = option$1('language');
  10323. const getLanguageUrl = option$1('language_url');
  10324. const shouldIndentUseMargin = option$1('indent_use_margin');
  10325. const getIndentation = option$1('indentation');
  10326. const getContentCss = option$1('content_css');
  10327. const getContentStyle = option$1('content_style');
  10328. const getFontCss = option$1('font_css');
  10329. const getDirectionality = option$1('directionality');
  10330. const getInlineBoundarySelector = option$1('inline_boundaries_selector');
  10331. const getObjectResizing = option$1('object_resizing');
  10332. const getResizeImgProportional = option$1('resize_img_proportional');
  10333. const getPlaceholder = option$1('placeholder');
  10334. const getEventRoot = option$1('event_root');
  10335. const getServiceMessage = option$1('service_message');
  10336. const getTheme = option$1('theme');
  10337. const getThemeUrl = option$1('theme_url');
  10338. const getModel = option$1('model');
  10339. const getModelUrl = option$1('model_url');
  10340. const isInlineBoundariesEnabled = option$1('inline_boundaries');
  10341. const getFormats = option$1('formats');
  10342. const getPreviewStyles = option$1('preview_styles');
  10343. const canFormatEmptyLines = option$1('format_empty_lines');
  10344. const getFormatNoneditableSelector = option$1('format_noneditable_selector');
  10345. const getCustomUiSelector = option$1('custom_ui_selector');
  10346. const isInline$2 = option$1('inline');
  10347. const hasHiddenInput = option$1('hidden_input');
  10348. const shouldPatchSubmit = option$1('submit_patch');
  10349. const shouldAddFormSubmitTrigger = option$1('add_form_submit_trigger');
  10350. const shouldAddUnloadTrigger = option$1('add_unload_trigger');
  10351. const getCustomUndoRedoLevels = option$1('custom_undo_redo_levels');
  10352. const shouldDisableNodeChange = option$1('disable_nodechange');
  10353. const isReadOnly$1 = option$1('readonly');
  10354. const hasEditableRoot$1 = option$1('editable_root');
  10355. const hasContentCssCors = option$1('content_css_cors');
  10356. const getPlugins = option$1('plugins');
  10357. const getExternalPlugins$1 = option$1('external_plugins');
  10358. const shouldBlockUnsupportedDrop = option$1('block_unsupported_drop');
  10359. const isVisualAidsEnabled = option$1('visual');
  10360. const getVisualAidsTableClass = option$1('visual_table_class');
  10361. const getVisualAidsAnchorClass = option$1('visual_anchor_class');
  10362. const getIframeAriaText = option$1('iframe_aria_text');
  10363. const getSetupCallback = option$1('setup');
  10364. const getInitInstanceCallback = option$1('init_instance_callback');
  10365. const getUrlConverterCallback = option$1('urlconverter_callback');
  10366. const getAutoFocus = option$1('auto_focus');
  10367. const shouldBrowserSpellcheck = option$1('browser_spellcheck');
  10368. const getProtect = option$1('protect');
  10369. const shouldPasteBlockDrop = option$1('paste_block_drop');
  10370. const shouldPasteDataImages = option$1('paste_data_images');
  10371. const getPastePreProcess = option$1('paste_preprocess');
  10372. const getPastePostProcess = option$1('paste_postprocess');
  10373. const getNewDocumentContent = option$1('newdocument_content');
  10374. const getPasteWebkitStyles = option$1('paste_webkit_styles');
  10375. const shouldPasteRemoveWebKitStyles = option$1('paste_remove_styles_if_webkit');
  10376. const shouldPasteMergeFormats = option$1('paste_merge_formats');
  10377. const isSmartPasteEnabled = option$1('smart_paste');
  10378. const isPasteAsTextEnabled = option$1('paste_as_text');
  10379. const getPasteTabSpaces = option$1('paste_tab_spaces');
  10380. const shouldAllowHtmlDataUrls = option$1('allow_html_data_urls');
  10381. const getTextPatterns = option$1('text_patterns');
  10382. const getTextPatternsLookup = option$1('text_patterns_lookup');
  10383. const getNonEditableClass = option$1('noneditable_class');
  10384. const getEditableClass = option$1('editable_class');
  10385. const getNonEditableRegExps = option$1('noneditable_regexp');
  10386. const shouldPreserveCData = option$1('preserve_cdata');
  10387. const shouldHighlightOnFocus = option$1('highlight_on_focus');
  10388. const shouldSanitizeXss = option$1('xss_sanitization');
  10389. const shouldUseDocumentWrite = option$1('init_content_sync');
  10390. const hasTextPatternsLookup = (editor) => editor.options.isSet('text_patterns_lookup');
  10391. const getFontStyleValues = (editor) => Tools.explode(editor.options.get('font_size_style_values'));
  10392. const getFontSizeClasses = (editor) => Tools.explode(editor.options.get('font_size_classes'));
  10393. const isEncodingXml = (editor) => editor.options.get('encoding') === 'xml';
  10394. const getAllowedImageFileTypes = (editor) => Tools.explode(editor.options.get('images_file_types'));
  10395. const hasTableTabNavigation = option$1('table_tab_navigation');
  10396. const getDetailsInitialState = option$1('details_initial_state');
  10397. const getDetailsSerializedState = option$1('details_serialized_state');
  10398. const shouldSandboxIframes = option$1('sandbox_iframes');
  10399. const getSandboxIframesExclusions = (editor) => editor.options.get('sandbox_iframes_exclusions');
  10400. const shouldConvertUnsafeEmbeds = option$1('convert_unsafe_embeds');
  10401. const getLicenseKey = option$1('license_key');
  10402. const getApiKey = option$1('api_key');
  10403. const isDisabled$1 = option$1('disabled');
  10404. const getUserId = option$1('user_id');
  10405. const getFetchUsers = option$1('fetch_users');
  10406. const shouldIndentOnTab = option$1('lists_indent_on_tab');
  10407. const getListMaxDepth = (editor) => Optional.from(editor.options.get('list_max_depth'));
  10408. const isElement$4 = isElement$7;
  10409. const isText$5 = isText$b;
  10410. const removeNode$1 = (node) => {
  10411. const parentNode = node.parentNode;
  10412. if (parentNode) {
  10413. parentNode.removeChild(node);
  10414. }
  10415. };
  10416. const trimCount = (text) => {
  10417. const trimmedText = trim$2(text);
  10418. return {
  10419. count: text.length - trimmedText.length,
  10420. text: trimmedText
  10421. };
  10422. };
  10423. const deleteZwspChars = (caretContainer) => {
  10424. // We use the Text.deleteData API here so as to preserve selection offsets
  10425. let idx;
  10426. while ((idx = caretContainer.data.lastIndexOf(ZWSP$1)) !== -1) {
  10427. caretContainer.deleteData(idx, 1);
  10428. }
  10429. };
  10430. const removeUnchanged = (caretContainer, pos) => {
  10431. remove$2(caretContainer);
  10432. return pos;
  10433. };
  10434. const removeTextAndReposition = (caretContainer, pos) => {
  10435. const before = trimCount(caretContainer.data.substr(0, pos.offset()));
  10436. const after = trimCount(caretContainer.data.substr(pos.offset()));
  10437. const text = before.text + after.text;
  10438. if (text.length > 0) {
  10439. deleteZwspChars(caretContainer);
  10440. return CaretPosition(caretContainer, pos.offset() - before.count);
  10441. }
  10442. else {
  10443. return pos;
  10444. }
  10445. };
  10446. const removeElementAndReposition = (caretContainer, pos) => {
  10447. const parentNode = pos.container();
  10448. const newPosition = indexOf$1(from(parentNode.childNodes), caretContainer).map((index) => {
  10449. return index < pos.offset() ? CaretPosition(parentNode, pos.offset() - 1) : pos;
  10450. }).getOr(pos);
  10451. remove$2(caretContainer);
  10452. return newPosition;
  10453. };
  10454. const removeTextCaretContainer = (caretContainer, pos) => isText$5(caretContainer) && pos.container() === caretContainer ? removeTextAndReposition(caretContainer, pos) : removeUnchanged(caretContainer, pos);
  10455. const removeElementCaretContainer = (caretContainer, pos) => pos.container() === caretContainer.parentNode ? removeElementAndReposition(caretContainer, pos) : removeUnchanged(caretContainer, pos);
  10456. const removeAndReposition = (container, pos) => CaretPosition.isTextPosition(pos) ? removeTextCaretContainer(container, pos) : removeElementCaretContainer(container, pos);
  10457. const remove$2 = (caretContainerNode) => {
  10458. if (isElement$4(caretContainerNode) && isCaretContainer$2(caretContainerNode)) {
  10459. if (hasContent(caretContainerNode)) {
  10460. caretContainerNode.removeAttribute('data-mce-caret');
  10461. }
  10462. else {
  10463. removeNode$1(caretContainerNode);
  10464. }
  10465. }
  10466. if (isText$5(caretContainerNode)) {
  10467. deleteZwspChars(caretContainerNode);
  10468. if (caretContainerNode.data.length === 0) {
  10469. removeNode$1(caretContainerNode);
  10470. }
  10471. }
  10472. };
  10473. const isContentEditableFalse$7 = isContentEditableFalse$a;
  10474. const isMedia$1 = isMedia$2;
  10475. const isTableCell$1 = isTableCell$3;
  10476. const inlineFakeCaretSelector = '*[contentEditable=false],video,audio,embed,object';
  10477. const getAbsoluteClientRect = (root, element, before) => {
  10478. const clientRect = collapse(element.getBoundingClientRect(), before);
  10479. let scrollX;
  10480. let scrollY;
  10481. if (root.tagName === 'BODY') {
  10482. const docElm = root.ownerDocument.documentElement;
  10483. scrollX = root.scrollLeft || docElm.scrollLeft;
  10484. scrollY = root.scrollTop || docElm.scrollTop;
  10485. }
  10486. else {
  10487. const rootRect = root.getBoundingClientRect();
  10488. scrollX = root.scrollLeft - rootRect.left;
  10489. scrollY = root.scrollTop - rootRect.top;
  10490. }
  10491. clientRect.left += scrollX;
  10492. clientRect.right += scrollX;
  10493. clientRect.top += scrollY;
  10494. clientRect.bottom += scrollY;
  10495. clientRect.width = 1;
  10496. let margin = element.offsetWidth - element.clientWidth;
  10497. if (margin > 0) {
  10498. if (before) {
  10499. margin *= -1;
  10500. }
  10501. clientRect.left += margin;
  10502. clientRect.right += margin;
  10503. }
  10504. return clientRect;
  10505. };
  10506. const trimInlineCaretContainers = (root) => {
  10507. var _a, _b;
  10508. const fakeCaretTargetNodes = descendants(SugarElement.fromDom(root), inlineFakeCaretSelector);
  10509. for (let i = 0; i < fakeCaretTargetNodes.length; i++) {
  10510. const node = fakeCaretTargetNodes[i].dom;
  10511. let sibling = node.previousSibling;
  10512. if (endsWithCaretContainer$1(sibling)) {
  10513. const data = sibling.data;
  10514. if (data.length === 1) {
  10515. (_a = sibling.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(sibling);
  10516. }
  10517. else {
  10518. sibling.deleteData(data.length - 1, 1);
  10519. }
  10520. }
  10521. sibling = node.nextSibling;
  10522. if (startsWithCaretContainer$1(sibling)) {
  10523. const data = sibling.data;
  10524. if (data.length === 1) {
  10525. (_b = sibling.parentNode) === null || _b === void 0 ? void 0 : _b.removeChild(sibling);
  10526. }
  10527. else {
  10528. sibling.deleteData(0, 1);
  10529. }
  10530. }
  10531. }
  10532. };
  10533. const FakeCaret = (editor, root, isBlock, hasFocus) => {
  10534. const lastVisualCaret = value$1();
  10535. let cursorInterval;
  10536. let caretContainerNode;
  10537. const caretBlock = getForcedRootBlock(editor);
  10538. const dom = editor.dom;
  10539. const show = (before, element) => {
  10540. let rng;
  10541. hide();
  10542. if (isTableCell$1(element)) {
  10543. return null;
  10544. }
  10545. if (isBlock(element)) {
  10546. const caretContainer = insertBlock(caretBlock, element, before);
  10547. const clientRect = getAbsoluteClientRect(root, element, before);
  10548. dom.setStyle(caretContainer, 'top', clientRect.top);
  10549. dom.setStyle(caretContainer, 'caret-color', 'transparent');
  10550. caretContainerNode = caretContainer;
  10551. const caret = dom.create('div', { 'class': 'mce-visual-caret', 'data-mce-bogus': 'all' });
  10552. dom.setStyles(caret, { ...clientRect });
  10553. dom.add(root, caret);
  10554. lastVisualCaret.set({ caret, element, before });
  10555. if (before) {
  10556. dom.addClass(caret, 'mce-visual-caret-before');
  10557. }
  10558. startBlink();
  10559. rng = element.ownerDocument.createRange();
  10560. rng.setStart(caretContainer, 0);
  10561. rng.setEnd(caretContainer, 0);
  10562. }
  10563. else {
  10564. caretContainerNode = insertInline$1(element, before);
  10565. rng = element.ownerDocument.createRange();
  10566. if (isInlineFakeCaretTarget(caretContainerNode.nextSibling)) {
  10567. rng.setStart(caretContainerNode, 0);
  10568. rng.setEnd(caretContainerNode, 0);
  10569. }
  10570. else {
  10571. rng.setStart(caretContainerNode, 1);
  10572. rng.setEnd(caretContainerNode, 1);
  10573. }
  10574. return rng;
  10575. }
  10576. return rng;
  10577. };
  10578. const hide = () => {
  10579. // TODO: TINY-6015 - Ensure cleaning up the fake caret preserves the selection, as currently
  10580. // the CaretContainerRemove.remove below will change the selection in some cases
  10581. trimInlineCaretContainers(root);
  10582. if (caretContainerNode) {
  10583. remove$2(caretContainerNode);
  10584. caretContainerNode = null;
  10585. }
  10586. lastVisualCaret.on((caretState) => {
  10587. dom.remove(caretState.caret);
  10588. lastVisualCaret.clear();
  10589. });
  10590. if (cursorInterval) {
  10591. clearInterval(cursorInterval);
  10592. cursorInterval = undefined;
  10593. }
  10594. };
  10595. const startBlink = () => {
  10596. cursorInterval = window.setInterval(() => {
  10597. lastVisualCaret.on((caretState) => {
  10598. if (hasFocus()) {
  10599. dom.toggleClass(caretState.caret, 'mce-visual-caret-hidden');
  10600. }
  10601. else {
  10602. dom.addClass(caretState.caret, 'mce-visual-caret-hidden');
  10603. }
  10604. });
  10605. }, 500);
  10606. };
  10607. const reposition = () => {
  10608. lastVisualCaret.on((caretState) => {
  10609. const clientRect = getAbsoluteClientRect(root, caretState.element, caretState.before);
  10610. dom.setStyles(caretState.caret, { ...clientRect });
  10611. });
  10612. };
  10613. const destroy = () => clearInterval(cursorInterval);
  10614. const getCss = () => ('.mce-visual-caret {' +
  10615. 'position: absolute;' +
  10616. 'background-color: black;' +
  10617. 'background-color: currentcolor;' +
  10618. // 'background-color: red;' +
  10619. '}' +
  10620. '.mce-visual-caret-hidden {' +
  10621. 'display: none;' +
  10622. '}' +
  10623. '*[data-mce-caret] {' +
  10624. 'position: absolute;' +
  10625. 'left: -1000px;' +
  10626. 'right: auto;' +
  10627. 'top: 0;' +
  10628. 'margin: 0;' +
  10629. 'padding: 0;' +
  10630. '}');
  10631. return {
  10632. isShowing: lastVisualCaret.isSet,
  10633. show,
  10634. hide,
  10635. getCss,
  10636. reposition,
  10637. destroy
  10638. };
  10639. };
  10640. const isFakeCaretTableBrowser = () => Env.browser.isFirefox();
  10641. const isInlineFakeCaretTarget = (node) => isContentEditableFalse$7(node) || isMedia$1(node);
  10642. const isFakeCaretTarget = (node) => {
  10643. const isTarget = isInlineFakeCaretTarget(node) || (isTable$2(node) && isFakeCaretTableBrowser());
  10644. return isTarget && parentElement(SugarElement.fromDom(node)).exists(isEditable$2);
  10645. };
  10646. const isContentEditableTrue$1 = isContentEditableTrue$3;
  10647. const isContentEditableFalse$6 = isContentEditableFalse$a;
  10648. const isMedia = isMedia$2;
  10649. const isBlockLike = matchStyleValues('display', 'block table table-cell table-row table-caption list-item');
  10650. const isCaretContainer = isCaretContainer$2;
  10651. const isCaretContainerBlock = isCaretContainerBlock$1;
  10652. const isElement$3 = isElement$7;
  10653. const isText$4 = isText$b;
  10654. const isCaretCandidate$1 = isCaretCandidate$3;
  10655. const isForwards = (direction) => direction === 1 /* HDirection.Forwards */;
  10656. const isBackwards = (direction) => direction === -1 /* HDirection.Backwards */;
  10657. const skipCaretContainers = (walk, shallow) => {
  10658. let node;
  10659. while ((node = walk(shallow))) {
  10660. if (!isCaretContainerBlock(node)) {
  10661. return node;
  10662. }
  10663. }
  10664. return null;
  10665. };
  10666. const findNode = (node, direction, predicateFn, rootNode, shallow) => {
  10667. const walker = new DomTreeWalker(node, rootNode);
  10668. const isCefOrCaretContainer = isContentEditableFalse$6(node) || isCaretContainerBlock(node);
  10669. let tempNode;
  10670. if (isBackwards(direction)) {
  10671. if (isCefOrCaretContainer) {
  10672. tempNode = skipCaretContainers(walker.prev.bind(walker), true);
  10673. if (predicateFn(tempNode)) {
  10674. return tempNode;
  10675. }
  10676. }
  10677. while ((tempNode = skipCaretContainers(walker.prev.bind(walker), shallow))) {
  10678. if (predicateFn(tempNode)) {
  10679. return tempNode;
  10680. }
  10681. }
  10682. }
  10683. if (isForwards(direction)) {
  10684. if (isCefOrCaretContainer) {
  10685. tempNode = skipCaretContainers(walker.next.bind(walker), true);
  10686. if (predicateFn(tempNode)) {
  10687. return tempNode;
  10688. }
  10689. }
  10690. while ((tempNode = skipCaretContainers(walker.next.bind(walker), shallow))) {
  10691. if (predicateFn(tempNode)) {
  10692. return tempNode;
  10693. }
  10694. }
  10695. }
  10696. return null;
  10697. };
  10698. const getEditingHost = (node, rootNode) => {
  10699. const isCETrue = (node) => isContentEditableTrue$1(node.dom);
  10700. const isRoot = (node) => node.dom === rootNode;
  10701. return ancestor$5(SugarElement.fromDom(node), isCETrue, isRoot)
  10702. .map((elm) => elm.dom)
  10703. .getOr(rootNode);
  10704. };
  10705. const getParentBlock$3 = (node, rootNode) => {
  10706. while (node && node !== rootNode) {
  10707. if (isBlockLike(node)) {
  10708. return node;
  10709. }
  10710. node = node.parentNode;
  10711. }
  10712. return null;
  10713. };
  10714. const isInSameBlock = (caretPosition1, caretPosition2, rootNode) => getParentBlock$3(caretPosition1.container(), rootNode) === getParentBlock$3(caretPosition2.container(), rootNode);
  10715. const getChildNodeAtRelativeOffset = (relativeOffset, caretPosition) => {
  10716. if (!caretPosition) {
  10717. return Optional.none();
  10718. }
  10719. const container = caretPosition.container();
  10720. const offset = caretPosition.offset();
  10721. if (!isElement$3(container)) {
  10722. return Optional.none();
  10723. }
  10724. return Optional.from(container.childNodes[offset + relativeOffset]);
  10725. };
  10726. const beforeAfter = (before, node) => {
  10727. var _a;
  10728. const doc = (_a = node.ownerDocument) !== null && _a !== void 0 ? _a : document;
  10729. const range = doc.createRange();
  10730. if (before) {
  10731. range.setStartBefore(node);
  10732. range.setEndBefore(node);
  10733. }
  10734. else {
  10735. range.setStartAfter(node);
  10736. range.setEndAfter(node);
  10737. }
  10738. return range;
  10739. };
  10740. const isNodesInSameBlock = (root, node1, node2) => getParentBlock$3(node1, root) === getParentBlock$3(node2, root);
  10741. const lean = (left, root, node) => {
  10742. const siblingName = left ? 'previousSibling' : 'nextSibling';
  10743. let tempNode = node;
  10744. while (tempNode && tempNode !== root) {
  10745. let sibling = tempNode[siblingName];
  10746. if (sibling && isCaretContainer(sibling)) {
  10747. sibling = sibling[siblingName];
  10748. }
  10749. if (isContentEditableFalse$6(sibling) || isMedia(sibling)) {
  10750. if (isNodesInSameBlock(root, sibling, tempNode)) {
  10751. return sibling;
  10752. }
  10753. break;
  10754. }
  10755. if (isCaretCandidate$1(sibling)) {
  10756. break;
  10757. }
  10758. tempNode = tempNode.parentNode;
  10759. }
  10760. return null;
  10761. };
  10762. const before$1 = curry(beforeAfter, true);
  10763. const after$1 = curry(beforeAfter, false);
  10764. const normalizeRange$2 = (direction, root, range) => {
  10765. let node;
  10766. const leanLeft = curry(lean, true, root);
  10767. const leanRight = curry(lean, false, root);
  10768. const container = range.startContainer;
  10769. const offset = range.startOffset;
  10770. if (isCaretContainerBlock$1(container)) {
  10771. const block = isText$4(container) ? container.parentNode : container;
  10772. const location = block.getAttribute('data-mce-caret');
  10773. if (location === 'before') {
  10774. node = block.nextSibling;
  10775. if (isFakeCaretTarget(node)) {
  10776. return before$1(node);
  10777. }
  10778. }
  10779. if (location === 'after') {
  10780. node = block.previousSibling;
  10781. if (isFakeCaretTarget(node)) {
  10782. return after$1(node);
  10783. }
  10784. }
  10785. }
  10786. if (!range.collapsed) {
  10787. return range;
  10788. }
  10789. if (isText$b(container)) {
  10790. if (isCaretContainer(container)) {
  10791. if (direction === 1) {
  10792. node = leanRight(container);
  10793. if (node) {
  10794. return before$1(node);
  10795. }
  10796. node = leanLeft(container);
  10797. if (node) {
  10798. return after$1(node);
  10799. }
  10800. }
  10801. if (direction === -1) {
  10802. node = leanLeft(container);
  10803. if (node) {
  10804. return after$1(node);
  10805. }
  10806. node = leanRight(container);
  10807. if (node) {
  10808. return before$1(node);
  10809. }
  10810. }
  10811. return range;
  10812. }
  10813. if (endsWithCaretContainer$1(container) && offset >= container.data.length - 1) {
  10814. if (direction === 1) {
  10815. node = leanRight(container);
  10816. if (node) {
  10817. return before$1(node);
  10818. }
  10819. }
  10820. return range;
  10821. }
  10822. if (startsWithCaretContainer$1(container) && offset <= 1) {
  10823. if (direction === -1) {
  10824. node = leanLeft(container);
  10825. if (node) {
  10826. return after$1(node);
  10827. }
  10828. }
  10829. return range;
  10830. }
  10831. if (offset === container.data.length) {
  10832. node = leanRight(container);
  10833. if (node) {
  10834. return before$1(node);
  10835. }
  10836. return range;
  10837. }
  10838. if (offset === 0) {
  10839. node = leanLeft(container);
  10840. if (node) {
  10841. return after$1(node);
  10842. }
  10843. return range;
  10844. }
  10845. }
  10846. return range;
  10847. };
  10848. const getRelativeCefElm = (forward, caretPosition) => getChildNodeAtRelativeOffset(forward ? 0 : -1, caretPosition).filter(isContentEditableFalse$6);
  10849. const getNormalizedRangeEndPoint = (direction, root, range) => {
  10850. const normalizedRange = normalizeRange$2(direction, root, range);
  10851. return direction === -1 ? CaretPosition.fromRangeStart(normalizedRange) : CaretPosition.fromRangeEnd(normalizedRange);
  10852. };
  10853. const getElementFromPosition = (pos) => Optional.from(pos.getNode()).map(SugarElement.fromDom);
  10854. const getElementFromPrevPosition = (pos) => Optional.from(pos.getNode(true)).map(SugarElement.fromDom);
  10855. const getVisualCaretPosition = (walkFn, caretPosition) => {
  10856. let pos = caretPosition;
  10857. while ((pos = walkFn(pos))) {
  10858. if (pos.isVisible()) {
  10859. return pos;
  10860. }
  10861. }
  10862. return pos;
  10863. };
  10864. const isMoveInsideSameBlock = (from, to) => {
  10865. const inSameBlock = isInSameBlock(from, to);
  10866. // Handle bogus BR <p>abc|<br></p>
  10867. if (!inSameBlock && isBr$7(from.getNode())) {
  10868. return true;
  10869. }
  10870. return inSameBlock;
  10871. };
  10872. const isContentEditableFalse$5 = isContentEditableFalse$a;
  10873. const isText$3 = isText$b;
  10874. const isElement$2 = isElement$7;
  10875. const isBr$3 = isBr$7;
  10876. const isCaretCandidate = isCaretCandidate$3;
  10877. const isAtomic = isAtomic$1;
  10878. const isEditableCaretCandidate = isEditableCaretCandidate$1;
  10879. const getParents$3 = (node, root) => {
  10880. const parents = [];
  10881. let tempNode = node;
  10882. while (tempNode && tempNode !== root) {
  10883. parents.push(tempNode);
  10884. tempNode = tempNode.parentNode;
  10885. }
  10886. return parents;
  10887. };
  10888. const nodeAtIndex = (container, offset) => {
  10889. if (container.hasChildNodes() && offset < container.childNodes.length) {
  10890. return container.childNodes[offset];
  10891. }
  10892. return null;
  10893. };
  10894. const getCaretCandidatePosition = (direction, node) => {
  10895. if (isForwards(direction)) {
  10896. if (isCaretCandidate(node.previousSibling) && !isText$3(node.previousSibling)) {
  10897. return CaretPosition.before(node);
  10898. }
  10899. if (isText$3(node)) {
  10900. return CaretPosition(node, 0);
  10901. }
  10902. }
  10903. if (isBackwards(direction)) {
  10904. if (isCaretCandidate(node.nextSibling) && !isText$3(node.nextSibling)) {
  10905. return CaretPosition.after(node);
  10906. }
  10907. if (isText$3(node)) {
  10908. return CaretPosition(node, node.data.length);
  10909. }
  10910. }
  10911. if (isBackwards(direction)) {
  10912. if (isBr$3(node)) {
  10913. return CaretPosition.before(node);
  10914. }
  10915. return CaretPosition.after(node);
  10916. }
  10917. return CaretPosition.before(node);
  10918. };
  10919. const moveForwardFromBr = (root, nextNode) => {
  10920. const nextSibling = nextNode.nextSibling;
  10921. if (nextSibling && isCaretCandidate(nextSibling)) {
  10922. if (isText$3(nextSibling)) {
  10923. return CaretPosition(nextSibling, 0);
  10924. }
  10925. else {
  10926. return CaretPosition.before(nextSibling);
  10927. }
  10928. }
  10929. else {
  10930. return findCaretPosition$1(1 /* HDirection.Forwards */, CaretPosition.after(nextNode), root);
  10931. }
  10932. };
  10933. const findCaretPosition$1 = (direction, startPos, root) => {
  10934. let node;
  10935. let nextNode;
  10936. let innerNode;
  10937. let caretPosition;
  10938. if (!isElement$2(root) || !startPos) {
  10939. return null;
  10940. }
  10941. if (startPos.isEqual(CaretPosition.after(root)) && root.lastChild) {
  10942. caretPosition = CaretPosition.after(root.lastChild);
  10943. if (isBackwards(direction) && isCaretCandidate(root.lastChild) && isElement$2(root.lastChild)) {
  10944. return isBr$3(root.lastChild) ? CaretPosition.before(root.lastChild) : caretPosition;
  10945. }
  10946. }
  10947. else {
  10948. caretPosition = startPos;
  10949. }
  10950. const container = caretPosition.container();
  10951. let offset = caretPosition.offset();
  10952. if (isText$3(container)) {
  10953. if (isBackwards(direction) && offset > 0) {
  10954. return CaretPosition(container, --offset);
  10955. }
  10956. if (isForwards(direction) && offset < container.length) {
  10957. return CaretPosition(container, ++offset);
  10958. }
  10959. node = container;
  10960. }
  10961. else {
  10962. if (isBackwards(direction) && offset > 0) {
  10963. nextNode = nodeAtIndex(container, offset - 1);
  10964. if (isCaretCandidate(nextNode)) {
  10965. if (!isAtomic(nextNode)) {
  10966. innerNode = findNode(nextNode, direction, isEditableCaretCandidate, nextNode);
  10967. if (innerNode) {
  10968. if (isText$3(innerNode)) {
  10969. return CaretPosition(innerNode, innerNode.data.length);
  10970. }
  10971. return CaretPosition.after(innerNode);
  10972. }
  10973. }
  10974. if (isText$3(nextNode)) {
  10975. return CaretPosition(nextNode, nextNode.data.length);
  10976. }
  10977. return CaretPosition.before(nextNode);
  10978. }
  10979. }
  10980. if (isForwards(direction) && offset < container.childNodes.length) {
  10981. nextNode = nodeAtIndex(container, offset);
  10982. if (isCaretCandidate(nextNode)) {
  10983. if (isBr$3(nextNode)) {
  10984. return moveForwardFromBr(root, nextNode);
  10985. }
  10986. if (!isAtomic(nextNode)) {
  10987. innerNode = findNode(nextNode, direction, isEditableCaretCandidate, nextNode);
  10988. if (innerNode) {
  10989. if (isText$3(innerNode)) {
  10990. return CaretPosition(innerNode, 0);
  10991. }
  10992. return CaretPosition.before(innerNode);
  10993. }
  10994. }
  10995. if (isText$3(nextNode)) {
  10996. return CaretPosition(nextNode, 0);
  10997. }
  10998. return CaretPosition.after(nextNode);
  10999. }
  11000. }
  11001. node = nextNode ? nextNode : caretPosition.getNode();
  11002. }
  11003. if (node && ((isForwards(direction) && caretPosition.isAtEnd()) || (isBackwards(direction) && caretPosition.isAtStart()))) {
  11004. node = findNode(node, direction, always, root, true);
  11005. if (isEditableCaretCandidate(node, root)) {
  11006. return getCaretCandidatePosition(direction, node);
  11007. }
  11008. }
  11009. nextNode = node ? findNode(node, direction, isEditableCaretCandidate, root) : node;
  11010. const rootContentEditableFalseElm = last(filter$5(getParents$3(container, root), isContentEditableFalse$5));
  11011. if (rootContentEditableFalseElm && (!nextNode || !rootContentEditableFalseElm.contains(nextNode))) {
  11012. if (isForwards(direction)) {
  11013. caretPosition = CaretPosition.after(rootContentEditableFalseElm);
  11014. }
  11015. else {
  11016. caretPosition = CaretPosition.before(rootContentEditableFalseElm);
  11017. }
  11018. return caretPosition;
  11019. }
  11020. if (nextNode) {
  11021. return getCaretCandidatePosition(direction, nextNode);
  11022. }
  11023. return null;
  11024. };
  11025. const CaretWalker = (root) => ({
  11026. /**
  11027. * Returns the next logical caret position from the specified input
  11028. * caretPosition or null if there isn't any more positions left for example
  11029. * at the end specified root element.
  11030. *
  11031. * @method next
  11032. * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from.
  11033. * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found.
  11034. */
  11035. next: (caretPosition) => {
  11036. return findCaretPosition$1(1 /* HDirection.Forwards */, caretPosition, root);
  11037. },
  11038. /**
  11039. * Returns the previous logical caret position from the specified input
  11040. * caretPosition or null if there isn't any more positions left for example
  11041. * at the end specified root element.
  11042. *
  11043. * @method prev
  11044. * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from.
  11045. * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found.
  11046. */
  11047. prev: (caretPosition) => {
  11048. return findCaretPosition$1(-1 /* HDirection.Backwards */, caretPosition, root);
  11049. }
  11050. });
  11051. const walkToPositionIn = (forward, root, start) => {
  11052. const position = forward ? CaretPosition.before(start) : CaretPosition.after(start);
  11053. return fromPosition(forward, root, position);
  11054. };
  11055. const afterElement = (node) => isBr$7(node) ? CaretPosition.before(node) : CaretPosition.after(node);
  11056. const isBeforeOrStart = (position) => {
  11057. if (CaretPosition.isTextPosition(position)) {
  11058. return position.offset() === 0;
  11059. }
  11060. else {
  11061. return isCaretCandidate$3(position.getNode());
  11062. }
  11063. };
  11064. const isAfterOrEnd = (position) => {
  11065. if (CaretPosition.isTextPosition(position)) {
  11066. const container = position.container();
  11067. return position.offset() === container.data.length;
  11068. }
  11069. else {
  11070. return isCaretCandidate$3(position.getNode(true));
  11071. }
  11072. };
  11073. const isBeforeAfterSameElement = (from, to) => !CaretPosition.isTextPosition(from) && !CaretPosition.isTextPosition(to) && from.getNode() === to.getNode(true);
  11074. const isAtBr = (position) => !CaretPosition.isTextPosition(position) && isBr$7(position.getNode());
  11075. const shouldSkipPosition = (forward, from, to) => {
  11076. if (forward) {
  11077. return !isBeforeAfterSameElement(from, to) && !isAtBr(from) && isAfterOrEnd(from) && isBeforeOrStart(to);
  11078. }
  11079. else {
  11080. return !isBeforeAfterSameElement(to, from) && isBeforeOrStart(from) && isAfterOrEnd(to);
  11081. }
  11082. };
  11083. // Finds: <p>a|<b>b</b></p> -> <p>a<b>|b</b></p>
  11084. const fromPosition = (forward, root, pos) => {
  11085. const walker = CaretWalker(root);
  11086. return Optional.from(forward ? walker.next(pos) : walker.prev(pos));
  11087. };
  11088. // Finds: <p>a|<b>b</b></p> -> <p>a<b>b|</b></p>
  11089. const navigate = (forward, root, from) => fromPosition(forward, root, from).bind((to) => {
  11090. if (isInSameBlock(from, to, root) && shouldSkipPosition(forward, from, to)) {
  11091. return fromPosition(forward, root, to);
  11092. }
  11093. else {
  11094. return Optional.some(to);
  11095. }
  11096. });
  11097. const navigateIgnore = (forward, root, from, ignoreFilter) => navigate(forward, root, from)
  11098. .bind((pos) => ignoreFilter(pos) ? navigateIgnore(forward, root, pos, ignoreFilter) : Optional.some(pos));
  11099. const positionIn = (forward, element) => {
  11100. const startNode = forward ? element.firstChild : element.lastChild;
  11101. if (isText$b(startNode)) {
  11102. return Optional.some(CaretPosition(startNode, forward ? 0 : startNode.data.length));
  11103. }
  11104. else if (startNode) {
  11105. if (isCaretCandidate$3(startNode)) {
  11106. return Optional.some(forward ? CaretPosition.before(startNode) : afterElement(startNode));
  11107. }
  11108. else {
  11109. return walkToPositionIn(forward, element, startNode);
  11110. }
  11111. }
  11112. else {
  11113. return Optional.none();
  11114. }
  11115. };
  11116. const nextPosition = curry(fromPosition, true);
  11117. const prevPosition = curry(fromPosition, false);
  11118. const firstPositionIn = curry(positionIn, true);
  11119. const lastPositionIn = curry(positionIn, false);
  11120. const CARET_ID = '_mce_caret';
  11121. const isCaretNode = (node) => isElement$7(node) && node.id === CARET_ID;
  11122. const getParentCaretContainer = (body, node) => {
  11123. let currentNode = node;
  11124. while (currentNode && currentNode !== body) {
  11125. if (isCaretNode(currentNode)) {
  11126. return currentNode;
  11127. }
  11128. currentNode = currentNode.parentNode;
  11129. }
  11130. return null;
  11131. };
  11132. const isStringPathBookmark = (bookmark) => isString(bookmark.start);
  11133. const isRangeBookmark = (bookmark) => has$2(bookmark, 'rng');
  11134. const isIdBookmark = (bookmark) => has$2(bookmark, 'id');
  11135. const isIndexBookmark = (bookmark) => has$2(bookmark, 'name');
  11136. const isPathBookmark = (bookmark) => Tools.isArray(bookmark.start);
  11137. const isForwardBookmark = (bookmark) => !isIndexBookmark(bookmark) && isBoolean(bookmark.forward) ? bookmark.forward : true;
  11138. const addBogus = (dom, node) => {
  11139. // Adds a bogus BR element for empty block elements
  11140. if (isElement$7(node) && dom.isBlock(node) && !node.innerHTML) {
  11141. node.innerHTML = '<br data-mce-bogus="1" />';
  11142. }
  11143. return node;
  11144. };
  11145. const resolveCaretPositionBookmark = (dom, bookmark) => {
  11146. const startPos = Optional.from(resolve$1(dom.getRoot(), bookmark.start));
  11147. const endPos = Optional.from(resolve$1(dom.getRoot(), bookmark.end));
  11148. return lift2(startPos, endPos, (start, end) => {
  11149. const range = dom.createRng();
  11150. range.setStart(start.container(), start.offset());
  11151. range.setEnd(end.container(), end.offset());
  11152. return { range, forward: isForwardBookmark(bookmark) };
  11153. });
  11154. };
  11155. const insertZwsp = (node, rng) => {
  11156. var _a;
  11157. const doc = (_a = node.ownerDocument) !== null && _a !== void 0 ? _a : document;
  11158. const textNode = doc.createTextNode(ZWSP$1);
  11159. node.appendChild(textNode);
  11160. rng.setStart(textNode, 0);
  11161. rng.setEnd(textNode, 0);
  11162. };
  11163. const isEmpty$3 = (node) => !node.hasChildNodes();
  11164. const tryFindRangePosition = (node, rng) => lastPositionIn(node).fold(never, (pos) => {
  11165. rng.setStart(pos.container(), pos.offset());
  11166. rng.setEnd(pos.container(), pos.offset());
  11167. return true;
  11168. });
  11169. // Since we trim zwsp from undo levels the caret format containers
  11170. // may be empty if so pad them with a zwsp and move caret there
  11171. const padEmptyCaretContainer = (root, node, rng) => {
  11172. if (isEmpty$3(node) && getParentCaretContainer(root, node)) {
  11173. insertZwsp(node, rng);
  11174. return true;
  11175. }
  11176. else {
  11177. return false;
  11178. }
  11179. };
  11180. const setEndPoint = (dom, start, bookmark, rng) => {
  11181. const point = bookmark[start ? 'start' : 'end'];
  11182. const root = dom.getRoot();
  11183. if (point) {
  11184. let node = root;
  11185. let offset = point[0];
  11186. // Find container node
  11187. for (let i = point.length - 1; node && i >= 1; i--) {
  11188. const children = node.childNodes;
  11189. if (padEmptyCaretContainer(root, node, rng)) {
  11190. return true;
  11191. }
  11192. if (point[i] > children.length - 1) {
  11193. if (padEmptyCaretContainer(root, node, rng)) {
  11194. return true;
  11195. }
  11196. return tryFindRangePosition(node, rng);
  11197. }
  11198. node = children[point[i]];
  11199. }
  11200. // Move text offset to best suitable location
  11201. if (isText$b(node)) {
  11202. offset = Math.min(point[0], node.data.length);
  11203. }
  11204. // Move element offset to best suitable location
  11205. if (isElement$7(node)) {
  11206. offset = Math.min(point[0], node.childNodes.length);
  11207. }
  11208. // Set offset within container node
  11209. if (start) {
  11210. rng.setStart(node, offset);
  11211. }
  11212. else {
  11213. rng.setEnd(node, offset);
  11214. }
  11215. }
  11216. return true;
  11217. };
  11218. const isValidTextNode = (node) => isText$b(node) && node.data.length > 0;
  11219. const restoreEndPoint = (dom, suffix, bookmark) => {
  11220. const marker = dom.get(bookmark.id + '_' + suffix);
  11221. const markerParent = marker === null || marker === void 0 ? void 0 : marker.parentNode;
  11222. const keep = bookmark.keep;
  11223. if (marker && markerParent) {
  11224. let container;
  11225. let offset;
  11226. if (suffix === 'start') {
  11227. if (!keep) {
  11228. container = markerParent;
  11229. offset = dom.nodeIndex(marker);
  11230. }
  11231. else {
  11232. if (marker.hasChildNodes()) {
  11233. container = marker.firstChild;
  11234. offset = 1;
  11235. }
  11236. else if (isValidTextNode(marker.nextSibling)) {
  11237. container = marker.nextSibling;
  11238. offset = 0;
  11239. }
  11240. else if (isValidTextNode(marker.previousSibling)) {
  11241. container = marker.previousSibling;
  11242. offset = marker.previousSibling.data.length;
  11243. }
  11244. else {
  11245. container = markerParent;
  11246. offset = dom.nodeIndex(marker) + 1;
  11247. }
  11248. }
  11249. }
  11250. else {
  11251. if (!keep) {
  11252. container = markerParent;
  11253. offset = dom.nodeIndex(marker);
  11254. }
  11255. else {
  11256. if (marker.hasChildNodes()) {
  11257. container = marker.firstChild;
  11258. offset = 1;
  11259. }
  11260. else if (isValidTextNode(marker.previousSibling)) {
  11261. container = marker.previousSibling;
  11262. offset = marker.previousSibling.data.length;
  11263. }
  11264. else {
  11265. container = markerParent;
  11266. offset = dom.nodeIndex(marker);
  11267. }
  11268. }
  11269. }
  11270. if (!keep) {
  11271. const prev = marker.previousSibling;
  11272. const next = marker.nextSibling;
  11273. // Remove all marker text nodes
  11274. Tools.each(Tools.grep(marker.childNodes), (node) => {
  11275. if (isText$b(node)) {
  11276. node.data = node.data.replace(/\uFEFF/g, '');
  11277. }
  11278. });
  11279. // Remove marker but keep children if for example contents where inserted into the marker
  11280. // Also remove duplicated instances of the marker for example by a
  11281. // split operation or by WebKit auto split on paste feature
  11282. let otherMarker;
  11283. while ((otherMarker = dom.get(bookmark.id + '_' + suffix))) {
  11284. dom.remove(otherMarker, true);
  11285. }
  11286. // If siblings are text nodes then merge them unless it's Opera since it some how removes the node
  11287. // and we are sniffing since adding a lot of detection code for a browser with 3% of the market
  11288. // isn't worth the effort. Sorry, Opera but it's just a fact
  11289. if (isText$b(next) && isText$b(prev) && !Env.browser.isOpera()) {
  11290. const idx = prev.data.length;
  11291. prev.appendData(next.data);
  11292. dom.remove(next);
  11293. container = prev;
  11294. offset = idx;
  11295. }
  11296. }
  11297. return Optional.some(CaretPosition(container, offset));
  11298. }
  11299. else {
  11300. return Optional.none();
  11301. }
  11302. };
  11303. const resolvePaths = (dom, bookmark) => {
  11304. const range = dom.createRng();
  11305. if (setEndPoint(dom, true, bookmark, range) && setEndPoint(dom, false, bookmark, range)) {
  11306. return Optional.some({ range, forward: isForwardBookmark(bookmark) });
  11307. }
  11308. else {
  11309. return Optional.none();
  11310. }
  11311. };
  11312. const resolveId = (dom, bookmark) => {
  11313. const startPos = restoreEndPoint(dom, 'start', bookmark);
  11314. const endPos = restoreEndPoint(dom, 'end', bookmark);
  11315. return lift2(startPos, endPos.or(startPos), (spos, epos) => {
  11316. const range = dom.createRng();
  11317. range.setStart(addBogus(dom, spos.container()), spos.offset());
  11318. range.setEnd(addBogus(dom, epos.container()), epos.offset());
  11319. return { range, forward: isForwardBookmark(bookmark) };
  11320. });
  11321. };
  11322. const resolveIndex = (dom, bookmark) => Optional.from(dom.select(bookmark.name)[bookmark.index]).map((elm) => {
  11323. const range = dom.createRng();
  11324. range.selectNode(elm);
  11325. return { range, forward: true };
  11326. });
  11327. const resolve = (selection, bookmark) => {
  11328. const dom = selection.dom;
  11329. if (bookmark) {
  11330. if (isPathBookmark(bookmark)) {
  11331. return resolvePaths(dom, bookmark);
  11332. }
  11333. else if (isStringPathBookmark(bookmark)) {
  11334. return resolveCaretPositionBookmark(dom, bookmark);
  11335. }
  11336. else if (isIdBookmark(bookmark)) {
  11337. return resolveId(dom, bookmark);
  11338. }
  11339. else if (isIndexBookmark(bookmark)) {
  11340. return resolveIndex(dom, bookmark);
  11341. }
  11342. else if (isRangeBookmark(bookmark)) {
  11343. return Optional.some({ range: bookmark.rng, forward: isForwardBookmark(bookmark) });
  11344. }
  11345. }
  11346. return Optional.none();
  11347. };
  11348. const getBookmark$2 = (selection, type, normalized) => {
  11349. return getBookmark$3(selection, type, normalized);
  11350. };
  11351. const moveToBookmark = (selection, bookmark) => {
  11352. resolve(selection, bookmark).each(({ range, forward }) => {
  11353. selection.setRng(range, forward);
  11354. });
  11355. };
  11356. const isBookmarkNode$1 = (node) => {
  11357. return isElement$7(node) && node.tagName === 'SPAN' && node.getAttribute('data-mce-type') === 'bookmark';
  11358. };
  11359. const is = (expected) => (actual) => expected === actual;
  11360. const isNbsp = is(nbsp);
  11361. const isWhiteSpace = (chr) => chr !== '' && ' \f\n\r\t\v'.indexOf(chr) !== -1;
  11362. const isContent = (chr) => !isWhiteSpace(chr) && !isNbsp(chr) && !isZwsp$2(chr);
  11363. const getRanges = (selection) => {
  11364. const ranges = [];
  11365. if (selection) {
  11366. for (let i = 0; i < selection.rangeCount; i++) {
  11367. ranges.push(selection.getRangeAt(i));
  11368. }
  11369. }
  11370. return ranges;
  11371. };
  11372. const getSelectedNodes = (ranges) => {
  11373. return bind$3(ranges, (range) => {
  11374. const node = getSelectedNode(range);
  11375. return node ? [SugarElement.fromDom(node)] : [];
  11376. });
  11377. };
  11378. const hasMultipleRanges = (selection) => {
  11379. return getRanges(selection).length > 1;
  11380. };
  11381. const getCellsFromRanges = (ranges) => filter$5(getSelectedNodes(ranges), isTableCell$2);
  11382. const getCellsFromElement = (elm) => descendants(elm, 'td[data-mce-selected],th[data-mce-selected]');
  11383. const getCellsFromElementOrRanges = (ranges, element) => {
  11384. const selectedCells = getCellsFromElement(element);
  11385. return selectedCells.length > 0 ? selectedCells : getCellsFromRanges(ranges);
  11386. };
  11387. const getCellsFromEditor = (editor) => getCellsFromElementOrRanges(getRanges(editor.selection.getSel()), SugarElement.fromDom(editor.getBody()));
  11388. const getClosestTable = (cell, isRoot) => ancestor$4(cell, 'table', isRoot);
  11389. const getStartNode = (rng) => {
  11390. const sc = rng.startContainer, so = rng.startOffset;
  11391. if (isText$b(sc)) {
  11392. return so === 0 ? Optional.some(SugarElement.fromDom(sc)) : Optional.none();
  11393. }
  11394. else {
  11395. return Optional.from(sc.childNodes[so]).map(SugarElement.fromDom);
  11396. }
  11397. };
  11398. const getEndNode = (rng) => {
  11399. const ec = rng.endContainer, eo = rng.endOffset;
  11400. if (isText$b(ec)) {
  11401. return eo === ec.data.length ? Optional.some(SugarElement.fromDom(ec)) : Optional.none();
  11402. }
  11403. else {
  11404. return Optional.from(ec.childNodes[eo - 1]).map(SugarElement.fromDom);
  11405. }
  11406. };
  11407. const getFirstChildren = (node) => {
  11408. return firstChild(node).fold(constant([node]), (child) => {
  11409. return [node].concat(getFirstChildren(child));
  11410. });
  11411. };
  11412. const getLastChildren = (node) => {
  11413. return lastChild(node).fold(constant([node]), (child) => {
  11414. if (name(child) === 'br') {
  11415. return prevSibling(child).map((sibling) => {
  11416. return [node].concat(getLastChildren(sibling));
  11417. }).getOr([]);
  11418. }
  11419. else {
  11420. return [node].concat(getLastChildren(child));
  11421. }
  11422. });
  11423. };
  11424. const hasAllContentsSelected = (elm, rng) => {
  11425. return lift2(getStartNode(rng), getEndNode(rng), (startNode, endNode) => {
  11426. const start = find$2(getFirstChildren(elm), curry(eq, startNode));
  11427. const end = find$2(getLastChildren(elm), curry(eq, endNode));
  11428. return start.isSome() && end.isSome();
  11429. }).getOr(false);
  11430. };
  11431. const moveEndPoint = (dom, rng, node, start) => {
  11432. const root = node;
  11433. const walker = new DomTreeWalker(node, root);
  11434. const moveCaretBeforeOnEnterElementsMap = filter$4(dom.schema.getMoveCaretBeforeOnEnterElements(), (_, name) => !contains$2(['td', 'th', 'table'], name.toLowerCase()));
  11435. let currentNode = node;
  11436. do {
  11437. if (isText$b(currentNode) && Tools.trim(currentNode.data).length !== 0) {
  11438. if (start) {
  11439. rng.setStart(currentNode, 0);
  11440. }
  11441. else {
  11442. rng.setEnd(currentNode, currentNode.data.length);
  11443. }
  11444. return;
  11445. }
  11446. // BR/IMG/INPUT elements but not table cells
  11447. if (moveCaretBeforeOnEnterElementsMap[currentNode.nodeName]) {
  11448. if (start) {
  11449. rng.setStartBefore(currentNode);
  11450. }
  11451. else {
  11452. if (currentNode.nodeName === 'BR') {
  11453. rng.setEndBefore(currentNode);
  11454. }
  11455. else {
  11456. rng.setEndAfter(currentNode);
  11457. }
  11458. }
  11459. return;
  11460. }
  11461. } while ((currentNode = (start ? walker.next() : walker.prev())));
  11462. // Failed to find any text node or other suitable location then move to the root of body
  11463. if (root.nodeName === 'BODY') {
  11464. if (start) {
  11465. rng.setStart(root, 0);
  11466. }
  11467. else {
  11468. rng.setEnd(root, root.childNodes.length);
  11469. }
  11470. }
  11471. };
  11472. const hasAnyRanges = (editor) => {
  11473. const sel = editor.selection.getSel();
  11474. return isNonNullable(sel) && sel.rangeCount > 0;
  11475. };
  11476. const runOnRanges = (editor, executor) => {
  11477. // Check to see if a fake selection is active. If so then we are simulating a multi range
  11478. // selection so we should return a range for each selected node.
  11479. // Note: Currently tables are the only thing supported for fake selections.
  11480. const fakeSelectionNodes = getCellsFromEditor(editor);
  11481. if (fakeSelectionNodes.length > 0) {
  11482. each$e(fakeSelectionNodes, (elem) => {
  11483. const node = elem.dom;
  11484. const fakeNodeRng = editor.dom.createRng();
  11485. fakeNodeRng.setStartBefore(node);
  11486. fakeNodeRng.setEndAfter(node);
  11487. executor(fakeNodeRng, true);
  11488. });
  11489. }
  11490. else {
  11491. executor(editor.selection.getRng(), false);
  11492. }
  11493. };
  11494. const preserve = (selection, fillBookmark, executor) => {
  11495. const bookmark = getPersistentBookmark(selection, fillBookmark);
  11496. executor(bookmark);
  11497. selection.moveToBookmark(bookmark);
  11498. };
  11499. const isSelectionOverWholeNode = (range, nodeTypePredicate) => range.startContainer === range.endContainer
  11500. && range.endOffset - range.startOffset === 1
  11501. && nodeTypePredicate(range.startContainer.childNodes[range.startOffset]);
  11502. const isSelectionOverWholeHTMLElement = (range) => isSelectionOverWholeNode(range, isHTMLElement);
  11503. const isSelectionOverWholeTextNode = (range) => isSelectionOverWholeNode(range, isText$b);
  11504. const isSelectionOverWholeAnchor = (range) => isSelectionOverWholeNode(range, isAnchor);
  11505. const isNode = (node) => isNumber(node === null || node === void 0 ? void 0 : node.nodeType);
  11506. const isElementNode$1 = (node) => isElement$7(node) && !isBookmarkNode$1(node) && !isCaretNode(node) && !isBogus$1(node);
  11507. // In TinyMCE, directly selected elements are indicated with the data-mce-selected attribute
  11508. // Elements that can be directly selected include control elements such as img, media elements, noneditable elements and others
  11509. const isElementDirectlySelected = (dom, node) => {
  11510. // Table cells are a special case and are separately handled from native editor selection
  11511. if (isElementNode$1(node) && !/^(TD|TH)$/.test(node.nodeName)) {
  11512. const selectedAttr = dom.getAttrib(node, 'data-mce-selected');
  11513. const value = parseInt(selectedAttr, 10);
  11514. // Avoid cases where data-mce-selected is not a positive number e.g. inline-boundary
  11515. return !isNaN(value) && value > 0;
  11516. }
  11517. else {
  11518. return false;
  11519. }
  11520. };
  11521. // TODO: TINY-9130 Look at making SelectionUtils.preserve maintain the noneditable selection instead
  11522. const preserveSelection = (editor, action, shouldMoveStart) => {
  11523. const { selection, dom } = editor;
  11524. const selectedNodeBeforeAction = selection.getNode();
  11525. const isSelectedBeforeNodeNoneditable = isContentEditableFalse$a(selectedNodeBeforeAction);
  11526. preserve(selection, true, () => {
  11527. action();
  11528. });
  11529. // Check previous selected node before the action still exists in the DOM
  11530. // and is still noneditable
  11531. const isBeforeNodeStillNoneditable = isSelectedBeforeNodeNoneditable && isContentEditableFalse$a(selectedNodeBeforeAction);
  11532. if (isBeforeNodeStillNoneditable && dom.isChildOf(selectedNodeBeforeAction, editor.getBody())) {
  11533. editor.selection.select(selectedNodeBeforeAction);
  11534. }
  11535. else if (shouldMoveStart(selection.getStart())) {
  11536. moveStartToNearestText(dom, selection);
  11537. }
  11538. };
  11539. // Note: The reason why we only care about moving the start is because MatchFormat and its function use the start of the selection to determine if a selection has a given format or not
  11540. const moveStartToNearestText = (dom, selection) => {
  11541. var _a, _b;
  11542. const rng = selection.getRng();
  11543. const { startContainer, startOffset } = rng;
  11544. const selectedNode = selection.getNode();
  11545. if (isElementDirectlySelected(dom, selectedNode)) {
  11546. return;
  11547. }
  11548. // Try move startContainer/startOffset to a suitable text node
  11549. if (isElement$7(startContainer)) {
  11550. const nodes = startContainer.childNodes;
  11551. const root = dom.getRoot();
  11552. let walker;
  11553. if (startOffset < nodes.length) {
  11554. const startNode = nodes[startOffset];
  11555. walker = new DomTreeWalker(startNode, (_a = dom.getParent(startNode, dom.isBlock)) !== null && _a !== void 0 ? _a : root);
  11556. }
  11557. else {
  11558. const startNode = nodes[nodes.length - 1];
  11559. walker = new DomTreeWalker(startNode, (_b = dom.getParent(startNode, dom.isBlock)) !== null && _b !== void 0 ? _b : root);
  11560. walker.next(true);
  11561. }
  11562. for (let node = walker.current(); node; node = walker.next()) {
  11563. // If we have found a noneditable element before we have found any text
  11564. // then we cannot move forward any further as otherwise the start could be put inside
  11565. // the non-editable element which is not valid
  11566. if (dom.getContentEditable(node) === 'false') {
  11567. return;
  11568. }
  11569. else if (isText$b(node) && !isWhiteSpaceNode$1(node)) {
  11570. rng.setStart(node, 0);
  11571. selection.setRng(rng);
  11572. return;
  11573. }
  11574. }
  11575. }
  11576. };
  11577. /**
  11578. * Returns the next/previous non whitespace node.
  11579. *
  11580. * @private
  11581. * @param {Node} node Node to start at.
  11582. * @param {Boolean} next (Optional) Include next or previous node defaults to previous.
  11583. * @param {Boolean} inc (Optional) Include the current node in checking. Defaults to false.
  11584. * @return {Node} Next or previous node or undefined if it wasn't found.
  11585. */
  11586. const getNonWhiteSpaceSibling = (node, next, inc) => {
  11587. if (node) {
  11588. const nextName = next ? 'nextSibling' : 'previousSibling';
  11589. for (node = inc ? node : node[nextName]; node; node = node[nextName]) {
  11590. if (isElement$7(node) || !isWhiteSpaceNode$1(node)) {
  11591. return node;
  11592. }
  11593. }
  11594. }
  11595. return undefined;
  11596. };
  11597. const isTextBlock$2 = (schema, node) => !!schema.getTextBlockElements()[node.nodeName.toLowerCase()] || isTransparentBlock(schema, node);
  11598. const isValid = (ed, parent, child) => {
  11599. return ed.schema.isValidChild(parent, child);
  11600. };
  11601. const isWhiteSpaceNode$1 = (node, allowSpaces = false) => {
  11602. if (isNonNullable(node) && isText$b(node)) {
  11603. // If spaces are allowed, treat them as a non-breaking space
  11604. const data = allowSpaces ? node.data.replace(/ /g, '\u00a0') : node.data;
  11605. return isWhitespaceText(data);
  11606. }
  11607. else {
  11608. return false;
  11609. }
  11610. };
  11611. const isEmptyTextNode$1 = (node) => {
  11612. return isNonNullable(node) && isText$b(node) && node.length === 0;
  11613. };
  11614. const isWrapNoneditableTarget = (editor, node) => {
  11615. const baseDataSelector = '[data-mce-cef-wrappable]';
  11616. const formatNoneditableSelector = getFormatNoneditableSelector(editor);
  11617. const selector = isEmpty$5(formatNoneditableSelector) ? baseDataSelector : `${baseDataSelector},${formatNoneditableSelector}`;
  11618. return is$2(SugarElement.fromDom(node), selector);
  11619. };
  11620. // A noneditable element is wrappable if it:
  11621. // - is valid target (has data-mce-cef-wrappable attribute or matches selector from option)
  11622. // - has no editable descendants - removing formats in the editable region can result in the wrapped noneditable being split which is undesirable
  11623. const isWrappableNoneditable = (editor, node) => {
  11624. const dom = editor.dom;
  11625. return (isElementNode$1(node) &&
  11626. dom.getContentEditable(node) === 'false' &&
  11627. isWrapNoneditableTarget(editor, node) &&
  11628. dom.select('[contenteditable="true"]', node).length === 0);
  11629. };
  11630. /**
  11631. * Replaces variables in the value. The variable format is %var.
  11632. *
  11633. * @private
  11634. * @param {String} value Value to replace variables in.
  11635. * @param {Object} vars Name/value array with variables to replace.
  11636. * @return {String} New value with replaced variables.
  11637. */
  11638. const replaceVars = (value, vars) => {
  11639. if (isFunction(value)) {
  11640. return value(vars);
  11641. }
  11642. else if (isNonNullable(vars)) {
  11643. value = value.replace(/%(\w+)/g, (str, name) => {
  11644. return vars[name] || str;
  11645. });
  11646. }
  11647. return value;
  11648. };
  11649. /**
  11650. * Compares two string/nodes regardless of their case.
  11651. *
  11652. * @private
  11653. * @param {String/Node} str1 Node or string to compare.
  11654. * @param {String/Node} str2 Node or string to compare.
  11655. * @return {Boolean} True/false if they match.
  11656. */
  11657. const isEq$5 = (str1, str2) => {
  11658. str1 = str1 || '';
  11659. str2 = str2 || '';
  11660. str1 = '' + (str1.nodeName || str1);
  11661. str2 = '' + (str2.nodeName || str2);
  11662. return str1.toLowerCase() === str2.toLowerCase();
  11663. };
  11664. const normalizeStyleValue = (value, name) => {
  11665. if (isNullable(value)) {
  11666. return null;
  11667. }
  11668. else {
  11669. let strValue = String(value);
  11670. // Force the format to hex
  11671. if (name === 'color' || name === 'backgroundColor') {
  11672. strValue = rgbaToHexString(strValue);
  11673. }
  11674. // Opera will return bold as 700
  11675. if (name === 'fontWeight' && value === 700) {
  11676. strValue = 'bold';
  11677. }
  11678. // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font"
  11679. if (name === 'fontFamily') {
  11680. strValue = strValue.replace(/[\'\"]/g, '').replace(/,\s+/g, ',');
  11681. }
  11682. return strValue;
  11683. }
  11684. };
  11685. const getStyle = (dom, node, name) => {
  11686. const style = dom.getStyle(node, name);
  11687. return normalizeStyleValue(style, name);
  11688. };
  11689. const getTextDecoration = (dom, node) => {
  11690. let decoration;
  11691. dom.getParent(node, (n) => {
  11692. if (isElement$7(n)) {
  11693. decoration = dom.getStyle(n, 'text-decoration');
  11694. return !!decoration && decoration !== 'none';
  11695. }
  11696. else {
  11697. return false;
  11698. }
  11699. });
  11700. return decoration;
  11701. };
  11702. const getParents$2 = (dom, node, selector) => {
  11703. return dom.getParents(node, selector, dom.getRoot());
  11704. };
  11705. const isFormatPredicate = (editor, formatName, predicate) => {
  11706. const formats = editor.formatter.get(formatName);
  11707. return isNonNullable(formats) && exists(formats, predicate);
  11708. };
  11709. const isVariableFormatName = (editor, formatName) => {
  11710. const hasVariableValues = (format) => {
  11711. const isVariableValue = (val) => isFunction(val) || val.length > 1 && val.charAt(0) === '%';
  11712. return exists(['styles', 'attributes'], (key) => get$a(format, key).exists((field) => {
  11713. const fieldValues = isArray$1(field) ? field : values(field);
  11714. return exists(fieldValues, isVariableValue);
  11715. }));
  11716. };
  11717. return isFormatPredicate(editor, formatName, hasVariableValues);
  11718. };
  11719. /**
  11720. * Checks if the two formats are similar based on the format type, attributes, styles and classes
  11721. */
  11722. const areSimilarFormats = (editor, formatName, otherFormatName) => {
  11723. // Note: MatchFormat.matchNode() uses these parameters to check if a format matches a node
  11724. // Therefore, these are ideal to check if two formats are similar
  11725. const validKeys = ['inline', 'block', 'selector', 'attributes', 'styles', 'classes'];
  11726. const filterObj = (format) => filter$4(format, (_, key) => exists(validKeys, (validKey) => validKey === key));
  11727. return isFormatPredicate(editor, formatName, (fmt1) => {
  11728. const filteredFmt1 = filterObj(fmt1);
  11729. return isFormatPredicate(editor, otherFormatName, (fmt2) => {
  11730. const filteredFmt2 = filterObj(fmt2);
  11731. return equal$1(filteredFmt1, filteredFmt2);
  11732. });
  11733. });
  11734. };
  11735. const isBlockFormat = (format) => hasNonNullableKey(format, 'block');
  11736. const isWrappingBlockFormat = (format) => isBlockFormat(format) && format.wrapper === true;
  11737. const isNonWrappingBlockFormat = (format) => isBlockFormat(format) && format.wrapper !== true;
  11738. const isSelectorFormat = (format) => hasNonNullableKey(format, 'selector');
  11739. const isInlineFormat = (format) => hasNonNullableKey(format, 'inline');
  11740. const isMixedFormat = (format) => isSelectorFormat(format) && isInlineFormat(format) && is$4(get$a(format, 'mixed'), true);
  11741. const shouldExpandToSelector = (format) => isSelectorFormat(format) && format.expand !== false && !isInlineFormat(format);
  11742. const getEmptyCaretContainers = (node) => {
  11743. const nodes = [];
  11744. let tempNode = node;
  11745. while (tempNode) {
  11746. if ((isText$b(tempNode) && tempNode.data !== ZWSP$1) || tempNode.childNodes.length > 1) {
  11747. return [];
  11748. }
  11749. // Collect nodes
  11750. if (isElement$7(tempNode)) {
  11751. nodes.push(tempNode);
  11752. }
  11753. tempNode = tempNode.firstChild;
  11754. }
  11755. return nodes;
  11756. };
  11757. const isCaretContainerEmpty = (node) => {
  11758. return getEmptyCaretContainers(node).length > 0;
  11759. };
  11760. const isEmptyCaretFormatElement = (element) => {
  11761. return isCaretNode(element.dom) && isCaretContainerEmpty(element.dom);
  11762. };
  11763. const isBookmarkNode = isBookmarkNode$1;
  11764. const getParents$1 = getParents$2;
  11765. const isWhiteSpaceNode = isWhiteSpaceNode$1;
  11766. const isTextBlock$1 = isTextBlock$2;
  11767. const isBogusBr$1 = (node) => {
  11768. return isBr$7(node) && node.getAttribute('data-mce-bogus') && !node.nextSibling;
  11769. };
  11770. // Expands the node to the closes contentEditable false element if it exists
  11771. const findParentContentEditable = (dom, node) => {
  11772. let parent = node;
  11773. while (parent) {
  11774. if (isElement$7(parent) && dom.getContentEditable(parent)) {
  11775. return dom.getContentEditable(parent) === 'false' ? parent : node;
  11776. }
  11777. parent = parent.parentNode;
  11778. }
  11779. return node;
  11780. };
  11781. const walkText = (start, node, offset, predicate) => {
  11782. const str = node.data;
  11783. if (start) {
  11784. for (let i = offset; i > 0; i--) {
  11785. if (predicate(str.charAt(i - 1))) {
  11786. return i;
  11787. }
  11788. }
  11789. }
  11790. else {
  11791. for (let i = offset; i < str.length; i++) {
  11792. if (predicate(str.charAt(i))) {
  11793. return i;
  11794. }
  11795. }
  11796. }
  11797. return -1;
  11798. };
  11799. const findSpace = (start, node, offset) => walkText(start, node, offset, (c) => isNbsp(c) || isWhiteSpace(c));
  11800. const findContent = (start, node, offset) => walkText(start, node, offset, isContent);
  11801. const findWordEndPoint = (dom, body, container, offset, start, includeTrailingSpaces) => {
  11802. let lastTextNode;
  11803. const closestRoot = dom.getParent(container, (node) => isEditingHost(node) || dom.isBlock(node));
  11804. const rootNode = isNonNullable(closestRoot) ? closestRoot : body;
  11805. const walk = (container, offset, pred) => {
  11806. const textSeeker = TextSeeker(dom);
  11807. const walker = start ? textSeeker.backwards : textSeeker.forwards;
  11808. return Optional.from(walker(container, offset, (text, textOffset) => {
  11809. if (isBookmarkNode(text.parentNode)) {
  11810. return -1;
  11811. }
  11812. else {
  11813. lastTextNode = text;
  11814. return pred(start, text, textOffset);
  11815. }
  11816. }, rootNode));
  11817. };
  11818. const spaceResult = walk(container, offset, findSpace);
  11819. return spaceResult.bind((result) => includeTrailingSpaces ?
  11820. walk(result.container, result.offset + (start ? -1 : 0), findContent) :
  11821. Optional.some(result)).orThunk(() => lastTextNode ?
  11822. Optional.some({ container: lastTextNode, offset: start ? 0 : lastTextNode.length }) :
  11823. Optional.none());
  11824. };
  11825. const findSelectorEndPoint = (dom, formatList, rng, container, siblingName) => {
  11826. const sibling = container[siblingName];
  11827. if (isText$b(container) && isEmpty$5(container.data) && sibling) {
  11828. container = sibling;
  11829. }
  11830. const parents = getParents$1(dom, container);
  11831. for (let i = 0; i < parents.length; i++) {
  11832. for (let y = 0; y < formatList.length; y++) {
  11833. const curFormat = formatList[y];
  11834. // If collapsed state is set then skip formats that doesn't match that
  11835. if (isNonNullable(curFormat.collapsed) && curFormat.collapsed !== rng.collapsed) {
  11836. continue;
  11837. }
  11838. if (isSelectorFormat(curFormat) && dom.is(parents[i], curFormat.selector)) {
  11839. return parents[i];
  11840. }
  11841. }
  11842. }
  11843. return container;
  11844. };
  11845. const findBlockEndPoint = (dom, formatList, container, siblingName) => {
  11846. var _a;
  11847. let node = container;
  11848. const root = dom.getRoot();
  11849. const format = formatList[0];
  11850. // Expand to block of similar type
  11851. if (isBlockFormat(format)) {
  11852. node = format.wrapper ? null : dom.getParent(container, format.block, root);
  11853. }
  11854. // Expand to first wrappable block element or any block element
  11855. if (!node) {
  11856. const scopeRoot = (_a = dom.getParent(container, 'LI,TD,TH,SUMMARY')) !== null && _a !== void 0 ? _a : root;
  11857. node = dom.getParent(isText$b(container) ? container.parentNode : container,
  11858. // Fixes #6183 where it would expand to editable parent element in inline mode
  11859. (node) => node !== root && isTextBlock$1(dom.schema, node), scopeRoot);
  11860. }
  11861. // Exclude inner lists from wrapping
  11862. if (node && isBlockFormat(format) && format.wrapper) {
  11863. node = getParents$1(dom, node, 'ul,ol').reverse()[0] || node;
  11864. }
  11865. // Didn't find a block element look for first/last wrappable element
  11866. if (!node) {
  11867. node = container;
  11868. while (node && node[siblingName] && !dom.isBlock(node[siblingName])) {
  11869. node = node[siblingName];
  11870. // Break on BR but include it will be removed later on
  11871. // we can't remove it now since we need to check if it can be wrapped
  11872. if (isEq$5(node, 'br')) {
  11873. break;
  11874. }
  11875. }
  11876. }
  11877. return node || container;
  11878. };
  11879. // We're at the edge if the parent is a block and there's no next sibling. Alternatively,
  11880. // if we reach the root or can't walk further we also consider it to be a boundary.
  11881. const isAtBlockBoundary$1 = (dom, root, container, siblingName) => {
  11882. const parent = container.parentNode;
  11883. if (isNonNullable(container[siblingName])) {
  11884. return false;
  11885. }
  11886. else if (parent === root || isNullable(parent) || dom.isBlock(parent)) {
  11887. return true;
  11888. }
  11889. else {
  11890. return isAtBlockBoundary$1(dom, root, parent, siblingName);
  11891. }
  11892. };
  11893. // This function walks up the tree if there is no siblings before/after the node.
  11894. // If a sibling is found then the container is returned
  11895. const findParentContainer = (dom, formatList, container, offset, start, expandToBlock) => {
  11896. let parent = container;
  11897. const siblingName = start ? 'previousSibling' : 'nextSibling';
  11898. const root = dom.getRoot();
  11899. // If it's a text node and the offset is inside the text
  11900. if (isText$b(container) && !isWhiteSpaceNode(container)) {
  11901. if (start ? offset > 0 : offset < container.data.length) {
  11902. return container;
  11903. }
  11904. }
  11905. while (parent) {
  11906. if (isEditingHost(parent)) {
  11907. return container;
  11908. }
  11909. // Stop expanding on block elements
  11910. if (!formatList[0].block_expand && dom.isBlock(parent)) {
  11911. return expandToBlock ? parent : container;
  11912. }
  11913. // Walk left/right
  11914. for (let sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) {
  11915. // Allow spaces if not at the edge of a block element, as the spaces won't have been collapsed
  11916. const allowSpaces = isText$b(sibling) && !isAtBlockBoundary$1(dom, root, sibling, siblingName);
  11917. if (!isBookmarkNode(sibling) && !isBogusBr$1(sibling) && !isWhiteSpaceNode(sibling, allowSpaces)) {
  11918. return parent;
  11919. }
  11920. }
  11921. // Check if we can move up are we at root level or body level
  11922. if (parent === root || parent.parentNode === root) {
  11923. container = parent;
  11924. break;
  11925. }
  11926. parent = parent.parentNode;
  11927. }
  11928. return container;
  11929. };
  11930. const isSelfOrParentBookmark = (container) => isBookmarkNode(container.parentNode) || isBookmarkNode(container);
  11931. const expandRng = (dom, rng, formatList, expandOptions = {}) => {
  11932. const { includeTrailingSpace = false, expandToBlock = true } = expandOptions;
  11933. const editableHost = dom.getParent(rng.commonAncestorContainer, (node) => isEditingHost(node));
  11934. const root = isNonNullable(editableHost) ? editableHost : dom.getRoot();
  11935. let { startContainer, startOffset, endContainer, endOffset } = rng;
  11936. const format = formatList[0];
  11937. // If index based start position then resolve it
  11938. if (isElement$7(startContainer) && startContainer.hasChildNodes()) {
  11939. startContainer = getNode$1(startContainer, startOffset);
  11940. if (isText$b(startContainer)) {
  11941. startOffset = 0;
  11942. }
  11943. }
  11944. // If index based end position then resolve it
  11945. if (isElement$7(endContainer) && endContainer.hasChildNodes()) {
  11946. endContainer = getNode$1(endContainer, rng.collapsed ? endOffset : endOffset - 1);
  11947. if (isText$b(endContainer)) {
  11948. endOffset = endContainer.data.length;
  11949. }
  11950. }
  11951. // Expand to closest contentEditable element
  11952. startContainer = findParentContentEditable(dom, startContainer);
  11953. endContainer = findParentContentEditable(dom, endContainer);
  11954. // Exclude bookmark nodes if possible
  11955. if (isSelfOrParentBookmark(startContainer)) {
  11956. startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode;
  11957. if (rng.collapsed) {
  11958. startContainer = startContainer.previousSibling || startContainer;
  11959. }
  11960. else {
  11961. startContainer = startContainer.nextSibling || startContainer;
  11962. }
  11963. if (isText$b(startContainer)) {
  11964. startOffset = rng.collapsed ? startContainer.length : 0;
  11965. }
  11966. }
  11967. if (isSelfOrParentBookmark(endContainer)) {
  11968. endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode;
  11969. if (rng.collapsed) {
  11970. endContainer = endContainer.nextSibling || endContainer;
  11971. }
  11972. else {
  11973. endContainer = endContainer.previousSibling || endContainer;
  11974. }
  11975. if (isText$b(endContainer)) {
  11976. endOffset = rng.collapsed ? 0 : endContainer.length;
  11977. }
  11978. }
  11979. if (rng.collapsed) {
  11980. // Expand left to closest word boundary
  11981. const startPoint = findWordEndPoint(dom, root, startContainer, startOffset, true, includeTrailingSpace);
  11982. startPoint.each(({ container, offset }) => {
  11983. startContainer = container;
  11984. startOffset = offset;
  11985. });
  11986. // Expand right to closest word boundary
  11987. const endPoint = findWordEndPoint(dom, root, endContainer, endOffset, false, includeTrailingSpace);
  11988. endPoint.each(({ container, offset }) => {
  11989. endContainer = container;
  11990. endOffset = offset;
  11991. });
  11992. }
  11993. // Move start/end point up the tree if the leaves are sharp and if we are in different containers
  11994. // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>!
  11995. // This will reduce the number of wrapper elements that needs to be created
  11996. // Move start point up the tree
  11997. if (isInlineFormat(format) || format.block_expand) {
  11998. if (!isInlineFormat(format) || (!isText$b(startContainer) || startOffset === 0)) {
  11999. startContainer = findParentContainer(dom, formatList, startContainer, startOffset, true, expandToBlock);
  12000. }
  12001. if (!isInlineFormat(format) || (!isText$b(endContainer) || endOffset === endContainer.data.length)) {
  12002. endContainer = findParentContainer(dom, formatList, endContainer, endOffset, false, expandToBlock);
  12003. }
  12004. }
  12005. // Expand start/end container to matching selector
  12006. if (shouldExpandToSelector(format)) {
  12007. // Find new startContainer/endContainer if there is better one
  12008. startContainer = findSelectorEndPoint(dom, formatList, rng, startContainer, 'previousSibling');
  12009. endContainer = findSelectorEndPoint(dom, formatList, rng, endContainer, 'nextSibling');
  12010. }
  12011. // Expand start/end container to matching block element or text node
  12012. if (isBlockFormat(format) || isSelectorFormat(format)) {
  12013. // Find new startContainer/endContainer if there is better one
  12014. startContainer = findBlockEndPoint(dom, formatList, startContainer, 'previousSibling');
  12015. endContainer = findBlockEndPoint(dom, formatList, endContainer, 'nextSibling');
  12016. // Non block element then try to expand up the leaf
  12017. if (isBlockFormat(format)) {
  12018. if (!dom.isBlock(startContainer)) {
  12019. startContainer = findParentContainer(dom, formatList, startContainer, startOffset, true, expandToBlock);
  12020. if (isText$b(startContainer)) {
  12021. startOffset = 0;
  12022. }
  12023. }
  12024. if (!dom.isBlock(endContainer)) {
  12025. endContainer = findParentContainer(dom, formatList, endContainer, endOffset, false, expandToBlock);
  12026. if (isText$b(endContainer)) {
  12027. endOffset = endContainer.data.length;
  12028. }
  12029. }
  12030. }
  12031. }
  12032. // Setup index for startContainer
  12033. if (isElement$7(startContainer) && startContainer.parentNode) {
  12034. startOffset = dom.nodeIndex(startContainer);
  12035. startContainer = startContainer.parentNode;
  12036. }
  12037. // Setup index for endContainer
  12038. if (isElement$7(endContainer) && endContainer.parentNode) {
  12039. endOffset = dom.nodeIndex(endContainer) + 1;
  12040. endContainer = endContainer.parentNode;
  12041. }
  12042. // Return new range like object
  12043. return {
  12044. startContainer,
  12045. startOffset,
  12046. endContainer,
  12047. endOffset
  12048. };
  12049. };
  12050. const walk$3 = (dom, rng, callback) => {
  12051. var _a;
  12052. const startOffset = rng.startOffset;
  12053. const startContainer = getNode$1(rng.startContainer, startOffset);
  12054. const endOffset = rng.endOffset;
  12055. const endContainer = getNode$1(rng.endContainer, endOffset - 1);
  12056. /**
  12057. * Excludes start/end text node if they are out side the range
  12058. *
  12059. * @private
  12060. * @param {Array} nodes Nodes to exclude items from.
  12061. * @return {Array} Array with nodes excluding the start/end container if needed.
  12062. */
  12063. const exclude = (nodes) => {
  12064. // First node is excluded
  12065. const firstNode = nodes[0];
  12066. if (isText$b(firstNode) && firstNode === startContainer && startOffset >= firstNode.data.length) {
  12067. nodes.splice(0, 1);
  12068. }
  12069. // Last node is excluded
  12070. const lastNode = nodes[nodes.length - 1];
  12071. if (endOffset === 0 && nodes.length > 0 && lastNode === endContainer && isText$b(lastNode)) {
  12072. nodes.splice(nodes.length - 1, 1);
  12073. }
  12074. return nodes;
  12075. };
  12076. const collectSiblings = (node, name, endNode) => {
  12077. const siblings = [];
  12078. for (; node && node !== endNode; node = node[name]) {
  12079. siblings.push(node);
  12080. }
  12081. return siblings;
  12082. };
  12083. const findEndPoint = (node, root) => dom.getParent(node, (node) => node.parentNode === root, root);
  12084. const walkBoundary = (startNode, endNode, next) => {
  12085. const siblingName = next ? 'nextSibling' : 'previousSibling';
  12086. for (let node = startNode, parent = node.parentNode; node && node !== endNode; node = parent) {
  12087. parent = node.parentNode;
  12088. const siblings = collectSiblings(node === startNode ? node : node[siblingName], siblingName);
  12089. if (siblings.length) {
  12090. if (!next) {
  12091. siblings.reverse();
  12092. }
  12093. callback(exclude(siblings));
  12094. }
  12095. }
  12096. };
  12097. // Same container
  12098. if (startContainer === endContainer) {
  12099. return callback(exclude([startContainer]));
  12100. }
  12101. // Find common ancestor and end points
  12102. const ancestor = (_a = dom.findCommonAncestor(startContainer, endContainer)) !== null && _a !== void 0 ? _a : dom.getRoot();
  12103. // Process left side
  12104. if (dom.isChildOf(startContainer, endContainer)) {
  12105. return walkBoundary(startContainer, ancestor, true);
  12106. }
  12107. // Process right side
  12108. if (dom.isChildOf(endContainer, startContainer)) {
  12109. return walkBoundary(endContainer, ancestor);
  12110. }
  12111. // Find start/end point
  12112. const startPoint = findEndPoint(startContainer, ancestor) || startContainer;
  12113. const endPoint = findEndPoint(endContainer, ancestor) || endContainer;
  12114. // Walk left leaf
  12115. walkBoundary(startContainer, startPoint, true);
  12116. // Walk the middle from start to end point
  12117. const siblings = collectSiblings(startPoint === startContainer ? startPoint : startPoint.nextSibling, 'nextSibling', endPoint === endContainer ? endPoint.nextSibling : endPoint);
  12118. if (siblings.length) {
  12119. callback(exclude(siblings));
  12120. }
  12121. // Walk right leaf
  12122. walkBoundary(endContainer, endPoint);
  12123. };
  12124. const validBlocks = [
  12125. // Codesample plugin
  12126. 'pre[class*=language-][contenteditable="false"]',
  12127. // Image plugin - captioned image
  12128. 'figure.image',
  12129. // Mediaembed plugin
  12130. 'div[data-ephox-embed-iri]',
  12131. // Pageembed plugin
  12132. 'div.tiny-pageembed',
  12133. // Tableofcontents plugin
  12134. 'div.mce-toc',
  12135. 'div[data-mce-toc]',
  12136. // Footnootes plugin
  12137. 'div.mce-footnotes'
  12138. ];
  12139. const isZeroWidth = (elem) => isText$c(elem) && get$4(elem) === ZWSP$1;
  12140. const context = (editor, elem, wrapName, nodeName) => parentElement(elem).fold(() => "skipping" /* ChildContext.Skipping */, (parent) => {
  12141. // We used to skip these, but given that they might be representing empty paragraphs, it probably
  12142. // makes sense to treat them just like text nodes
  12143. if (nodeName === 'br' || isZeroWidth(elem)) {
  12144. return "valid" /* ChildContext.Valid */;
  12145. }
  12146. else if (isAnnotation(elem)) {
  12147. return "existing" /* ChildContext.Existing */;
  12148. }
  12149. else if (isCaretNode(elem.dom)) {
  12150. return "caret" /* ChildContext.Caret */;
  12151. }
  12152. else if (exists(validBlocks, (selector) => is$2(elem, selector))) {
  12153. return "valid-block" /* ChildContext.ValidBlock */;
  12154. }
  12155. else if (!isValid(editor, wrapName, nodeName) || !isValid(editor, name(parent), wrapName)) {
  12156. return "invalid-child" /* ChildContext.InvalidChild */;
  12157. }
  12158. else {
  12159. return "valid" /* ChildContext.Valid */;
  12160. }
  12161. });
  12162. const applyWordGrab = (editor, rng) => {
  12163. const r = expandRng(editor.dom, rng, [{ inline: 'span' }]);
  12164. rng.setStart(r.startContainer, r.startOffset);
  12165. rng.setEnd(r.endContainer, r.endOffset);
  12166. editor.selection.setRng(rng);
  12167. };
  12168. const applyAnnotation = (elem, masterUId, data, annotationName, decorate, directAnnotation) => {
  12169. const { uid = masterUId, ...otherData } = data;
  12170. add$2(elem, annotation());
  12171. set$4(elem, `${dataAnnotationId()}`, uid);
  12172. set$4(elem, `${dataAnnotation()}`, annotationName);
  12173. const { attributes = {}, classes = [] } = decorate(uid, otherData);
  12174. setAll$1(elem, attributes);
  12175. add$1(elem, classes);
  12176. if (directAnnotation) {
  12177. if (classes.length > 0) {
  12178. set$4(elem, `${dataAnnotationClasses()}`, classes.join(','));
  12179. }
  12180. const attributeNames = keys(attributes);
  12181. if (attributeNames.length > 0) {
  12182. set$4(elem, `${dataAnnotationAttributes()}`, attributeNames.join(','));
  12183. }
  12184. }
  12185. };
  12186. const removeDirectAnnotation = (elem) => {
  12187. remove$4(elem, annotation());
  12188. remove$9(elem, `${dataAnnotationId()}`);
  12189. remove$9(elem, `${dataAnnotation()}`);
  12190. remove$9(elem, `${dataAnnotationActive()}`);
  12191. const customAttrNames = getOpt(elem, `${dataAnnotationAttributes()}`).map((names) => names.split(',')).getOr([]);
  12192. const customClasses = getOpt(elem, `${dataAnnotationClasses()}`).map((names) => names.split(',')).getOr([]);
  12193. each$e(customAttrNames, (name) => remove$9(elem, name));
  12194. remove$3(elem, customClasses);
  12195. remove$9(elem, `${dataAnnotationClasses()}`);
  12196. remove$9(elem, `${dataAnnotationAttributes()}`);
  12197. };
  12198. const makeAnnotation = (eDoc, uid, data, annotationName, decorate) => {
  12199. const master = SugarElement.fromTag('span', eDoc);
  12200. applyAnnotation(master, uid, data, annotationName, decorate, false);
  12201. return master;
  12202. };
  12203. const annotate = (editor, rng, uid, annotationName, decorate, data) => {
  12204. // Setup all the wrappers that are going to be used.
  12205. const newWrappers = [];
  12206. // Setup the spans for the comments
  12207. const master = makeAnnotation(editor.getDoc(), uid, data, annotationName, decorate);
  12208. // Set the current wrapping element
  12209. const wrapper = value$1();
  12210. // Clear the current wrapping element, so that subsequent calls to
  12211. // getOrOpenWrapper spawns a new one.
  12212. const finishWrapper = () => {
  12213. wrapper.clear();
  12214. };
  12215. // Get the existing wrapper, or spawn a new one.
  12216. const getOrOpenWrapper = () => wrapper.get().getOrThunk(() => {
  12217. const nu = shallow(master);
  12218. newWrappers.push(nu);
  12219. wrapper.set(nu);
  12220. return nu;
  12221. });
  12222. const processElements = (elems) => {
  12223. each$e(elems, processElement);
  12224. };
  12225. const processElement = (elem) => {
  12226. const ctx = context(editor, elem, 'span', name(elem));
  12227. switch (ctx) {
  12228. case "invalid-child" /* ChildContext.InvalidChild */: {
  12229. finishWrapper();
  12230. const children = children$1(elem);
  12231. processElements(children);
  12232. finishWrapper();
  12233. break;
  12234. }
  12235. case "valid-block" /* ChildContext.ValidBlock */: {
  12236. finishWrapper();
  12237. applyAnnotation(elem, uid, data, annotationName, decorate, true);
  12238. break;
  12239. }
  12240. case "valid" /* ChildContext.Valid */: {
  12241. const w = getOrOpenWrapper();
  12242. wrap$2(elem, w);
  12243. break;
  12244. }
  12245. }
  12246. };
  12247. const processNodes = (nodes) => {
  12248. const elems = map$3(nodes, SugarElement.fromDom);
  12249. processElements(elems);
  12250. };
  12251. walk$3(editor.dom, rng, (nodes) => {
  12252. finishWrapper();
  12253. processNodes(nodes);
  12254. });
  12255. return newWrappers;
  12256. };
  12257. const annotateWithBookmark = (editor, name, settings, data) => {
  12258. editor.undoManager.transact(() => {
  12259. const selection = editor.selection;
  12260. const initialRng = selection.getRng();
  12261. const hasFakeSelection = getCellsFromEditor(editor).length > 0;
  12262. const masterUid = generate$1('mce-annotation');
  12263. if (initialRng.collapsed && !hasFakeSelection) {
  12264. applyWordGrab(editor, initialRng);
  12265. }
  12266. // Even after applying word grab, we could not find a selection. Therefore,
  12267. // just make a wrapper and insert it at the current cursor
  12268. if (selection.getRng().collapsed && !hasFakeSelection) {
  12269. const wrapper = makeAnnotation(editor.getDoc(), masterUid, data, name, settings.decorate);
  12270. // Put something visible in the marker
  12271. set$3(wrapper, nbsp);
  12272. selection.getRng().insertNode(wrapper.dom);
  12273. selection.select(wrapper.dom);
  12274. }
  12275. else {
  12276. // The bookmark is responsible for splitting the nodes beforehand at the selection points
  12277. // The "false" here means a zero width cursor is NOT put in the bookmark. It seems to be required
  12278. // to stop an empty paragraph splitting into two paragraphs. Probably a better way exists.
  12279. preserve(selection, false, () => {
  12280. runOnRanges(editor, (selectionRng) => {
  12281. annotate(editor, selectionRng, masterUid, name, settings.decorate, data);
  12282. });
  12283. });
  12284. }
  12285. });
  12286. };
  12287. const Annotator = (editor) => {
  12288. const registry = create$a();
  12289. setup$D(editor, registry);
  12290. const changes = setup$E(editor, registry);
  12291. const isSpan = isTag('span');
  12292. const removeAnnotations = (elements) => {
  12293. each$e(elements, (element) => {
  12294. if (isSpan(element)) {
  12295. unwrap(element);
  12296. }
  12297. else {
  12298. removeDirectAnnotation(element);
  12299. }
  12300. });
  12301. };
  12302. return {
  12303. /**
  12304. * Registers a specific annotator by name
  12305. *
  12306. * @method register
  12307. * @param {String} name the name of the annotation
  12308. * @param {Object} settings settings for the annotation (e.g. decorate)
  12309. */
  12310. register: (name, settings) => {
  12311. registry.register(name, settings);
  12312. },
  12313. /**
  12314. * Applies the annotation at the current selection using data
  12315. *
  12316. * @method annotate
  12317. * @param {String} name the name of the annotation to apply
  12318. * @param {Object} data information to pass through to this particular
  12319. * annotation
  12320. */
  12321. annotate: (name, data) => {
  12322. registry.lookup(name).each((settings) => {
  12323. annotateWithBookmark(editor, name, settings, data);
  12324. });
  12325. },
  12326. /**
  12327. * Executes the specified callback when the current selection matches the annotation or not.
  12328. *
  12329. * @method annotationChanged
  12330. * @param {String} name Name of annotation to listen for
  12331. * @param {Function} callback Callback with (state, name, and data) fired when the annotation
  12332. * at the cursor changes. If state if false, data will not be provided.
  12333. */
  12334. annotationChanged: (name, callback) => {
  12335. changes.addListener(name, callback);
  12336. },
  12337. /**
  12338. * Removes any annotations from the current selection that match
  12339. * the name
  12340. *
  12341. * @method remove
  12342. * @param {String} name the name of the annotation to remove
  12343. */
  12344. remove: (name) => {
  12345. identify(editor, Optional.some(name)).each(({ elements }) => {
  12346. /**
  12347. * TINY-9399: It is important to keep the bookmarking in the callback
  12348. * because it adjusts selection in a way that `identify` function
  12349. * cannot retain the selected word.
  12350. */
  12351. const bookmark = editor.selection.getBookmark();
  12352. removeAnnotations(elements);
  12353. editor.selection.moveToBookmark(bookmark);
  12354. });
  12355. },
  12356. /**
  12357. * Removes all annotations that match the specified name from the entire document.
  12358. *
  12359. * @method removeAll
  12360. * @param {String} name the name of the annotation to remove
  12361. */
  12362. removeAll: (name) => {
  12363. const bookmark = editor.selection.getBookmark();
  12364. each$d(findAll(editor, name), (elements, _) => {
  12365. removeAnnotations(elements);
  12366. });
  12367. editor.selection.moveToBookmark(bookmark);
  12368. },
  12369. /**
  12370. * Retrieve all the annotations for a given name
  12371. *
  12372. * @method getAll
  12373. * @param {String} name the name of the annotations to retrieve
  12374. * @return {Object} an index of annotations from uid => DOM nodes
  12375. */
  12376. getAll: (name) => {
  12377. const directory = findAll(editor, name);
  12378. return map$2(directory, (elems) => map$3(elems, (elem) => elem.dom));
  12379. }
  12380. };
  12381. };
  12382. /**
  12383. * Constructs a new BookmarkManager instance for a specific selection instance.
  12384. *
  12385. * @constructor
  12386. * @method BookmarkManager
  12387. * @param {tinymce.dom.Selection} selection Selection instance to handle bookmarks for.
  12388. */
  12389. const BookmarkManager = (selection) => {
  12390. return {
  12391. /**
  12392. * Returns a bookmark location for the current selection. This bookmark object
  12393. * can then be used to restore the selection after some content modification to the document.
  12394. *
  12395. * @method getBookmark
  12396. * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
  12397. * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
  12398. * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
  12399. * @example
  12400. * // Stores a bookmark of the current selection
  12401. * const bm = tinymce.activeEditor.selection.getBookmark();
  12402. *
  12403. * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
  12404. *
  12405. * // Restore the selection bookmark
  12406. * tinymce.activeEditor.selection.moveToBookmark(bm);
  12407. */
  12408. getBookmark: curry(getBookmark$2, selection),
  12409. /**
  12410. * Restores the selection to the specified bookmark.
  12411. *
  12412. * @method moveToBookmark
  12413. * @param {Object} bookmark Bookmark to restore selection from.
  12414. * @example
  12415. * // Stores a bookmark of the current selection
  12416. * const bm = tinymce.activeEditor.selection.getBookmark();
  12417. *
  12418. * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
  12419. *
  12420. * // Restore the selection bookmark
  12421. * tinymce.activeEditor.selection.moveToBookmark(bm);
  12422. */
  12423. moveToBookmark: curry(moveToBookmark, selection)
  12424. };
  12425. };
  12426. /**
  12427. * Returns true/false if the specified node is a bookmark node or not.
  12428. *
  12429. * @static
  12430. * @method isBookmarkNode
  12431. * @param {DOMNode} node DOM Node to check if it's a bookmark node or not.
  12432. * @return {Boolean} true/false if the node is a bookmark node or not.
  12433. */
  12434. BookmarkManager.isBookmarkNode = isBookmarkNode$1;
  12435. const isXYWithinRange = (clientX, clientY, range) => {
  12436. if (range.collapsed) {
  12437. return false;
  12438. }
  12439. else {
  12440. return exists(range.getClientRects(), (rect) => containsXY(rect, clientX, clientY));
  12441. }
  12442. };
  12443. const clamp$1 = (offset, element) => {
  12444. const max = isText$c(element) ? get$4(element).length : children$1(element).length + 1;
  12445. if (offset > max) {
  12446. return max;
  12447. }
  12448. else if (offset < 0) {
  12449. return 0;
  12450. }
  12451. return offset;
  12452. };
  12453. const normalizeRng = (rng) => SimSelection.range(rng.start, clamp$1(rng.soffset, rng.start), rng.finish, clamp$1(rng.foffset, rng.finish));
  12454. const isOrContains = (root, elm) => !isRestrictedNode(elm.dom) && (contains(root, elm) || eq(root, elm));
  12455. const isRngInRoot = (root) => (rng) => isOrContains(root, rng.start) && isOrContains(root, rng.finish);
  12456. // TINY-9259: We need to store the selection on Firefox since if the editor is hidden the selection.getRng() api will not work as expected.
  12457. const shouldStore = (editor) => editor.inline || Env.browser.isFirefox();
  12458. const nativeRangeToSelectionRange = (r) => SimSelection.range(SugarElement.fromDom(r.startContainer), r.startOffset, SugarElement.fromDom(r.endContainer), r.endOffset);
  12459. const readRange = (win) => {
  12460. const selection = win.getSelection();
  12461. const rng = !selection || selection.rangeCount === 0 ? Optional.none() : Optional.from(selection.getRangeAt(0));
  12462. return rng.map(nativeRangeToSelectionRange);
  12463. };
  12464. const getBookmark$1 = (root) => {
  12465. const win = defaultView(root);
  12466. return readRange(win.dom)
  12467. .filter(isRngInRoot(root));
  12468. };
  12469. const validate = (root, bookmark) => Optional.from(bookmark)
  12470. .filter(isRngInRoot(root))
  12471. .map(normalizeRng);
  12472. const bookmarkToNativeRng = (bookmark) => {
  12473. const rng = document.createRange();
  12474. try {
  12475. // Might throw IndexSizeError
  12476. rng.setStart(bookmark.start.dom, bookmark.soffset);
  12477. rng.setEnd(bookmark.finish.dom, bookmark.foffset);
  12478. return Optional.some(rng);
  12479. }
  12480. catch (_a) {
  12481. return Optional.none();
  12482. }
  12483. };
  12484. const store = (editor) => {
  12485. const newBookmark = shouldStore(editor) ? getBookmark$1(SugarElement.fromDom(editor.getBody())) : Optional.none();
  12486. editor.bookmark = newBookmark.isSome() ? newBookmark : editor.bookmark;
  12487. };
  12488. const getRng = (editor) => {
  12489. const bookmark = editor.bookmark ? editor.bookmark : Optional.none();
  12490. return bookmark
  12491. .bind((x) => validate(SugarElement.fromDom(editor.getBody()), x))
  12492. .bind(bookmarkToNativeRng);
  12493. };
  12494. const restore = (editor) => {
  12495. getRng(editor).each((rng) => editor.selection.setRng(rng));
  12496. };
  12497. /**
  12498. * This class manages the focus/blur state of the editor. This class is needed since some
  12499. * browsers fire false focus/blur states when the selection is moved to a UI dialog or similar.
  12500. *
  12501. * This class will fire two events focus and blur on the editor instances that got affected.
  12502. * It will also handle the restore of selection when the focus is lost and returned.
  12503. *
  12504. * @class tinymce.FocusManager
  12505. * @private
  12506. */
  12507. /**
  12508. * Returns true if the specified element is part of the UI for example an button or text input.
  12509. *
  12510. * @static
  12511. * @method isEditorUIElement
  12512. * @param {Element} elm Element to check if it's part of the UI or not.
  12513. * @return {Boolean} True/false state if the element is part of the UI or not.
  12514. */
  12515. const isEditorUIElement$1 = (elm) => {
  12516. // Needs to be converted to string since svg can have focus: #6776
  12517. const className = elm.className.toString();
  12518. return className.indexOf('tox-') !== -1 || className.indexOf('mce-') !== -1;
  12519. };
  12520. const FocusManager = {
  12521. isEditorUIElement: isEditorUIElement$1
  12522. };
  12523. /**
  12524. * Utility class for working with delayed actions like setTimeout.
  12525. *
  12526. * @class tinymce.util.Delay
  12527. */
  12528. const wrappedSetTimeout = (callback, time) => {
  12529. if (!isNumber(time)) {
  12530. time = 0;
  12531. }
  12532. return window.setTimeout(callback, time);
  12533. };
  12534. const wrappedSetInterval = (callback, time) => {
  12535. if (!isNumber(time)) {
  12536. time = 0;
  12537. }
  12538. return window.setInterval(callback, time);
  12539. };
  12540. const Delay = {
  12541. /**
  12542. * Sets a timeout that's similar to the native browser <a href="https://developer.mozilla.org/en-US/docs/Web/API/setTimeout">setTimeout</a>
  12543. * API, except that it checks if the editor instance is still alive when the callback gets executed.
  12544. *
  12545. * @method setEditorTimeout
  12546. * @param {tinymce.Editor} editor Editor instance to check the removed state on.
  12547. * @param {Function} callback Callback to execute when timer runs out.
  12548. * @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
  12549. * @return {Number} Timeout id number.
  12550. */
  12551. setEditorTimeout: (editor, callback, time) => {
  12552. return wrappedSetTimeout(() => {
  12553. if (!editor.removed) {
  12554. callback();
  12555. }
  12556. }, time);
  12557. },
  12558. /**
  12559. * Sets an interval timer that's similar to native browser <a href="https://developer.mozilla.org/en-US/docs/Web/API/setInterval">setInterval</a>
  12560. * API, except that it checks if the editor instance is still alive when the callback gets executed.
  12561. *
  12562. * @method setEditorInterval
  12563. * @param {Function} callback Callback to execute when interval time runs out.
  12564. * @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
  12565. * @return {Number} Timeout id number.
  12566. */
  12567. setEditorInterval: (editor, callback, time) => {
  12568. const timer = wrappedSetInterval(() => {
  12569. if (!editor.removed) {
  12570. callback();
  12571. }
  12572. else {
  12573. window.clearInterval(timer);
  12574. }
  12575. }, time);
  12576. return timer;
  12577. }
  12578. };
  12579. const walkUp = (navigation, doc) => {
  12580. const frame = navigation.view(doc);
  12581. return frame.fold(constant([]), (f) => {
  12582. const parent = navigation.owner(f);
  12583. const rest = walkUp(navigation, parent);
  12584. return [f].concat(rest);
  12585. });
  12586. };
  12587. const pathTo = (element, navigation) => {
  12588. const d = navigation.owner(element);
  12589. return walkUp(navigation, d);
  12590. };
  12591. const view = (doc) => {
  12592. var _a;
  12593. // Only walk up to the document this script is defined in.
  12594. // This prevents walking up to the parent window when the editor is in an iframe.
  12595. const element = doc.dom === document ? Optional.none() : Optional.from((_a = doc.dom.defaultView) === null || _a === void 0 ? void 0 : _a.frameElement);
  12596. return element.map(SugarElement.fromDom);
  12597. };
  12598. const owner = (element) => documentOrOwner(element);
  12599. var Navigation = /*#__PURE__*/Object.freeze({
  12600. __proto__: null,
  12601. view: view,
  12602. owner: owner
  12603. });
  12604. const find = (element) => {
  12605. const doc = getDocument();
  12606. const scroll = get$5(doc);
  12607. const frames = pathTo(element, Navigation);
  12608. const offset = viewport(element);
  12609. const r = foldr(frames, (b, a) => {
  12610. const loc = viewport(a);
  12611. return {
  12612. left: b.left + loc.left,
  12613. top: b.top + loc.top
  12614. };
  12615. }, { left: 0, top: 0 });
  12616. return SugarPosition(r.left + offset.left + scroll.left, r.top + offset.top + scroll.top);
  12617. };
  12618. const isManualNodeChange = (e) => {
  12619. return e.type === 'nodechange' && e.selectionChange;
  12620. };
  12621. const registerPageMouseUp = (editor, throttledStore) => {
  12622. const mouseUpPage = () => {
  12623. throttledStore.throttle();
  12624. };
  12625. DOMUtils.DOM.bind(document, 'mouseup', mouseUpPage);
  12626. editor.on('remove', () => {
  12627. DOMUtils.DOM.unbind(document, 'mouseup', mouseUpPage);
  12628. });
  12629. };
  12630. const registerMouseUp = (editor, throttledStore) => {
  12631. editor.on('mouseup touchend', (_e) => {
  12632. throttledStore.throttle();
  12633. });
  12634. };
  12635. const registerEditorEvents = (editor, throttledStore) => {
  12636. registerMouseUp(editor, throttledStore);
  12637. editor.on('keyup NodeChange AfterSetSelectionRange', (e) => {
  12638. if (!isManualNodeChange(e)) {
  12639. store(editor);
  12640. }
  12641. });
  12642. };
  12643. const register$6 = (editor) => {
  12644. const throttledStore = first$1(() => {
  12645. store(editor);
  12646. }, 0);
  12647. editor.on('init', () => {
  12648. if (editor.inline) {
  12649. registerPageMouseUp(editor, throttledStore);
  12650. }
  12651. registerEditorEvents(editor, throttledStore);
  12652. });
  12653. editor.on('remove', () => {
  12654. throttledStore.cancel();
  12655. });
  12656. };
  12657. let documentFocusInHandler;
  12658. const DOM$d = DOMUtils.DOM;
  12659. const isEditorUIElement = (elm) => {
  12660. // Since this can be overridden by third party we need to use the API reference here
  12661. return isElement$7(elm) && FocusManager.isEditorUIElement(elm);
  12662. };
  12663. const isEditorContentAreaElement = (elm) => {
  12664. const classList = elm.classList;
  12665. if (classList !== undefined) {
  12666. // tox-edit-area__iframe === iframe container element
  12667. // mce-content-body === inline body element
  12668. return classList.contains('tox-edit-area') || classList.contains('tox-edit-area__iframe') || classList.contains('mce-content-body');
  12669. }
  12670. else {
  12671. return false;
  12672. }
  12673. };
  12674. const isUIElement = (editor, elm) => {
  12675. const customSelector = getCustomUiSelector(editor);
  12676. const parent = DOM$d.getParent(elm, (elm) => {
  12677. return (isEditorUIElement(elm) ||
  12678. (customSelector ? editor.dom.is(elm, customSelector) : false));
  12679. });
  12680. return parent !== null;
  12681. };
  12682. const getActiveElement = (editor) => {
  12683. try {
  12684. const root = getRootNode(SugarElement.fromDom(editor.getElement()));
  12685. return active(root).fold(() => document.body, (x) => x.dom);
  12686. }
  12687. catch (_a) {
  12688. // IE sometimes fails to get the activeElement when resizing table
  12689. // TODO: Investigate this
  12690. return document.body;
  12691. }
  12692. };
  12693. const registerEvents$1 = (editorManager, e) => {
  12694. const editor = e.editor;
  12695. register$6(editor);
  12696. const toggleContentAreaOnFocus = (editor, fn) => {
  12697. // Inline editors have a different approach to highlight the content area on focus
  12698. if (shouldHighlightOnFocus(editor) && editor.inline !== true) {
  12699. const contentArea = SugarElement.fromDom(editor.getContainer());
  12700. fn(contentArea, 'tox-edit-focus');
  12701. }
  12702. };
  12703. const bringEditorIntoView = (editor) => {
  12704. const minimumVisibility = 25;
  12705. if (!editor.iframeElement) {
  12706. return;
  12707. }
  12708. const element = SugarElement.fromDom(editor.iframeElement);
  12709. const op = find(element);
  12710. const viewportBounds = getBounds(window);
  12711. if (op.top < viewportBounds.y || op.top > (viewportBounds.bottom - minimumVisibility)) {
  12712. element.dom.scrollIntoView({ block: 'center' });
  12713. }
  12714. };
  12715. editor.on('focusin', () => {
  12716. const focusedEditor = editorManager.focusedEditor;
  12717. if (isEditorContentAreaElement(getActiveElement(editor))) {
  12718. toggleContentAreaOnFocus(editor, add$2);
  12719. }
  12720. if (focusedEditor !== editor) {
  12721. if (focusedEditor) {
  12722. focusedEditor.dispatch('blur', { focusedEditor: editor });
  12723. }
  12724. editorManager.setActive(editor);
  12725. editorManager.focusedEditor = editor;
  12726. editor.dispatch('focus', { blurredEditor: focusedEditor });
  12727. editor.focus(true);
  12728. const browser = detect$1().browser;
  12729. if (editor.inline !== true && (browser.isSafari() || browser.isChromium())) {
  12730. bringEditorIntoView(editor);
  12731. }
  12732. }
  12733. });
  12734. editor.on('focusout', () => {
  12735. Delay.setEditorTimeout(editor, () => {
  12736. const focusedEditor = editorManager.focusedEditor;
  12737. // Remove focus highlight when the content area is no longer the active editor element, or if the highlighted editor is not the current focused editor
  12738. if (!isEditorContentAreaElement(getActiveElement(editor)) || focusedEditor !== editor) {
  12739. toggleContentAreaOnFocus(editor, remove$4);
  12740. }
  12741. // Still the same editor the blur was outside any editor UI
  12742. if (!isUIElement(editor, getActiveElement(editor)) && focusedEditor === editor) {
  12743. editor.dispatch('blur', { focusedEditor: null });
  12744. editorManager.focusedEditor = null;
  12745. }
  12746. });
  12747. });
  12748. // Check if focus is moved to an element outside the active editor by checking if the target node
  12749. // isn't within the body of the activeEditor nor a UI element such as a dialog child control
  12750. if (!documentFocusInHandler) {
  12751. documentFocusInHandler = (e) => {
  12752. const activeEditor = editorManager.activeEditor;
  12753. if (activeEditor) {
  12754. getOriginalEventTarget(e).each((target) => {
  12755. const elem = target;
  12756. if (elem.ownerDocument === document) {
  12757. // Fire a blur event if the element isn't a UI element
  12758. if (elem !== document.body && !isUIElement(activeEditor, elem) && editorManager.focusedEditor === activeEditor) {
  12759. activeEditor.dispatch('blur', { focusedEditor: null });
  12760. editorManager.focusedEditor = null;
  12761. }
  12762. }
  12763. });
  12764. }
  12765. };
  12766. DOM$d.bind(document, 'focusin', documentFocusInHandler);
  12767. }
  12768. };
  12769. const unregisterDocumentEvents = (editorManager, e) => {
  12770. if (editorManager.focusedEditor === e.editor) {
  12771. editorManager.focusedEditor = null;
  12772. }
  12773. if (!editorManager.activeEditor && documentFocusInHandler) {
  12774. DOM$d.unbind(document, 'focusin', documentFocusInHandler);
  12775. documentFocusInHandler = null;
  12776. }
  12777. };
  12778. const setup$C = (editorManager) => {
  12779. editorManager.on('AddEditor', curry(registerEvents$1, editorManager));
  12780. editorManager.on('RemoveEditor', curry(unregisterDocumentEvents, editorManager));
  12781. };
  12782. const getContentEditableHost = (editor, node) => editor.dom.getParent(node, (node) => editor.dom.getContentEditable(node) === 'true');
  12783. const hasContentEditableFalseParent$1 = (editor, node) => editor.dom.getParent(node, (node) => editor.dom.getContentEditable(node) === 'false') !== null;
  12784. const getCollapsedNode = (rng) => rng.collapsed ? Optional.from(getNode$1(rng.startContainer, rng.startOffset)).map(SugarElement.fromDom) : Optional.none();
  12785. const getFocusInElement = (root, rng) => getCollapsedNode(rng).bind((node) => {
  12786. if (isTableSection(node)) {
  12787. return Optional.some(node);
  12788. }
  12789. else if (!contains(root, node)) {
  12790. return Optional.some(root);
  12791. }
  12792. else {
  12793. return Optional.none();
  12794. }
  12795. });
  12796. const normalizeSelection = (editor, rng) => {
  12797. getFocusInElement(SugarElement.fromDom(editor.getBody()), rng).bind((elm) => {
  12798. return firstPositionIn(elm.dom);
  12799. }).fold(() => {
  12800. editor.selection.normalize();
  12801. }, (caretPos) => editor.selection.setRng(caretPos.toRange()));
  12802. };
  12803. const focusBody = (body) => {
  12804. if (body.setActive) {
  12805. // IE 11 sometimes throws "Invalid function" then fallback to focus
  12806. // setActive is better since it doesn't scroll to the element being focused
  12807. try {
  12808. body.setActive();
  12809. }
  12810. catch (_a) {
  12811. body.focus();
  12812. }
  12813. }
  12814. else {
  12815. body.focus();
  12816. }
  12817. };
  12818. const hasElementFocus = (elm) => hasFocus$1(elm) || search(elm).isSome();
  12819. const hasIframeFocus = (editor) => isNonNullable(editor.iframeElement) && hasFocus$1(SugarElement.fromDom(editor.iframeElement));
  12820. const hasInlineFocus = (editor) => {
  12821. const rawBody = editor.getBody();
  12822. return rawBody && hasElementFocus(SugarElement.fromDom(rawBody));
  12823. };
  12824. const hasUiFocus = (editor) => {
  12825. const dos = getRootNode(SugarElement.fromDom(editor.getElement()));
  12826. // Editor container is the obvious one (Menubar, Toolbar, Status bar, Sidebar) and dialogs and menus are in an auxiliary element (silver theme specific)
  12827. // This can't use Focus.search() because only the theme has this element reference
  12828. return active(dos)
  12829. .filter((elem) => !isEditorContentAreaElement(elem.dom) && isUIElement(editor, elem.dom))
  12830. .isSome();
  12831. };
  12832. const hasFocus = (editor) => editor.inline ? hasInlineFocus(editor) : hasIframeFocus(editor);
  12833. const hasEditorOrUiFocus = (editor) => hasFocus(editor) || hasUiFocus(editor);
  12834. const focusEditor = (editor) => {
  12835. const selection = editor.selection;
  12836. const body = editor.getBody();
  12837. let rng = selection.getRng();
  12838. editor.quirks.refreshContentEditable();
  12839. const restoreBookmark = (editor) => {
  12840. getRng(editor).each((bookmarkRng) => {
  12841. editor.selection.setRng(bookmarkRng);
  12842. rng = bookmarkRng;
  12843. });
  12844. };
  12845. if (!hasFocus(editor) && editor.hasEditableRoot()) {
  12846. restoreBookmark(editor);
  12847. }
  12848. // Move focus to contentEditable=true child if needed
  12849. const contentEditableHost = getContentEditableHost(editor, selection.getNode());
  12850. if (contentEditableHost && editor.dom.isChildOf(contentEditableHost, body)) {
  12851. if (!hasContentEditableFalseParent$1(editor, contentEditableHost)) {
  12852. focusBody(body);
  12853. }
  12854. focusBody(contentEditableHost);
  12855. if (!editor.hasEditableRoot()) {
  12856. restoreBookmark(editor);
  12857. }
  12858. normalizeSelection(editor, rng);
  12859. activateEditor(editor);
  12860. return;
  12861. }
  12862. // Focus the window iframe
  12863. if (!editor.inline) {
  12864. // WebKit needs this call to fire focusin event properly see #5948
  12865. // But Opera pre Blink engine will produce an empty selection so skip Opera
  12866. if (!Env.browser.isOpera()) {
  12867. focusBody(body);
  12868. }
  12869. editor.getWin().focus();
  12870. }
  12871. // Focus the body as well since it's contentEditable
  12872. if (Env.browser.isFirefox() || editor.inline) {
  12873. focusBody(body);
  12874. normalizeSelection(editor, rng);
  12875. }
  12876. activateEditor(editor);
  12877. };
  12878. const activateEditor = (editor) => editor.editorManager.setActive(editor);
  12879. const focus = (editor, skipFocus) => {
  12880. if (editor.removed) {
  12881. return;
  12882. }
  12883. if (skipFocus) {
  12884. activateEditor(editor);
  12885. }
  12886. else {
  12887. focusEditor(editor);
  12888. }
  12889. };
  12890. /**
  12891. * This file exposes a set of the common KeyCodes for use. Please grow it as needed.
  12892. */
  12893. const VK = {
  12894. BACKSPACE: 8,
  12895. DELETE: 46,
  12896. DOWN: 40,
  12897. ENTER: 13,
  12898. ESC: 27,
  12899. LEFT: 37,
  12900. RIGHT: 39,
  12901. SPACEBAR: 32,
  12902. TAB: 9,
  12903. UP: 38,
  12904. PAGE_UP: 33,
  12905. PAGE_DOWN: 34,
  12906. END: 35,
  12907. HOME: 36,
  12908. modifierPressed: (e) => {
  12909. return e.shiftKey || e.ctrlKey || e.altKey || VK.metaKeyPressed(e);
  12910. },
  12911. metaKeyPressed: (e) => {
  12912. // Check if ctrl or meta key is pressed. Edge case for AltGr on Windows where it produces ctrlKey+altKey states
  12913. return Env.os.isMacOS() || Env.os.isiOS() ? e.metaKey : e.ctrlKey && !e.altKey;
  12914. }
  12915. };
  12916. const elementSelectionAttr = 'data-mce-selected';
  12917. const controlElmSelector = 'table,img,figure.image,hr,video,span.mce-preview-object,details';
  12918. const abs = Math.abs;
  12919. const round$1 = Math.round;
  12920. // Details about each resize handle how to scale etc
  12921. const resizeHandles = {
  12922. // Name: x multiplier, y multiplier, delta size x, delta size y
  12923. nw: [0, 0, -1, -1],
  12924. ne: [1, 0, 1, -1],
  12925. se: [1, 1, 1, 1],
  12926. sw: [0, 1, -1, 1]
  12927. };
  12928. const isTouchEvent = (evt) => evt.type === 'longpress' || evt.type.indexOf('touch') === 0;
  12929. /**
  12930. * This class handles control selection of elements. Controls are elements
  12931. * that can be resized and needs to be selected as a whole. It adds custom resize handles
  12932. * to all browser engines that support properly disabling the built in resize logic.
  12933. *
  12934. * @private
  12935. * @class tinymce.dom.ControlSelection
  12936. */
  12937. const ControlSelection = (selection, editor) => {
  12938. const dom = editor.dom;
  12939. const editableDoc = editor.getDoc();
  12940. const rootDocument = document;
  12941. const rootElement = editor.getBody();
  12942. let selectedElm, selectedElmGhost, resizeHelper, selectedHandle, resizeBackdrop;
  12943. let startX, startY, startW, startH, ratio, resizeStarted;
  12944. let width;
  12945. let height;
  12946. let startScrollWidth;
  12947. let startScrollHeight;
  12948. const isImage = (elm) => isNonNullable(elm) && (isImg(elm) || dom.is(elm, 'figure.image'));
  12949. const isMedia = (elm) => isMedia$2(elm) || dom.hasClass(elm, 'mce-preview-object');
  12950. const isEventOnImageOutsideRange = (evt, range) => {
  12951. if (isTouchEvent(evt)) {
  12952. const touch = evt.touches[0];
  12953. return isImage(evt.target) && !isXYWithinRange(touch.clientX, touch.clientY, range);
  12954. }
  12955. else {
  12956. return isImage(evt.target) && !isXYWithinRange(evt.clientX, evt.clientY, range);
  12957. }
  12958. };
  12959. const contextMenuSelectImage = (evt) => {
  12960. const target = evt.target;
  12961. if (isEventOnImageOutsideRange(evt, editor.selection.getRng()) && !evt.isDefaultPrevented()) {
  12962. editor.selection.select(target);
  12963. }
  12964. };
  12965. const getResizeTargets = (elm) => {
  12966. if (dom.hasClass(elm, 'mce-preview-object') && isNonNullable(elm.firstElementChild)) {
  12967. // When resizing a preview object we need to resize both the original element and the wrapper span
  12968. return [elm, elm.firstElementChild];
  12969. }
  12970. else if (dom.is(elm, 'figure.image')) {
  12971. return [elm.querySelector('img')];
  12972. }
  12973. else {
  12974. return [elm];
  12975. }
  12976. };
  12977. const isResizable = (elm) => {
  12978. const selector = getObjectResizing(editor);
  12979. if (!selector || editor.mode.isReadOnly()) {
  12980. return false;
  12981. }
  12982. if (elm.getAttribute('data-mce-resize') === 'false') {
  12983. return false;
  12984. }
  12985. if (elm === editor.getBody()) {
  12986. return false;
  12987. }
  12988. if (dom.hasClass(elm, 'mce-preview-object') && isNonNullable(elm.firstElementChild)) {
  12989. return is$2(SugarElement.fromDom(elm.firstElementChild), selector);
  12990. }
  12991. else {
  12992. return is$2(SugarElement.fromDom(elm), selector);
  12993. }
  12994. };
  12995. const createGhostElement = (dom, elm) => {
  12996. if (isMedia(elm)) {
  12997. return dom.create('img', { src: Env.transparentSrc });
  12998. }
  12999. else if (isTable$2(elm)) {
  13000. const isNorth = startsWith(selectedHandle.name, 'n');
  13001. const rowSelect = isNorth ? head : last$2;
  13002. const tableElm = elm.cloneNode(true);
  13003. // Get row, remove all height styles
  13004. rowSelect(dom.select('tr', tableElm)).each((tr) => {
  13005. const cells = dom.select('td,th', tr);
  13006. dom.setStyle(tr, 'height', null);
  13007. each$e(cells, (cell) => dom.setStyle(cell, 'height', null));
  13008. });
  13009. return tableElm;
  13010. }
  13011. else {
  13012. return elm.cloneNode(true);
  13013. }
  13014. };
  13015. const setSizeProp = (element, name, value) => {
  13016. if (isNonNullable(value)) {
  13017. // Resize by using style or attribute
  13018. const targets = getResizeTargets(element);
  13019. each$e(targets, (target) => {
  13020. if (target.style[name] || !editor.schema.isValid(target.nodeName.toLowerCase(), name)) {
  13021. dom.setStyle(target, name, value);
  13022. }
  13023. else {
  13024. dom.setAttrib(target, name, '' + value);
  13025. }
  13026. });
  13027. }
  13028. };
  13029. const setGhostElmSize = (ghostElm, width, height) => {
  13030. setSizeProp(ghostElm, 'width', width);
  13031. setSizeProp(ghostElm, 'height', height);
  13032. };
  13033. const resizeGhostElement = (e) => {
  13034. let deltaX, deltaY, proportional;
  13035. let resizeHelperX, resizeHelperY;
  13036. // Calc new width/height
  13037. deltaX = e.screenX - startX;
  13038. deltaY = e.screenY - startY;
  13039. // Calc new size
  13040. width = deltaX * selectedHandle[2] + startW;
  13041. height = deltaY * selectedHandle[3] + startH;
  13042. // Never scale down lower than 5 pixels
  13043. width = width < 5 ? 5 : width;
  13044. height = height < 5 ? 5 : height;
  13045. if ((isImage(selectedElm) || isMedia(selectedElm)) && getResizeImgProportional(editor) !== false) {
  13046. proportional = !VK.modifierPressed(e);
  13047. }
  13048. else {
  13049. proportional = VK.modifierPressed(e);
  13050. }
  13051. // Constrain proportions
  13052. if (proportional) {
  13053. if (abs(deltaX) > abs(deltaY)) {
  13054. height = round$1(width * ratio);
  13055. width = round$1(height / ratio);
  13056. }
  13057. else {
  13058. width = round$1(height / ratio);
  13059. height = round$1(width * ratio);
  13060. }
  13061. }
  13062. // Update ghost size
  13063. setGhostElmSize(selectedElmGhost, width, height);
  13064. // Update resize helper position
  13065. resizeHelperX = selectedHandle.startPos.x + deltaX;
  13066. resizeHelperY = selectedHandle.startPos.y + deltaY;
  13067. resizeHelperX = resizeHelperX > 0 ? resizeHelperX : 0;
  13068. resizeHelperY = resizeHelperY > 0 ? resizeHelperY : 0;
  13069. dom.setStyles(resizeHelper, {
  13070. left: resizeHelperX,
  13071. top: resizeHelperY,
  13072. display: 'block'
  13073. });
  13074. resizeHelper.innerHTML = width + ' &times; ' + height;
  13075. /* TODO: TINY-11702 dom.setStyle() has no effect because the value is NaN
  13076. // Update ghost X position if needed
  13077. if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) {
  13078. dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width));
  13079. }
  13080. // Update ghost Y position if needed
  13081. if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) {
  13082. dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height));
  13083. }
  13084. */
  13085. // Calculate how must overflow we got
  13086. deltaX = rootElement.scrollWidth - startScrollWidth;
  13087. deltaY = rootElement.scrollHeight - startScrollHeight;
  13088. // Re-position the resize helper based on the overflow
  13089. if (deltaX + deltaY !== 0) {
  13090. dom.setStyles(resizeHelper, {
  13091. left: resizeHelperX - deltaX,
  13092. top: resizeHelperY - deltaY
  13093. });
  13094. }
  13095. if (!resizeStarted) {
  13096. fireObjectResizeStart(editor, selectedElm, startW, startH, 'corner-' + selectedHandle.name);
  13097. resizeStarted = true;
  13098. }
  13099. };
  13100. const endGhostResize = () => {
  13101. const wasResizeStarted = resizeStarted;
  13102. resizeStarted = false;
  13103. // Set width/height properties
  13104. if (wasResizeStarted) {
  13105. setSizeProp(selectedElm, 'width', width);
  13106. setSizeProp(selectedElm, 'height', height);
  13107. }
  13108. dom.unbind(editableDoc, 'mousemove', resizeGhostElement);
  13109. dom.unbind(editableDoc, 'mouseup', endGhostResize);
  13110. if (rootDocument !== editableDoc) {
  13111. dom.unbind(rootDocument, 'mousemove', resizeGhostElement);
  13112. dom.unbind(rootDocument, 'mouseup', endGhostResize);
  13113. }
  13114. // Remove ghost/helper and update resize handle positions
  13115. dom.remove(selectedElmGhost);
  13116. dom.remove(resizeHelper);
  13117. dom.remove(resizeBackdrop);
  13118. showResizeRect(selectedElm);
  13119. if (wasResizeStarted) {
  13120. fireObjectResized(editor, selectedElm, width, height, 'corner-' + selectedHandle.name);
  13121. dom.setAttrib(selectedElm, 'style', dom.getAttrib(selectedElm, 'style'));
  13122. }
  13123. editor.nodeChanged();
  13124. };
  13125. const showResizeRect = (targetElm) => {
  13126. unbindResizeHandleEvents();
  13127. // Get position and size of target
  13128. const position = dom.getPos(targetElm, rootElement);
  13129. const selectedElmX = position.x;
  13130. const selectedElmY = position.y;
  13131. const rect = targetElm.getBoundingClientRect(); // Fix for Gecko offsetHeight for table with caption
  13132. const targetWidth = rect.width || (rect.right - rect.left);
  13133. const targetHeight = rect.height || (rect.bottom - rect.top);
  13134. // Reset width/height if user selects a new image/table
  13135. if (selectedElm !== targetElm) {
  13136. hideResizeRect();
  13137. selectedElm = targetElm;
  13138. width = height = 0;
  13139. }
  13140. // Makes it possible to disable resizing
  13141. const e = editor.dispatch('ObjectSelected', { target: targetElm });
  13142. if (isResizable(targetElm) && !e.isDefaultPrevented()) {
  13143. each$d(resizeHandles, (handle, name) => {
  13144. const startDrag = (e) => {
  13145. // Note: We're guaranteed to have at least one target here
  13146. const target = getResizeTargets(selectedElm)[0];
  13147. startX = e.screenX;
  13148. startY = e.screenY;
  13149. startW = target.clientWidth;
  13150. startH = target.clientHeight;
  13151. ratio = startH / startW;
  13152. selectedHandle = handle;
  13153. selectedHandle.name = name;
  13154. selectedHandle.startPos = {
  13155. x: targetWidth * handle[0] + selectedElmX,
  13156. y: targetHeight * handle[1] + selectedElmY
  13157. };
  13158. startScrollWidth = rootElement.scrollWidth;
  13159. startScrollHeight = rootElement.scrollHeight;
  13160. resizeBackdrop = dom.add(rootElement, 'div', {
  13161. 'class': 'mce-resize-backdrop',
  13162. 'data-mce-bogus': 'all'
  13163. });
  13164. dom.setStyles(resizeBackdrop, {
  13165. position: 'fixed',
  13166. left: '0',
  13167. top: '0',
  13168. width: '100%',
  13169. height: '100%'
  13170. });
  13171. selectedElmGhost = createGhostElement(dom, selectedElm);
  13172. dom.addClass(selectedElmGhost, 'mce-clonedresizable');
  13173. dom.setAttrib(selectedElmGhost, 'data-mce-bogus', 'all');
  13174. selectedElmGhost.contentEditable = 'false'; // Hides IE move layer cursor
  13175. dom.setStyles(selectedElmGhost, {
  13176. left: selectedElmX,
  13177. top: selectedElmY,
  13178. margin: 0
  13179. });
  13180. // Set initial ghost size
  13181. setGhostElmSize(selectedElmGhost, targetWidth, targetHeight);
  13182. selectedElmGhost.removeAttribute(elementSelectionAttr);
  13183. rootElement.appendChild(selectedElmGhost);
  13184. dom.bind(editableDoc, 'mousemove', resizeGhostElement);
  13185. dom.bind(editableDoc, 'mouseup', endGhostResize);
  13186. if (rootDocument !== editableDoc) {
  13187. dom.bind(rootDocument, 'mousemove', resizeGhostElement);
  13188. dom.bind(rootDocument, 'mouseup', endGhostResize);
  13189. }
  13190. resizeHelper = dom.add(rootElement, 'div', {
  13191. 'class': 'mce-resize-helper',
  13192. 'data-mce-bogus': 'all'
  13193. }, startW + ' &times; ' + startH);
  13194. };
  13195. // Get existing or render resize handle
  13196. let handleElm = dom.get('mceResizeHandle' + name);
  13197. if (handleElm) {
  13198. dom.remove(handleElm);
  13199. }
  13200. handleElm = dom.add(rootElement, 'div', {
  13201. 'id': 'mceResizeHandle' + name,
  13202. 'data-mce-bogus': 'all',
  13203. 'class': 'mce-resizehandle',
  13204. 'unselectable': true,
  13205. 'style': 'cursor:' + name + '-resize; margin:0; padding:0'
  13206. });
  13207. dom.bind(handleElm, 'mousedown', (e) => {
  13208. e.stopImmediatePropagation();
  13209. e.preventDefault();
  13210. startDrag(e);
  13211. });
  13212. handle.elm = handleElm;
  13213. // Position element
  13214. dom.setStyles(handleElm, {
  13215. left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2),
  13216. top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2)
  13217. });
  13218. });
  13219. }
  13220. else {
  13221. hideResizeRect(false);
  13222. }
  13223. };
  13224. const throttledShowResizeRect = first$1(showResizeRect, 0);
  13225. const hideResizeRect = (removeSelected = true) => {
  13226. throttledShowResizeRect.cancel();
  13227. unbindResizeHandleEvents();
  13228. if (selectedElm && removeSelected) {
  13229. selectedElm.removeAttribute(elementSelectionAttr);
  13230. }
  13231. each$d(resizeHandles, (value, name) => {
  13232. const handleElm = dom.get('mceResizeHandle' + name);
  13233. if (handleElm) {
  13234. dom.unbind(handleElm);
  13235. dom.remove(handleElm);
  13236. }
  13237. });
  13238. };
  13239. const isChildOrEqual = (node, parent) => dom.isChildOf(node, parent);
  13240. const updateResizeRect = (e) => {
  13241. // Ignore all events while resizing, if the editor instance is composing or the editor was removed
  13242. if (resizeStarted || editor.removed || editor.composing) {
  13243. return;
  13244. }
  13245. const targetElm = e.type === 'mousedown' ? e.target : selection.getNode();
  13246. const controlElm = closest$3(SugarElement.fromDom(targetElm), controlElmSelector)
  13247. .map((e) => e.dom)
  13248. .filter((e) => dom.isEditable(e.parentElement) || (e.nodeName === 'IMG' && dom.isEditable(e)))
  13249. .getOrUndefined();
  13250. // Store the original data-mce-selected value or fallback to '1' if not set
  13251. const selectedValue = isNonNullable(controlElm) ? dom.getAttrib(controlElm, elementSelectionAttr, '1') : '1';
  13252. // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v
  13253. each$e(dom.select(`img[${elementSelectionAttr}],hr[${elementSelectionAttr}]`), (img) => {
  13254. img.removeAttribute(elementSelectionAttr);
  13255. });
  13256. if (isNonNullable(controlElm) && isChildOrEqual(controlElm, rootElement) && hasEditorOrUiFocus(editor)) {
  13257. disableGeckoResize();
  13258. const startElm = selection.getStart(true);
  13259. if (isChildOrEqual(startElm, controlElm) && isChildOrEqual(selection.getEnd(true), controlElm)) {
  13260. // Note: We must ensure the selected attribute is added first before showing the rect so that we don't get any selection flickering
  13261. dom.setAttrib(controlElm, elementSelectionAttr, selectedValue);
  13262. throttledShowResizeRect.throttle(controlElm);
  13263. return;
  13264. }
  13265. }
  13266. hideResizeRect();
  13267. };
  13268. const unbindResizeHandleEvents = () => {
  13269. each$d(resizeHandles, (handle) => {
  13270. if (handle.elm) {
  13271. dom.unbind(handle.elm);
  13272. // eslint-disable-next-line @typescript-eslint/no-array-delete
  13273. delete handle.elm;
  13274. }
  13275. });
  13276. };
  13277. const disableGeckoResize = () => {
  13278. try {
  13279. // Disable object resizing on Gecko
  13280. editor.getDoc().execCommand('enableObjectResizing', false, 'false');
  13281. }
  13282. catch (_a) {
  13283. // Ignore
  13284. }
  13285. };
  13286. editor.on('init', () => {
  13287. disableGeckoResize();
  13288. editor.on('NodeChange ResizeEditor ResizeWindow ResizeContent drop', updateResizeRect);
  13289. // Update resize rect while typing in a table
  13290. editor.on('keyup compositionend', (e) => {
  13291. // Don't update the resize rect while composing since it blows away the IME see: #2710
  13292. if (selectedElm && selectedElm.nodeName === 'TABLE') {
  13293. updateResizeRect(e);
  13294. }
  13295. });
  13296. editor.on('hide blur', hideResizeRect);
  13297. editor.on('contextmenu longpress', contextMenuSelectImage, true);
  13298. // Hide rect on focusout since it would float on top of windows otherwise
  13299. // editor.on('focusout', hideResizeRect);
  13300. });
  13301. editor.on('remove', unbindResizeHandleEvents);
  13302. const destroy = () => {
  13303. throttledShowResizeRect.cancel();
  13304. selectedElm = selectedElmGhost = resizeBackdrop = null;
  13305. };
  13306. return {
  13307. isResizable,
  13308. showResizeRect,
  13309. hideResizeRect,
  13310. updateResizeRect,
  13311. destroy
  13312. };
  13313. };
  13314. const fromPoint = (clientX, clientY, doc) => {
  13315. const win = defaultView(SugarElement.fromDom(doc));
  13316. return getAtPoint(win.dom, clientX, clientY).map((simRange) => {
  13317. const rng = doc.createRange();
  13318. rng.setStart(simRange.start.dom, simRange.soffset);
  13319. rng.setEnd(simRange.finish.dom, simRange.foffset);
  13320. return rng;
  13321. }).getOrUndefined();
  13322. };
  13323. const isEq$4 = (rng1, rng2) => {
  13324. return isNonNullable(rng1) && isNonNullable(rng2) &&
  13325. (rng1.startContainer === rng2.startContainer && rng1.startOffset === rng2.startOffset) &&
  13326. (rng1.endContainer === rng2.endContainer && rng1.endOffset === rng2.endOffset);
  13327. };
  13328. const findParent = (node, rootNode, predicate) => {
  13329. let currentNode = node;
  13330. while (currentNode && currentNode !== rootNode) {
  13331. if (predicate(currentNode)) {
  13332. return currentNode;
  13333. }
  13334. currentNode = currentNode.parentNode;
  13335. }
  13336. return null;
  13337. };
  13338. const hasParent$1 = (node, rootNode, predicate) => findParent(node, rootNode, predicate) !== null;
  13339. const hasParentWithName = (node, rootNode, name) => hasParent$1(node, rootNode, (node) => node.nodeName === name);
  13340. const isCeFalseCaretContainer = (node, rootNode) => isCaretContainer$2(node) && !hasParent$1(node, rootNode, isCaretNode);
  13341. const hasBrBeforeAfter = (dom, node, left) => {
  13342. const parentNode = node.parentNode;
  13343. if (parentNode) {
  13344. const walker = new DomTreeWalker(node, dom.getParent(parentNode, dom.isBlock) || dom.getRoot());
  13345. let currentNode;
  13346. while ((currentNode = walker[left ? 'prev' : 'next']())) {
  13347. if (isBr$7(currentNode)) {
  13348. return true;
  13349. }
  13350. }
  13351. }
  13352. return false;
  13353. };
  13354. const isPrevNode = (node, name) => { var _a; return ((_a = node.previousSibling) === null || _a === void 0 ? void 0 : _a.nodeName) === name; };
  13355. const hasContentEditableFalseParent = (root, node) => {
  13356. let currentNode = node;
  13357. while (currentNode && currentNode !== root) {
  13358. if (isContentEditableFalse$a(currentNode)) {
  13359. return true;
  13360. }
  13361. currentNode = currentNode.parentNode;
  13362. }
  13363. return false;
  13364. };
  13365. // Walks the dom left/right to find a suitable text node to move the endpoint into
  13366. // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG
  13367. const findTextNodeRelative = (dom, isAfterNode, collapsed, left, startNode) => {
  13368. const body = dom.getRoot();
  13369. const nonEmptyElementsMap = dom.schema.getNonEmptyElements();
  13370. const parentNode = startNode.parentNode;
  13371. let lastInlineElement;
  13372. let node;
  13373. if (!parentNode) {
  13374. return Optional.none();
  13375. }
  13376. const parentBlockContainer = dom.getParent(parentNode, dom.isBlock) || body;
  13377. // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680
  13378. // This: <p><br>|</p> becomes <p>|<br></p>
  13379. if (left && isBr$7(startNode) && isAfterNode && dom.isEmpty(parentBlockContainer)) {
  13380. return Optional.some(CaretPosition(parentNode, dom.nodeIndex(startNode)));
  13381. }
  13382. // Walk left until we hit a text node we can move to or a block/br/img
  13383. const walker = new DomTreeWalker(startNode, parentBlockContainer);
  13384. while ((node = walker[left ? 'prev' : 'next']())) {
  13385. // Break if we hit a non content editable node
  13386. if (dom.getContentEditableParent(node) === 'false' || isCeFalseCaretContainer(node, body)) {
  13387. return Optional.none();
  13388. }
  13389. // Found text node that has a length
  13390. if (isText$b(node) && node.data.length > 0) {
  13391. if (!hasParentWithName(node, body, 'A')) {
  13392. return Optional.some(CaretPosition(node, left ? node.data.length : 0));
  13393. }
  13394. return Optional.none();
  13395. }
  13396. // Break if we find a block or a BR/IMG/INPUT etc
  13397. if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
  13398. return Optional.none();
  13399. }
  13400. lastInlineElement = node;
  13401. }
  13402. if (isComment(lastInlineElement)) {
  13403. return Optional.none();
  13404. }
  13405. // Only fetch the last inline element when in caret mode for now
  13406. if (collapsed && lastInlineElement) {
  13407. return Optional.some(CaretPosition(lastInlineElement, 0));
  13408. }
  13409. return Optional.none();
  13410. };
  13411. const normalizeEndPoint = (dom, collapsed, start, rng) => {
  13412. const body = dom.getRoot();
  13413. let node;
  13414. let normalized = false;
  13415. let container = start ? rng.startContainer : rng.endContainer;
  13416. let offset = start ? rng.startOffset : rng.endOffset;
  13417. const isAfterNode = isElement$7(container) && offset === container.childNodes.length;
  13418. const nonEmptyElementsMap = dom.schema.getNonEmptyElements();
  13419. let directionLeft = start;
  13420. if (isCaretContainer$2(container)) {
  13421. return Optional.none();
  13422. }
  13423. if (isElement$7(container) && offset > container.childNodes.length - 1) {
  13424. directionLeft = false;
  13425. }
  13426. // If the container is a document move it to the body element
  13427. if (isDocument$1(container)) {
  13428. container = body;
  13429. offset = 0;
  13430. }
  13431. // If the container is body try move it into the closest text node or position
  13432. if (container === body) {
  13433. // If start is before/after a image, table etc
  13434. if (directionLeft) {
  13435. node = container.childNodes[offset > 0 ? offset - 1 : 0];
  13436. if (node) {
  13437. if (isCaretContainer$2(node)) {
  13438. return Optional.none();
  13439. }
  13440. if (nonEmptyElementsMap[node.nodeName] || isTable$2(node)) {
  13441. return Optional.none();
  13442. }
  13443. }
  13444. }
  13445. // Resolve the index
  13446. if (container.hasChildNodes()) {
  13447. offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1);
  13448. container = container.childNodes[offset];
  13449. offset = isText$b(container) && isAfterNode ? container.data.length : 0;
  13450. // Don't normalize non collapsed selections like <p>[a</p><table></table>]
  13451. if (!collapsed && container === body.lastChild && isTable$2(container)) {
  13452. return Optional.none();
  13453. }
  13454. if (hasContentEditableFalseParent(body, container) || isCaretContainer$2(container)) {
  13455. return Optional.none();
  13456. }
  13457. if (isDetails(container)) {
  13458. return Optional.none();
  13459. }
  13460. // Don't walk into elements that doesn't have any child nodes like a IMG
  13461. if (container.hasChildNodes() && !isTable$2(container)) {
  13462. // Walk the DOM to find a text node to place the caret at or a BR
  13463. node = container;
  13464. const walker = new DomTreeWalker(container, body);
  13465. do {
  13466. if (isContentEditableFalse$a(node) || isCaretContainer$2(node)) {
  13467. normalized = false;
  13468. break;
  13469. }
  13470. // Found a text node use that position
  13471. if (isText$b(node) && node.data.length > 0) {
  13472. offset = directionLeft ? 0 : node.data.length;
  13473. container = node;
  13474. normalized = true;
  13475. break;
  13476. }
  13477. // Found a BR/IMG/PRE element that we can place the caret before
  13478. if (nonEmptyElementsMap[node.nodeName.toLowerCase()] && !isTableCellOrCaption(node)) {
  13479. offset = dom.nodeIndex(node);
  13480. container = node.parentNode;
  13481. // Put caret after image and pre tag when moving the end point
  13482. if (!directionLeft) {
  13483. offset++;
  13484. }
  13485. normalized = true;
  13486. break;
  13487. }
  13488. } while ((node = (directionLeft ? walker.next() : walker.prev())));
  13489. }
  13490. }
  13491. }
  13492. // Lean the caret to the left if possible
  13493. if (collapsed) {
  13494. // So this: <b>x</b><i>|x</i>
  13495. // Becomes: <b>x|</b><i>x</i>
  13496. // Seems that only gecko has issues with this
  13497. if (isText$b(container) && offset === 0) {
  13498. findTextNodeRelative(dom, isAfterNode, collapsed, true, container).each((pos) => {
  13499. container = pos.container();
  13500. offset = pos.offset();
  13501. normalized = true;
  13502. });
  13503. }
  13504. // Lean left into empty inline elements when the caret is before a BR
  13505. // So this: <i><b></b><i>|<br></i>
  13506. // Becomes: <i><b>|</b><i><br></i>
  13507. // Seems that only gecko has issues with this.
  13508. // Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p>
  13509. if (isElement$7(container)) {
  13510. node = container.childNodes[offset];
  13511. // Offset is after the containers last child
  13512. // then use the previous child for normalization
  13513. if (!node) {
  13514. node = container.childNodes[offset - 1];
  13515. }
  13516. if (node && isBr$7(node) && !isPrevNode(node, 'A') &&
  13517. !hasBrBeforeAfter(dom, node, false) && !hasBrBeforeAfter(dom, node, true)) {
  13518. findTextNodeRelative(dom, isAfterNode, collapsed, true, node).each((pos) => {
  13519. container = pos.container();
  13520. offset = pos.offset();
  13521. normalized = true;
  13522. });
  13523. }
  13524. }
  13525. }
  13526. // Lean the start of the selection right if possible
  13527. // So this: x[<b>x]</b>
  13528. // Becomes: x<b>[x]</b>
  13529. if (directionLeft && !collapsed && isText$b(container) && offset === container.data.length) {
  13530. findTextNodeRelative(dom, isAfterNode, collapsed, false, container).each((pos) => {
  13531. container = pos.container();
  13532. offset = pos.offset();
  13533. normalized = true;
  13534. });
  13535. }
  13536. return normalized && container ? Optional.some(CaretPosition(container, offset)) : Optional.none();
  13537. };
  13538. const normalize$2 = (dom, rng) => {
  13539. const collapsed = rng.collapsed, normRng = rng.cloneRange();
  13540. const startPos = CaretPosition.fromRangeStart(rng);
  13541. normalizeEndPoint(dom, collapsed, true, normRng).each((pos) => {
  13542. // #TINY-1595: Do not move the caret to previous line
  13543. if (!collapsed || !CaretPosition.isAbove(startPos, pos)) {
  13544. normRng.setStart(pos.container(), pos.offset());
  13545. }
  13546. });
  13547. if (!collapsed) {
  13548. normalizeEndPoint(dom, collapsed, false, normRng).each((pos) => {
  13549. normRng.setEnd(pos.container(), pos.offset());
  13550. });
  13551. }
  13552. // If it was collapsed then make sure it still is
  13553. if (collapsed) {
  13554. normRng.collapse(true);
  13555. }
  13556. return isEq$4(rng, normRng) ? Optional.none() : Optional.some(normRng);
  13557. };
  13558. const splitText = (node, offset) => {
  13559. return node.splitText(offset);
  13560. };
  13561. const split = (rng) => {
  13562. let startContainer = rng.startContainer, startOffset = rng.startOffset, endContainer = rng.endContainer, endOffset = rng.endOffset;
  13563. // Handle single text node
  13564. if (startContainer === endContainer && isText$b(startContainer)) {
  13565. if (startOffset > 0 && startOffset < startContainer.data.length) {
  13566. endContainer = splitText(startContainer, startOffset);
  13567. startContainer = endContainer.previousSibling;
  13568. if (endOffset > startOffset) {
  13569. endOffset = endOffset - startOffset;
  13570. const newContainer = splitText(endContainer, endOffset).previousSibling;
  13571. startContainer = endContainer = newContainer;
  13572. endOffset = newContainer.data.length;
  13573. startOffset = 0;
  13574. }
  13575. else {
  13576. endOffset = 0;
  13577. }
  13578. }
  13579. }
  13580. else {
  13581. // Split startContainer text node if needed
  13582. if (isText$b(startContainer) && startOffset > 0 && startOffset < startContainer.data.length) {
  13583. startContainer = splitText(startContainer, startOffset);
  13584. startOffset = 0;
  13585. }
  13586. // Split endContainer text node if needed
  13587. if (isText$b(endContainer) && endOffset > 0 && endOffset < endContainer.data.length) {
  13588. const newContainer = splitText(endContainer, endOffset).previousSibling;
  13589. endContainer = newContainer;
  13590. endOffset = newContainer.data.length;
  13591. }
  13592. }
  13593. return {
  13594. startContainer,
  13595. startOffset,
  13596. endContainer,
  13597. endOffset
  13598. };
  13599. };
  13600. /**
  13601. * This class contains a few utility methods for ranges.
  13602. *
  13603. * @class tinymce.dom.RangeUtils
  13604. */
  13605. const RangeUtils = (dom) => {
  13606. /**
  13607. * Walks the specified range like object and executes the callback for each sibling collection it finds.
  13608. *
  13609. * @private
  13610. * @method walk
  13611. * @param {RangeObject} rng Range like object.
  13612. * @param {Function} callback Callback function to execute for each sibling collection.
  13613. */
  13614. const walk = (rng, callback) => {
  13615. return walk$3(dom, rng, callback);
  13616. };
  13617. /**
  13618. * Splits the specified range at it's start/end points.
  13619. *
  13620. * @private
  13621. * @param {RangeObject} rng Range to split.
  13622. * @return {RangeObject} Range position object.
  13623. */
  13624. const split$1 = split;
  13625. /**
  13626. * Normalizes the specified range by finding the closest best suitable caret location.
  13627. *
  13628. * @private
  13629. * @param {Range} rng Range to normalize.
  13630. * @return {Boolean} True or false if the specified range was normalized or not.
  13631. */
  13632. const normalize = (rng) => {
  13633. return normalize$2(dom, rng).fold(never, (normalizedRng) => {
  13634. rng.setStart(normalizedRng.startContainer, normalizedRng.startOffset);
  13635. rng.setEnd(normalizedRng.endContainer, normalizedRng.endOffset);
  13636. return true;
  13637. });
  13638. };
  13639. /**
  13640. * Returns a range expanded around the entire word the provided selection was collapsed within.
  13641. *
  13642. * @method expand
  13643. * @param {Range} rng The initial range to work from.
  13644. * @param {Object} options Optional options provided to the expansion. Defaults to { type: 'word' }
  13645. * @return {Range} Returns the expanded range.
  13646. */
  13647. const expand = (rng, options = { type: 'word' }) => {
  13648. if (options.type === 'word') {
  13649. const rangeLike = expandRng(dom, rng, [{ inline: 'span' }], { includeTrailingSpace: false, expandToBlock: false });
  13650. const newRange = dom.createRng();
  13651. newRange.setStart(rangeLike.startContainer, rangeLike.startOffset);
  13652. newRange.setEnd(rangeLike.endContainer, rangeLike.endOffset);
  13653. return newRange;
  13654. }
  13655. return rng;
  13656. };
  13657. return {
  13658. walk,
  13659. split: split$1,
  13660. expand,
  13661. normalize
  13662. };
  13663. };
  13664. /**
  13665. * Compares two ranges and checks if they are equal.
  13666. *
  13667. * @static
  13668. * @method compareRanges
  13669. * @param {RangeObject} rng1 First range to compare.
  13670. * @param {RangeObject} rng2 First range to compare.
  13671. * @return {Boolean} True or false if the ranges are equal.
  13672. */
  13673. RangeUtils.compareRanges = isEq$4;
  13674. /**
  13675. * Gets the caret range for the given x/y location.
  13676. *
  13677. * @static
  13678. * @method getCaretRangeFromPoint
  13679. * @param {Number} clientX X coordinate for range
  13680. * @param {Number} clientY Y coordinate for range
  13681. * @param {Document} doc Document that the x and y coordinates are relative to
  13682. * @returns {Range} Caret range
  13683. */
  13684. RangeUtils.getCaretRangeFromPoint = fromPoint;
  13685. RangeUtils.getSelectedNode = getSelectedNode;
  13686. RangeUtils.getNode = getNode$1;
  13687. const excludeFromDescend = (element) => name(element) === 'textarea';
  13688. const fireScrollIntoViewEvent = (editor, data) => {
  13689. const scrollEvent = editor.dispatch('ScrollIntoView', data);
  13690. return scrollEvent.isDefaultPrevented();
  13691. };
  13692. const fireAfterScrollIntoViewEvent = (editor, data) => {
  13693. editor.dispatch('AfterScrollIntoView', data);
  13694. };
  13695. const descend = (element, offset) => {
  13696. const children = children$1(element);
  13697. if (children.length === 0 || excludeFromDescend(element)) {
  13698. return { element, offset };
  13699. }
  13700. else if (offset < children.length && !excludeFromDescend(children[offset])) {
  13701. return { element: children[offset], offset: 0 };
  13702. }
  13703. else {
  13704. const last = children[children.length - 1];
  13705. if (excludeFromDescend(last)) {
  13706. return { element, offset };
  13707. }
  13708. else {
  13709. if (name(last) === 'img') {
  13710. return { element: last, offset: 1 };
  13711. }
  13712. else if (isText$c(last)) {
  13713. return { element: last, offset: get$4(last).length };
  13714. }
  13715. else {
  13716. return { element: last, offset: children$1(last).length };
  13717. }
  13718. }
  13719. }
  13720. };
  13721. const markerInfo = (element, cleanupFun) => {
  13722. const pos = absolute(element);
  13723. const height = get$6(element);
  13724. return {
  13725. element,
  13726. bottom: pos.top + height,
  13727. height,
  13728. pos,
  13729. cleanup: cleanupFun
  13730. };
  13731. };
  13732. const createMarker$1 = (element, offset) => {
  13733. const startPoint = descend(element, offset);
  13734. const span = SugarElement.fromHtml('<span data-mce-bogus="all" style="display: inline-block;">' + ZWSP$1 + '</span>');
  13735. before$4(startPoint.element, span);
  13736. return markerInfo(span, () => remove$8(span));
  13737. };
  13738. const elementMarker = (element) => markerInfo(SugarElement.fromDom(element), noop);
  13739. const withMarker = (editor, f, rng, alignToTop) => {
  13740. preserveWith(editor, (_s, _e) => applyWithMarker(editor, f, rng, alignToTop), rng);
  13741. };
  13742. const withScrollEvents = (editor, doc, f, marker, alignToTop) => {
  13743. const data = { elm: marker.element.dom, alignToTop };
  13744. if (fireScrollIntoViewEvent(editor, data)) {
  13745. return;
  13746. }
  13747. const scrollTop = get$5(doc).top;
  13748. f(editor, doc, scrollTop, marker, alignToTop);
  13749. fireAfterScrollIntoViewEvent(editor, data);
  13750. };
  13751. const applyWithMarker = (editor, f, rng, alignToTop) => {
  13752. const body = SugarElement.fromDom(editor.getBody());
  13753. const doc = SugarElement.fromDom(editor.getDoc());
  13754. reflow(body);
  13755. const marker = createMarker$1(SugarElement.fromDom(rng.startContainer), rng.startOffset);
  13756. withScrollEvents(editor, doc, f, marker, alignToTop);
  13757. marker.cleanup();
  13758. };
  13759. const withElement = (editor, element, f, alignToTop) => {
  13760. const doc = SugarElement.fromDom(editor.getDoc());
  13761. withScrollEvents(editor, doc, f, elementMarker(element), alignToTop);
  13762. };
  13763. const preserveWith = (editor, f, rng) => {
  13764. const startElement = rng.startContainer;
  13765. const startOffset = rng.startOffset;
  13766. const endElement = rng.endContainer;
  13767. const endOffset = rng.endOffset;
  13768. f(SugarElement.fromDom(startElement), SugarElement.fromDom(endElement));
  13769. const newRng = editor.dom.createRng();
  13770. newRng.setStart(startElement, startOffset);
  13771. newRng.setEnd(endElement, endOffset);
  13772. editor.selection.setRng(rng);
  13773. };
  13774. const scrollToMarker = (editor, marker, viewHeight, alignToTop, doc) => {
  13775. const pos = marker.pos;
  13776. // with default font size 16px font and 1.3 line height (~21px per line),
  13777. // adding roughly 50% extra space gives about 30px of breathing room ensuring comfortable spacing.
  13778. const scrollMargin = 30;
  13779. if (alignToTop) {
  13780. // When scrolling to top, add margin to the top position
  13781. to(pos.left, Math.max(0, pos.top - scrollMargin), doc);
  13782. }
  13783. else {
  13784. // The position we want to scroll to is the...
  13785. // (absolute position of the marker, minus the view height) plus (the height of the marker)
  13786. // When scrolling to bottom, add margin to ensure content isn't at the very bottom
  13787. const y = (pos.top - viewHeight) + marker.height + scrollMargin;
  13788. to(-editor.getBody().getBoundingClientRect().left, y, doc);
  13789. }
  13790. };
  13791. const intoWindowIfNeeded = (editor, doc, scrollTop, viewHeight, marker, alignToTop) => {
  13792. const viewportBottom = viewHeight + scrollTop;
  13793. const markerTop = marker.pos.top;
  13794. const markerBottom = marker.bottom;
  13795. const largerThanViewport = markerBottom - markerTop >= viewHeight;
  13796. // above the screen, scroll to top by default
  13797. if (markerTop < scrollTop) {
  13798. scrollToMarker(editor, marker, viewHeight, alignToTop !== false, doc);
  13799. // completely below the screen. Default scroll to the top if element height is larger
  13800. // than the viewport, otherwise default to scrolling to the bottom
  13801. }
  13802. else if (markerTop > viewportBottom) {
  13803. const align = largerThanViewport ? alignToTop !== false : alignToTop === true;
  13804. scrollToMarker(editor, marker, viewHeight, align, doc);
  13805. // partially below the bottom, only scroll if element height is less than viewport
  13806. }
  13807. else if (markerBottom > viewportBottom && !largerThanViewport) {
  13808. scrollToMarker(editor, marker, viewHeight, alignToTop === true, doc);
  13809. }
  13810. };
  13811. const intoWindow = (editor, doc, scrollTop, marker, alignToTop) => {
  13812. const viewHeight = defaultView(doc).dom.innerHeight;
  13813. intoWindowIfNeeded(editor, doc, scrollTop, viewHeight, marker, alignToTop);
  13814. };
  13815. const intoFrame = (editor, doc, scrollTop, marker, alignToTop) => {
  13816. const frameViewHeight = defaultView(doc).dom.innerHeight; // height of iframe container
  13817. // If the position is outside the iframe viewport, scroll to it
  13818. intoWindowIfNeeded(editor, doc, scrollTop, frameViewHeight, marker, alignToTop);
  13819. // If the new position is outside the window viewport, scroll to it
  13820. const op = find(marker.element);
  13821. const viewportBounds = getBounds(window);
  13822. if (op.top < viewportBounds.y) {
  13823. intoView(marker.element, alignToTop !== false);
  13824. }
  13825. else if (op.top > viewportBounds.bottom) {
  13826. intoView(marker.element, alignToTop === true);
  13827. }
  13828. };
  13829. const rangeIntoWindow = (editor, rng, alignToTop) => withMarker(editor, intoWindow, rng, alignToTop);
  13830. const elementIntoWindow = (editor, element, alignToTop) => withElement(editor, element, intoWindow, alignToTop);
  13831. const rangeIntoFrame = (editor, rng, alignToTop) => withMarker(editor, intoFrame, rng, alignToTop);
  13832. const elementIntoFrame = (editor, element, alignToTop) => withElement(editor, element, intoFrame, alignToTop);
  13833. const scrollElementIntoView = (editor, element, alignToTop) => {
  13834. const scroller = editor.inline ? elementIntoWindow : elementIntoFrame;
  13835. scroller(editor, element, alignToTop);
  13836. };
  13837. // This method is made to deal with the user pressing enter, it is not useful
  13838. // if we want for example scroll in content after a paste event.
  13839. const scrollRangeIntoView = (editor, rng, alignToTop) => {
  13840. const scroller = editor.inline ? rangeIntoWindow : rangeIntoFrame;
  13841. scroller(editor, rng, alignToTop);
  13842. };
  13843. const isEditableRange = (dom, rng) => {
  13844. if (rng.collapsed) {
  13845. return dom.isEditable(rng.startContainer);
  13846. }
  13847. else {
  13848. return dom.isEditable(rng.startContainer) && dom.isEditable(rng.endContainer);
  13849. }
  13850. };
  13851. const getEndpointElement = (root, rng, start, real, resolve) => {
  13852. const container = start ? rng.startContainer : rng.endContainer;
  13853. const offset = start ? rng.startOffset : rng.endOffset;
  13854. return Optional.from(container)
  13855. .map(SugarElement.fromDom)
  13856. .map((elm) => !real || !rng.collapsed ? child$1(elm, resolve(elm, offset)).getOr(elm) : elm)
  13857. .bind((elm) => isElement$8(elm) ? Optional.some(elm) : parent(elm).filter(isElement$8))
  13858. .map((elm) => elm.dom)
  13859. .getOr(root);
  13860. };
  13861. const getStart = (root, rng, real = false) => getEndpointElement(root, rng, true, real, (elm, offset) => Math.min(childNodesCount(elm), offset));
  13862. const getEnd = (root, rng, real = false) => getEndpointElement(root, rng, false, real, (elm, offset) => offset > 0 ? offset - 1 : offset);
  13863. const skipEmptyTextNodes = (node, forwards) => {
  13864. const orig = node;
  13865. while (node && isText$b(node) && node.length === 0) {
  13866. node = forwards ? node.nextSibling : node.previousSibling;
  13867. }
  13868. return node || orig;
  13869. };
  13870. const getNode = (root, rng) => {
  13871. // Range maybe lost after the editor is made visible again
  13872. if (!rng) {
  13873. return root;
  13874. }
  13875. let startContainer = rng.startContainer;
  13876. let endContainer = rng.endContainer;
  13877. const startOffset = rng.startOffset;
  13878. const endOffset = rng.endOffset;
  13879. let node = rng.commonAncestorContainer;
  13880. // Handle selection a image or other control like element such as anchors
  13881. if (!rng.collapsed) {
  13882. if (startContainer === endContainer) {
  13883. if (endOffset - startOffset < 2) {
  13884. if (startContainer.hasChildNodes()) {
  13885. node = startContainer.childNodes[startOffset];
  13886. }
  13887. }
  13888. }
  13889. // If the anchor node is a element instead of a text node then return this element
  13890. // if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1)
  13891. // return sel.anchorNode.childNodes[sel.anchorOffset];
  13892. // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent.
  13893. // This happens when you double click an underlined word in FireFox.
  13894. if (isText$b(startContainer) && isText$b(endContainer)) {
  13895. if (startContainer.length === startOffset) {
  13896. startContainer = skipEmptyTextNodes(startContainer.nextSibling, true);
  13897. }
  13898. else {
  13899. startContainer = startContainer.parentNode;
  13900. }
  13901. if (endOffset === 0) {
  13902. endContainer = skipEmptyTextNodes(endContainer.previousSibling, false);
  13903. }
  13904. else {
  13905. endContainer = endContainer.parentNode;
  13906. }
  13907. if (startContainer && startContainer === endContainer) {
  13908. node = startContainer;
  13909. }
  13910. }
  13911. }
  13912. const elm = isText$b(node) ? node.parentNode : node;
  13913. return isHTMLElement(elm) ? elm : root;
  13914. };
  13915. const getSelectedBlocks = (dom, rng, startElm, endElm) => {
  13916. const selectedBlocks = [];
  13917. const root = dom.getRoot();
  13918. const start = dom.getParent(startElm || getStart(root, rng, rng.collapsed), dom.isBlock);
  13919. const end = dom.getParent(endElm || getEnd(root, rng, rng.collapsed), dom.isBlock);
  13920. if (start && start !== root) {
  13921. selectedBlocks.push(start);
  13922. }
  13923. if (start && end && start !== end) {
  13924. let node;
  13925. const walker = new DomTreeWalker(start, root);
  13926. while ((node = walker.next()) && node !== end) {
  13927. if (dom.isBlock(node)) {
  13928. selectedBlocks.push(node);
  13929. }
  13930. }
  13931. }
  13932. if (end && start !== end && end !== root) {
  13933. selectedBlocks.push(end);
  13934. }
  13935. return selectedBlocks;
  13936. };
  13937. const select = (dom, node, content) => Optional.from(node).bind((node) => Optional.from(node.parentNode).map((parent) => {
  13938. const idx = dom.nodeIndex(node);
  13939. const rng = dom.createRng();
  13940. rng.setStart(parent, idx);
  13941. rng.setEnd(parent, idx + 1);
  13942. // Find first/last text node or BR element
  13943. if (content) {
  13944. moveEndPoint(dom, rng, node, true);
  13945. moveEndPoint(dom, rng, node, false);
  13946. }
  13947. return rng;
  13948. }));
  13949. const processRanges = (editor, ranges) => map$3(ranges, (range) => {
  13950. const evt = editor.dispatch('GetSelectionRange', { range });
  13951. return evt.range !== range ? evt.range : range;
  13952. });
  13953. const typeLookup = {
  13954. '#text': 3,
  13955. '#comment': 8,
  13956. '#cdata': 4,
  13957. '#pi': 7,
  13958. '#doctype': 10,
  13959. '#document-fragment': 11
  13960. };
  13961. // Walks the tree left/right
  13962. const walk$2 = (node, root, prev) => {
  13963. const startName = prev ? 'lastChild' : 'firstChild';
  13964. const siblingName = prev ? 'prev' : 'next';
  13965. // Walk into nodes if it has a start
  13966. if (node[startName]) {
  13967. return node[startName];
  13968. }
  13969. // Return the sibling if it has one
  13970. if (node !== root) {
  13971. let sibling = node[siblingName];
  13972. if (sibling) {
  13973. return sibling;
  13974. }
  13975. // Walk up the parents to look for siblings
  13976. for (let parent = node.parent; parent && parent !== root; parent = parent.parent) {
  13977. sibling = parent[siblingName];
  13978. if (sibling) {
  13979. return sibling;
  13980. }
  13981. }
  13982. }
  13983. return undefined;
  13984. };
  13985. const isEmptyTextNode = (node) => {
  13986. var _a;
  13987. const text = (_a = node.value) !== null && _a !== void 0 ? _a : '';
  13988. // Non whitespace content
  13989. if (!isWhitespaceText(text)) {
  13990. return false;
  13991. }
  13992. // Parent is not a span and only spaces or is a span but has styles
  13993. const parentNode = node.parent;
  13994. if (parentNode && (parentNode.name !== 'span' || parentNode.attr('style')) && /^[ ]+$/.test(text)) {
  13995. return false;
  13996. }
  13997. return true;
  13998. };
  13999. // Check if node contains data-bookmark attribute, name attribute, id attribute or is a named anchor
  14000. const isNonEmptyElement = (node) => {
  14001. const isNamedAnchor = node.name === 'a' && !node.attr('href') && node.attr('id');
  14002. return (node.attr('name') || (node.attr('id') && !node.firstChild) || node.attr('data-mce-bookmark') || isNamedAnchor);
  14003. };
  14004. /**
  14005. * This class is a minimalistic implementation of a DOM like node used by the DomParser class.
  14006. *
  14007. * @class tinymce.html.Node
  14008. * @version 3.4
  14009. * @example
  14010. * const node = new tinymce.html.Node('strong', 1);
  14011. * someRoot.append(node);
  14012. */
  14013. class AstNode {
  14014. /**
  14015. * Creates a node of a specific type.
  14016. *
  14017. * @static
  14018. * @method create
  14019. * @param {String} name Name of the node type to create for example "b" or "#text".
  14020. * @param {Object} attrs Name/value collection of attributes that will be applied to elements.
  14021. */
  14022. static create(name, attrs) {
  14023. // Create node
  14024. const node = new AstNode(name, typeLookup[name] || 1);
  14025. // Add attributes if needed
  14026. if (attrs) {
  14027. each$d(attrs, (value, attrName) => {
  14028. node.attr(attrName, value);
  14029. });
  14030. }
  14031. return node;
  14032. }
  14033. /**
  14034. * Constructs a new Node instance.
  14035. *
  14036. * @constructor
  14037. * @method Node
  14038. * @param {String} name Name of the node type.
  14039. * @param {Number} type Numeric type representing the node.
  14040. */
  14041. constructor(name, type) {
  14042. this.name = name;
  14043. this.type = type;
  14044. if (type === 1) {
  14045. this.attributes = [];
  14046. this.attributes.map = {}; // Should be considered internal
  14047. }
  14048. }
  14049. /**
  14050. * Replaces the current node with the specified one.
  14051. *
  14052. * @method replace
  14053. * @param {tinymce.html.Node} node Node to replace the current node with.
  14054. * @return {tinymce.html.Node} The old node that got replaced.
  14055. * @example
  14056. * someNode.replace(someNewNode);
  14057. */
  14058. replace(node) {
  14059. const self = this;
  14060. if (node.parent) {
  14061. node.remove();
  14062. }
  14063. self.insert(node, self);
  14064. self.remove();
  14065. return self;
  14066. }
  14067. attr(name, value) {
  14068. const self = this;
  14069. if (!isString(name)) {
  14070. if (isNonNullable(name)) {
  14071. each$d(name, (value, key) => {
  14072. self.attr(key, value);
  14073. });
  14074. }
  14075. return self;
  14076. }
  14077. const attrs = self.attributes;
  14078. if (attrs) {
  14079. if (value !== undefined) {
  14080. // Remove attribute
  14081. if (value === null) {
  14082. if (name in attrs.map) {
  14083. delete attrs.map[name];
  14084. let i = attrs.length;
  14085. while (i--) {
  14086. if (attrs[i].name === name) {
  14087. attrs.splice(i, 1);
  14088. return self;
  14089. }
  14090. }
  14091. }
  14092. return self;
  14093. }
  14094. // Set attribute
  14095. if (name in attrs.map) {
  14096. // Set attribute
  14097. let i = attrs.length;
  14098. while (i--) {
  14099. if (attrs[i].name === name) {
  14100. attrs[i].value = value;
  14101. break;
  14102. }
  14103. }
  14104. }
  14105. else {
  14106. attrs.push({ name, value });
  14107. }
  14108. attrs.map[name] = value;
  14109. return self;
  14110. }
  14111. return attrs.map[name];
  14112. }
  14113. return undefined;
  14114. }
  14115. /**
  14116. * Does a shallow clones the node into a new node. It will also exclude id attributes since
  14117. * there should only be one id per document.
  14118. *
  14119. * @method clone
  14120. * @return {tinymce.html.Node} New copy of the original node.
  14121. * @example
  14122. * const clonedNode = node.clone();
  14123. */
  14124. clone() {
  14125. const self = this;
  14126. const clone = new AstNode(self.name, self.type);
  14127. const selfAttrs = self.attributes;
  14128. // Clone element attributes
  14129. if (selfAttrs) {
  14130. const cloneAttrs = [];
  14131. cloneAttrs.map = {};
  14132. for (let i = 0, l = selfAttrs.length; i < l; i++) {
  14133. const selfAttr = selfAttrs[i];
  14134. // Clone everything except id
  14135. if (selfAttr.name !== 'id') {
  14136. cloneAttrs[cloneAttrs.length] = { name: selfAttr.name, value: selfAttr.value };
  14137. cloneAttrs.map[selfAttr.name] = selfAttr.value;
  14138. }
  14139. }
  14140. clone.attributes = cloneAttrs;
  14141. }
  14142. clone.value = self.value;
  14143. return clone;
  14144. }
  14145. /**
  14146. * Wraps the node in in another node.
  14147. *
  14148. * @method wrap
  14149. * @example
  14150. * node.wrap(wrapperNode);
  14151. */
  14152. wrap(wrapper) {
  14153. const self = this;
  14154. if (self.parent) {
  14155. self.parent.insert(wrapper, self);
  14156. wrapper.append(self);
  14157. }
  14158. return self;
  14159. }
  14160. /**
  14161. * Unwraps the node in other words it removes the node but keeps the children.
  14162. *
  14163. * @method unwrap
  14164. * @example
  14165. * node.unwrap();
  14166. */
  14167. unwrap() {
  14168. const self = this;
  14169. for (let node = self.firstChild; node;) {
  14170. const next = node.next;
  14171. self.insert(node, self, true);
  14172. node = next;
  14173. }
  14174. self.remove();
  14175. }
  14176. /**
  14177. * Removes the node from it's parent.
  14178. *
  14179. * @method remove
  14180. * @return {tinymce.html.Node} Current node that got removed.
  14181. * @example
  14182. * node.remove();
  14183. */
  14184. remove() {
  14185. const self = this, parent = self.parent, next = self.next, prev = self.prev;
  14186. if (parent) {
  14187. if (parent.firstChild === self) {
  14188. parent.firstChild = next;
  14189. if (next) {
  14190. next.prev = null;
  14191. }
  14192. }
  14193. else if (prev) {
  14194. prev.next = next;
  14195. }
  14196. if (parent.lastChild === self) {
  14197. parent.lastChild = prev;
  14198. if (prev) {
  14199. prev.next = null;
  14200. }
  14201. }
  14202. else if (next) {
  14203. next.prev = prev;
  14204. }
  14205. self.parent = self.next = self.prev = null;
  14206. }
  14207. return self;
  14208. }
  14209. /**
  14210. * Appends a new node as a child of the current node.
  14211. *
  14212. * @method append
  14213. * @param {tinymce.html.Node} node Node to append as a child of the current one.
  14214. * @return {tinymce.html.Node} The node that got appended.
  14215. * @example
  14216. * node.append(someNode);
  14217. */
  14218. append(node) {
  14219. const self = this;
  14220. if (node.parent) {
  14221. node.remove();
  14222. }
  14223. const last = self.lastChild;
  14224. if (last) {
  14225. last.next = node;
  14226. node.prev = last;
  14227. self.lastChild = node;
  14228. }
  14229. else {
  14230. self.lastChild = self.firstChild = node;
  14231. }
  14232. node.parent = self;
  14233. return node;
  14234. }
  14235. /**
  14236. * Inserts a node at a specific position as a child of this node.
  14237. *
  14238. * @method insert
  14239. * @param {tinymce.html.Node} node Node to insert as a child of this node.
  14240. * @param {tinymce.html.Node} refNode Reference node to set node before/after.
  14241. * @param {Boolean} before Optional state to insert the node before the reference node.
  14242. * @return {tinymce.html.Node} The node that got inserted.
  14243. * @example
  14244. * parentNode.insert(newChildNode, oldChildNode);
  14245. */
  14246. insert(node, refNode, before) {
  14247. if (node.parent) {
  14248. node.remove();
  14249. }
  14250. const parent = refNode.parent || this;
  14251. if (before) {
  14252. if (refNode === parent.firstChild) {
  14253. parent.firstChild = node;
  14254. }
  14255. else if (refNode.prev) {
  14256. refNode.prev.next = node;
  14257. }
  14258. node.prev = refNode.prev;
  14259. node.next = refNode;
  14260. refNode.prev = node;
  14261. }
  14262. else {
  14263. if (refNode === parent.lastChild) {
  14264. parent.lastChild = node;
  14265. }
  14266. else if (refNode.next) {
  14267. refNode.next.prev = node;
  14268. }
  14269. node.next = refNode.next;
  14270. node.prev = refNode;
  14271. refNode.next = node;
  14272. }
  14273. node.parent = parent;
  14274. return node;
  14275. }
  14276. /**
  14277. * Get all descendants by name.
  14278. *
  14279. * @method getAll
  14280. * @param {String} name Name of the descendant nodes to collect.
  14281. * @return {Array} Array with descendant nodes matching the specified name.
  14282. */
  14283. getAll(name) {
  14284. const self = this;
  14285. const collection = [];
  14286. for (let node = self.firstChild; node; node = walk$2(node, self)) {
  14287. if (node.name === name) {
  14288. collection.push(node);
  14289. }
  14290. }
  14291. return collection;
  14292. }
  14293. /**
  14294. * Get all children of this node.
  14295. *
  14296. * @method children
  14297. * @return {Array} Array containing child nodes.
  14298. */
  14299. children() {
  14300. const self = this;
  14301. const collection = [];
  14302. for (let node = self.firstChild; node; node = node.next) {
  14303. collection.push(node);
  14304. }
  14305. return collection;
  14306. }
  14307. /**
  14308. * Removes all children of the current node.
  14309. *
  14310. * @method empty
  14311. * @return {tinymce.html.Node} The current node that got cleared.
  14312. */
  14313. empty() {
  14314. const self = this;
  14315. // Remove all children
  14316. if (self.firstChild) {
  14317. const nodes = [];
  14318. // Collect the children
  14319. for (let node = self.firstChild; node; node = walk$2(node, self)) {
  14320. nodes.push(node);
  14321. }
  14322. // Remove the children
  14323. let i = nodes.length;
  14324. while (i--) {
  14325. const node = nodes[i];
  14326. node.parent = node.firstChild = node.lastChild = node.next = node.prev = null;
  14327. }
  14328. }
  14329. self.firstChild = self.lastChild = null;
  14330. return self;
  14331. }
  14332. /**
  14333. * Returns true/false if the node is to be considered empty or not.
  14334. *
  14335. * @method isEmpty
  14336. * @param {Object} elements Name/value object with elements that are automatically treated as non empty elements.
  14337. * @param {Object} whitespace Name/value object with elements that are automatically treated whitespace preservables.
  14338. * @param {Function} predicate Optional predicate that gets called after the other rules determine that the node is empty. Should return true if the node is a content node.
  14339. * @return {Boolean} true/false if the node is empty or not.
  14340. * @example
  14341. * node.isEmpty({ img: true });
  14342. */
  14343. isEmpty(elements, whitespace = {}, predicate) {
  14344. var _a;
  14345. const self = this;
  14346. let node = self.firstChild;
  14347. if (isNonEmptyElement(self)) {
  14348. return false;
  14349. }
  14350. if (node) {
  14351. do {
  14352. if (node.type === 1) {
  14353. // Ignore bogus elements
  14354. if (node.attr('data-mce-bogus')) {
  14355. continue;
  14356. }
  14357. // Keep empty elements like <img />
  14358. if (elements[node.name]) {
  14359. return false;
  14360. }
  14361. if (isNonEmptyElement(node)) {
  14362. return false;
  14363. }
  14364. }
  14365. // Keep comments
  14366. if (node.type === 8) {
  14367. return false;
  14368. }
  14369. // Keep non whitespace text nodes
  14370. if (node.type === 3 && !isEmptyTextNode(node)) {
  14371. return false;
  14372. }
  14373. // Keep whitespace preserve elements
  14374. if (node.type === 3 && node.parent && whitespace[node.parent.name] && isWhitespaceText((_a = node.value) !== null && _a !== void 0 ? _a : '')) {
  14375. return false;
  14376. }
  14377. // Predicate tells that the node is contents
  14378. if (predicate && predicate(node)) {
  14379. return false;
  14380. }
  14381. } while ((node = walk$2(node, self)));
  14382. }
  14383. return true;
  14384. }
  14385. /**
  14386. * Walks to the next or previous node and returns that node or null if it wasn't found.
  14387. *
  14388. * @method walk
  14389. * @param {Boolean} prev Optional previous node state defaults to false.
  14390. * @return {tinymce.html.Node} Node that is next to or previous of the current node.
  14391. */
  14392. walk(prev) {
  14393. return walk$2(this, null, prev);
  14394. }
  14395. }
  14396. // TINY-10305: Map over array for faster lookup.
  14397. const unescapedTextParents = Tools.makeMap('NOSCRIPT STYLE SCRIPT XMP IFRAME NOEMBED NOFRAMES PLAINTEXT', ' ');
  14398. const containsZwsp = (node) => isString(node.nodeValue) && node.nodeValue.includes(ZWSP$1);
  14399. const getTemporaryNodeSelector = (tempAttrs) => `${tempAttrs.length === 0 ? '' : `${map$3(tempAttrs, (attr) => `[${attr}]`).join(',')},`}[data-mce-bogus="all"]`;
  14400. const getTemporaryNodes = (tempAttrs, body) => body.querySelectorAll(getTemporaryNodeSelector(tempAttrs));
  14401. const createZwspCommentWalker = (body) => document.createTreeWalker(body, NodeFilter.SHOW_COMMENT, (node) => containsZwsp(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP);
  14402. const createUnescapedZwspTextWalker = (body) => document.createTreeWalker(body, NodeFilter.SHOW_TEXT, (node) => {
  14403. if (containsZwsp(node)) {
  14404. const parent = node.parentNode;
  14405. return parent && has$2(unescapedTextParents, parent.nodeName) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
  14406. }
  14407. else {
  14408. return NodeFilter.FILTER_SKIP;
  14409. }
  14410. });
  14411. const hasZwspComment = (body) => createZwspCommentWalker(body).nextNode() !== null;
  14412. const hasUnescapedZwspText = (body) => createUnescapedZwspTextWalker(body).nextNode() !== null;
  14413. const hasTemporaryNode = (tempAttrs, body) => body.querySelector(getTemporaryNodeSelector(tempAttrs)) !== null;
  14414. const trimTemporaryNodes = (tempAttrs, body) => {
  14415. each$e(getTemporaryNodes(tempAttrs, body), (elm) => {
  14416. const element = SugarElement.fromDom(elm);
  14417. if (get$9(element, 'data-mce-bogus') === 'all') {
  14418. remove$8(element);
  14419. }
  14420. else {
  14421. each$e(tempAttrs, (attr) => {
  14422. if (has$1(element, attr)) {
  14423. remove$9(element, attr);
  14424. }
  14425. });
  14426. }
  14427. });
  14428. };
  14429. const emptyAllNodeValuesInWalker = (walker) => {
  14430. let curr = walker.nextNode();
  14431. while (curr !== null) {
  14432. curr.nodeValue = null;
  14433. curr = walker.nextNode();
  14434. }
  14435. };
  14436. const emptyZwspComments = compose(emptyAllNodeValuesInWalker, createZwspCommentWalker);
  14437. const emptyUnescapedZwspTexts = compose(emptyAllNodeValuesInWalker, createUnescapedZwspTextWalker);
  14438. const trim$1 = (body, tempAttrs) => {
  14439. const conditionalTrims = [
  14440. {
  14441. condition: curry(hasTemporaryNode, tempAttrs),
  14442. action: curry(trimTemporaryNodes, tempAttrs)
  14443. },
  14444. {
  14445. condition: hasZwspComment,
  14446. action: emptyZwspComments
  14447. },
  14448. {
  14449. condition: hasUnescapedZwspText,
  14450. action: emptyUnescapedZwspTexts
  14451. }
  14452. ];
  14453. let trimmed = body;
  14454. let cloned = false;
  14455. each$e(conditionalTrims, ({ condition, action }) => {
  14456. if (condition(trimmed)) {
  14457. if (!cloned) {
  14458. trimmed = body.cloneNode(true);
  14459. cloned = true;
  14460. }
  14461. action(trimmed);
  14462. }
  14463. });
  14464. return trimmed;
  14465. };
  14466. const cleanupBogusElements = (parent) => {
  14467. const bogusElements = descendants(parent, '[data-mce-bogus]');
  14468. each$e(bogusElements, (elem) => {
  14469. const bogusValue = get$9(elem, 'data-mce-bogus');
  14470. if (bogusValue === 'all') {
  14471. remove$8(elem);
  14472. }
  14473. else if (isBr$6(elem)) {
  14474. // Need to keep bogus padding brs represented as a zero-width space so that they aren't collapsed by the browser
  14475. before$4(elem, SugarElement.fromText(zeroWidth));
  14476. remove$8(elem);
  14477. }
  14478. else {
  14479. unwrap(elem);
  14480. }
  14481. });
  14482. };
  14483. const cleanupInputNames = (parent) => {
  14484. const inputs = descendants(parent, 'input');
  14485. each$e(inputs, (input) => {
  14486. remove$9(input, 'name');
  14487. });
  14488. };
  14489. const trimEmptyContents = (editor, html) => {
  14490. const blockName = getForcedRootBlock(editor);
  14491. const emptyRegExp = new RegExp(`^(<${blockName}[^>]*>(&nbsp;|&#160;|\\s|\u00a0|<br \\/>|)<\\/${blockName}>[\r\n]*|<br \\/>[\r\n]*)$`);
  14492. return html.replace(emptyRegExp, '');
  14493. };
  14494. const getPlainTextContent = (editor, body) => {
  14495. const doc = editor.getDoc();
  14496. const dos = getRootNode(SugarElement.fromDom(editor.getBody()));
  14497. const offscreenDiv = SugarElement.fromTag('div', doc);
  14498. set$4(offscreenDiv, 'data-mce-bogus', 'all');
  14499. setAll(offscreenDiv, {
  14500. position: 'fixed',
  14501. left: '-9999999px',
  14502. top: '0'
  14503. });
  14504. set$3(offscreenDiv, body.innerHTML);
  14505. cleanupBogusElements(offscreenDiv);
  14506. cleanupInputNames(offscreenDiv);
  14507. // Append the wrapper element so that the browser will evaluate styles when getting the `innerText`
  14508. const root = getContentContainer(dos);
  14509. append$1(root, offscreenDiv);
  14510. const content = trim$2(offscreenDiv.dom.innerText);
  14511. remove$8(offscreenDiv);
  14512. return content;
  14513. };
  14514. const getContentFromBody = (editor, args, body) => {
  14515. let content;
  14516. if (args.format === 'raw') {
  14517. content = Tools.trim(trim$2(trim$1(body, editor.serializer.getTempAttrs()).innerHTML));
  14518. }
  14519. else if (args.format === 'text') {
  14520. content = getPlainTextContent(editor, body);
  14521. }
  14522. else if (args.format === 'tree') {
  14523. content = editor.serializer.serialize(body, args);
  14524. }
  14525. else {
  14526. content = trimEmptyContents(editor, editor.serializer.serialize(body, args));
  14527. }
  14528. // Trim if not using a whitespace preserve format/element
  14529. const shouldTrim = args.format !== 'text' && !isWsPreserveElement(SugarElement.fromDom(body));
  14530. return shouldTrim && isString(content) ? Tools.trim(content) : content;
  14531. };
  14532. const getContentInternal = (editor, args) => Optional.from(editor.getBody())
  14533. .fold(constant(args.format === 'tree' ? new AstNode('body', 11) : ''), (body) => getContentFromBody(editor, args, body));
  14534. /**
  14535. * This class is used to write HTML tags out it can be used with the Serializer.
  14536. *
  14537. * @class tinymce.html.Writer
  14538. * @version 3.4
  14539. * @example
  14540. * const writer = tinymce.html.Writer({ indent: true });
  14541. * writer.start('node', { attr: 'value' });
  14542. * writer.end('node');
  14543. * console.log(writer.getContent());
  14544. */
  14545. const makeMap$1 = Tools.makeMap;
  14546. const Writer = (settings) => {
  14547. const html = [];
  14548. settings = settings || {};
  14549. const indent = settings.indent;
  14550. const indentBefore = makeMap$1(settings.indent_before || '');
  14551. const indentAfter = makeMap$1(settings.indent_after || '');
  14552. const encode = Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities);
  14553. const htmlOutput = settings.element_format !== 'xhtml';
  14554. return {
  14555. /**
  14556. * Writes a start element, such as `<p id="a">`.
  14557. *
  14558. * @method start
  14559. * @param {String} name Name of the element.
  14560. * @param {Array} attrs Optional array of objects containing an attribute name and value, or undefined if the element has no attributes.
  14561. * @param {Boolean} empty Optional empty state if the tag should serialize as a void element. For example: `<img />`
  14562. */
  14563. start: (name, attrs, empty) => {
  14564. if (indent && indentBefore[name] && html.length > 0) {
  14565. const value = html[html.length - 1];
  14566. if (value.length > 0 && value !== '\n') {
  14567. html.push('\n');
  14568. }
  14569. }
  14570. html.push('<', name);
  14571. if (attrs) {
  14572. for (let i = 0, l = attrs.length; i < l; i++) {
  14573. const attr = attrs[i];
  14574. html.push(' ', attr.name, '="', encode(attr.value, true), '"');
  14575. }
  14576. }
  14577. if (!empty || htmlOutput) {
  14578. html[html.length] = '>';
  14579. }
  14580. else {
  14581. html[html.length] = ' />';
  14582. }
  14583. if (empty && indent && indentAfter[name] && html.length > 0) {
  14584. const value = html[html.length - 1];
  14585. if (value.length > 0 && value !== '\n') {
  14586. html.push('\n');
  14587. }
  14588. }
  14589. },
  14590. /**
  14591. * Writes an end element, such as `</p>`.
  14592. *
  14593. * @method end
  14594. * @param {String} name Name of the element.
  14595. */
  14596. end: (name) => {
  14597. let value;
  14598. /* if (indent && indentBefore[name] && html.length > 0) {
  14599. value = html[html.length - 1];
  14600. if (value.length > 0 && value !== '\n')
  14601. html.push('\n');
  14602. }*/
  14603. html.push('</', name, '>');
  14604. if (indent && indentAfter[name] && html.length > 0) {
  14605. value = html[html.length - 1];
  14606. if (value.length > 0 && value !== '\n') {
  14607. html.push('\n');
  14608. }
  14609. }
  14610. },
  14611. /**
  14612. * Writes a text node.
  14613. *
  14614. * @method text
  14615. * @param {String} text String to write out.
  14616. * @param {Boolean} raw Optional raw state. If true, the contents won't get encoded.
  14617. */
  14618. text: (text, raw) => {
  14619. if (text.length > 0) {
  14620. html[html.length] = raw ? text : encode(text);
  14621. }
  14622. },
  14623. /**
  14624. * Writes a cdata node, such as `<![CDATA[data]]>`.
  14625. *
  14626. * @method cdata
  14627. * @param {String} text String to write out inside the cdata.
  14628. */
  14629. cdata: (text) => {
  14630. html.push('<![CDATA[', text, ']]>');
  14631. },
  14632. /**
  14633. * Writes a comment node, such as `<!-- Comment -->`.
  14634. *
  14635. * @method comment
  14636. * @param {String} text String to write out inside the comment.
  14637. */
  14638. comment: (text) => {
  14639. html.push('<!--', text, '-->');
  14640. },
  14641. /**
  14642. * Writes a processing instruction (PI) node, such as `<?xml attr="value" ?>`.
  14643. *
  14644. * @method pi
  14645. * @param {String} name Name of the pi.
  14646. * @param {String} text String to write out inside the pi.
  14647. */
  14648. pi: (name, text) => {
  14649. if (text) {
  14650. html.push('<?', name, ' ', encode(text), '?>');
  14651. }
  14652. else {
  14653. html.push('<?', name, '?>');
  14654. }
  14655. if (indent) {
  14656. html.push('\n');
  14657. }
  14658. },
  14659. /**
  14660. * Writes a doctype node, such as `<!DOCTYPE data>`.
  14661. *
  14662. * @method doctype
  14663. * @param {String} text String to write out inside the doctype.
  14664. */
  14665. doctype: (text) => {
  14666. html.push('<!DOCTYPE', text, '>', indent ? '\n' : '');
  14667. },
  14668. /**
  14669. * Resets the internal buffer. For example, if one wants to reuse the writer.
  14670. *
  14671. * @method reset
  14672. */
  14673. reset: () => {
  14674. html.length = 0;
  14675. },
  14676. /**
  14677. * Returns the contents that was serialized.
  14678. *
  14679. * @method getContent
  14680. * @return {String} HTML contents that got written down.
  14681. */
  14682. getContent: () => {
  14683. return html.join('').replace(/\n$/, '');
  14684. }
  14685. };
  14686. };
  14687. /**
  14688. * This class is used to serialize down the DOM tree into a string using a Writer instance.
  14689. *
  14690. * @class tinymce.html.Serializer
  14691. * @version 3.4
  14692. * @example
  14693. * tinymce.html.Serializer().serialize(tinymce.html.DomParser().parse('<p>text</p>'));
  14694. */
  14695. const HtmlSerializer = (settings = {}, schema = Schema()) => {
  14696. const writer = Writer(settings);
  14697. settings.validate = 'validate' in settings ? settings.validate : true;
  14698. /**
  14699. * Serializes the specified node into a string.
  14700. *
  14701. * @method serialize
  14702. * @param {tinymce.html.Node} node Node instance to serialize.
  14703. * @return {String} String with HTML based on the DOM tree.
  14704. * @example
  14705. * tinymce.html.Serializer().serialize(tinymce.html.DomParser().parse('<p>text</p>'));
  14706. */
  14707. const serialize = (node) => {
  14708. const validate = settings.validate;
  14709. const handlers = {
  14710. // #text
  14711. 3: (node) => {
  14712. var _a;
  14713. writer.text((_a = node.value) !== null && _a !== void 0 ? _a : '', node.raw);
  14714. },
  14715. // #comment
  14716. 8: (node) => {
  14717. var _a;
  14718. writer.comment((_a = node.value) !== null && _a !== void 0 ? _a : '');
  14719. },
  14720. // Processing instruction
  14721. 7: (node) => {
  14722. writer.pi(node.name, node.value);
  14723. },
  14724. // Doctype
  14725. 10: (node) => {
  14726. var _a;
  14727. writer.doctype((_a = node.value) !== null && _a !== void 0 ? _a : '');
  14728. },
  14729. // CDATA
  14730. 4: (node) => {
  14731. var _a;
  14732. writer.cdata((_a = node.value) !== null && _a !== void 0 ? _a : '');
  14733. },
  14734. // Document fragment
  14735. 11: (node) => {
  14736. let tempNode = node;
  14737. if ((tempNode = tempNode.firstChild)) {
  14738. do {
  14739. walk(tempNode);
  14740. } while ((tempNode = tempNode.next));
  14741. }
  14742. }
  14743. };
  14744. writer.reset();
  14745. const walk = (node) => {
  14746. var _a;
  14747. const handler = handlers[node.type];
  14748. if (!handler) {
  14749. const name = node.name;
  14750. const isEmpty = name in schema.getVoidElements();
  14751. let attrs = node.attributes;
  14752. // Sort attributes
  14753. if (validate && attrs && attrs.length > 1) {
  14754. const sortedAttrs = [];
  14755. sortedAttrs.map = {};
  14756. const elementRule = schema.getElementRule(node.name);
  14757. if (elementRule) {
  14758. for (let i = 0, l = elementRule.attributesOrder.length; i < l; i++) {
  14759. const attrName = elementRule.attributesOrder[i];
  14760. if (attrName in attrs.map) {
  14761. const attrValue = attrs.map[attrName];
  14762. sortedAttrs.map[attrName] = attrValue;
  14763. sortedAttrs.push({ name: attrName, value: attrValue });
  14764. }
  14765. }
  14766. for (let i = 0, l = attrs.length; i < l; i++) {
  14767. const attrName = attrs[i].name;
  14768. if (!(attrName in sortedAttrs.map)) {
  14769. const attrValue = attrs.map[attrName];
  14770. sortedAttrs.map[attrName] = attrValue;
  14771. sortedAttrs.push({ name: attrName, value: attrValue });
  14772. }
  14773. }
  14774. attrs = sortedAttrs;
  14775. }
  14776. }
  14777. writer.start(name, attrs, isEmpty);
  14778. if (isNonHtmlElementRootName(name)) {
  14779. if (isString(node.value)) {
  14780. writer.text(node.value, true);
  14781. }
  14782. writer.end(name);
  14783. }
  14784. else {
  14785. if (!isEmpty) {
  14786. let child = node.firstChild;
  14787. if (child) {
  14788. // Pre and textarea elements treat the first newline character as optional and will omit it. As such, if the content starts
  14789. // with a newline we need to add in an additional newline to prevent the current newline in the value being treated as optional
  14790. // See https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
  14791. if ((name === 'pre' || name === 'textarea') && child.type === 3 && ((_a = child.value) === null || _a === void 0 ? void 0 : _a[0]) === '\n') {
  14792. writer.text('\n', true);
  14793. }
  14794. do {
  14795. walk(child);
  14796. } while ((child = child.next));
  14797. }
  14798. writer.end(name);
  14799. }
  14800. }
  14801. }
  14802. else {
  14803. handler(node);
  14804. }
  14805. };
  14806. // Serialize element or text nodes and treat all other nodes as fragments
  14807. if (node.type === 1 && !settings.inner) {
  14808. walk(node);
  14809. }
  14810. else if (node.type === 3) {
  14811. handlers[3](node);
  14812. }
  14813. else {
  14814. handlers[11](node);
  14815. }
  14816. return writer.getContent();
  14817. };
  14818. return {
  14819. serialize
  14820. };
  14821. };
  14822. const nonInheritableStyles = new Set();
  14823. (() => {
  14824. // TODO: TINY-7326 Figure out what else should go in the nonInheritableStyles list
  14825. const nonInheritableStylesArr = [
  14826. 'margin', 'margin-left', 'margin-right', 'margin-top', 'margin-bottom',
  14827. 'padding', 'padding-left', 'padding-right', 'padding-top', 'padding-bottom',
  14828. 'border', 'border-width', 'border-style', 'border-color',
  14829. 'background', 'background-attachment', 'background-clip',
  14830. 'background-image', 'background-origin', 'background-position', 'background-repeat', 'background-size',
  14831. 'float', 'position', 'left', 'right', 'top', 'bottom',
  14832. 'z-index', 'display', 'transform',
  14833. 'width', 'max-width', 'min-width', 'height', 'max-height', 'min-height',
  14834. 'overflow', 'overflow-x', 'overflow-y', 'text-overflow', 'vertical-align',
  14835. 'transition', 'transition-delay', 'transition-duration', 'transition-property', 'transition-timing-function'
  14836. ];
  14837. each$e(nonInheritableStylesArr, (style) => {
  14838. nonInheritableStyles.add(style);
  14839. });
  14840. })();
  14841. const conditionalNonInheritableStyles = new Set();
  14842. (() => {
  14843. // These styles are only noninheritable when applied to an element with a noninheritable style
  14844. // For example, background-color is visible on an element with padding, even when children have background-color;
  14845. // however, when the element has no padding, background-color is either visible or overridden by children
  14846. const conditionalNonInheritableStylesArr = [
  14847. 'background-color'
  14848. ];
  14849. each$e(conditionalNonInheritableStylesArr, (style) => {
  14850. conditionalNonInheritableStyles.add(style);
  14851. });
  14852. })();
  14853. // TODO: TINY-7326 Figure out what else should be added to the shorthandStyleProps list
  14854. // Does not include non-inherited shorthand style properties
  14855. const shorthandStyleProps = ['font', 'text-decoration', 'text-emphasis'];
  14856. const getStyles$1 = (dom, node) => dom.parseStyle(dom.getAttrib(node, 'style'));
  14857. const getStyleProps = (dom, node) => keys(getStyles$1(dom, node));
  14858. const isNonInheritableStyle = (style) => nonInheritableStyles.has(style);
  14859. const isConditionalNonInheritableStyle = (style) => conditionalNonInheritableStyles.has(style);
  14860. const hasNonInheritableStyles = (dom, node) => exists(getStyleProps(dom, node), (style) => isNonInheritableStyle(style));
  14861. const hasConditionalNonInheritableStyles = (dom, node) => hasNonInheritableStyles(dom, node) &&
  14862. exists(getStyleProps(dom, node), (style) => isConditionalNonInheritableStyle(style));
  14863. const getLonghandStyleProps = (styles) => filter$5(styles, (style) => exists(shorthandStyleProps, (prop) => startsWith(style, prop)));
  14864. const hasStyleConflict = (dom, node, parentNode) => {
  14865. const nodeStyleProps = getStyleProps(dom, node);
  14866. const parentNodeStyleProps = getStyleProps(dom, parentNode);
  14867. const valueMismatch = (prop) => {
  14868. var _a, _b;
  14869. const nodeValue = (_a = dom.getStyle(node, prop)) !== null && _a !== void 0 ? _a : '';
  14870. const parentValue = (_b = dom.getStyle(parentNode, prop)) !== null && _b !== void 0 ? _b : '';
  14871. return isNotEmpty(nodeValue) && isNotEmpty(parentValue) && nodeValue !== parentValue;
  14872. };
  14873. return exists(nodeStyleProps, (nodeStyleProp) => {
  14874. const propExists = (props) => exists(props, (prop) => prop === nodeStyleProp);
  14875. // If parent has a longhand property e.g. margin-left but the child (node) style is margin, need to get the margin-left value of node to be able to do a proper comparison
  14876. // This is because getting the style using the key of 'margin' on a 'margin-left' parent would give a string of space separated values or empty string depending on the browser
  14877. if (!propExists(parentNodeStyleProps) && propExists(shorthandStyleProps)) {
  14878. const longhandProps = getLonghandStyleProps(parentNodeStyleProps);
  14879. return exists(longhandProps, valueMismatch);
  14880. }
  14881. else {
  14882. return valueMismatch(nodeStyleProp);
  14883. }
  14884. });
  14885. };
  14886. const isChar = (forward, predicate, pos) => Optional.from(pos.container()).filter(isText$b).exists((text) => {
  14887. const delta = forward ? 0 : -1;
  14888. return predicate(text.data.charAt(pos.offset() + delta));
  14889. });
  14890. const isBeforeSpace = curry(isChar, true, isWhiteSpace);
  14891. const isAfterSpace = curry(isChar, false, isWhiteSpace);
  14892. const isEmptyText = (pos) => {
  14893. const container = pos.container();
  14894. return isText$b(container) && (container.data.length === 0 || isZwsp(container.data) && BookmarkManager.isBookmarkNode(container.parentNode));
  14895. };
  14896. const matchesElementPosition = (before, predicate) => (pos) => getChildNodeAtRelativeOffset(before ? 0 : -1, pos).filter(predicate).isSome();
  14897. const isImageBlock = (node) => isImg(node) && get$7(SugarElement.fromDom(node), 'display') === 'block';
  14898. const isCefNode = (node) => isContentEditableFalse$a(node) && !isBogusAll(node);
  14899. const isBeforeImageBlock = matchesElementPosition(true, isImageBlock);
  14900. const isAfterImageBlock = matchesElementPosition(false, isImageBlock);
  14901. const isBeforeMedia = matchesElementPosition(true, isMedia$2);
  14902. const isAfterMedia = matchesElementPosition(false, isMedia$2);
  14903. const isBeforeTable = matchesElementPosition(true, isTable$2);
  14904. const isAfterTable = matchesElementPosition(false, isTable$2);
  14905. const isBeforeContentEditableFalse = matchesElementPosition(true, isCefNode);
  14906. const isAfterContentEditableFalse = matchesElementPosition(false, isCefNode);
  14907. const dropLast = (xs) => xs.slice(0, -1);
  14908. const parentsUntil = (start, root, predicate) => {
  14909. if (contains(root, start)) {
  14910. return dropLast(parents$1(start, (elm) => {
  14911. return predicate(elm) || eq(elm, root);
  14912. }));
  14913. }
  14914. else {
  14915. return [];
  14916. }
  14917. };
  14918. const parents = (start, root) => parentsUntil(start, root, never);
  14919. const parentsAndSelf = (start, root) => [start].concat(parents(start, root));
  14920. const navigateIgnoreEmptyTextNodes = (forward, root, from) => navigateIgnore(forward, root, from, isEmptyText);
  14921. const isBlock$2 = (schema) => (el) => schema.isBlock(name(el));
  14922. const getClosestBlock$1 = (root, pos, schema) => find$2(parentsAndSelf(SugarElement.fromDom(pos.container()), root), isBlock$2(schema));
  14923. const isAtBeforeAfterBlockBoundary = (forward, root, pos, schema) => navigateIgnoreEmptyTextNodes(forward, root.dom, pos)
  14924. .forall((newPos) => getClosestBlock$1(root, pos, schema).fold(() => !isInSameBlock(newPos, pos, root.dom), (fromBlock) => !isInSameBlock(newPos, pos, root.dom) && contains(fromBlock, SugarElement.fromDom(newPos.container()))));
  14925. const isAtBlockBoundary = (forward, root, pos, schema) => getClosestBlock$1(root, pos, schema).fold(() => navigateIgnoreEmptyTextNodes(forward, root.dom, pos).forall((newPos) => !isInSameBlock(newPos, pos, root.dom)), (parent) => navigateIgnoreEmptyTextNodes(forward, parent.dom, pos).isNone());
  14926. const isAtStartOfBlock = curry(isAtBlockBoundary, false);
  14927. const isAtEndOfBlock = curry(isAtBlockBoundary, true);
  14928. const isBeforeBlock = curry(isAtBeforeAfterBlockBoundary, false);
  14929. const isAfterBlock = curry(isAtBeforeAfterBlockBoundary, true);
  14930. const isBr$2 = (pos) => getElementFromPosition(pos).exists(isBr$6);
  14931. const findBr = (forward, root, pos, schema) => {
  14932. const parentBlocks = filter$5(parentsAndSelf(SugarElement.fromDom(pos.container()), root), (el) => schema.isBlock(name(el)));
  14933. const scope = head(parentBlocks).getOr(root);
  14934. return fromPosition(forward, scope.dom, pos).filter(isBr$2);
  14935. };
  14936. const isBeforeBr$1 = (root, pos, schema) => getElementFromPosition(pos).exists(isBr$6) || findBr(true, root, pos, schema).isSome();
  14937. const isAfterBr = (root, pos, schema) => getElementFromPrevPosition(pos).exists(isBr$6) || findBr(false, root, pos, schema).isSome();
  14938. const findPreviousBr = curry(findBr, false);
  14939. const findNextBr = curry(findBr, true);
  14940. const isInMiddleOfText = (pos) => CaretPosition.isTextPosition(pos) && !pos.isAtStart() && !pos.isAtEnd();
  14941. const getClosestBlock = (root, pos, schema) => {
  14942. const parentBlocks = filter$5(parentsAndSelf(SugarElement.fromDom(pos.container()), root), (el) => schema.isBlock(name(el)));
  14943. return head(parentBlocks).getOr(root);
  14944. };
  14945. const hasSpaceBefore = (root, pos, schema) => {
  14946. if (isInMiddleOfText(pos)) {
  14947. return isAfterSpace(pos);
  14948. }
  14949. else {
  14950. return isAfterSpace(pos) || prevPosition(getClosestBlock(root, pos, schema).dom, pos).exists(isAfterSpace);
  14951. }
  14952. };
  14953. const hasSpaceAfter = (root, pos, schema) => {
  14954. if (isInMiddleOfText(pos)) {
  14955. return isBeforeSpace(pos);
  14956. }
  14957. else {
  14958. return isBeforeSpace(pos) || nextPosition(getClosestBlock(root, pos, schema).dom, pos).exists(isBeforeSpace);
  14959. }
  14960. };
  14961. const isPreValue = (value) => contains$2(['pre', 'pre-wrap'], value);
  14962. const isInPre = (pos) => getElementFromPosition(pos)
  14963. .bind((elm) => closest$4(elm, isElement$8))
  14964. .exists((elm) => isPreValue(get$7(elm, 'white-space')));
  14965. const isAtBeginningOfBody = (root, pos) => prevPosition(root.dom, pos).isNone();
  14966. const isAtEndOfBody = (root, pos) => nextPosition(root.dom, pos).isNone();
  14967. const isAtLineBoundary = (root, pos, schema) => (isAtBeginningOfBody(root, pos) ||
  14968. isAtEndOfBody(root, pos) ||
  14969. isAtStartOfBlock(root, pos, schema) ||
  14970. isAtEndOfBlock(root, pos, schema) ||
  14971. isAfterBr(root, pos, schema) ||
  14972. isBeforeBr$1(root, pos, schema));
  14973. const isCefBlock = (node) => isNonNullable(node) && isContentEditableFalse$a(node) && isBlockLike(node);
  14974. // Check the next/previous element in case it is a cef and the next/previous caret position then would skip it, then check
  14975. // the next next/previous caret position ( for example in case the next element is a strong, containing a cef ).
  14976. const isSiblingCefBlock = (root, direction) => (container) => {
  14977. return isCefBlock(new DomTreeWalker(container, root)[direction]());
  14978. };
  14979. const isBeforeCefBlock = (root, pos) => {
  14980. const nextPos = nextPosition(root.dom, pos).getOr(pos);
  14981. const isNextCefBlock = isSiblingCefBlock(root.dom, 'next');
  14982. return pos.isAtEnd() && (isNextCefBlock(pos.container()) || isNextCefBlock(nextPos.container()));
  14983. };
  14984. const isAfterCefBlock = (root, pos) => {
  14985. const prevPos = prevPosition(root.dom, pos).getOr(pos);
  14986. const isPrevCefBlock = isSiblingCefBlock(root.dom, 'prev');
  14987. return pos.isAtStart() && (isPrevCefBlock(pos.container()) || isPrevCefBlock(prevPos.container()));
  14988. };
  14989. const needsToHaveNbsp = (root, pos, schema) => {
  14990. if (isInPre(pos)) {
  14991. return false;
  14992. }
  14993. else {
  14994. return isAtLineBoundary(root, pos, schema) || hasSpaceBefore(root, pos, schema) || hasSpaceAfter(root, pos, schema);
  14995. }
  14996. };
  14997. const needsToBeNbspLeft = (root, pos, schema) => {
  14998. if (isInPre(pos)) {
  14999. return false;
  15000. }
  15001. else {
  15002. return isAtStartOfBlock(root, pos, schema) || isBeforeBlock(root, pos, schema) || isAfterBr(root, pos, schema) || hasSpaceBefore(root, pos, schema) || isAfterCefBlock(root, pos);
  15003. }
  15004. };
  15005. const leanRight = (pos) => {
  15006. const container = pos.container();
  15007. const offset = pos.offset();
  15008. if (isText$b(container) && offset < container.data.length) {
  15009. return CaretPosition(container, offset + 1);
  15010. }
  15011. else {
  15012. return pos;
  15013. }
  15014. };
  15015. const needsToBeNbspRight = (root, pos, schema) => {
  15016. if (isInPre(pos)) {
  15017. return false;
  15018. }
  15019. else {
  15020. return isAtEndOfBlock(root, pos, schema) || isAfterBlock(root, pos, schema) || isBeforeBr$1(root, pos, schema) || hasSpaceAfter(root, pos, schema) || isBeforeCefBlock(root, pos);
  15021. }
  15022. };
  15023. const needsToBeNbsp = (root, pos, schema) => needsToBeNbspLeft(root, pos, schema) || needsToBeNbspRight(root, leanRight(pos), schema);
  15024. const isNbspAt = (text, offset) => isNbsp(text.charAt(offset));
  15025. const isWhiteSpaceAt = (text, offset) => isWhiteSpace(text.charAt(offset));
  15026. const hasNbsp = (pos) => {
  15027. const container = pos.container();
  15028. return isText$b(container) && contains$1(container.data, nbsp);
  15029. };
  15030. const normalizeNbspMiddle = (text) => {
  15031. const chars = text.split('');
  15032. return map$3(chars, (chr, i) => {
  15033. if (isNbsp(chr) && i > 0 && i < chars.length - 1 && isContent(chars[i - 1]) && isContent(chars[i + 1])) {
  15034. return ' ';
  15035. }
  15036. else {
  15037. return chr;
  15038. }
  15039. }).join('');
  15040. };
  15041. const normalizeNbspAtStart = (root, node, makeNbsp, schema) => {
  15042. const text = node.data;
  15043. const firstPos = CaretPosition(node, 0);
  15044. if (!makeNbsp && isNbspAt(text, 0) && !needsToBeNbsp(root, firstPos, schema)) {
  15045. node.data = ' ' + text.slice(1);
  15046. return true;
  15047. }
  15048. else if (makeNbsp && isWhiteSpaceAt(text, 0) && needsToBeNbspLeft(root, firstPos, schema)) {
  15049. node.data = nbsp + text.slice(1);
  15050. return true;
  15051. }
  15052. else {
  15053. return false;
  15054. }
  15055. };
  15056. const normalizeNbspInMiddleOfTextNode = (node) => {
  15057. const text = node.data;
  15058. const newText = normalizeNbspMiddle(text);
  15059. if (newText !== text) {
  15060. node.data = newText;
  15061. return true;
  15062. }
  15063. else {
  15064. return false;
  15065. }
  15066. };
  15067. const normalizeNbspAtEnd = (root, node, makeNbsp, schema) => {
  15068. const text = node.data;
  15069. const lastPos = CaretPosition(node, text.length - 1);
  15070. if (!makeNbsp && isNbspAt(text, text.length - 1) && !needsToBeNbsp(root, lastPos, schema)) {
  15071. node.data = text.slice(0, -1) + ' ';
  15072. return true;
  15073. }
  15074. else if (makeNbsp && isWhiteSpaceAt(text, text.length - 1) && needsToBeNbspRight(root, lastPos, schema)) {
  15075. node.data = text.slice(0, -1) + nbsp;
  15076. return true;
  15077. }
  15078. else {
  15079. return false;
  15080. }
  15081. };
  15082. const normalizeNbsps$1 = (root, pos, schema) => {
  15083. const container = pos.container();
  15084. if (!isText$b(container)) {
  15085. return Optional.none();
  15086. }
  15087. if (hasNbsp(pos)) {
  15088. const normalized = normalizeNbspAtStart(root, container, false, schema) || normalizeNbspInMiddleOfTextNode(container) || normalizeNbspAtEnd(root, container, false, schema);
  15089. return someIf(normalized, pos);
  15090. }
  15091. else if (needsToBeNbsp(root, pos, schema)) {
  15092. const normalized = normalizeNbspAtStart(root, container, true, schema) || normalizeNbspAtEnd(root, container, true, schema);
  15093. return someIf(normalized, pos);
  15094. }
  15095. else {
  15096. return Optional.none();
  15097. }
  15098. };
  15099. const normalizeNbspsInEditor = (editor) => {
  15100. const root = SugarElement.fromDom(editor.getBody());
  15101. if (editor.selection.isCollapsed()) {
  15102. normalizeNbsps$1(root, CaretPosition.fromRangeStart(editor.selection.getRng()), editor.schema).each((pos) => {
  15103. editor.selection.setRng(pos.toRange());
  15104. });
  15105. }
  15106. };
  15107. const normalize$1 = (node, offset, count, schema) => {
  15108. if (count === 0) {
  15109. return;
  15110. }
  15111. const elm = SugarElement.fromDom(node);
  15112. const root = ancestor$5(elm, (el) => schema.isBlock(name(el))).getOr(elm);
  15113. // Get the whitespace
  15114. const whitespace = node.data.slice(offset, offset + count);
  15115. // Determine if we're at the end or start of the content
  15116. const isEndOfContent = offset + count >= node.data.length && needsToBeNbspRight(root, CaretPosition(node, node.data.length), schema);
  15117. const isStartOfContent = offset === 0 && needsToBeNbspLeft(root, CaretPosition(node, 0), schema);
  15118. // Replace the original whitespace with the normalized whitespace content
  15119. node.replaceData(offset, count, normalize$4(whitespace, 4, isStartOfContent, isEndOfContent));
  15120. };
  15121. const normalizeWhitespaceAfter = (node, offset, schema) => {
  15122. const content = node.data.slice(offset);
  15123. const whitespaceCount = content.length - lTrim(content).length;
  15124. normalize$1(node, offset, whitespaceCount, schema);
  15125. };
  15126. const normalizeWhitespaceBefore = (node, offset, schema) => {
  15127. const content = node.data.slice(0, offset);
  15128. const whitespaceCount = content.length - rTrim(content).length;
  15129. normalize$1(node, offset - whitespaceCount, whitespaceCount, schema);
  15130. };
  15131. const mergeTextNodes = (prevNode, nextNode, schema, normalizeWhitespace, mergeToPrev = true) => {
  15132. const whitespaceOffset = rTrim(prevNode.data).length;
  15133. const newNode = mergeToPrev ? prevNode : nextNode;
  15134. const removeNode = mergeToPrev ? nextNode : prevNode;
  15135. // Merge the elements
  15136. if (mergeToPrev) {
  15137. newNode.appendData(removeNode.data);
  15138. }
  15139. else {
  15140. newNode.insertData(0, removeNode.data);
  15141. }
  15142. remove$8(SugarElement.fromDom(removeNode));
  15143. // Normalize the whitespace around the merged elements, to ensure it doesn't get lost
  15144. if (normalizeWhitespace) {
  15145. normalizeWhitespaceAfter(newNode, whitespaceOffset, schema);
  15146. }
  15147. return newNode;
  15148. };
  15149. const needsReposition = (pos, elm) => {
  15150. const container = pos.container();
  15151. const offset = pos.offset();
  15152. return !CaretPosition.isTextPosition(pos) && container === elm.parentNode && offset > CaretPosition.before(elm).offset();
  15153. };
  15154. const reposition = (elm, pos) => needsReposition(pos, elm) ? CaretPosition(pos.container(), pos.offset() - 1) : pos;
  15155. const beforeOrStartOf = (node) => isText$b(node) ? CaretPosition(node, 0) : CaretPosition.before(node);
  15156. const afterOrEndOf = (node) => isText$b(node) ? CaretPosition(node, node.data.length) : CaretPosition.after(node);
  15157. const getPreviousSiblingCaretPosition = (elm) => {
  15158. if (isCaretCandidate$3(elm.previousSibling)) {
  15159. return Optional.some(afterOrEndOf(elm.previousSibling));
  15160. }
  15161. else {
  15162. return elm.previousSibling ? lastPositionIn(elm.previousSibling) : Optional.none();
  15163. }
  15164. };
  15165. const getNextSiblingCaretPosition = (elm) => {
  15166. if (isCaretCandidate$3(elm.nextSibling)) {
  15167. return Optional.some(beforeOrStartOf(elm.nextSibling));
  15168. }
  15169. else {
  15170. return elm.nextSibling ? firstPositionIn(elm.nextSibling) : Optional.none();
  15171. }
  15172. };
  15173. const findCaretPositionBackwardsFromElm = (rootElement, elm) => {
  15174. return Optional.from(elm.previousSibling ? elm.previousSibling : elm.parentNode)
  15175. .bind((node) => prevPosition(rootElement, CaretPosition.before(node)))
  15176. .orThunk(() => nextPosition(rootElement, CaretPosition.after(elm)));
  15177. };
  15178. const findCaretPositionForwardsFromElm = (rootElement, elm) => nextPosition(rootElement, CaretPosition.after(elm)).orThunk(() => prevPosition(rootElement, CaretPosition.before(elm)));
  15179. const findCaretPositionBackwards = (rootElement, elm) => getPreviousSiblingCaretPosition(elm).orThunk(() => getNextSiblingCaretPosition(elm))
  15180. .orThunk(() => findCaretPositionBackwardsFromElm(rootElement, elm));
  15181. const findCaretPositionForward = (rootElement, elm) => getNextSiblingCaretPosition(elm)
  15182. .orThunk(() => getPreviousSiblingCaretPosition(elm))
  15183. .orThunk(() => findCaretPositionForwardsFromElm(rootElement, elm));
  15184. const findCaretPosition = (forward, rootElement, elm) => forward ? findCaretPositionForward(rootElement, elm) : findCaretPositionBackwards(rootElement, elm);
  15185. const findCaretPosOutsideElmAfterDelete = (forward, rootElement, elm) => findCaretPosition(forward, rootElement, elm).map(curry(reposition, elm));
  15186. const setSelection$1 = (editor, forward, pos) => {
  15187. pos.fold(() => {
  15188. editor.focus();
  15189. }, (pos) => {
  15190. editor.selection.setRng(pos.toRange(), forward);
  15191. });
  15192. };
  15193. const eqRawNode = (rawNode) => (elm) => elm.dom === rawNode;
  15194. const isBlock$1 = (editor, elm) => elm && has$2(editor.schema.getBlockElements(), name(elm));
  15195. const paddEmptyBlock = (schema, elm, preserveEmptyCaret) => {
  15196. if (isEmpty$4(schema, elm)) {
  15197. const br = SugarElement.fromHtml('<br data-mce-bogus="1">');
  15198. // Remove all bogus elements except caret
  15199. if (preserveEmptyCaret) {
  15200. each$e(children$1(elm), (node) => {
  15201. if (!isEmptyCaretFormatElement(node)) {
  15202. remove$8(node);
  15203. }
  15204. });
  15205. }
  15206. else {
  15207. empty(elm);
  15208. }
  15209. append$1(elm, br);
  15210. return Optional.some(CaretPosition.before(br.dom));
  15211. }
  15212. else {
  15213. return Optional.none();
  15214. }
  15215. };
  15216. const deleteNormalized = (elm, afterDeletePosOpt, schema, normalizeWhitespace) => {
  15217. const prevTextOpt = prevSibling(elm).filter(isText$c);
  15218. const nextTextOpt = nextSibling(elm).filter(isText$c);
  15219. // Delete the element
  15220. remove$8(elm);
  15221. // Merge and normalize any prev/next text nodes, so that they are merged and don't lose meaningful whitespace
  15222. // eg. <p>a <span></span> b</p> -> <p>a &nsbp;b</p> or <p><span></span> a</p> -> <p>&nbsp;a</a>
  15223. return lift3(prevTextOpt, nextTextOpt, afterDeletePosOpt, (prev, next, pos) => {
  15224. const prevNode = prev.dom, nextNode = next.dom;
  15225. const offset = prevNode.data.length;
  15226. mergeTextNodes(prevNode, nextNode, schema, normalizeWhitespace);
  15227. // Update the cursor position if required
  15228. return pos.container() === nextNode ? CaretPosition(prevNode, offset) : pos;
  15229. }).orThunk(() => {
  15230. if (normalizeWhitespace) {
  15231. prevTextOpt.each((elm) => normalizeWhitespaceBefore(elm.dom, elm.dom.length, schema));
  15232. nextTextOpt.each((elm) => normalizeWhitespaceAfter(elm.dom, 0, schema));
  15233. }
  15234. return afterDeletePosOpt;
  15235. });
  15236. };
  15237. const isInlineElement = (editor, element) => has$2(editor.schema.getTextInlineElements(), name(element));
  15238. const deleteElement$2 = (editor, forward, elm, moveCaret = true, preserveEmptyCaret = false) => {
  15239. // Existing delete logic
  15240. const afterDeletePos = findCaretPosOutsideElmAfterDelete(forward, editor.getBody(), elm.dom);
  15241. const parentBlock = ancestor$5(elm, curry(isBlock$1, editor), eqRawNode(editor.getBody()));
  15242. const normalizedAfterDeletePos = deleteNormalized(elm, afterDeletePos, editor.schema, isInlineElement(editor, elm));
  15243. if (editor.dom.isEmpty(editor.getBody())) {
  15244. editor.setContent('');
  15245. editor.selection.setCursorLocation();
  15246. }
  15247. else {
  15248. parentBlock.bind((elm) => paddEmptyBlock(editor.schema, elm, preserveEmptyCaret)).fold(() => {
  15249. if (moveCaret) {
  15250. setSelection$1(editor, forward, normalizedAfterDeletePos);
  15251. }
  15252. }, (paddPos) => {
  15253. if (moveCaret) {
  15254. setSelection$1(editor, forward, Optional.some(paddPos));
  15255. }
  15256. });
  15257. }
  15258. };
  15259. const strongRtl = /[\u0591-\u07FF\uFB1D-\uFDFF\uFE70-\uFEFC]/;
  15260. const hasStrongRtl = (text) => strongRtl.test(text);
  15261. const isInlineTarget = (editor, elm) => is$2(SugarElement.fromDom(elm), getInlineBoundarySelector(editor))
  15262. && !isTransparentBlock(editor.schema, elm)
  15263. && editor.dom.isEditable(elm);
  15264. const isRtl = (element) => { var _a; return DOMUtils.DOM.getStyle(element, 'direction', true) === 'rtl' || hasStrongRtl((_a = element.textContent) !== null && _a !== void 0 ? _a : ''); };
  15265. const findInlineParents = (isInlineTarget, rootNode, pos) => filter$5(DOMUtils.DOM.getParents(pos.container(), '*', rootNode), isInlineTarget);
  15266. const findRootInline = (isInlineTarget, rootNode, pos) => {
  15267. const parents = findInlineParents(isInlineTarget, rootNode, pos);
  15268. return Optional.from(parents[parents.length - 1]);
  15269. };
  15270. const hasSameParentBlock = (rootNode, node1, node2) => {
  15271. const block1 = getParentBlock$3(node1, rootNode);
  15272. const block2 = getParentBlock$3(node2, rootNode);
  15273. return isNonNullable(block1) && block1 === block2;
  15274. };
  15275. const isAtZwsp = (pos) => isBeforeInline(pos) || isAfterInline(pos);
  15276. const normalizePosition = (forward, pos) => {
  15277. const container = pos.container(), offset = pos.offset();
  15278. if (forward) {
  15279. if (isCaretContainerInline(container)) {
  15280. if (isText$b(container.nextSibling)) {
  15281. return CaretPosition(container.nextSibling, 0);
  15282. }
  15283. else {
  15284. return CaretPosition.after(container);
  15285. }
  15286. }
  15287. else {
  15288. return isBeforeInline(pos) ? CaretPosition(container, offset + 1) : pos;
  15289. }
  15290. }
  15291. else {
  15292. if (isCaretContainerInline(container)) {
  15293. if (isText$b(container.previousSibling)) {
  15294. return CaretPosition(container.previousSibling, container.previousSibling.data.length);
  15295. }
  15296. else {
  15297. return CaretPosition.before(container);
  15298. }
  15299. }
  15300. else {
  15301. return isAfterInline(pos) ? CaretPosition(container, offset - 1) : pos;
  15302. }
  15303. }
  15304. };
  15305. const normalizeForwards = curry(normalizePosition, true);
  15306. const normalizeBackwards = curry(normalizePosition, false);
  15307. const execCommandIgnoreInputEvents = (editor, command) => {
  15308. // We need to prevent the input events from being fired by execCommand when delete is used internally
  15309. const inputBlocker = (e) => e.stopImmediatePropagation();
  15310. editor.on('beforeinput input', inputBlocker, true);
  15311. editor.getDoc().execCommand(command);
  15312. editor.off('beforeinput input', inputBlocker);
  15313. };
  15314. // ASSUMPTION: The editor command 'delete' doesn't have any `beforeinput` and `input` trapping
  15315. // because those events are only triggered by native contenteditable behaviour.
  15316. const execEditorDeleteCommand = (editor) => {
  15317. editor.execCommand('delete');
  15318. };
  15319. const execNativeDeleteCommand = (editor) => execCommandIgnoreInputEvents(editor, 'Delete');
  15320. const execNativeForwardDeleteCommand = (editor) => execCommandIgnoreInputEvents(editor, 'ForwardDelete');
  15321. const isBeforeRoot = (rootNode) => (elm) => is$4(parent(elm), rootNode, eq);
  15322. const isTextBlockOrListItem = (element) => isTextBlock$3(element) || isListItem$2(element);
  15323. const getParentBlock$2 = (rootNode, elm) => {
  15324. if (contains(rootNode, elm)) {
  15325. return closest$4(elm, isTextBlockOrListItem, isBeforeRoot(rootNode));
  15326. }
  15327. else {
  15328. return Optional.none();
  15329. }
  15330. };
  15331. const paddEmptyBody = (editor, moveSelection = true) => {
  15332. if (editor.dom.isEmpty(editor.getBody())) {
  15333. editor.setContent('', { no_selection: !moveSelection });
  15334. }
  15335. };
  15336. const willDeleteLastPositionInElement = (forward, fromPos, elm) => lift2(firstPositionIn(elm), lastPositionIn(elm), (firstPos, lastPos) => {
  15337. const normalizedFirstPos = normalizePosition(true, firstPos);
  15338. const normalizedLastPos = normalizePosition(false, lastPos);
  15339. const normalizedFromPos = normalizePosition(false, fromPos);
  15340. if (forward) {
  15341. return nextPosition(elm, normalizedFromPos).exists((nextPos) => nextPos.isEqual(normalizedLastPos) && fromPos.isEqual(normalizedFirstPos));
  15342. }
  15343. else {
  15344. return prevPosition(elm, normalizedFromPos).exists((prevPos) => prevPos.isEqual(normalizedFirstPos) && fromPos.isEqual(normalizedLastPos));
  15345. }
  15346. }).getOr(true);
  15347. const freefallRtl = (root) => {
  15348. const child = isComment$1(root) ? prevSibling(root) : lastChild(root);
  15349. return child.bind(freefallRtl).orThunk(() => Optional.some(root));
  15350. };
  15351. const deleteRangeContents = (editor, rng, root, moveSelection = true) => {
  15352. var _a;
  15353. rng.deleteContents();
  15354. // Pad the last block node
  15355. const lastNode = freefallRtl(root).getOr(root);
  15356. const lastBlock = SugarElement.fromDom((_a = editor.dom.getParent(lastNode.dom, editor.dom.isBlock)) !== null && _a !== void 0 ? _a : root.dom);
  15357. // If the block is the editor body then we need to insert the root block as well
  15358. if (lastBlock.dom === editor.getBody()) {
  15359. paddEmptyBody(editor, moveSelection);
  15360. }
  15361. else if (isEmpty$4(editor.schema, lastBlock, { checkRootAsContent: false })) {
  15362. fillWithPaddingBr(lastBlock);
  15363. if (moveSelection) {
  15364. editor.selection.setCursorLocation(lastBlock.dom, 0);
  15365. }
  15366. }
  15367. // Clean up any additional leftover nodes. If the last block wasn't a direct child, then we also need to clean up siblings
  15368. if (!eq(root, lastBlock)) {
  15369. const additionalCleanupNodes = is$4(parent(lastBlock), root) ? [] : siblings(lastBlock);
  15370. each$e(additionalCleanupNodes.concat(children$1(root)), (node) => {
  15371. if (!eq(node, lastBlock) && !contains(node, lastBlock) && isEmpty$4(editor.schema, node)) {
  15372. remove$8(node);
  15373. }
  15374. });
  15375. }
  15376. };
  15377. const isRootFromElement = (root) => (cur) => eq(root, cur);
  15378. const getTableCells = (table) => descendants(table, 'td,th');
  15379. const getTable$1 = (node, isRoot) => getClosestTable(SugarElement.fromDom(node), isRoot);
  15380. const selectionInTableWithNestedTable = (details) => {
  15381. return lift2(details.startTable, details.endTable, (startTable, endTable) => {
  15382. const isStartTableParentOfEndTable = descendant(startTable, (t) => eq(t, endTable));
  15383. const isEndTableParentOfStartTable = descendant(endTable, (t) => eq(t, startTable));
  15384. return !isStartTableParentOfEndTable && !isEndTableParentOfStartTable ? details : {
  15385. ...details,
  15386. startTable: isStartTableParentOfEndTable ? Optional.none() : details.startTable,
  15387. endTable: isEndTableParentOfStartTable ? Optional.none() : details.endTable,
  15388. isSameTable: false,
  15389. isMultiTable: false
  15390. };
  15391. }).getOr(details);
  15392. };
  15393. const adjustQuirksInDetails = (details) => {
  15394. return selectionInTableWithNestedTable(details);
  15395. };
  15396. const getTableDetailsFromRange = (rng, isRoot) => {
  15397. const startTable = getTable$1(rng.startContainer, isRoot);
  15398. const endTable = getTable$1(rng.endContainer, isRoot);
  15399. const isStartInTable = startTable.isSome();
  15400. const isEndInTable = endTable.isSome();
  15401. // Partial selection - selection is not within the same table
  15402. const isSameTable = lift2(startTable, endTable, eq).getOr(false);
  15403. const isMultiTable = !isSameTable && isStartInTable && isEndInTable;
  15404. return adjustQuirksInDetails({
  15405. startTable,
  15406. endTable,
  15407. isStartInTable,
  15408. isEndInTable,
  15409. isSameTable,
  15410. isMultiTable
  15411. });
  15412. };
  15413. const tableCellRng = (start, end) => ({
  15414. start,
  15415. end,
  15416. });
  15417. const tableSelection = (rng, table, cells) => ({
  15418. rng,
  15419. table,
  15420. cells
  15421. });
  15422. const deleteAction = Adt.generate([
  15423. { singleCellTable: ['rng', 'cell'] },
  15424. { fullTable: ['table'] },
  15425. { partialTable: ['cells', 'outsideDetails'] },
  15426. { multiTable: ['startTableCells', 'endTableCells', 'betweenRng'] },
  15427. ]);
  15428. const getClosestCell$1 = (container, isRoot) => closest$3(SugarElement.fromDom(container), 'td,th', isRoot);
  15429. const isExpandedCellRng = (cellRng) => !eq(cellRng.start, cellRng.end);
  15430. const getTableFromCellRng = (cellRng, isRoot) => getClosestTable(cellRng.start, isRoot)
  15431. .bind((startParentTable) => getClosestTable(cellRng.end, isRoot)
  15432. .bind((endParentTable) => someIf(eq(startParentTable, endParentTable), startParentTable)));
  15433. const isSingleCellTable = (cellRng, isRoot) => !isExpandedCellRng(cellRng) &&
  15434. getTableFromCellRng(cellRng, isRoot).exists((table) => {
  15435. const rows = table.dom.rows;
  15436. return rows.length === 1 && rows[0].cells.length === 1;
  15437. });
  15438. const getCellRng = (rng, isRoot) => {
  15439. const startCell = getClosestCell$1(rng.startContainer, isRoot);
  15440. const endCell = getClosestCell$1(rng.endContainer, isRoot);
  15441. return lift2(startCell, endCell, tableCellRng);
  15442. };
  15443. const getCellRangeFromStartTable = (isRoot) => (startCell) => getClosestTable(startCell, isRoot).bind((table) => last$2(getTableCells(table)).map((endCell) => tableCellRng(startCell, endCell)));
  15444. const getCellRangeFromEndTable = (isRoot) => (endCell) => getClosestTable(endCell, isRoot).bind((table) => head(getTableCells(table)).map((startCell) => tableCellRng(startCell, endCell)));
  15445. const getTableSelectionFromCellRng = (isRoot) => (cellRng) => getTableFromCellRng(cellRng, isRoot).map((table) => tableSelection(cellRng, table, getTableCells(table)));
  15446. const getTableSelections = (cellRng, selectionDetails, rng, isRoot) => {
  15447. if (rng.collapsed || !cellRng.forall(isExpandedCellRng)) {
  15448. return Optional.none();
  15449. }
  15450. else if (selectionDetails.isSameTable) {
  15451. const sameTableSelection = cellRng.bind(getTableSelectionFromCellRng(isRoot));
  15452. return Optional.some({
  15453. start: sameTableSelection,
  15454. end: sameTableSelection
  15455. });
  15456. }
  15457. else {
  15458. // Covers partial table selection (either start or end will have a tableSelection) and multitable selection (both start and end will have a tableSelection)
  15459. const startCell = getClosestCell$1(rng.startContainer, isRoot);
  15460. const endCell = getClosestCell$1(rng.endContainer, isRoot);
  15461. const startTableSelection = startCell
  15462. .bind(getCellRangeFromStartTable(isRoot))
  15463. .bind(getTableSelectionFromCellRng(isRoot));
  15464. const endTableSelection = endCell
  15465. .bind(getCellRangeFromEndTable(isRoot))
  15466. .bind(getTableSelectionFromCellRng(isRoot));
  15467. return Optional.some({
  15468. start: startTableSelection,
  15469. end: endTableSelection
  15470. });
  15471. }
  15472. };
  15473. const getCellIndex = (cells, cell) => findIndex$2(cells, (x) => eq(x, cell));
  15474. const getSelectedCells = (tableSelection) => lift2(getCellIndex(tableSelection.cells, tableSelection.rng.start), getCellIndex(tableSelection.cells, tableSelection.rng.end), (startIndex, endIndex) => tableSelection.cells.slice(startIndex, endIndex + 1));
  15475. const isSingleCellTableContentSelected = (optCellRng, rng, isRoot) => optCellRng.exists((cellRng) => isSingleCellTable(cellRng, isRoot) && hasAllContentsSelected(cellRng.start, rng));
  15476. const unselectCells = (rng, selectionDetails) => {
  15477. const { startTable, endTable } = selectionDetails;
  15478. const otherContentRng = rng.cloneRange();
  15479. // If the table is some, it should be unselected (works for single table and multitable cases)
  15480. startTable.each((table) => otherContentRng.setStartAfter(table.dom));
  15481. endTable.each((table) => otherContentRng.setEndBefore(table.dom));
  15482. return otherContentRng;
  15483. };
  15484. const handleSingleTable = (cellRng, selectionDetails, rng, isRoot) => getTableSelections(cellRng, selectionDetails, rng, isRoot)
  15485. .bind(({ start, end }) => start.or(end))
  15486. .bind((tableSelection) => {
  15487. const { isSameTable } = selectionDetails;
  15488. const selectedCells = getSelectedCells(tableSelection).getOr([]);
  15489. if (isSameTable && tableSelection.cells.length === selectedCells.length) {
  15490. return Optional.some(deleteAction.fullTable(tableSelection.table));
  15491. }
  15492. else if (selectedCells.length > 0) {
  15493. if (isSameTable) {
  15494. return Optional.some(deleteAction.partialTable(selectedCells, Optional.none()));
  15495. }
  15496. else {
  15497. const otherContentRng = unselectCells(rng, selectionDetails);
  15498. return Optional.some(deleteAction.partialTable(selectedCells, Optional.some({
  15499. ...selectionDetails,
  15500. rng: otherContentRng
  15501. })));
  15502. }
  15503. }
  15504. else {
  15505. return Optional.none();
  15506. }
  15507. });
  15508. const handleMultiTable = (cellRng, selectionDetails, rng, isRoot) => getTableSelections(cellRng, selectionDetails, rng, isRoot)
  15509. .bind(({ start, end }) => {
  15510. const startTableSelectedCells = start.bind(getSelectedCells).getOr([]);
  15511. const endTableSelectedCells = end.bind(getSelectedCells).getOr([]);
  15512. if (startTableSelectedCells.length > 0 && endTableSelectedCells.length > 0) {
  15513. const otherContentRng = unselectCells(rng, selectionDetails);
  15514. return Optional.some(deleteAction.multiTable(startTableSelectedCells, endTableSelectedCells, otherContentRng));
  15515. }
  15516. else {
  15517. return Optional.none();
  15518. }
  15519. });
  15520. const getActionFromRange = (root, rng) => {
  15521. const isRoot = isRootFromElement(root);
  15522. const optCellRng = getCellRng(rng, isRoot);
  15523. const selectionDetails = getTableDetailsFromRange(rng, isRoot);
  15524. if (isSingleCellTableContentSelected(optCellRng, rng, isRoot)) {
  15525. // SingleCellTable
  15526. return optCellRng.map((cellRng) => deleteAction.singleCellTable(rng, cellRng.start));
  15527. }
  15528. else if (selectionDetails.isMultiTable) {
  15529. // MultiTable
  15530. return handleMultiTable(optCellRng, selectionDetails, rng, isRoot);
  15531. }
  15532. else {
  15533. // FullTable, PartialTable with no rng or PartialTable with outside rng
  15534. return handleSingleTable(optCellRng, selectionDetails, rng, isRoot);
  15535. }
  15536. };
  15537. // Reset the contenteditable state and fill the content with a padding br
  15538. const cleanCells = (cells) => each$e(cells, (cell) => {
  15539. remove$9(cell, 'contenteditable');
  15540. fillWithPaddingBr(cell);
  15541. });
  15542. const getOutsideBlock = (editor, container) => Optional.from(editor.dom.getParent(container, editor.dom.isBlock)).map(SugarElement.fromDom);
  15543. const handleEmptyBlock = (editor, startInTable, emptyBlock) => {
  15544. emptyBlock.each((block) => {
  15545. if (startInTable) {
  15546. // Note that we don't need to set the selection as it'll be within the table
  15547. remove$8(block);
  15548. }
  15549. else {
  15550. // Set the cursor location as it'll move when filling with padding
  15551. fillWithPaddingBr(block);
  15552. editor.selection.setCursorLocation(block.dom, 0);
  15553. }
  15554. });
  15555. };
  15556. const deleteContentInsideCell = (editor, cell, rng, isFirstCellInSelection) => {
  15557. const insideTableRng = rng.cloneRange();
  15558. if (isFirstCellInSelection) {
  15559. insideTableRng.setStart(rng.startContainer, rng.startOffset);
  15560. insideTableRng.setEndAfter(cell.dom.lastChild);
  15561. }
  15562. else {
  15563. insideTableRng.setStartBefore(cell.dom.firstChild);
  15564. insideTableRng.setEnd(rng.endContainer, rng.endOffset);
  15565. }
  15566. deleteCellContents(editor, insideTableRng, cell, false).each((action) => action());
  15567. };
  15568. const collapseAndRestoreCellSelection = (editor) => {
  15569. const selectedCells = getCellsFromEditor(editor);
  15570. const selectedNode = SugarElement.fromDom(editor.selection.getNode());
  15571. if (isTableCell$3(selectedNode.dom) && isEmpty$4(editor.schema, selectedNode)) {
  15572. editor.selection.setCursorLocation(selectedNode.dom, 0);
  15573. }
  15574. else {
  15575. editor.selection.collapse(true);
  15576. }
  15577. // Restore the data-mce-selected attribute if multiple cells were selected, as if it was a cef element
  15578. // then selection overrides would remove it as it was using an offscreen selection clone.
  15579. if (selectedCells.length > 1 && exists(selectedCells, (cell) => eq(cell, selectedNode))) {
  15580. set$4(selectedNode, 'data-mce-selected', '1');
  15581. }
  15582. };
  15583. /*
  15584. * Runs when
  15585. * - the start and end of the selection is contained within the same table (called directly from deleteRange)
  15586. * - part of a table and content outside is selected
  15587. */
  15588. const emptySingleTableCells = (editor, cells, outsideDetails) => Optional.some(() => {
  15589. const editorRng = editor.selection.getRng();
  15590. const cellsToClean = outsideDetails.bind(({ rng, isStartInTable }) => {
  15591. /*
  15592. * Delete all content outside of the table that is in the selection
  15593. * - Get the outside block before deleting the contents
  15594. * - Delete the contents outside
  15595. * - Handle the block outside the table if it is empty since rng.deleteContents leaves it
  15596. */
  15597. const outsideBlock = getOutsideBlock(editor, isStartInTable ? rng.endContainer : rng.startContainer);
  15598. rng.deleteContents();
  15599. handleEmptyBlock(editor, isStartInTable, outsideBlock.filter(curry(isEmpty$4, editor.schema)));
  15600. /*
  15601. * The only time we can have only part of the cell contents selected is when part of the selection
  15602. * is outside the table (otherwise we use the Darwin fake selection, which always selects entire cells),
  15603. * in which case we need to delete the contents inside and check if the entire contents of the cell have been deleted.
  15604. *
  15605. * Note: The endPointCell is the only cell which may have only part of its contents selected.
  15606. */
  15607. const endPointCell = isStartInTable ? cells[0] : cells[cells.length - 1];
  15608. deleteContentInsideCell(editor, endPointCell, editorRng, isStartInTable);
  15609. if (!isEmpty$4(editor.schema, endPointCell)) {
  15610. return Optional.some(isStartInTable ? cells.slice(1) : cells.slice(0, -1));
  15611. }
  15612. else {
  15613. return Optional.none();
  15614. }
  15615. }).getOr(cells);
  15616. // Remove content from cells we need to clean
  15617. cleanCells(cellsToClean);
  15618. // Collapse the original selection after deleting everything
  15619. collapseAndRestoreCellSelection(editor);
  15620. });
  15621. /*
  15622. * Runs when the start of the selection is in a table and the end of the selection is in another table
  15623. */
  15624. const emptyMultiTableCells = (editor, startTableCells, endTableCells, betweenRng) => Optional.some(() => {
  15625. const rng = editor.selection.getRng();
  15626. const startCell = startTableCells[0];
  15627. const endCell = endTableCells[endTableCells.length - 1];
  15628. deleteContentInsideCell(editor, startCell, rng, true);
  15629. deleteContentInsideCell(editor, endCell, rng, false);
  15630. // Only clean empty cells, the first and last cells have the potential to still have content
  15631. const startTableCellsToClean = isEmpty$4(editor.schema, startCell) ? startTableCells : startTableCells.slice(1);
  15632. const endTableCellsToClean = isEmpty$4(editor.schema, endCell) ? endTableCells : endTableCells.slice(0, -1);
  15633. cleanCells(startTableCellsToClean.concat(endTableCellsToClean));
  15634. // Delete all content in between the start table and end table
  15635. betweenRng.deleteContents();
  15636. // This will collapse the selection into the cell of the start table
  15637. collapseAndRestoreCellSelection(editor);
  15638. });
  15639. // Delete the contents of a range inside a cell. Runs on tables that are a single cell or partial selections that need to be cleaned up.
  15640. const deleteCellContents = (editor, rng, cell, moveSelection = true) => Optional.some(() => {
  15641. deleteRangeContents(editor, rng, cell, moveSelection);
  15642. });
  15643. const deleteTableElement = (editor, table) => Optional.some(() => deleteElement$2(editor, false, table));
  15644. const deleteCellRange = (editor, rootElm, rng) => getActionFromRange(rootElm, rng)
  15645. .bind((action) => action.fold(curry(deleteCellContents, editor), curry(deleteTableElement, editor), curry(emptySingleTableCells, editor), curry(emptyMultiTableCells, editor)));
  15646. const deleteCaptionRange = (editor, caption) => emptyElement(editor, caption);
  15647. const deleteTableRange = (editor, rootElm, rng, startElm) => getParentCaption(rootElm, startElm).fold(() => deleteCellRange(editor, rootElm, rng), (caption) => deleteCaptionRange(editor, caption));
  15648. const deleteRange$4 = (editor, startElm, selectedCells) => {
  15649. const rootNode = SugarElement.fromDom(editor.getBody());
  15650. const rng = editor.selection.getRng();
  15651. return selectedCells.length !== 0 ?
  15652. emptySingleTableCells(editor, selectedCells, Optional.none()) :
  15653. deleteTableRange(editor, rootNode, rng, startElm);
  15654. };
  15655. const getParentCell = (rootElm, elm) => find$2(parentsAndSelf(elm, rootElm), isTableCell$2);
  15656. const getParentCaption = (rootElm, elm) => find$2(parentsAndSelf(elm, rootElm), isTag('caption'));
  15657. const deleteBetweenCells = (editor, rootElm, forward, fromCell, from) =>
  15658. // TODO: TINY-8865 - This may not be safe to cast as Node below and alternative solutions need to be looked into
  15659. navigate(forward, editor.getBody(), from)
  15660. .bind((to) => getParentCell(rootElm, SugarElement.fromDom(to.getNode()))
  15661. .bind((toCell) => eq(toCell, fromCell) ? Optional.none() : Optional.some(noop)));
  15662. const emptyElement = (editor, elm) => Optional.some(() => {
  15663. fillWithPaddingBr(elm);
  15664. editor.selection.setCursorLocation(elm.dom, 0);
  15665. });
  15666. const isDeleteOfLastCharPos = (fromCaption, forward, from, to) => firstPositionIn(fromCaption.dom).bind((first) => lastPositionIn(fromCaption.dom).map((last) => forward ?
  15667. from.isEqual(first) && to.isEqual(last) :
  15668. from.isEqual(last) && to.isEqual(first))).getOr(true);
  15669. const emptyCaretCaption = (editor, elm) => emptyElement(editor, elm);
  15670. const validateCaretCaption = (rootElm, fromCaption, to) =>
  15671. // TODO: TINY-8865 - This may not be safe to cast as Node below and alternative solutions need to be looked into
  15672. getParentCaption(rootElm, SugarElement.fromDom(to.getNode()))
  15673. .fold(() => Optional.some(noop), (toCaption) => someIf(!eq(toCaption, fromCaption), noop));
  15674. const deleteCaretInsideCaption = (editor, rootElm, forward, fromCaption, from) => navigate(forward, editor.getBody(), from).fold(() => Optional.some(noop), (to) => isDeleteOfLastCharPos(fromCaption, forward, from, to) ?
  15675. emptyCaretCaption(editor, fromCaption) :
  15676. validateCaretCaption(rootElm, fromCaption, to));
  15677. const deleteCaretCells = (editor, forward, rootElm, startElm) => {
  15678. const from = CaretPosition.fromRangeStart(editor.selection.getRng());
  15679. return getParentCell(rootElm, startElm).bind((fromCell) => isEmpty$4(editor.schema, fromCell, { checkRootAsContent: false }) ?
  15680. emptyElement(editor, fromCell) :
  15681. deleteBetweenCells(editor, rootElm, forward, fromCell, from));
  15682. };
  15683. const deleteCaretCaption = (editor, forward, rootElm, fromCaption) => {
  15684. const from = CaretPosition.fromRangeStart(editor.selection.getRng());
  15685. return isEmpty$4(editor.schema, fromCaption) ?
  15686. emptyElement(editor, fromCaption) :
  15687. deleteCaretInsideCaption(editor, rootElm, forward, fromCaption, from);
  15688. };
  15689. const isNearTable = (forward, pos) => forward ? isBeforeTable(pos) : isAfterTable(pos);
  15690. const isBeforeOrAfterTable = (editor, forward) => {
  15691. const fromPos = CaretPosition.fromRangeStart(editor.selection.getRng());
  15692. return isNearTable(forward, fromPos) || fromPosition(forward, editor.getBody(), fromPos)
  15693. .exists((pos) => isNearTable(forward, pos));
  15694. };
  15695. const deleteCaret$3 = (editor, forward, startElm) => {
  15696. const rootElm = SugarElement.fromDom(editor.getBody());
  15697. return getParentCaption(rootElm, startElm).fold(() => deleteCaretCells(editor, forward, rootElm, startElm)
  15698. .orThunk(() => someIf(isBeforeOrAfterTable(editor, forward), noop)), (fromCaption) => deleteCaretCaption(editor, forward, rootElm, fromCaption));
  15699. };
  15700. const backspaceDelete$d = (editor, forward) => {
  15701. const startElm = SugarElement.fromDom(editor.selection.getStart(true));
  15702. const cells = getCellsFromEditor(editor);
  15703. return editor.selection.isCollapsed() && cells.length === 0 ?
  15704. deleteCaret$3(editor, forward, startElm) :
  15705. deleteRange$4(editor, startElm, cells);
  15706. };
  15707. const getContentEditableRoot$1 = (root, node) => {
  15708. let tempNode = node;
  15709. while (tempNode && tempNode !== root) {
  15710. if (isContentEditableTrue$3(tempNode) || isContentEditableFalse$a(tempNode)) {
  15711. return tempNode;
  15712. }
  15713. tempNode = tempNode.parentNode;
  15714. }
  15715. return null;
  15716. };
  15717. const internalAttributesPrefixes = [
  15718. 'data-ephox-',
  15719. 'data-mce-',
  15720. 'data-alloy-',
  15721. 'data-snooker-',
  15722. '_'
  15723. ];
  15724. /**
  15725. * Utility class for various element specific functions.
  15726. *
  15727. * @private
  15728. * @class tinymce.dom.ElementUtils
  15729. */
  15730. const each$9 = Tools.each;
  15731. const ElementUtils = (editor) => {
  15732. const dom = editor.dom;
  15733. const internalAttributes = new Set(editor.serializer.getTempAttrs());
  15734. /**
  15735. * Compares two nodes and checks if it's attributes and styles matches.
  15736. * This doesn't compare classes as items since their order is significant.
  15737. *
  15738. * @method compare
  15739. * @param {Node} node1 First node to compare with.
  15740. * @param {Node} node2 Second node to compare with.
  15741. * @return {Boolean} True/false if the nodes are the same or not.
  15742. */
  15743. const compare = (node1, node2) => {
  15744. // Not the same name or type
  15745. if (node1.nodeName !== node2.nodeName || node1.nodeType !== node2.nodeType) {
  15746. return false;
  15747. }
  15748. /**
  15749. * Returns all the nodes attributes excluding internal ones, styles and classes.
  15750. *
  15751. * @private
  15752. * @param {Node} node Node to get attributes from.
  15753. * @return {Object} Name/value object with attributes and attribute values.
  15754. */
  15755. const getAttribs = (node) => {
  15756. const attribs = {};
  15757. each$9(dom.getAttribs(node), (attr) => {
  15758. const name = attr.nodeName.toLowerCase();
  15759. // Don't compare internal attributes or style
  15760. if (name !== 'style' && !isAttributeInternal(name)) {
  15761. attribs[name] = dom.getAttrib(node, name);
  15762. }
  15763. });
  15764. return attribs;
  15765. };
  15766. /**
  15767. * Compares two objects checks if it's key + value exists in the other one.
  15768. *
  15769. * @private
  15770. * @param {Object} obj1 First object to compare.
  15771. * @param {Object} obj2 Second object to compare.
  15772. * @return {Boolean} True/false if the objects matches or not.
  15773. */
  15774. const compareObjects = (obj1, obj2) => {
  15775. for (const name in obj1) {
  15776. // Obj1 has item obj2 doesn't have
  15777. if (has$2(obj1, name)) {
  15778. const value = obj2[name];
  15779. // Obj2 doesn't have obj1 item
  15780. if (isUndefined(value)) {
  15781. return false;
  15782. }
  15783. // Obj2 item has a different value
  15784. if (obj1[name] !== value) {
  15785. return false;
  15786. }
  15787. // Delete similar value
  15788. delete obj2[name];
  15789. }
  15790. }
  15791. // Check if obj 2 has something obj 1 doesn't have
  15792. for (const name in obj2) {
  15793. // Obj2 has item obj1 doesn't have
  15794. if (has$2(obj2, name)) {
  15795. return false;
  15796. }
  15797. }
  15798. return true;
  15799. };
  15800. if (isElement$7(node1) && isElement$7(node2)) {
  15801. // Attribs are not the same
  15802. if (!compareObjects(getAttribs(node1), getAttribs(node2))) {
  15803. return false;
  15804. }
  15805. // Styles are not the same
  15806. if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) {
  15807. return false;
  15808. }
  15809. }
  15810. return !isBookmarkNode$1(node1) && !isBookmarkNode$1(node2);
  15811. };
  15812. const isAttributeInternal = (attributeName) => exists(internalAttributesPrefixes, (value) => startsWith(attributeName, value)) || internalAttributes.has(attributeName);
  15813. return {
  15814. compare,
  15815. isAttributeInternal
  15816. };
  15817. };
  15818. const getNormalizedPoint$1 = (container, offset) => {
  15819. if (isText$b(container)) {
  15820. return { container, offset };
  15821. }
  15822. const node = RangeUtils.getNode(container, offset);
  15823. if (isText$b(node)) {
  15824. return {
  15825. container: node,
  15826. offset: offset >= container.childNodes.length ? node.data.length : 0
  15827. };
  15828. }
  15829. else if (node.previousSibling && isText$b(node.previousSibling)) {
  15830. return {
  15831. container: node.previousSibling,
  15832. offset: node.previousSibling.data.length
  15833. };
  15834. }
  15835. else if (node.nextSibling && isText$b(node.nextSibling)) {
  15836. return {
  15837. container: node.nextSibling,
  15838. offset: 0
  15839. };
  15840. }
  15841. return { container, offset };
  15842. };
  15843. const normalizeRange$1 = (rng) => {
  15844. const outRng = rng.cloneRange();
  15845. const rangeStart = getNormalizedPoint$1(rng.startContainer, rng.startOffset);
  15846. outRng.setStart(rangeStart.container, rangeStart.offset);
  15847. const rangeEnd = getNormalizedPoint$1(rng.endContainer, rng.endOffset);
  15848. outRng.setEnd(rangeEnd.container, rangeEnd.offset);
  15849. return outRng;
  15850. };
  15851. // TODO: This is a clone of the list bookmark code if we move lists to core then de-duplicate this #TINY-12172
  15852. const DOM$c = DOMUtils.DOM;
  15853. /**
  15854. * Returns a range bookmark. This will convert indexed bookmarks into temporary span elements with
  15855. * index 0 so that they can be restored properly after the DOM has been modified. Text bookmarks will not have spans
  15856. * added to them since they can be restored after a dom operation.
  15857. *
  15858. * So this: <p><b>|</b><b>|</b></p>
  15859. * becomes: <p><b><span data-mce-type="bookmark">|</span></b><b data-mce-type="bookmark">|</span></b></p>
  15860. */
  15861. const createBookmark$1 = (rng) => {
  15862. const bookmark = {};
  15863. const setupEndPoint = (start) => {
  15864. let container = rng[start ? 'startContainer' : 'endContainer'];
  15865. let offset = rng[start ? 'startOffset' : 'endOffset'];
  15866. if (isElement$7(container)) {
  15867. const offsetNode = DOM$c.create('span', { 'data-mce-type': 'bookmark' });
  15868. if (container.hasChildNodes()) {
  15869. if (offset === container.childNodes.length) {
  15870. container.appendChild(offsetNode);
  15871. }
  15872. else {
  15873. container.insertBefore(offsetNode, container.childNodes[offset]);
  15874. }
  15875. }
  15876. else {
  15877. container.appendChild(offsetNode);
  15878. }
  15879. container = offsetNode;
  15880. offset = 0;
  15881. }
  15882. bookmark[start ? 'startContainer' : 'endContainer'] = container;
  15883. bookmark[start ? 'startOffset' : 'endOffset'] = offset;
  15884. };
  15885. setupEndPoint(true);
  15886. if (!rng.collapsed) {
  15887. setupEndPoint();
  15888. }
  15889. return bookmark;
  15890. };
  15891. const resolveBookmark$2 = (bookmark) => {
  15892. const restoreEndPoint = (start) => {
  15893. const nodeIndex = (container) => {
  15894. var _a;
  15895. let node = (_a = container.parentNode) === null || _a === void 0 ? void 0 : _a.firstChild;
  15896. let idx = 0;
  15897. while (node) {
  15898. if (node === container) {
  15899. return idx;
  15900. }
  15901. // Skip data-mce-type=bookmark nodes
  15902. if (!isElement$7(node) || node.getAttribute('data-mce-type') !== 'bookmark') {
  15903. idx++;
  15904. }
  15905. node = node.nextSibling;
  15906. }
  15907. return -1;
  15908. };
  15909. let container = bookmark[start ? 'startContainer' : 'endContainer'];
  15910. let offset = bookmark[start ? 'startOffset' : 'endOffset'];
  15911. if (!container) {
  15912. return;
  15913. }
  15914. if (isElement$7(container) && container.parentNode) {
  15915. const node = container;
  15916. offset = nodeIndex(container);
  15917. container = container.parentNode;
  15918. DOM$c.remove(node);
  15919. if (!container.hasChildNodes() && DOM$c.isBlock(container)) {
  15920. container.appendChild(DOM$c.create('br'));
  15921. }
  15922. }
  15923. bookmark[start ? 'startContainer' : 'endContainer'] = container;
  15924. bookmark[start ? 'startOffset' : 'endOffset'] = offset;
  15925. };
  15926. restoreEndPoint(true);
  15927. restoreEndPoint();
  15928. const rng = DOM$c.createRng();
  15929. rng.setStart(bookmark.startContainer, bookmark.startOffset);
  15930. if (bookmark.endContainer) {
  15931. rng.setEnd(bookmark.endContainer, bookmark.endOffset);
  15932. }
  15933. return normalizeRange$1(rng);
  15934. };
  15935. const applyStyles = (dom, elm, format, vars) => {
  15936. Tools.each(format.styles, (value, name) => {
  15937. dom.setStyle(elm, name, replaceVars(value, vars));
  15938. });
  15939. // Needed for the WebKit span spam bug
  15940. // TODO: Remove this once WebKit/Blink fixes this
  15941. if (format.styles) {
  15942. const styleVal = dom.getAttrib(elm, 'style');
  15943. if (styleVal) {
  15944. dom.setAttrib(elm, 'data-mce-style', styleVal);
  15945. }
  15946. }
  15947. };
  15948. const setElementFormat = (ed, elm, fmt, vars, node) => {
  15949. const dom = ed.dom;
  15950. if (isFunction(fmt.onformat)) {
  15951. fmt.onformat(elm, fmt, vars, node);
  15952. }
  15953. applyStyles(dom, elm, fmt, vars);
  15954. Tools.each(fmt.attributes, (value, name) => {
  15955. dom.setAttrib(elm, name, replaceVars(value, vars));
  15956. });
  15957. Tools.each(fmt.classes, (value) => {
  15958. const newValue = replaceVars(value, vars);
  15959. if (!dom.hasClass(elm, newValue)) {
  15960. dom.addClass(elm, newValue);
  15961. }
  15962. });
  15963. };
  15964. const isApplyFormat = (format) => !isArray$1(format.attributes) && !isArray$1(format.styles);
  15965. const isEq$3 = isEq$5;
  15966. const matchesUnInheritedFormatSelector = (ed, node, name) => {
  15967. const formatList = ed.formatter.get(name);
  15968. if (formatList) {
  15969. for (let i = 0; i < formatList.length; i++) {
  15970. const format = formatList[i];
  15971. if (isSelectorFormat(format) && format.inherit === false && ed.dom.is(node, format.selector)) {
  15972. return true;
  15973. }
  15974. }
  15975. }
  15976. return false;
  15977. };
  15978. const matchParents = (editor, node, name, vars, similar) => {
  15979. const root = editor.dom.getRoot();
  15980. if (node === root) {
  15981. return false;
  15982. }
  15983. // Find first node with similar format settings
  15984. const matchedNode = editor.dom.getParent(node, (elm) => {
  15985. if (matchesUnInheritedFormatSelector(editor, elm, name)) {
  15986. return true;
  15987. }
  15988. return elm.parentNode === root || !!matchNode$1(editor, elm, name, vars, true);
  15989. });
  15990. // Do an exact check on the similar format element
  15991. return !!matchNode$1(editor, matchedNode, name, vars, similar);
  15992. };
  15993. const matchName = (dom, node, format) => {
  15994. // Check for inline match
  15995. if (isInlineFormat(format) && isEq$3(node, format.inline)) {
  15996. return true;
  15997. }
  15998. // Check for block match
  15999. if (isBlockFormat(format) && isEq$3(node, format.block)) {
  16000. return true;
  16001. }
  16002. // Check for selector match
  16003. if (isSelectorFormat(format)) {
  16004. return isElement$7(node) && dom.is(node, format.selector);
  16005. }
  16006. return false;
  16007. };
  16008. const matchItems = (dom, node, format, itemName, similar, vars) => {
  16009. const items = format[itemName];
  16010. const matchAttributes = itemName === 'attributes';
  16011. // Custom match
  16012. if (isFunction(format.onmatch)) {
  16013. // onmatch is generic in a way that we can't really express without casting
  16014. return format.onmatch(node, format, itemName);
  16015. }
  16016. // Check all items
  16017. if (items) {
  16018. // Non indexed object
  16019. if (!isArrayLike(items)) {
  16020. for (const key in items) {
  16021. if (has$2(items, key)) {
  16022. const value = matchAttributes ? dom.getAttrib(node, key) : getStyle(dom, node, key);
  16023. const expectedValue = replaceVars(items[key], vars);
  16024. const isEmptyValue = isNullable(value) || isEmpty$5(value);
  16025. if (isEmptyValue && isNullable(expectedValue)) {
  16026. continue;
  16027. }
  16028. if (similar && isEmptyValue && !format.exact) {
  16029. return false;
  16030. }
  16031. if ((!similar || format.exact) && !isEq$3(value, normalizeStyleValue(expectedValue, key))) {
  16032. return false;
  16033. }
  16034. }
  16035. }
  16036. }
  16037. else {
  16038. // Only one match needed for indexed arrays
  16039. for (let i = 0; i < items.length; i++) {
  16040. if (matchAttributes ? dom.getAttrib(node, items[i]) : getStyle(dom, node, items[i])) {
  16041. return true;
  16042. }
  16043. }
  16044. }
  16045. }
  16046. return true;
  16047. };
  16048. const matchNode$1 = (ed, node, name, vars, similar) => {
  16049. const formatList = ed.formatter.get(name);
  16050. const dom = ed.dom;
  16051. if (formatList && isElement$7(node)) {
  16052. // Check each format in list
  16053. for (let i = 0; i < formatList.length; i++) {
  16054. const format = formatList[i];
  16055. // Name name, attributes, styles and classes
  16056. if (matchName(ed.dom, node, format) && matchItems(dom, node, format, 'attributes', similar, vars) && matchItems(dom, node, format, 'styles', similar, vars)) {
  16057. // Match classes
  16058. const classes = format.classes;
  16059. if (classes) {
  16060. for (let x = 0; x < classes.length; x++) {
  16061. if (!ed.dom.hasClass(node, replaceVars(classes[x], vars))) {
  16062. return;
  16063. }
  16064. }
  16065. }
  16066. return format;
  16067. }
  16068. }
  16069. }
  16070. return undefined;
  16071. };
  16072. const match$2 = (editor, name, vars, node, similar) => {
  16073. // Check specified node
  16074. if (node) {
  16075. return matchParents(editor, node, name, vars, similar);
  16076. }
  16077. // Check selected node
  16078. node = editor.selection.getNode();
  16079. if (matchParents(editor, node, name, vars, similar)) {
  16080. return true;
  16081. }
  16082. // Check start node if it's different
  16083. const startNode = editor.selection.getStart();
  16084. if (startNode !== node) {
  16085. if (matchParents(editor, startNode, name, vars, similar)) {
  16086. return true;
  16087. }
  16088. }
  16089. return false;
  16090. };
  16091. const matchAll = (editor, names, vars) => {
  16092. const matchedFormatNames = [];
  16093. const checkedMap = {};
  16094. // Check start of selection for formats
  16095. const startElement = editor.selection.getStart();
  16096. editor.dom.getParent(startElement, (node) => {
  16097. for (let i = 0; i < names.length; i++) {
  16098. const name = names[i];
  16099. if (!checkedMap[name] && matchNode$1(editor, node, name, vars)) {
  16100. checkedMap[name] = true;
  16101. matchedFormatNames.push(name);
  16102. }
  16103. }
  16104. }, editor.dom.getRoot());
  16105. return matchedFormatNames;
  16106. };
  16107. const closest = (editor, names) => {
  16108. const isRoot = (elm) => eq(elm, SugarElement.fromDom(editor.getBody()));
  16109. const match = (elm, name) => matchNode$1(editor, elm.dom, name) ? Optional.some(name) : Optional.none();
  16110. return Optional.from(editor.selection.getStart(true)).bind((rawElm) => closest$1(SugarElement.fromDom(rawElm), (elm) => findMap(names, (name) => match(elm, name)), isRoot)).getOrNull();
  16111. };
  16112. const canApply = (editor, name) => {
  16113. const formatList = editor.formatter.get(name);
  16114. const dom = editor.dom;
  16115. if (formatList && editor.selection.isEditable()) {
  16116. const startNode = editor.selection.getStart();
  16117. const parents = getParents$2(dom, startNode);
  16118. for (let x = formatList.length - 1; x >= 0; x--) {
  16119. const format = formatList[x];
  16120. // Format is not selector based then always return TRUE
  16121. if (!isSelectorFormat(format)) {
  16122. return true;
  16123. }
  16124. for (let i = parents.length - 1; i >= 0; i--) {
  16125. if (dom.is(parents[i], format.selector)) {
  16126. return true;
  16127. }
  16128. }
  16129. }
  16130. }
  16131. return false;
  16132. };
  16133. /**
  16134. * Get all of the format names present on the specified node
  16135. */
  16136. const matchAllOnNode = (editor, node, formatNames) => foldl(formatNames, (acc, name) => {
  16137. const matchSimilar = isVariableFormatName(editor, name);
  16138. if (editor.formatter.matchNode(node, name, {}, matchSimilar)) {
  16139. return acc.concat([name]);
  16140. }
  16141. else {
  16142. return acc;
  16143. }
  16144. }, []);
  16145. const ZWSP = ZWSP$1;
  16146. const importNode = (ownerDocument, node) => {
  16147. return ownerDocument.importNode(node, true);
  16148. };
  16149. const findFirstTextNode = (node) => {
  16150. if (node) {
  16151. const walker = new DomTreeWalker(node, node);
  16152. for (let tempNode = walker.current(); tempNode; tempNode = walker.next()) {
  16153. if (isText$b(tempNode)) {
  16154. return tempNode;
  16155. }
  16156. }
  16157. }
  16158. return null;
  16159. };
  16160. const createCaretContainer = (fill) => {
  16161. const caretContainer = SugarElement.fromTag('span');
  16162. setAll$1(caretContainer, {
  16163. // style: 'color:red',
  16164. 'id': CARET_ID,
  16165. 'data-mce-bogus': '1',
  16166. 'data-mce-type': 'format-caret'
  16167. });
  16168. if (fill) {
  16169. append$1(caretContainer, SugarElement.fromText(ZWSP));
  16170. }
  16171. return caretContainer;
  16172. };
  16173. const trimZwspFromCaretContainer = (caretContainerNode) => {
  16174. const textNode = findFirstTextNode(caretContainerNode);
  16175. if (textNode && textNode.data.charAt(0) === ZWSP) {
  16176. textNode.deleteData(0, 1);
  16177. }
  16178. return textNode;
  16179. };
  16180. const removeCaretContainerNode = (editor, node, moveCaret) => {
  16181. const dom = editor.dom, selection = editor.selection;
  16182. if (isCaretContainerEmpty(node)) {
  16183. deleteElement$2(editor, false, SugarElement.fromDom(node), moveCaret, true);
  16184. }
  16185. else {
  16186. const rng = selection.getRng();
  16187. const block = dom.getParent(node, dom.isBlock);
  16188. // Store the current selection offsets
  16189. const startContainer = rng.startContainer;
  16190. const startOffset = rng.startOffset;
  16191. const endContainer = rng.endContainer;
  16192. const endOffset = rng.endOffset;
  16193. const textNode = trimZwspFromCaretContainer(node);
  16194. dom.remove(node, true);
  16195. // Restore the selection after unwrapping the node and removing the zwsp
  16196. if (startContainer === textNode && startOffset > 0) {
  16197. rng.setStart(textNode, startOffset - 1);
  16198. }
  16199. if (endContainer === textNode && endOffset > 0) {
  16200. rng.setEnd(textNode, endOffset - 1);
  16201. }
  16202. if (block && dom.isEmpty(block)) {
  16203. fillWithPaddingBr(SugarElement.fromDom(block));
  16204. }
  16205. selection.setRng(rng);
  16206. }
  16207. };
  16208. // Removes the caret container for the specified node or all on the current document
  16209. const removeCaretContainer = (editor, node, moveCaret) => {
  16210. const dom = editor.dom, selection = editor.selection;
  16211. if (!node) {
  16212. node = getParentCaretContainer(editor.getBody(), selection.getStart());
  16213. if (!node) {
  16214. while ((node = dom.get(CARET_ID))) {
  16215. removeCaretContainerNode(editor, node, moveCaret);
  16216. }
  16217. }
  16218. }
  16219. else {
  16220. removeCaretContainerNode(editor, node, moveCaret);
  16221. }
  16222. };
  16223. const insertCaretContainerNode = (editor, caretContainer, formatNode) => {
  16224. var _a, _b;
  16225. const dom = editor.dom;
  16226. const block = dom.getParent(formatNode, curry(isTextBlock$2, editor.schema));
  16227. if (block && dom.isEmpty(block)) {
  16228. // Replace formatNode with caretContainer when removing format from empty block like <p><b>|</b></p>
  16229. (_a = formatNode.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(caretContainer, formatNode);
  16230. }
  16231. else {
  16232. removeTrailingBr(SugarElement.fromDom(formatNode));
  16233. if (dom.isEmpty(formatNode)) {
  16234. (_b = formatNode.parentNode) === null || _b === void 0 ? void 0 : _b.replaceChild(caretContainer, formatNode);
  16235. }
  16236. else {
  16237. dom.insertAfter(caretContainer, formatNode);
  16238. }
  16239. }
  16240. };
  16241. const appendNode = (parentNode, node) => {
  16242. parentNode.appendChild(node);
  16243. return node;
  16244. };
  16245. const insertFormatNodesIntoCaretContainer = (formatNodes, caretContainer) => {
  16246. var _a;
  16247. const innerMostFormatNode = foldr(formatNodes, (parentNode, formatNode) => {
  16248. return appendNode(parentNode, formatNode.cloneNode(false));
  16249. }, caretContainer);
  16250. const doc = (_a = innerMostFormatNode.ownerDocument) !== null && _a !== void 0 ? _a : document;
  16251. return appendNode(innerMostFormatNode, doc.createTextNode(ZWSP));
  16252. };
  16253. const cleanFormatNode = (editor, caretContainer, formatNode, name, vars, similar) => {
  16254. const formatter = editor.formatter;
  16255. const dom = editor.dom;
  16256. // Find all formats present on the format node
  16257. const validFormats = filter$5(keys(formatter.get()), (formatName) => formatName !== name && !contains$1(formatName, 'removeformat'));
  16258. const matchedFormats = matchAllOnNode(editor, formatNode, validFormats);
  16259. // Filter out any matched formats that are 'visually' equivalent to the 'name' format since they are not unique formats on the node
  16260. const uniqueFormats = filter$5(matchedFormats, (fmtName) => !areSimilarFormats(editor, fmtName, name));
  16261. // If more than one format is present, then there's additional formats that should be retained. So clone the node,
  16262. // remove the format and then return cleaned format node
  16263. if (uniqueFormats.length > 0) {
  16264. const clonedFormatNode = formatNode.cloneNode(false);
  16265. dom.add(caretContainer, clonedFormatNode);
  16266. formatter.remove(name, vars, clonedFormatNode, similar);
  16267. dom.remove(clonedFormatNode);
  16268. return Optional.some(clonedFormatNode);
  16269. }
  16270. else {
  16271. return Optional.none();
  16272. }
  16273. };
  16274. const normalizeNbsps = (node) => set$1(node, get$4(node).replace(new RegExp(`${nbsp}$`), ' '));
  16275. const normalizeNbspsBetween = (editor, caretContainer) => {
  16276. const handler = () => {
  16277. if (caretContainer !== null && !editor.dom.isEmpty(caretContainer)) {
  16278. prevSibling(SugarElement.fromDom(caretContainer)).each((node) => {
  16279. if (isText$c(node)) {
  16280. normalizeNbsps(node);
  16281. }
  16282. else {
  16283. descendant$2(node, (e) => isText$c(e)).each((textNode) => {
  16284. if (isText$c(textNode)) {
  16285. normalizeNbsps(textNode);
  16286. }
  16287. });
  16288. }
  16289. });
  16290. }
  16291. };
  16292. editor.once('input', (e) => {
  16293. if (e.data && !isWhiteSpace(e.data)) {
  16294. if (!e.isComposing) {
  16295. handler();
  16296. }
  16297. else {
  16298. editor.once('compositionend', () => {
  16299. handler();
  16300. });
  16301. }
  16302. }
  16303. });
  16304. };
  16305. const applyCaretFormat = (editor, name, vars) => {
  16306. let caretContainer;
  16307. const selection = editor.selection;
  16308. const formatList = editor.formatter.get(name);
  16309. if (!formatList) {
  16310. return;
  16311. }
  16312. const selectionRng = selection.getRng();
  16313. let offset = selectionRng.startOffset;
  16314. const container = selectionRng.startContainer;
  16315. const text = container.nodeValue;
  16316. caretContainer = getParentCaretContainer(editor.getBody(), selection.getStart());
  16317. // Expand to word if caret is in the middle of a text node and the char before/after is a alpha numeric character
  16318. const wordcharRegex = /[^\s\u00a0\u00ad\u200b\ufeff]/;
  16319. if (text && offset > 0 && offset < text.length &&
  16320. wordcharRegex.test(text.charAt(offset)) && wordcharRegex.test(text.charAt(offset - 1))) {
  16321. // Get bookmark of caret position
  16322. const bookmark = selection.getBookmark();
  16323. // Collapse bookmark range (WebKit)
  16324. selectionRng.collapse(true);
  16325. // Expand the range to the closest word and split it at those points
  16326. let rng = expandRng(editor.dom, selectionRng, formatList);
  16327. rng = split(rng);
  16328. // Apply the format to the range
  16329. editor.formatter.apply(name, vars, rng);
  16330. // Move selection back to caret position
  16331. selection.moveToBookmark(bookmark);
  16332. }
  16333. else {
  16334. let textNode = caretContainer ? findFirstTextNode(caretContainer) : null;
  16335. if (!caretContainer || (textNode === null || textNode === void 0 ? void 0 : textNode.data) !== ZWSP) {
  16336. // Need to import the node into the document on IE or we get a lovely WrongDocument exception
  16337. caretContainer = importNode(editor.getDoc(), createCaretContainer(true).dom);
  16338. textNode = caretContainer.firstChild;
  16339. selectionRng.insertNode(caretContainer);
  16340. offset = 1;
  16341. normalizeNbspsBetween(editor, caretContainer);
  16342. editor.formatter.apply(name, vars, caretContainer);
  16343. }
  16344. else {
  16345. editor.formatter.apply(name, vars, caretContainer);
  16346. }
  16347. // Move selection to text node
  16348. selection.setCursorLocation(textNode, offset);
  16349. }
  16350. };
  16351. const removeCaretFormat = (editor, name, vars, similar) => {
  16352. const dom = editor.dom;
  16353. const selection = editor.selection;
  16354. let hasContentAfter = false;
  16355. const formatList = editor.formatter.get(name);
  16356. if (!formatList) {
  16357. return;
  16358. }
  16359. const rng = selection.getRng();
  16360. const container = rng.startContainer;
  16361. const offset = rng.startOffset;
  16362. let node = container;
  16363. if (isText$b(container)) {
  16364. if (offset !== container.data.length) {
  16365. hasContentAfter = true;
  16366. }
  16367. node = node.parentNode;
  16368. }
  16369. const parents = [];
  16370. let formatNode;
  16371. while (node) {
  16372. if (matchNode$1(editor, node, name, vars, similar)) {
  16373. formatNode = node;
  16374. break;
  16375. }
  16376. if (node.nextSibling) {
  16377. hasContentAfter = true;
  16378. }
  16379. parents.push(node);
  16380. node = node.parentNode;
  16381. }
  16382. // Node doesn't have the specified format
  16383. if (!formatNode) {
  16384. return;
  16385. }
  16386. // Is there contents after the caret then remove the format on the element
  16387. if (hasContentAfter) {
  16388. const bookmark = selection.getBookmark();
  16389. // Collapse bookmark range (WebKit)
  16390. rng.collapse(true);
  16391. // Expand the range to the closest word and split it at those points
  16392. let expandedRng = expandRng(dom, rng, formatList, { includeTrailingSpace: true });
  16393. expandedRng = split(expandedRng);
  16394. // TODO: Figure out how on earth this works, as it shouldn't since remove format
  16395. // definitely seems to require an actual Range
  16396. editor.formatter.remove(name, vars, expandedRng, similar);
  16397. selection.moveToBookmark(bookmark);
  16398. }
  16399. else {
  16400. const caretContainer = getParentCaretContainer(editor.getBody(), formatNode);
  16401. const parentsAfter = isNonNullable(caretContainer) ? dom.getParents(formatNode.parentNode, always, caretContainer) : [];
  16402. const newCaretContainer = createCaretContainer(false).dom;
  16403. insertCaretContainerNode(editor, newCaretContainer, caretContainer !== null && caretContainer !== void 0 ? caretContainer : formatNode);
  16404. const cleanedFormatNode = cleanFormatNode(editor, newCaretContainer, formatNode, name, vars, similar);
  16405. const caretTextNode = insertFormatNodesIntoCaretContainer([
  16406. ...parents,
  16407. ...cleanedFormatNode.toArray(),
  16408. ...parentsAfter
  16409. ], newCaretContainer);
  16410. if (caretContainer) {
  16411. removeCaretContainerNode(editor, caretContainer, isNonNullable(caretContainer));
  16412. }
  16413. selection.setCursorLocation(caretTextNode, 1);
  16414. normalizeNbspsBetween(editor, newCaretContainer);
  16415. if (dom.isEmpty(formatNode)) {
  16416. dom.remove(formatNode);
  16417. }
  16418. }
  16419. };
  16420. const disableCaretContainer = (editor, keyCode, moveCaret) => {
  16421. const selection = editor.selection, body = editor.getBody();
  16422. removeCaretContainer(editor, null, moveCaret);
  16423. // Remove caret container if it's empty
  16424. if ((keyCode === 8 || keyCode === 46) && selection.isCollapsed() && selection.getStart().innerHTML === ZWSP) {
  16425. removeCaretContainer(editor, getParentCaretContainer(body, selection.getStart()), true);
  16426. }
  16427. // Remove caret container on keydown and it's left/right arrow keys
  16428. if (keyCode === 37 || keyCode === 39) {
  16429. removeCaretContainer(editor, getParentCaretContainer(body, selection.getStart()), true);
  16430. }
  16431. };
  16432. const endsWithNbsp = (element) => isText$b(element) && endsWith(element.data, nbsp);
  16433. const setup$B = (editor) => {
  16434. editor.on('mouseup keydown', (e) => {
  16435. disableCaretContainer(editor, e.keyCode, endsWithNbsp(editor.selection.getRng().endContainer));
  16436. });
  16437. };
  16438. const createCaretFormat = (formatNodes) => {
  16439. const caretContainer = createCaretContainer(false);
  16440. const innerMost = insertFormatNodesIntoCaretContainer(formatNodes, caretContainer.dom);
  16441. return { caretContainer, caretPosition: CaretPosition(innerMost, 0) };
  16442. };
  16443. const replaceWithCaretFormat = (targetNode, formatNodes) => {
  16444. const { caretContainer, caretPosition } = createCaretFormat(formatNodes);
  16445. before$4(SugarElement.fromDom(targetNode), caretContainer);
  16446. remove$8(SugarElement.fromDom(targetNode));
  16447. return caretPosition;
  16448. };
  16449. const createCaretFormatAtStart$1 = (rng, formatNodes) => {
  16450. const { caretContainer, caretPosition } = createCaretFormat(formatNodes);
  16451. rng.insertNode(caretContainer.dom);
  16452. return caretPosition;
  16453. };
  16454. const isFormatElement = (editor, element) => {
  16455. if (isCaretNode(element.dom)) {
  16456. return false;
  16457. }
  16458. const inlineElements = editor.schema.getTextInlineElements();
  16459. return has$2(inlineElements, name(element)) && !isCaretNode(element.dom) && !isBogus$1(element.dom);
  16460. };
  16461. const listItemStyles = ['fontWeight', 'fontStyle', 'color', 'fontSize', 'fontFamily'];
  16462. const hasListStyles = (fmt) => isObject(fmt.styles) && exists(keys(fmt.styles), (name) => contains$2(listItemStyles, name));
  16463. const findExpandedListItemFormat = (formats) => find$2(formats, (fmt) => isInlineFormat(fmt) && fmt.inline === 'span' && hasListStyles(fmt));
  16464. const getExpandedListItemFormat = (formatter, format) => {
  16465. const formatList = formatter.get(format);
  16466. return isArray$1(formatList) ? findExpandedListItemFormat(formatList) : Optional.none();
  16467. };
  16468. const isRngStartAtStartOfElement = (rng, elm) => prevPosition(elm, CaretPosition.fromRangeStart(rng)).isNone();
  16469. const isRngEndAtEndOfElement = (rng, elm) => {
  16470. return nextPosition(elm, CaretPosition.fromRangeEnd(rng))
  16471. .exists((pos) => !isBr$7(pos.getNode()) || nextPosition(elm, pos).isSome()) === false;
  16472. };
  16473. const isEditableListItem = (dom) => (elm) => isListItem$3(elm) && dom.isEditable(elm);
  16474. const getFullySelectedBlocks = (selection) => {
  16475. const blocks = selection.getSelectedBlocks();
  16476. const rng = selection.getRng();
  16477. if (selection.isCollapsed()) {
  16478. return [];
  16479. }
  16480. if (blocks.length === 1) {
  16481. return isRngStartAtStartOfElement(rng, blocks[0]) && isRngEndAtEndOfElement(rng, blocks[0]) ? blocks : [];
  16482. }
  16483. else {
  16484. const first = head(blocks).filter((elm) => isRngStartAtStartOfElement(rng, elm)).toArray();
  16485. const last = last$2(blocks).filter((elm) => isRngEndAtEndOfElement(rng, elm)).toArray();
  16486. const middle = blocks.slice(1, -1);
  16487. return first.concat(middle).concat(last);
  16488. }
  16489. };
  16490. const getFullySelectedListItems = (selection) => filter$5(getFullySelectedBlocks(selection), isEditableListItem(selection.dom));
  16491. const getPartiallySelectedListItems = (selection) => filter$5(selection.getSelectedBlocks(), isEditableListItem(selection.dom));
  16492. const each$8 = Tools.each;
  16493. const isElementNode = (node) => isElement$7(node) && !isBookmarkNode$1(node) && !isCaretNode(node) && !isBogus$1(node);
  16494. const findElementSibling = (node, siblingName) => {
  16495. for (let sibling = node; sibling; sibling = sibling[siblingName]) {
  16496. if (isText$b(sibling) && isNotEmpty(sibling.data)) {
  16497. return node;
  16498. }
  16499. if (isElement$7(sibling) && !isBookmarkNode$1(sibling)) {
  16500. return sibling;
  16501. }
  16502. }
  16503. return node;
  16504. };
  16505. const mergeSiblingsNodes = (editor, prev, next) => {
  16506. const elementUtils = ElementUtils(editor);
  16507. const isPrevEditable = isHTMLElement(prev) && editor.dom.isEditable(prev);
  16508. const isNextEditable = isHTMLElement(next) && editor.dom.isEditable(next);
  16509. // Check if next/prev exists and that they are elements
  16510. if (isPrevEditable && isNextEditable) {
  16511. // If previous sibling is empty then jump over it
  16512. const prevSibling = findElementSibling(prev, 'previousSibling');
  16513. const nextSibling = findElementSibling(next, 'nextSibling');
  16514. // Compare next and previous nodes
  16515. if (elementUtils.compare(prevSibling, nextSibling)) {
  16516. // Append nodes between
  16517. for (let sibling = prevSibling.nextSibling; sibling && sibling !== nextSibling;) {
  16518. const tmpSibling = sibling;
  16519. sibling = sibling.nextSibling;
  16520. prevSibling.appendChild(tmpSibling);
  16521. }
  16522. editor.dom.remove(nextSibling);
  16523. Tools.each(Tools.grep(nextSibling.childNodes), (node) => {
  16524. prevSibling.appendChild(node);
  16525. });
  16526. return prevSibling;
  16527. }
  16528. }
  16529. return next;
  16530. };
  16531. const mergeSiblings = (editor, format, vars, node) => {
  16532. var _a;
  16533. // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b>
  16534. // Note: mergeSiblingNodes attempts to not merge sibilings if they are noneditable
  16535. if (node && format.merge_siblings !== false) {
  16536. // Previous sibling
  16537. const newNode = (_a = mergeSiblingsNodes(editor, getNonWhiteSpaceSibling(node), node)) !== null && _a !== void 0 ? _a : node;
  16538. // Next sibling
  16539. mergeSiblingsNodes(editor, newNode, getNonWhiteSpaceSibling(newNode, true));
  16540. }
  16541. };
  16542. const clearChildStyles = (dom, format, node) => {
  16543. if (format.clear_child_styles) {
  16544. const selector = format.links ? '*:not(a)' : '*';
  16545. each$8(dom.select(selector, node), (childNode) => {
  16546. if (isElementNode(childNode) && dom.isEditable(childNode)) {
  16547. each$8(format.styles, (_value, name) => {
  16548. dom.setStyle(childNode, name, '');
  16549. });
  16550. }
  16551. });
  16552. }
  16553. };
  16554. const processChildElements = (node, filter, process) => {
  16555. each$8(node.childNodes, (node) => {
  16556. if (isElementNode(node)) {
  16557. if (filter(node)) {
  16558. process(node);
  16559. }
  16560. if (node.hasChildNodes()) {
  16561. processChildElements(node, filter, process);
  16562. }
  16563. }
  16564. });
  16565. };
  16566. const unwrapEmptySpan = (dom, node) => {
  16567. if (node.nodeName === 'SPAN' && dom.getAttribs(node).length === 0) {
  16568. dom.remove(node, true);
  16569. }
  16570. };
  16571. const hasStyle = (dom, name) => (node) => !!(node && getStyle(dom, node, name));
  16572. const applyStyle = (dom, name, value) => (node) => {
  16573. dom.setStyle(node, name, value);
  16574. if (node.getAttribute('style') === '') {
  16575. node.removeAttribute('style');
  16576. }
  16577. unwrapEmptySpan(dom, node);
  16578. };
  16579. const removeResult = Adt.generate([
  16580. { keep: [] },
  16581. { rename: ['name'] },
  16582. { removed: [] }
  16583. ]);
  16584. const MCE_ATTR_RE = /^(src|href|style)$/;
  16585. const each$7 = Tools.each;
  16586. const isEq$2 = isEq$5;
  16587. const isTableCellOrRow = (node) => /^(TR|TH|TD)$/.test(node.nodeName);
  16588. const isChildOfInlineParent = (dom, node, parent) => dom.isChildOf(node, parent) && node !== parent && !dom.isBlock(parent);
  16589. const getContainer = (ed, rng, start) => {
  16590. let container = rng[start ? 'startContainer' : 'endContainer'];
  16591. let offset = rng[start ? 'startOffset' : 'endOffset'];
  16592. if (isElement$7(container)) {
  16593. const lastIdx = container.childNodes.length - 1;
  16594. if (!start && offset) {
  16595. offset--;
  16596. }
  16597. container = container.childNodes[offset > lastIdx ? lastIdx : offset];
  16598. }
  16599. // If start text node is excluded then walk to the next node
  16600. if (isText$b(container) && start && offset >= container.data.length) {
  16601. container = new DomTreeWalker(container, ed.getBody()).next() || container;
  16602. }
  16603. // If end text node is excluded then walk to the previous node
  16604. if (isText$b(container) && !start && offset === 0) {
  16605. container = new DomTreeWalker(container, ed.getBody()).prev() || container;
  16606. }
  16607. return container;
  16608. };
  16609. const normalizeTableSelection = (node, start) => {
  16610. const prop = start ? 'firstChild' : 'lastChild';
  16611. const childNode = node[prop];
  16612. if (isTableCellOrRow(node) && childNode) {
  16613. if (node.nodeName === 'TR') {
  16614. return childNode[prop] || childNode;
  16615. }
  16616. else {
  16617. return childNode;
  16618. }
  16619. }
  16620. return node;
  16621. };
  16622. const wrap$1 = (dom, node, name, attrs) => {
  16623. var _a;
  16624. const wrapper = dom.create(name, attrs);
  16625. (_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(wrapper, node);
  16626. wrapper.appendChild(node);
  16627. return wrapper;
  16628. };
  16629. const wrapWithSiblings = (dom, node, next, name, attrs) => {
  16630. const start = SugarElement.fromDom(node);
  16631. const wrapper = SugarElement.fromDom(dom.create(name, attrs));
  16632. const siblings = next ? nextSiblings(start) : prevSiblings(start);
  16633. append(wrapper, siblings);
  16634. if (next) {
  16635. before$4(start, wrapper);
  16636. prepend(wrapper, start);
  16637. }
  16638. else {
  16639. after$4(start, wrapper);
  16640. append$1(wrapper, start);
  16641. }
  16642. return wrapper.dom;
  16643. };
  16644. const isColorFormatAndAnchor = (node, format) => format.links && node.nodeName === 'A';
  16645. /**
  16646. * Removes the node and wrap it's children in paragraphs before doing so or
  16647. * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled.
  16648. *
  16649. * If the div in the node below gets removed:
  16650. * text<div>text</div>text
  16651. *
  16652. * Output becomes:
  16653. * text<div><br />text<br /></div>text
  16654. *
  16655. * So when the div is removed the result is:
  16656. * text<br />text<br />text
  16657. *
  16658. * @private
  16659. * @param {Node} node Node to remove + apply BR/P elements to.
  16660. * @param {Object} format Format rule.
  16661. * @return {Node} Input node.
  16662. */
  16663. const removeNode = (ed, node, format) => {
  16664. const parentNode = node.parentNode;
  16665. let rootBlockElm;
  16666. const dom = ed.dom;
  16667. const forcedRootBlock = getForcedRootBlock(ed);
  16668. if (isBlockFormat(format)) {
  16669. // Wrap the block in a forcedRootBlock if we are at the root of document
  16670. if (parentNode === dom.getRoot()) {
  16671. if (!format.list_block || !isEq$2(node, format.list_block)) {
  16672. each$e(from(node.childNodes), (node) => {
  16673. if (isValid(ed, forcedRootBlock, node.nodeName.toLowerCase())) {
  16674. if (!rootBlockElm) {
  16675. rootBlockElm = wrap$1(dom, node, forcedRootBlock);
  16676. dom.setAttribs(rootBlockElm, getForcedRootBlockAttrs(ed));
  16677. }
  16678. else {
  16679. rootBlockElm.appendChild(node);
  16680. }
  16681. }
  16682. else {
  16683. rootBlockElm = null;
  16684. }
  16685. });
  16686. }
  16687. }
  16688. }
  16689. // Never remove nodes that aren't the specified inline element if a selector is specified too
  16690. if (isMixedFormat(format) && !isEq$2(format.inline, node)) {
  16691. return;
  16692. }
  16693. dom.remove(node, true);
  16694. };
  16695. // Attributes or styles can be either an array of names or an object containing name/value pairs
  16696. const processFormatAttrOrStyle = (name, value, vars) => {
  16697. // Indexed array
  16698. if (isNumber(name)) {
  16699. return {
  16700. name: value,
  16701. value: null
  16702. };
  16703. }
  16704. else {
  16705. return {
  16706. name,
  16707. value: replaceVars(value, vars)
  16708. };
  16709. }
  16710. };
  16711. const removeEmptyStyleAttributeIfNeeded = (dom, elm) => {
  16712. if (dom.getAttrib(elm, 'style') === '') {
  16713. elm.removeAttribute('style');
  16714. elm.removeAttribute('data-mce-style');
  16715. }
  16716. };
  16717. const removeStyles$1 = (dom, elm, format, vars, compareNode) => {
  16718. let stylesModified = false;
  16719. each$7(format.styles, (value, name) => {
  16720. const { name: styleName, value: styleValue } = processFormatAttrOrStyle(name, value, vars);
  16721. const normalizedStyleValue = normalizeStyleValue(styleValue, styleName);
  16722. if (format.remove_similar || isNull(styleValue) || !isElement$7(compareNode) || isEq$2(getStyle(dom, compareNode, styleName), normalizedStyleValue)) {
  16723. dom.setStyle(elm, styleName, '');
  16724. }
  16725. stylesModified = true;
  16726. });
  16727. if (stylesModified) {
  16728. removeEmptyStyleAttributeIfNeeded(dom, elm);
  16729. }
  16730. };
  16731. const removeListStyleFormats = (editor, name, vars) => {
  16732. if (name === 'removeformat') {
  16733. each$e(getPartiallySelectedListItems(editor.selection), (li) => {
  16734. each$e(listItemStyles, (name) => editor.dom.setStyle(li, name, ''));
  16735. removeEmptyStyleAttributeIfNeeded(editor.dom, li);
  16736. });
  16737. }
  16738. else {
  16739. getExpandedListItemFormat(editor.formatter, name).each((liFmt) => {
  16740. each$e(getPartiallySelectedListItems(editor.selection), (li) => removeStyles$1(editor.dom, li, liFmt, vars, null));
  16741. });
  16742. }
  16743. };
  16744. const removeNodeFormatInternal = (ed, format, vars, node, compareNode) => {
  16745. const dom = ed.dom;
  16746. const elementUtils = ElementUtils(ed);
  16747. const schema = ed.schema;
  16748. // Root level block transparents should get converted into regular text blocks
  16749. if (isInlineFormat(format) && isTransparentElementName(schema, format.inline) && isTransparentBlock(schema, node) && node.parentElement === ed.getBody()) {
  16750. removeNode(ed, node, format);
  16751. return removeResult.removed();
  16752. }
  16753. // Check if node is noneditable and can have the format removed from it
  16754. if (!format.ceFalseOverride && node && dom.getContentEditableParent(node) === 'false') {
  16755. return removeResult.keep();
  16756. }
  16757. // Check if node matches format
  16758. if (node && !matchName(dom, node, format) && !isColorFormatAndAnchor(node, format)) {
  16759. return removeResult.keep();
  16760. }
  16761. // "matchName" will made sure we're dealing with an element, so cast as one
  16762. const elm = node;
  16763. // Applies to styling elements like strong, em, i, u, etc. so that if they have styling attributes, the attributes can be kept but the styling element is removed
  16764. const preserveAttributes = format.preserve_attributes;
  16765. if (isInlineFormat(format) && format.remove === 'all' && isArray$1(preserveAttributes)) {
  16766. // Remove all attributes except for the attributes specified in preserve_attributes
  16767. const attrsToPreserve = filter$5(dom.getAttribs(elm), (attr) => contains$2(preserveAttributes, attr.name.toLowerCase()));
  16768. dom.removeAllAttribs(elm);
  16769. each$e(attrsToPreserve, (attr) => dom.setAttrib(elm, attr.name, attr.value));
  16770. // Note: If there are no attributes left, the element will be removed as normal at the end of the function
  16771. if (attrsToPreserve.length > 0) {
  16772. // Convert inline element to span if necessary
  16773. return removeResult.rename('span');
  16774. }
  16775. }
  16776. // Should we compare with format attribs and styles
  16777. if (format.remove !== 'all') {
  16778. removeStyles$1(dom, elm, format, vars, compareNode);
  16779. // Remove attributes
  16780. each$7(format.attributes, (value, name) => {
  16781. const { name: attrName, value: attrValue } = processFormatAttrOrStyle(name, value, vars);
  16782. if (format.remove_similar || isNull(attrValue) || !isElement$7(compareNode) || isEq$2(dom.getAttrib(compareNode, attrName), attrValue)) {
  16783. // Keep internal classes
  16784. if (attrName === 'class') {
  16785. const currentValue = dom.getAttrib(elm, attrName);
  16786. if (currentValue) {
  16787. // Build new class value where everything is removed except the internal prefixed classes
  16788. let valueOut = '';
  16789. each$e(currentValue.split(/\s+/), (cls) => {
  16790. if (/mce\-\w+/.test(cls)) {
  16791. valueOut += (valueOut ? ' ' : '') + cls;
  16792. }
  16793. });
  16794. // We got some internal classes left
  16795. if (valueOut) {
  16796. dom.setAttrib(elm, attrName, valueOut);
  16797. return;
  16798. }
  16799. }
  16800. }
  16801. // Remove mce prefixed attributes (must clean before short circuit operations)
  16802. if (MCE_ATTR_RE.test(attrName)) {
  16803. elm.removeAttribute('data-mce-' + attrName);
  16804. }
  16805. // keep style="list-style-type: none" on <li>s
  16806. if (attrName === 'style' && matchNodeNames$1(['li'])(elm) && dom.getStyle(elm, 'list-style-type') === 'none') {
  16807. elm.removeAttribute(attrName);
  16808. dom.setStyle(elm, 'list-style-type', 'none');
  16809. return;
  16810. }
  16811. // IE6 has a bug where the attribute doesn't get removed correctly
  16812. if (attrName === 'class') {
  16813. elm.removeAttribute('className');
  16814. }
  16815. elm.removeAttribute(attrName);
  16816. }
  16817. });
  16818. // Remove classes
  16819. each$7(format.classes, (value) => {
  16820. value = replaceVars(value, vars);
  16821. if (!isElement$7(compareNode) || dom.hasClass(compareNode, value)) {
  16822. dom.removeClass(elm, value);
  16823. }
  16824. });
  16825. // Check for non internal attributes
  16826. const attrs = dom.getAttribs(elm);
  16827. for (let i = 0; i < attrs.length; i++) {
  16828. const attrName = attrs[i].nodeName;
  16829. if (!elementUtils.isAttributeInternal(attrName)) {
  16830. return removeResult.keep();
  16831. }
  16832. }
  16833. }
  16834. // Remove the inline child if it's empty for example <b> or <span>
  16835. if (format.remove !== 'none') {
  16836. removeNode(ed, elm, format);
  16837. return removeResult.removed();
  16838. }
  16839. return removeResult.keep();
  16840. };
  16841. const findFormatRoot = (editor, container, name, vars, similar) => {
  16842. let formatRoot;
  16843. if (container.parentNode) {
  16844. // Find format root
  16845. each$e(getParents$2(editor.dom, container.parentNode).reverse(), (parent) => {
  16846. // Find format root element
  16847. if (!formatRoot && isElement$7(parent) && parent.id !== '_start' && parent.id !== '_end') {
  16848. // Is the node matching the format we are looking for
  16849. const format = matchNode$1(editor, parent, name, vars, similar);
  16850. if (format && format.split !== false) {
  16851. formatRoot = parent;
  16852. }
  16853. }
  16854. });
  16855. }
  16856. return formatRoot;
  16857. };
  16858. const removeNodeFormatFromClone = (editor, format, vars, clone) => removeNodeFormatInternal(editor, format, vars, clone, clone).fold(constant(clone), (newName) => {
  16859. // To rename a node, it needs to be a child of another node
  16860. const fragment = editor.dom.createFragment();
  16861. fragment.appendChild(clone);
  16862. // If renaming we are guaranteed this is a Element, so cast
  16863. return editor.dom.rename(clone, newName);
  16864. }, constant(null));
  16865. const wrapAndSplit = (editor, formatList, formatRoot, container, target, split, format, vars) => {
  16866. var _a, _b;
  16867. let lastClone;
  16868. let firstClone;
  16869. const dom = editor.dom;
  16870. // Format root found then clone formats and split it
  16871. if (formatRoot) {
  16872. const formatRootParent = formatRoot.parentNode;
  16873. for (let parent = container.parentNode; parent && parent !== formatRootParent; parent = parent.parentNode) {
  16874. let clone = dom.clone(parent, false);
  16875. for (let i = 0; i < formatList.length; i++) {
  16876. clone = removeNodeFormatFromClone(editor, formatList[i], vars, clone);
  16877. if (clone === null) {
  16878. break;
  16879. }
  16880. }
  16881. // Build wrapper node
  16882. if (clone) {
  16883. if (lastClone) {
  16884. clone.appendChild(lastClone);
  16885. }
  16886. if (!firstClone) {
  16887. firstClone = clone;
  16888. }
  16889. lastClone = clone;
  16890. }
  16891. }
  16892. // Never split block elements if the format is mixed
  16893. if (split && (!format.mixed || !dom.isBlock(formatRoot))) {
  16894. container = (_a = dom.split(formatRoot, container)) !== null && _a !== void 0 ? _a : container;
  16895. }
  16896. // Wrap container in cloned formats
  16897. if (lastClone && firstClone) {
  16898. (_b = target.parentNode) === null || _b === void 0 ? void 0 : _b.insertBefore(lastClone, target);
  16899. firstClone.appendChild(target);
  16900. // After splitting the nodes may match with other siblings so we need to attempt to merge them
  16901. // Note: We can't use MergeFormats, as that'd create a circular dependency
  16902. if (isInlineFormat(format)) {
  16903. mergeSiblings(editor, format, vars, lastClone);
  16904. }
  16905. }
  16906. }
  16907. return container;
  16908. };
  16909. const removeFormatInternal = (ed, name, vars, node, similar) => {
  16910. const formatList = ed.formatter.get(name);
  16911. const format = formatList[0];
  16912. const dom = ed.dom;
  16913. const selection = ed.selection;
  16914. const splitToFormatRoot = (container) => {
  16915. const formatRoot = findFormatRoot(ed, container, name, vars, similar);
  16916. return wrapAndSplit(ed, formatList, formatRoot, container, container, true, format, vars);
  16917. };
  16918. // Make sure to only check for bookmarks created here (eg _start or _end)
  16919. // as there maybe nested bookmarks
  16920. const isRemoveBookmarkNode = (node) => isBookmarkNode$1(node) && isElement$7(node) && (node.id === '_start' || node.id === '_end');
  16921. const removeFormatOnNode = (node) => exists(formatList, (fmt) => removeNodeFormat(ed, fmt, vars, node, node));
  16922. // Merges the styles for each node
  16923. const process = (node) => {
  16924. // Grab the children first since the nodelist might be changed
  16925. const children = from(node.childNodes);
  16926. // Process current node
  16927. const removed = removeFormatOnNode(node);
  16928. // TINY-6567/TINY-7393: Include the parent if using an expanded selector format and no match was found for the current node
  16929. const currentNodeMatches = removed || exists(formatList, (f) => matchName(dom, node, f));
  16930. const parentNode = node.parentNode;
  16931. if (!currentNodeMatches && isNonNullable(parentNode) && shouldExpandToSelector(format)) {
  16932. removeFormatOnNode(parentNode);
  16933. }
  16934. // Process the children
  16935. if (format.deep) {
  16936. if (children.length) {
  16937. for (let i = 0; i < children.length; i++) {
  16938. process(children[i]);
  16939. }
  16940. }
  16941. }
  16942. // Note: Assists with cleaning up any stray text decorations that may been applied when text decorations
  16943. // and text colors were merged together from an applied format
  16944. // Remove child span if it only contains text-decoration and a parent node also has the same text decoration.
  16945. const textDecorations = ['underline', 'line-through', 'overline'];
  16946. each$e(textDecorations, (decoration) => {
  16947. if (isElement$7(node) && ed.dom.getStyle(node, 'text-decoration') === decoration &&
  16948. node.parentNode && getTextDecoration(dom, node.parentNode) === decoration) {
  16949. removeNodeFormat(ed, {
  16950. deep: false,
  16951. exact: true,
  16952. inline: 'span',
  16953. styles: {
  16954. textDecoration: decoration
  16955. }
  16956. }, undefined, node);
  16957. }
  16958. });
  16959. };
  16960. const unwrap = (start) => {
  16961. const node = dom.get(start ? '_start' : '_end');
  16962. if (node) {
  16963. let out = node[start ? 'firstChild' : 'lastChild'];
  16964. // If the end is placed within the start the result will be removed
  16965. // So this checks if the out node is a bookmark node if it is it
  16966. // checks for another more suitable node
  16967. if (isRemoveBookmarkNode(out)) {
  16968. out = out[start ? 'firstChild' : 'lastChild'];
  16969. }
  16970. // Since dom.remove removes empty text nodes then we need to try to find a better node
  16971. if (isText$b(out) && out.data.length === 0) {
  16972. out = start ? node.previousSibling || node.nextSibling : node.nextSibling || node.previousSibling;
  16973. }
  16974. dom.remove(node, true);
  16975. return out;
  16976. }
  16977. else {
  16978. return null;
  16979. }
  16980. };
  16981. const removeRngStyle = (rng) => {
  16982. let startContainer;
  16983. let endContainer;
  16984. let expandedRng = expandRng(dom, rng, formatList, { includeTrailingSpace: rng.collapsed });
  16985. if (format.split) {
  16986. // Split text nodes
  16987. expandedRng = split(expandedRng);
  16988. startContainer = getContainer(ed, expandedRng, true);
  16989. endContainer = getContainer(ed, expandedRng);
  16990. if (startContainer !== endContainer) {
  16991. // WebKit will render the table incorrectly if we wrap a TH or TD in a SPAN
  16992. // so let's see if we can use the first/last child instead
  16993. // This will happen if you triple click a table cell and use remove formatting
  16994. startContainer = normalizeTableSelection(startContainer, true);
  16995. endContainer = normalizeTableSelection(endContainer, false);
  16996. // Wrap and split if nested
  16997. if (isChildOfInlineParent(dom, startContainer, endContainer)) {
  16998. const marker = Optional.from(startContainer.firstChild).getOr(startContainer);
  16999. splitToFormatRoot(wrapWithSiblings(dom, marker, true, 'span', { 'id': '_start', 'data-mce-type': 'bookmark' }));
  17000. unwrap(true);
  17001. return;
  17002. }
  17003. // Wrap and split if nested
  17004. if (isChildOfInlineParent(dom, endContainer, startContainer)) {
  17005. const marker = Optional.from(endContainer.lastChild).getOr(endContainer);
  17006. splitToFormatRoot(wrapWithSiblings(dom, marker, false, 'span', { 'id': '_end', 'data-mce-type': 'bookmark' }));
  17007. unwrap(false);
  17008. return;
  17009. }
  17010. // Wrap start/end nodes in span element since these might be cloned/moved
  17011. startContainer = wrap$1(dom, startContainer, 'span', { 'id': '_start', 'data-mce-type': 'bookmark' });
  17012. endContainer = wrap$1(dom, endContainer, 'span', { 'id': '_end', 'data-mce-type': 'bookmark' });
  17013. // Split start/end and anything in between
  17014. const newRng = dom.createRng();
  17015. newRng.setStartAfter(startContainer);
  17016. newRng.setEndBefore(endContainer);
  17017. walk$3(dom, newRng, (nodes) => {
  17018. each$e(nodes, (n) => {
  17019. if (!isBookmarkNode$1(n) && !isBookmarkNode$1(n.parentNode)) {
  17020. splitToFormatRoot(n);
  17021. }
  17022. });
  17023. });
  17024. splitToFormatRoot(startContainer);
  17025. splitToFormatRoot(endContainer);
  17026. // Unwrap start/end to get real elements again
  17027. // Note that the return value should always be a node since it's wrapped above
  17028. startContainer = unwrap(true);
  17029. endContainer = unwrap();
  17030. }
  17031. else {
  17032. startContainer = endContainer = splitToFormatRoot(startContainer);
  17033. }
  17034. // Update range positions since they might have changed after the split operations
  17035. expandedRng.startContainer = startContainer.parentNode ? startContainer.parentNode : startContainer;
  17036. expandedRng.startOffset = dom.nodeIndex(startContainer);
  17037. expandedRng.endContainer = endContainer.parentNode ? endContainer.parentNode : endContainer;
  17038. expandedRng.endOffset = dom.nodeIndex(endContainer) + 1;
  17039. }
  17040. // Remove items between start/end
  17041. walk$3(dom, expandedRng, (nodes) => {
  17042. each$e(nodes, process);
  17043. });
  17044. };
  17045. // Handle node
  17046. if (node) {
  17047. if (isNode(node)) {
  17048. const rng = dom.createRng();
  17049. rng.setStartBefore(node);
  17050. rng.setEndAfter(node);
  17051. removeRngStyle(rng);
  17052. }
  17053. else {
  17054. removeRngStyle(node);
  17055. }
  17056. fireFormatRemove(ed, name, node, vars);
  17057. return;
  17058. }
  17059. if (!selection.isCollapsed() || !isInlineFormat(format) || getCellsFromEditor(ed).length) {
  17060. // Remove formatting on the selection
  17061. preserveSelection(ed, () => runOnRanges(ed, removeRngStyle),
  17062. // Before trying to move the start of the selection, check if start element still has formatting then we are at: "<b>text|</b>text"
  17063. // and need to move the start into the next text node
  17064. (startNode) => isInlineFormat(format) && match$2(ed, name, vars, startNode));
  17065. ed.nodeChanged();
  17066. }
  17067. else {
  17068. removeCaretFormat(ed, name, vars, similar);
  17069. }
  17070. removeListStyleFormats(ed, name, vars);
  17071. fireFormatRemove(ed, name, node, vars);
  17072. };
  17073. const removeFormat$1 = (ed, name, vars, node, similar) => {
  17074. if (node || ed.selection.isEditable()) {
  17075. removeFormatInternal(ed, name, vars, node, similar);
  17076. }
  17077. };
  17078. const removeFormatOnElement = (editor, format, vars, node) => {
  17079. return removeNodeFormatInternal(editor, format, vars, node).fold(() => Optional.some(node), (newName) => Optional.some(editor.dom.rename(node, newName)), Optional.none);
  17080. };
  17081. /**
  17082. * Removes the specified format for the specified node. It will also remove the node if it doesn't have
  17083. * any attributes if the format specifies it to do so.
  17084. *
  17085. * @private
  17086. * @param {Object} format Format object with items to remove from node.
  17087. * @param {Object} vars Name/value object with variables to apply to format.
  17088. * @param {Node} node Node to remove the format styles on.
  17089. * @param {Node} compareNode Optional compare node, if specified the styles will be compared to that node.
  17090. * @return {Boolean} True/false if the node was removed or not.
  17091. */
  17092. const removeNodeFormat = (editor, format, vars, node, compareNode) => {
  17093. return removeNodeFormatInternal(editor, format, vars, node, compareNode).fold(never, (newName) => {
  17094. // If renaming we are guaranteed this is a Element, so cast
  17095. editor.dom.rename(node, newName);
  17096. return true;
  17097. }, always);
  17098. };
  17099. const fontSizeAlteringFormats = ['fontsize', 'subscript', 'superscript'];
  17100. const formatsToActOn = ['strikethrough', ...fontSizeAlteringFormats];
  17101. const hasFormat = (formatter, el, format) => isNonNullable(formatter.matchNode(el.dom, format, {}, format === 'fontsize'));
  17102. const isFontSizeAlteringElement = (formatter, el) => exists(fontSizeAlteringFormats, (format) => hasFormat(formatter, el, format));
  17103. const isNormalizingFormat = (format) => contains$2(formatsToActOn, format);
  17104. const gatherWrapperData = (isRoot, scope, hasFormat, createFormatElement, removeFormatFromElement) => {
  17105. const parents = parents$1(scope, isRoot).filter(isElement$8);
  17106. return findLastIndex(parents, hasFormat).map((index) => {
  17107. const container = parents[index];
  17108. const innerWrapper = createFormatElement(container);
  17109. const outerWrappers = [
  17110. ...removeFormatFromElement(shallow(container)).toArray(),
  17111. ...bind$3(parents.slice(0, index), (wrapper) => {
  17112. if (hasFormat(wrapper)) {
  17113. return removeFormatFromElement(wrapper).toArray();
  17114. }
  17115. else {
  17116. return [shallow(wrapper)];
  17117. }
  17118. })
  17119. ];
  17120. return { container, innerWrapper, outerWrappers };
  17121. });
  17122. };
  17123. const wrapChildrenInInnerWrapper = (target, wrapper, hasFormat, removeFormatFromElement) => {
  17124. each$e(children$1(target), (child) => {
  17125. if (isElement$8(child) && hasFormat(child)) {
  17126. if (removeFormatFromElement(child).isNone()) {
  17127. unwrap(child);
  17128. }
  17129. }
  17130. });
  17131. each$e(children$1(target), (child) => append$1(wrapper, child));
  17132. prepend(target, wrapper);
  17133. };
  17134. const wrapInOuterWrappers = (target, wrappers) => {
  17135. if (wrappers.length > 0) {
  17136. const outermost = wrappers[wrappers.length - 1];
  17137. before$4(target, outermost);
  17138. const innerMost = foldl(wrappers.slice(0, wrappers.length - 1), (acc, wrapper) => {
  17139. append$1(acc, wrapper);
  17140. return wrapper;
  17141. }, outermost);
  17142. append$1(innerMost, target);
  17143. }
  17144. };
  17145. const normalizeFontSizeElementsInternal = (domUtils, fontSizeElements, hasFormat, createFormatElement, removeFormatFromElement) => {
  17146. const isRoot = (el) => eq(SugarElement.fromDom(domUtils.getRoot()), el) || domUtils.isBlock(el.dom);
  17147. each$e(fontSizeElements, (fontSizeElement) => {
  17148. gatherWrapperData(isRoot, fontSizeElement, hasFormat, createFormatElement, removeFormatFromElement).each(({ container, innerWrapper, outerWrappers }) => {
  17149. domUtils.split(container.dom, fontSizeElement.dom);
  17150. wrapChildrenInInnerWrapper(fontSizeElement, innerWrapper, hasFormat, removeFormatFromElement);
  17151. wrapInOuterWrappers(fontSizeElement, outerWrappers);
  17152. });
  17153. });
  17154. };
  17155. const normalizeFontSizeElementsWithFormat = (editor, formatName, fontSizeElements) => {
  17156. const hasFormat = (el) => isNonNullable(matchNode$1(editor, el.dom, formatName));
  17157. const createFormatElement = (el) => {
  17158. const newEl = SugarElement.fromTag(name(el));
  17159. const format = matchNode$1(editor, el.dom, formatName, {});
  17160. if (isNonNullable(format) && isApplyFormat(format)) {
  17161. setElementFormat(editor, newEl.dom, format);
  17162. }
  17163. return newEl;
  17164. };
  17165. const removeFormatFromElement = (el) => {
  17166. const format = matchNode$1(editor, el.dom, formatName, {});
  17167. if (isNonNullable(format)) {
  17168. return removeFormatOnElement(editor, format, {}, el.dom).map(SugarElement.fromDom);
  17169. }
  17170. else {
  17171. return Optional.some(el);
  17172. }
  17173. };
  17174. const bookmark = createBookmark$1(editor.selection.getRng());
  17175. normalizeFontSizeElementsInternal(editor.dom, fontSizeElements, hasFormat, createFormatElement, removeFormatFromElement);
  17176. editor.selection.setRng(resolveBookmark$2(bookmark));
  17177. };
  17178. const collectFontSizeElements = (formatter, wrappers) => bind$3(wrappers, (wrapper) => {
  17179. const fontSizeDescendants = descendants$1(wrapper, (el) => isFontSizeAlteringElement(formatter, el));
  17180. return isFontSizeAlteringElement(formatter, wrapper) ? [wrapper, ...fontSizeDescendants] : fontSizeDescendants;
  17181. });
  17182. const normalizeFontSizeElementsAfterApply = (editor, appliedFormat, wrappers) => {
  17183. if (isNormalizingFormat(appliedFormat)) {
  17184. const fontSizeElements = collectFontSizeElements(editor.formatter, wrappers);
  17185. normalizeFontSizeElementsWithFormat(editor, 'strikethrough', fontSizeElements);
  17186. }
  17187. };
  17188. const normalizeElements = (editor, elements) => {
  17189. const fontSizeElements = filter$5(elements, (el) => isFontSizeAlteringElement(editor.formatter, el));
  17190. normalizeFontSizeElementsWithFormat(editor, 'strikethrough', fontSizeElements);
  17191. };
  17192. const isHeading = (node) => ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.name);
  17193. const isSummary = (node) => node.name === 'summary';
  17194. const traverse = (root, fn) => {
  17195. let node = root;
  17196. while ((node = node.walk())) {
  17197. fn(node);
  17198. }
  17199. };
  17200. // Test a single node against the current filters, and add it to any match lists if necessary
  17201. const matchNode = (nodeFilters, attributeFilters, node, matches) => {
  17202. const name = node.name;
  17203. // Match node filters
  17204. for (let ni = 0, nl = nodeFilters.length; ni < nl; ni++) {
  17205. const filter = nodeFilters[ni];
  17206. if (filter.name === name) {
  17207. const match = matches.nodes[name];
  17208. if (match) {
  17209. match.nodes.push(node);
  17210. }
  17211. else {
  17212. matches.nodes[name] = { filter, nodes: [node] };
  17213. }
  17214. }
  17215. }
  17216. // Match attribute filters
  17217. if (node.attributes) {
  17218. for (let ai = 0, al = attributeFilters.length; ai < al; ai++) {
  17219. const filter = attributeFilters[ai];
  17220. const attrName = filter.name;
  17221. if (attrName in node.attributes.map) {
  17222. const match = matches.attributes[attrName];
  17223. if (match) {
  17224. match.nodes.push(node);
  17225. }
  17226. else {
  17227. matches.attributes[attrName] = { filter, nodes: [node] };
  17228. }
  17229. }
  17230. }
  17231. }
  17232. };
  17233. const findMatchingNodes = (nodeFilters, attributeFilters, node) => {
  17234. const matches = { nodes: {}, attributes: {} };
  17235. if (node.firstChild) {
  17236. traverse(node, (childNode) => {
  17237. matchNode(nodeFilters, attributeFilters, childNode, matches);
  17238. });
  17239. }
  17240. return matches;
  17241. };
  17242. // Run all necessary node filters and attribute filters, based on a match set
  17243. const runFilters = (matches, args) => {
  17244. const run = (matchRecord, filteringAttributes) => {
  17245. each$d(matchRecord, (match) => {
  17246. // in theory we don't need to copy the array, it was created purely for this filtering, but the method is exported so we can't guarantee that
  17247. const nodes = from(match.nodes);
  17248. each$e(match.filter.callbacks, (callback) => {
  17249. // very very carefully mutate the nodes array based on whether the filter still matches them
  17250. for (let i = nodes.length - 1; i >= 0; i--) {
  17251. const node = nodes[i];
  17252. // Remove already removed children, and nodes that no longer match the filter
  17253. const valueMatches = filteringAttributes ? node.attr(match.filter.name) !== undefined : node.name === match.filter.name;
  17254. if (!valueMatches || isNullable(node.parent)) {
  17255. nodes.splice(i, 1);
  17256. }
  17257. }
  17258. if (nodes.length > 0) {
  17259. callback(nodes, match.filter.name, args);
  17260. }
  17261. });
  17262. });
  17263. };
  17264. run(matches.nodes, false);
  17265. run(matches.attributes, true);
  17266. };
  17267. const filter$1 = (nodeFilters, attributeFilters, node, args = {}) => {
  17268. const matches = findMatchingNodes(nodeFilters, attributeFilters, node);
  17269. runFilters(matches, args);
  17270. };
  17271. const paddEmptyNode = (settings, args, isBlock, node) => {
  17272. const brPreferred = settings.pad_empty_with_br || args.insert;
  17273. if (brPreferred && isBlock(node)) {
  17274. const astNode = new AstNode('br', 1);
  17275. if (args.insert) {
  17276. astNode.attr('data-mce-bogus', '1');
  17277. }
  17278. node.empty().append(astNode);
  17279. }
  17280. else {
  17281. node.empty().append(new AstNode('#text', 3)).value = nbsp;
  17282. }
  17283. };
  17284. const isPaddedWithNbsp = (node) => { var _a; return hasOnlyChild(node, '#text') && ((_a = node === null || node === void 0 ? void 0 : node.firstChild) === null || _a === void 0 ? void 0 : _a.value) === nbsp; };
  17285. const hasOnlyChild = (node, name) => {
  17286. const firstChild = node === null || node === void 0 ? void 0 : node.firstChild;
  17287. return isNonNullable(firstChild) && firstChild === node.lastChild && firstChild.name === name;
  17288. };
  17289. const isPadded = (schema, node) => {
  17290. const rule = schema.getElementRule(node.name);
  17291. return (rule === null || rule === void 0 ? void 0 : rule.paddEmpty) === true;
  17292. };
  17293. const isEmpty$2 = (schema, nonEmptyElements, whitespaceElements, node) => node.isEmpty(nonEmptyElements, whitespaceElements, (node) => isPadded(schema, node));
  17294. const isLineBreakNode = (node, isBlock) => isNonNullable(node) && (isBlock(node) || node.name === 'br');
  17295. const findClosestEditingHost = (scope) => {
  17296. let editableNode;
  17297. for (let node = scope; node; node = node.parent) {
  17298. const contentEditable = node.attr('contenteditable');
  17299. if (contentEditable === 'false') {
  17300. break;
  17301. }
  17302. else if (contentEditable === 'true') {
  17303. editableNode = node;
  17304. }
  17305. }
  17306. return Optional.from(editableNode);
  17307. };
  17308. const getAllDescendants = (scope) => {
  17309. const collection = [];
  17310. for (let node = scope.firstChild; isNonNullable(node); node = node.walk()) {
  17311. collection.push(node);
  17312. }
  17313. return collection;
  17314. };
  17315. const removeOrUnwrapInvalidNode = (node, schema, originalNodeParent = node.parent) => {
  17316. if (schema.getSpecialElements()[node.name]) {
  17317. node.empty().remove();
  17318. }
  17319. else {
  17320. // are the children of `node` valid children of the top level parent?
  17321. // if not, remove or unwrap them too
  17322. const children = node.children();
  17323. for (const childNode of children) {
  17324. if (originalNodeParent && !schema.isValidChild(originalNodeParent.name, childNode.name)) {
  17325. removeOrUnwrapInvalidNode(childNode, schema, originalNodeParent);
  17326. }
  17327. }
  17328. node.unwrap();
  17329. }
  17330. };
  17331. const cleanInvalidNodes = (nodes, schema, rootNode, onCreate = noop) => {
  17332. const textBlockElements = schema.getTextBlockElements();
  17333. const nonEmptyElements = schema.getNonEmptyElements();
  17334. const whitespaceElements = schema.getWhitespaceElements();
  17335. const nonSplittableElements = Tools.makeMap('tr,td,th,tbody,thead,tfoot,table,summary');
  17336. const fixed = new Set();
  17337. const isSplittableElement = (node) => node !== rootNode && !nonSplittableElements[node.name];
  17338. for (let ni = 0; ni < nodes.length; ni++) {
  17339. const node = nodes[ni];
  17340. let parent;
  17341. let newParent;
  17342. let tempNode;
  17343. // Don't bother if it's detached from the tree
  17344. if (!node.parent || fixed.has(node)) {
  17345. continue;
  17346. }
  17347. // If the invalid element is a text block, and the text block is within a parent LI element
  17348. // Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office
  17349. if (textBlockElements[node.name] && node.parent.name === 'li') {
  17350. // Move sibling text blocks after LI element
  17351. let sibling = node.next;
  17352. while (sibling) {
  17353. if (textBlockElements[sibling.name]) {
  17354. sibling.name = 'li';
  17355. fixed.add(sibling);
  17356. node.parent.insert(sibling, node.parent);
  17357. }
  17358. else {
  17359. break;
  17360. }
  17361. sibling = sibling.next;
  17362. }
  17363. // Unwrap current text block
  17364. node.unwrap();
  17365. continue;
  17366. }
  17367. // Get list of all parent nodes until we find a valid parent to stick the child into
  17368. const parents = [node];
  17369. for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && isSplittableElement(parent); parent = parent.parent) {
  17370. parents.push(parent);
  17371. }
  17372. // Found a suitable parent
  17373. if (parent && parents.length > 1) {
  17374. // If the node is a valid child of the parent, then try to move it. Otherwise unwrap it
  17375. if (!isInvalid(schema, node, parent)) {
  17376. // Reverse the array since it makes looping easier
  17377. parents.reverse();
  17378. // Clone the related parent and insert that after the moved node
  17379. newParent = parents[0].clone();
  17380. onCreate(newParent);
  17381. // Start cloning and moving children on the left side of the target node
  17382. let currentNode = newParent;
  17383. for (let i = 0; i < parents.length - 1; i++) {
  17384. if (schema.isValidChild(currentNode.name, parents[i].name) && i > 0) {
  17385. tempNode = parents[i].clone();
  17386. onCreate(tempNode);
  17387. currentNode.append(tempNode);
  17388. }
  17389. else {
  17390. tempNode = currentNode;
  17391. }
  17392. for (let childNode = parents[i].firstChild; childNode && childNode !== parents[i + 1];) {
  17393. const nextNode = childNode.next;
  17394. tempNode.append(childNode);
  17395. childNode = nextNode;
  17396. }
  17397. currentNode = tempNode;
  17398. }
  17399. if (!isEmpty$2(schema, nonEmptyElements, whitespaceElements, newParent)) {
  17400. parent.insert(newParent, parents[0], true);
  17401. parent.insert(node, newParent);
  17402. }
  17403. else {
  17404. parent.insert(node, parents[0], true);
  17405. }
  17406. // Check if the element is empty by looking through its contents, with special treatment for <p><br /></p>
  17407. parent = parents[0];
  17408. if (isEmpty$2(schema, nonEmptyElements, whitespaceElements, parent) || hasOnlyChild(parent, 'br')) {
  17409. parent.empty().remove();
  17410. }
  17411. }
  17412. else {
  17413. removeOrUnwrapInvalidNode(node, schema);
  17414. }
  17415. }
  17416. else if (node.parent) {
  17417. // If it's an LI try to find a UL/OL for it or wrap it
  17418. if (node.name === 'li') {
  17419. let sibling = node.prev;
  17420. if (sibling && (sibling.name === 'ul' || sibling.name === 'ol')) {
  17421. sibling.append(node);
  17422. continue;
  17423. }
  17424. sibling = node.next;
  17425. if (sibling && (sibling.name === 'ul' || sibling.name === 'ol') && sibling.firstChild) {
  17426. sibling.insert(node, sibling.firstChild, true);
  17427. continue;
  17428. }
  17429. const wrapper = new AstNode('ul', 1);
  17430. onCreate(wrapper);
  17431. node.wrap(wrapper);
  17432. continue;
  17433. }
  17434. // Try wrapping the element in a DIV
  17435. if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) {
  17436. const wrapper = new AstNode('div', 1);
  17437. onCreate(wrapper);
  17438. node.wrap(wrapper);
  17439. }
  17440. else {
  17441. // We failed wrapping it, remove or unwrap it
  17442. removeOrUnwrapInvalidNode(node, schema);
  17443. }
  17444. }
  17445. }
  17446. };
  17447. const hasClosest = (node, parentName) => {
  17448. let tempNode = node;
  17449. while (tempNode) {
  17450. if (tempNode.name === parentName) {
  17451. return true;
  17452. }
  17453. tempNode = tempNode.parent;
  17454. }
  17455. return false;
  17456. };
  17457. // The `parent` parameter of `isInvalid` function represents the closest valid parent
  17458. // under which the `node` is intended to be moved.
  17459. const isInvalid = (schema, node, parent = node.parent) => {
  17460. if (!parent) {
  17461. return false;
  17462. }
  17463. // Check if the node is a valid child of the parent node. If the child is
  17464. // unknown we don't collect it since it's probably a custom element
  17465. if (schema.children[node.name] && !schema.isValidChild(parent.name, node.name)) {
  17466. return true;
  17467. }
  17468. // Anchors are a special case and cannot be nested
  17469. if (node.name === 'a' && hasClosest(parent, 'a')) {
  17470. return true;
  17471. }
  17472. // heading element is valid if it is the only one child of summary
  17473. if (isSummary(parent) && isHeading(node)) {
  17474. return !((parent === null || parent === void 0 ? void 0 : parent.firstChild) === node && (parent === null || parent === void 0 ? void 0 : parent.lastChild) === node);
  17475. }
  17476. return false;
  17477. };
  17478. const createRange = (sc, so, ec, eo) => {
  17479. const rng = document.createRange();
  17480. rng.setStart(sc, so);
  17481. rng.setEnd(ec, eo);
  17482. return rng;
  17483. };
  17484. // If you triple click a paragraph in this case:
  17485. // <blockquote><p>a</p></blockquote><p>b</p>
  17486. // It would become this range in webkit:
  17487. // <blockquote><p>[a</p></blockquote><p>]b</p>
  17488. // We would want it to be:
  17489. // <blockquote><p>[a]</p></blockquote><p>b</p>
  17490. // Since it would otherwise produces spans out of thin air on insertContent for example.
  17491. const normalizeBlockSelectionRange = (rng) => {
  17492. const startPos = CaretPosition.fromRangeStart(rng);
  17493. const endPos = CaretPosition.fromRangeEnd(rng);
  17494. const rootNode = rng.commonAncestorContainer;
  17495. return fromPosition(false, rootNode, endPos)
  17496. .map((newEndPos) => {
  17497. if (!isInSameBlock(startPos, endPos, rootNode) && isInSameBlock(startPos, newEndPos, rootNode)) {
  17498. return createRange(startPos.container(), startPos.offset(), newEndPos.container(), newEndPos.offset());
  17499. }
  17500. else {
  17501. return rng;
  17502. }
  17503. }).getOr(rng);
  17504. };
  17505. const normalize = (rng) => rng.collapsed ? rng : normalizeBlockSelectionRange(rng);
  17506. const explode$1 = Tools.explode;
  17507. const create$8 = () => {
  17508. const filters = {};
  17509. const addFilter = (name, callback) => {
  17510. each$e(explode$1(name), (name) => {
  17511. if (!has$2(filters, name)) {
  17512. filters[name] = { name, callbacks: [] };
  17513. }
  17514. filters[name].callbacks.push(callback);
  17515. });
  17516. };
  17517. const getFilters = () => values(filters);
  17518. const removeFilter = (name, callback) => {
  17519. each$e(explode$1(name), (name) => {
  17520. if (has$2(filters, name)) {
  17521. if (isNonNullable(callback)) {
  17522. const filter = filters[name];
  17523. const newCallbacks = filter$5(filter.callbacks, (c) => c !== callback);
  17524. // If all callbacks have been removed then remove the filter reference
  17525. if (newCallbacks.length > 0) {
  17526. filter.callbacks = newCallbacks;
  17527. }
  17528. else {
  17529. delete filters[name];
  17530. }
  17531. }
  17532. else {
  17533. delete filters[name];
  17534. }
  17535. }
  17536. });
  17537. };
  17538. return {
  17539. addFilter,
  17540. getFilters,
  17541. removeFilter
  17542. };
  17543. };
  17544. const encodeData = (data) => data.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  17545. const decodeData$1 = (data) => data.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
  17546. const removeAttrs = (node, names) => {
  17547. each$e(names, (name) => {
  17548. node.attr(name, null);
  17549. });
  17550. };
  17551. const addFontToSpansFilter = (domParser, styles, fontSizes) => {
  17552. domParser.addNodeFilter('font', (nodes) => {
  17553. each$e(nodes, (node) => {
  17554. const props = styles.parse(node.attr('style'));
  17555. const color = node.attr('color');
  17556. const face = node.attr('face');
  17557. const size = node.attr('size');
  17558. if (color) {
  17559. props.color = color;
  17560. }
  17561. if (face) {
  17562. props['font-family'] = face;
  17563. }
  17564. if (size) {
  17565. toInt(size).each((num) => {
  17566. props['font-size'] = fontSizes[num - 1];
  17567. });
  17568. }
  17569. node.name = 'span';
  17570. node.attr('style', styles.serialize(props));
  17571. removeAttrs(node, ['color', 'face', 'size']);
  17572. });
  17573. });
  17574. };
  17575. const addStrikeFilter = (domParser, schema, styles) => {
  17576. domParser.addNodeFilter('strike', (nodes) => {
  17577. const convertToSTag = schema.type !== 'html4';
  17578. each$e(nodes, (node) => {
  17579. if (convertToSTag) {
  17580. node.name = 's';
  17581. }
  17582. else {
  17583. const props = styles.parse(node.attr('style'));
  17584. props['text-decoration'] = 'line-through';
  17585. node.name = 'span';
  17586. node.attr('style', styles.serialize(props));
  17587. }
  17588. });
  17589. });
  17590. };
  17591. const addFilters = (domParser, settings, schema) => {
  17592. var _a;
  17593. const styles = Styles();
  17594. if (settings.convert_fonts_to_spans) {
  17595. addFontToSpansFilter(domParser, styles, Tools.explode((_a = settings.font_size_legacy_values) !== null && _a !== void 0 ? _a : ''));
  17596. }
  17597. addStrikeFilter(domParser, schema, styles);
  17598. };
  17599. const register$5 = (domParser, settings, schema) => {
  17600. if (settings.inline_styles) {
  17601. addFilters(domParser, settings, schema);
  17602. }
  17603. };
  17604. const blobUriToBlob = (url) => fetch(url)
  17605. .then((res) => res.ok ? res.blob() : Promise.reject())
  17606. .catch(() => Promise.reject({
  17607. message: `Cannot convert ${url} to Blob. Resource might not exist or is inaccessible.`,
  17608. uriType: 'blob'
  17609. }));
  17610. const extractBase64Data = (data) => {
  17611. const matches = /([a-z0-9+\/=\s]+)/i.exec(data);
  17612. return matches ? matches[1] : '';
  17613. };
  17614. const decodeData = (data) => {
  17615. try {
  17616. return decodeURIComponent(data);
  17617. }
  17618. catch (_a) {
  17619. return data;
  17620. }
  17621. };
  17622. const parseDataUri = (uri) => {
  17623. const [type, ...rest] = uri.split(',');
  17624. const data = rest.join(',');
  17625. const matches = /data:([^/]+\/[^;]+)(;.+)?/.exec(type);
  17626. if (matches) {
  17627. const base64Encoded = matches[2] === ';base64';
  17628. const decodedData = decodeData(data);
  17629. const extractedData = base64Encoded ? extractBase64Data(decodedData) : decodedData;
  17630. return Optional.some({
  17631. type: matches[1],
  17632. data: extractedData,
  17633. base64Encoded
  17634. });
  17635. }
  17636. else {
  17637. return Optional.none();
  17638. }
  17639. };
  17640. const buildBlob = (type, data, base64Encoded = true) => {
  17641. let str = data;
  17642. if (base64Encoded) {
  17643. // Might throw error if data isn't proper base64
  17644. try {
  17645. str = atob(data);
  17646. }
  17647. catch (_a) {
  17648. return Optional.none();
  17649. }
  17650. }
  17651. const arr = new Uint8Array(str.length);
  17652. for (let i = 0; i < arr.length; i++) {
  17653. arr[i] = str.charCodeAt(i);
  17654. }
  17655. return Optional.some(new Blob([arr], { type }));
  17656. };
  17657. const dataUriToBlob = (uri) => {
  17658. return new Promise((resolve, reject) => {
  17659. parseDataUri(uri)
  17660. .bind(({ type, data, base64Encoded }) => buildBlob(type, data, base64Encoded))
  17661. .fold(() => reject('Invalid data URI'), resolve);
  17662. });
  17663. };
  17664. const uriToBlob = (url) => {
  17665. if (startsWith(url, 'blob:')) {
  17666. return blobUriToBlob(url);
  17667. }
  17668. else if (startsWith(url, 'data:')) {
  17669. return dataUriToBlob(url);
  17670. }
  17671. else {
  17672. return Promise.reject('Unknown URI format');
  17673. }
  17674. };
  17675. const blobToDataUri = (blob) => {
  17676. return new Promise((resolve, reject) => {
  17677. const reader = new FileReader();
  17678. reader.onloadend = () => {
  17679. resolve(reader.result);
  17680. };
  17681. reader.onerror = () => {
  17682. var _a;
  17683. reject((_a = reader.error) === null || _a === void 0 ? void 0 : _a.message);
  17684. };
  17685. reader.readAsDataURL(blob);
  17686. });
  17687. };
  17688. let count$1 = 0;
  17689. const uniqueId$1 = (prefix) => {
  17690. return (prefix || 'blobid') + (count$1++);
  17691. };
  17692. const processDataUri = (dataUri, base64Only, generateBlobInfo) => {
  17693. return parseDataUri(dataUri).bind(({ data, type, base64Encoded }) => {
  17694. if (base64Only && !base64Encoded) {
  17695. return Optional.none();
  17696. }
  17697. else {
  17698. const base64 = base64Encoded ? data : btoa(data);
  17699. return generateBlobInfo(base64, type);
  17700. }
  17701. });
  17702. };
  17703. const createBlobInfo$1 = (blobCache, blob, base64) => {
  17704. const blobInfo = blobCache.create(uniqueId$1(), blob, base64);
  17705. blobCache.add(blobInfo);
  17706. return blobInfo;
  17707. };
  17708. const dataUriToBlobInfo = (blobCache, dataUri, base64Only = false) => {
  17709. return processDataUri(dataUri, base64Only, (base64, type) => Optional.from(blobCache.getByData(base64, type)).orThunk(() => buildBlob(type, base64).map((blob) => createBlobInfo$1(blobCache, blob, base64))));
  17710. };
  17711. const imageToBlobInfo = (blobCache, imageSrc) => {
  17712. const invalidDataUri = () => Promise.reject('Invalid data URI');
  17713. if (startsWith(imageSrc, 'blob:')) {
  17714. const blobInfo = blobCache.getByUri(imageSrc);
  17715. if (isNonNullable(blobInfo)) {
  17716. return Promise.resolve(blobInfo);
  17717. }
  17718. else {
  17719. return uriToBlob(imageSrc).then((blob) => {
  17720. return blobToDataUri(blob).then((dataUri) => {
  17721. return processDataUri(dataUri, false, (base64) => {
  17722. return Optional.some(createBlobInfo$1(blobCache, blob, base64));
  17723. }).getOrThunk(invalidDataUri);
  17724. });
  17725. });
  17726. }
  17727. }
  17728. else if (startsWith(imageSrc, 'data:')) {
  17729. return dataUriToBlobInfo(blobCache, imageSrc).fold(invalidDataUri, (blobInfo) => Promise.resolve(blobInfo));
  17730. }
  17731. else {
  17732. // Not a blob or data URI so the image isn't a local image and isn't something that can be processed
  17733. return Promise.reject('Unknown image data format');
  17734. }
  17735. };
  17736. // TINY-10350: A modification of the Regexes.link regex to specifically capture host.
  17737. // eslint-disable-next-line max-len
  17738. const hostCaptureRegex = /^(?:(?:(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)([A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*))(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+)?)?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+)?)?)$/;
  17739. const extractHost = (url) => Optional.from(url.match(hostCaptureRegex)).bind((ms) => get$b(ms, 1)).map((h) => startsWith(h, 'www.') ? h.substring(4) : h);
  17740. const sandboxIframe = (iframeNode, exclusions) => {
  17741. if (Optional.from(iframeNode.attr('src')).bind(extractHost).forall((host) => !contains$2(exclusions, host))) {
  17742. iframeNode.attr('sandbox', '');
  17743. }
  17744. };
  17745. const isMimeType = (mime, type) => startsWith(mime, `${type}/`);
  17746. const getEmbedType = (type) => {
  17747. if (isUndefined(type)) {
  17748. return 'iframe';
  17749. }
  17750. else if (isMimeType(type, 'image')) {
  17751. return 'img';
  17752. }
  17753. else if (isMimeType(type, 'video')) {
  17754. return 'video';
  17755. }
  17756. else if (isMimeType(type, 'audio')) {
  17757. return 'audio';
  17758. }
  17759. else {
  17760. return 'iframe';
  17761. }
  17762. };
  17763. const createSafeEmbed = ({ type, src, width, height } = {}, sandboxIframes, sandboxIframesExclusions) => {
  17764. const name = getEmbedType(type);
  17765. const embed = new AstNode(name, 1);
  17766. embed.attr(name === 'audio' ? { src } : { src, width, height });
  17767. // TINY-10349: Show controls for audio and video so the replaced embed is visible in editor.
  17768. if (name === 'audio' || name === 'video') {
  17769. embed.attr('controls', '');
  17770. }
  17771. if (name === 'iframe' && sandboxIframes) {
  17772. sandboxIframe(embed, sandboxIframesExclusions);
  17773. }
  17774. return embed;
  17775. };
  17776. const isBogusImage = (img) => isNonNullable(img.attr('data-mce-bogus'));
  17777. const isInternalImageSource = (img) => img.attr('src') === Env.transparentSrc || isNonNullable(img.attr('data-mce-placeholder'));
  17778. const registerBase64ImageFilter = (parser, settings) => {
  17779. const { blob_cache: blobCache } = settings;
  17780. if (blobCache) {
  17781. const processImage = (img) => {
  17782. const inputSrc = img.attr('src');
  17783. if (isInternalImageSource(img) || isBogusImage(img) || isNullable(inputSrc)) {
  17784. return;
  17785. }
  17786. dataUriToBlobInfo(blobCache, inputSrc, true).each((blobInfo) => {
  17787. img.attr('src', blobInfo.blobUri());
  17788. });
  17789. };
  17790. parser.addAttributeFilter('src', (nodes) => each$e(nodes, processImage));
  17791. }
  17792. };
  17793. const register$4 = (parser, settings) => {
  17794. var _a, _b;
  17795. const schema = parser.schema;
  17796. parser.addAttributeFilter('href', (nodes) => {
  17797. let i = nodes.length;
  17798. const appendRel = (rel) => {
  17799. const parts = rel.split(' ').filter((p) => p.length > 0);
  17800. return parts.concat(['noopener']).sort().join(' ');
  17801. };
  17802. const addNoOpener = (rel) => {
  17803. const newRel = rel ? Tools.trim(rel) : '';
  17804. if (!/\b(noopener)\b/g.test(newRel)) {
  17805. return appendRel(newRel);
  17806. }
  17807. else {
  17808. return newRel;
  17809. }
  17810. };
  17811. if (!settings.allow_unsafe_link_target) {
  17812. while (i--) {
  17813. const node = nodes[i];
  17814. if (node.name === 'a' && node.attr('target') === '_blank') {
  17815. node.attr('rel', addNoOpener(node.attr('rel')));
  17816. }
  17817. }
  17818. }
  17819. });
  17820. // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included.
  17821. if (!settings.allow_html_in_named_anchor) {
  17822. parser.addAttributeFilter('id,name', (nodes) => {
  17823. let i = nodes.length, sibling, prevSibling, parent, node;
  17824. while (i--) {
  17825. node = nodes[i];
  17826. if (node.name === 'a' && node.firstChild && !node.attr('href')) {
  17827. parent = node.parent;
  17828. // Move children after current node
  17829. sibling = node.lastChild;
  17830. while (sibling && parent) {
  17831. prevSibling = sibling.prev;
  17832. parent.insert(sibling, node);
  17833. sibling = prevSibling;
  17834. }
  17835. }
  17836. }
  17837. });
  17838. }
  17839. if (settings.fix_list_elements) {
  17840. parser.addNodeFilter('ul,ol', (nodes) => {
  17841. let i = nodes.length, node, parentNode;
  17842. while (i--) {
  17843. node = nodes[i];
  17844. parentNode = node.parent;
  17845. if (parentNode && (parentNode.name === 'ul' || parentNode.name === 'ol')) {
  17846. if (node.prev && node.prev.name === 'li') {
  17847. node.prev.append(node);
  17848. }
  17849. else {
  17850. const li = new AstNode('li', 1);
  17851. li.attr('style', 'list-style-type: none');
  17852. node.wrap(li);
  17853. }
  17854. }
  17855. }
  17856. });
  17857. }
  17858. const validClasses = schema.getValidClasses();
  17859. if (settings.validate && validClasses) {
  17860. parser.addAttributeFilter('class', (nodes) => {
  17861. var _a;
  17862. let i = nodes.length;
  17863. while (i--) {
  17864. const node = nodes[i];
  17865. const clazz = (_a = node.attr('class')) !== null && _a !== void 0 ? _a : '';
  17866. const classList = Tools.explode(clazz, ' ');
  17867. let classValue = '';
  17868. for (let ci = 0; ci < classList.length; ci++) {
  17869. const className = classList[ci];
  17870. let valid = false;
  17871. let validClassesMap = validClasses['*'];
  17872. if (validClassesMap && validClassesMap[className]) {
  17873. valid = true;
  17874. }
  17875. validClassesMap = validClasses[node.name];
  17876. if (!valid && validClassesMap && validClassesMap[className]) {
  17877. valid = true;
  17878. }
  17879. if (valid) {
  17880. if (classValue) {
  17881. classValue += ' ';
  17882. }
  17883. classValue += className;
  17884. }
  17885. }
  17886. if (!classValue.length) {
  17887. classValue = null;
  17888. }
  17889. node.attr('class', classValue);
  17890. }
  17891. });
  17892. }
  17893. registerBase64ImageFilter(parser, settings);
  17894. const shouldSandboxIframes = (_a = settings.sandbox_iframes) !== null && _a !== void 0 ? _a : false;
  17895. const sandboxIframesExclusions = unique$1((_b = settings.sandbox_iframes_exclusions) !== null && _b !== void 0 ? _b : []);
  17896. if (settings.convert_unsafe_embeds) {
  17897. parser.addNodeFilter('object,embed', (nodes) => each$e(nodes, (node) => {
  17898. node.replace(createSafeEmbed({
  17899. type: node.attr('type'),
  17900. src: node.name === 'object' ? node.attr('data') : node.attr('src'),
  17901. width: node.attr('width'),
  17902. height: node.attr('height'),
  17903. }, shouldSandboxIframes, sandboxIframesExclusions));
  17904. }));
  17905. }
  17906. if (shouldSandboxIframes) {
  17907. parser.addNodeFilter('iframe', (nodes) => each$e(nodes, (node) => sandboxIframe(node, sandboxIframesExclusions)));
  17908. }
  17909. };
  17910. /*! @license DOMPurify 3.2.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.6/LICENSE */
  17911. const {
  17912. entries,
  17913. setPrototypeOf,
  17914. isFrozen,
  17915. getPrototypeOf,
  17916. getOwnPropertyDescriptor
  17917. } = Object;
  17918. let {
  17919. freeze,
  17920. seal,
  17921. create: create$7
  17922. } = Object; // eslint-disable-line import/no-mutable-exports
  17923. let {
  17924. apply,
  17925. construct
  17926. } = typeof Reflect !== 'undefined' && Reflect;
  17927. if (!freeze) {
  17928. freeze = function freeze(x) {
  17929. return x;
  17930. };
  17931. }
  17932. if (!seal) {
  17933. seal = function seal(x) {
  17934. return x;
  17935. };
  17936. }
  17937. if (!apply) {
  17938. apply = function apply(fun, thisValue, args) {
  17939. return fun.apply(thisValue, args);
  17940. };
  17941. }
  17942. if (!construct) {
  17943. construct = function construct(Func, args) {
  17944. return new Func(...args);
  17945. };
  17946. }
  17947. const arrayForEach = unapply(Array.prototype.forEach);
  17948. const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);
  17949. const arrayPop = unapply(Array.prototype.pop);
  17950. const arrayPush = unapply(Array.prototype.push);
  17951. const arraySplice = unapply(Array.prototype.splice);
  17952. const stringToLowerCase = unapply(String.prototype.toLowerCase);
  17953. const stringToString = unapply(String.prototype.toString);
  17954. const stringMatch = unapply(String.prototype.match);
  17955. const stringReplace = unapply(String.prototype.replace);
  17956. const stringIndexOf = unapply(String.prototype.indexOf);
  17957. const stringTrim = unapply(String.prototype.trim);
  17958. const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);
  17959. const regExpTest = unapply(RegExp.prototype.test);
  17960. const typeErrorCreate = unconstruct(TypeError);
  17961. /**
  17962. * Creates a new function that calls the given function with a specified thisArg and arguments.
  17963. *
  17964. * @param func - The function to be wrapped and called.
  17965. * @returns A new function that calls the given function with a specified thisArg and arguments.
  17966. */
  17967. function unapply(func) {
  17968. return function (thisArg) {
  17969. if (thisArg instanceof RegExp) {
  17970. thisArg.lastIndex = 0;
  17971. }
  17972. for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
  17973. args[_key - 1] = arguments[_key];
  17974. }
  17975. return apply(func, thisArg, args);
  17976. };
  17977. }
  17978. /**
  17979. * Creates a new function that constructs an instance of the given constructor function with the provided arguments.
  17980. *
  17981. * @param func - The constructor function to be wrapped and called.
  17982. * @returns A new function that constructs an instance of the given constructor function with the provided arguments.
  17983. */
  17984. function unconstruct(func) {
  17985. return function () {
  17986. for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
  17987. args[_key2] = arguments[_key2];
  17988. }
  17989. return construct(func, args);
  17990. };
  17991. }
  17992. /**
  17993. * Add properties to a lookup table
  17994. *
  17995. * @param set - The set to which elements will be added.
  17996. * @param array - The array containing elements to be added to the set.
  17997. * @param transformCaseFunc - An optional function to transform the case of each element before adding to the set.
  17998. * @returns The modified set with added elements.
  17999. */
  18000. function addToSet(set, array) {
  18001. let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase;
  18002. if (setPrototypeOf) {
  18003. // Make 'in' and truthy checks like Boolean(set.constructor)
  18004. // independent of any properties defined on Object.prototype.
  18005. // Prevent prototype setters from intercepting set as a this value.
  18006. setPrototypeOf(set, null);
  18007. }
  18008. let l = array.length;
  18009. while (l--) {
  18010. let element = array[l];
  18011. if (typeof element === 'string') {
  18012. const lcElement = transformCaseFunc(element);
  18013. if (lcElement !== element) {
  18014. // Config presets (e.g. tags.js, attrs.js) are immutable.
  18015. if (!isFrozen(array)) {
  18016. array[l] = lcElement;
  18017. }
  18018. element = lcElement;
  18019. }
  18020. }
  18021. set[element] = true;
  18022. }
  18023. return set;
  18024. }
  18025. /**
  18026. * Clean up an array to harden against CSPP
  18027. *
  18028. * @param array - The array to be cleaned.
  18029. * @returns The cleaned version of the array
  18030. */
  18031. function cleanArray(array) {
  18032. for (let index = 0; index < array.length; index++) {
  18033. const isPropertyExist = objectHasOwnProperty(array, index);
  18034. if (!isPropertyExist) {
  18035. array[index] = null;
  18036. }
  18037. }
  18038. return array;
  18039. }
  18040. /**
  18041. * Shallow clone an object
  18042. *
  18043. * @param object - The object to be cloned.
  18044. * @returns A new object that copies the original.
  18045. */
  18046. function clone(object) {
  18047. const newObject = create$7(null);
  18048. for (const [property, value] of entries(object)) {
  18049. const isPropertyExist = objectHasOwnProperty(object, property);
  18050. if (isPropertyExist) {
  18051. if (Array.isArray(value)) {
  18052. newObject[property] = cleanArray(value);
  18053. } else if (value && typeof value === 'object' && value.constructor === Object) {
  18054. newObject[property] = clone(value);
  18055. } else {
  18056. newObject[property] = value;
  18057. }
  18058. }
  18059. }
  18060. return newObject;
  18061. }
  18062. /**
  18063. * This method automatically checks if the prop is function or getter and behaves accordingly.
  18064. *
  18065. * @param object - The object to look up the getter function in its prototype chain.
  18066. * @param prop - The property name for which to find the getter function.
  18067. * @returns The getter function found in the prototype chain or a fallback function.
  18068. */
  18069. function lookupGetter(object, prop) {
  18070. while (object !== null) {
  18071. const desc = getOwnPropertyDescriptor(object, prop);
  18072. if (desc) {
  18073. if (desc.get) {
  18074. return unapply(desc.get);
  18075. }
  18076. if (typeof desc.value === 'function') {
  18077. return unapply(desc.value);
  18078. }
  18079. }
  18080. object = getPrototypeOf(object);
  18081. }
  18082. function fallbackValue() {
  18083. return null;
  18084. }
  18085. return fallbackValue;
  18086. }
  18087. const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);
  18088. const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);
  18089. const svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']);
  18090. // List of SVG elements that are disallowed by default.
  18091. // We still need to know them so that we can do namespace
  18092. // checks properly in case one wants to add them to
  18093. // allow-list.
  18094. const svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']);
  18095. const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']);
  18096. // Similarly to SVG, we want to know all MathML elements,
  18097. // even those that we disallow by default.
  18098. const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
  18099. const text = freeze(['#text']);
  18100. const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']);
  18101. const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
  18102. const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);
  18103. const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
  18104. // eslint-disable-next-line unicorn/better-regex
  18105. const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
  18106. const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
  18107. const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex
  18108. const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape
  18109. const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
  18110. const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
  18111. );
  18112. const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i);
  18113. const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex
  18114. );
  18115. const DOCTYPE_NAME = seal(/^html$/i);
  18116. const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
  18117. var EXPRESSIONS = /*#__PURE__*/Object.freeze({
  18118. __proto__: null,
  18119. ARIA_ATTR: ARIA_ATTR,
  18120. ATTR_WHITESPACE: ATTR_WHITESPACE,
  18121. CUSTOM_ELEMENT: CUSTOM_ELEMENT,
  18122. DATA_ATTR: DATA_ATTR,
  18123. DOCTYPE_NAME: DOCTYPE_NAME,
  18124. ERB_EXPR: ERB_EXPR,
  18125. IS_ALLOWED_URI: IS_ALLOWED_URI,
  18126. IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
  18127. MUSTACHE_EXPR: MUSTACHE_EXPR,
  18128. TMPLIT_EXPR: TMPLIT_EXPR
  18129. });
  18130. /* eslint-disable @typescript-eslint/indent */
  18131. // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
  18132. const NODE_TYPE = {
  18133. element: 1,
  18134. attribute: 2,
  18135. text: 3,
  18136. cdataSection: 4,
  18137. entityReference: 5,
  18138. // Deprecated
  18139. entityNode: 6,
  18140. // Deprecated
  18141. progressingInstruction: 7,
  18142. comment: 8,
  18143. document: 9,
  18144. documentType: 10,
  18145. documentFragment: 11,
  18146. notation: 12 // Deprecated
  18147. };
  18148. const getGlobal = function getGlobal() {
  18149. return typeof window === 'undefined' ? null : window;
  18150. };
  18151. /**
  18152. * Creates a no-op policy for internal use only.
  18153. * Don't export this function outside this module!
  18154. * @param trustedTypes The policy factory.
  18155. * @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).
  18156. * @return The policy created (or null, if Trusted Types
  18157. * are not supported or creating the policy failed).
  18158. */
  18159. const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) {
  18160. if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') {
  18161. return null;
  18162. }
  18163. // Allow the callers to control the unique policy name
  18164. // by adding a data-tt-policy-suffix to the script element with the DOMPurify.
  18165. // Policy creation with duplicate names throws in Trusted Types.
  18166. let suffix = null;
  18167. const ATTR_NAME = 'data-tt-policy-suffix';
  18168. if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {
  18169. suffix = purifyHostElement.getAttribute(ATTR_NAME);
  18170. }
  18171. const policyName = 'dompurify' + (suffix ? '#' + suffix : '');
  18172. try {
  18173. return trustedTypes.createPolicy(policyName, {
  18174. createHTML(html) {
  18175. return html;
  18176. },
  18177. createScriptURL(scriptUrl) {
  18178. return scriptUrl;
  18179. }
  18180. });
  18181. } catch (_) {
  18182. // Policy creation failed (most likely another DOMPurify script has
  18183. // already run). Skip creating the policy, as this will only cause errors
  18184. // if TT are enforced.
  18185. console.warn('TrustedTypes policy ' + policyName + ' could not be created.');
  18186. return null;
  18187. }
  18188. };
  18189. const _createHooksMap = function _createHooksMap() {
  18190. return {
  18191. afterSanitizeAttributes: [],
  18192. afterSanitizeElements: [],
  18193. afterSanitizeShadowDOM: [],
  18194. beforeSanitizeAttributes: [],
  18195. beforeSanitizeElements: [],
  18196. beforeSanitizeShadowDOM: [],
  18197. uponSanitizeAttribute: [],
  18198. uponSanitizeElement: [],
  18199. uponSanitizeShadowNode: []
  18200. };
  18201. };
  18202. function createDOMPurify() {
  18203. let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
  18204. const DOMPurify = root => createDOMPurify(root);
  18205. DOMPurify.version = '3.2.6';
  18206. DOMPurify.removed = [];
  18207. if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
  18208. // Not running in a browser, provide a factory function
  18209. // so that you can pass your own Window
  18210. DOMPurify.isSupported = false;
  18211. return DOMPurify;
  18212. }
  18213. let {
  18214. document
  18215. } = window;
  18216. const originalDocument = document;
  18217. const currentScript = originalDocument.currentScript;
  18218. const {
  18219. DocumentFragment,
  18220. HTMLTemplateElement,
  18221. Node,
  18222. Element,
  18223. NodeFilter,
  18224. NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
  18225. HTMLFormElement,
  18226. DOMParser,
  18227. trustedTypes
  18228. } = window;
  18229. const ElementPrototype = Element.prototype;
  18230. const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
  18231. const remove = lookupGetter(ElementPrototype, 'remove');
  18232. const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
  18233. const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
  18234. const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
  18235. // As per issue #47, the web-components registry is inherited by a
  18236. // new document created via createHTMLDocument. As per the spec
  18237. // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
  18238. // a new empty registry is used when creating a template contents owner
  18239. // document, so we use that as our parent document to ensure nothing
  18240. // is inherited.
  18241. if (typeof HTMLTemplateElement === 'function') {
  18242. const template = document.createElement('template');
  18243. if (template.content && template.content.ownerDocument) {
  18244. document = template.content.ownerDocument;
  18245. }
  18246. }
  18247. let trustedTypesPolicy;
  18248. let emptyHTML = '';
  18249. const {
  18250. implementation,
  18251. createNodeIterator,
  18252. createDocumentFragment,
  18253. getElementsByTagName
  18254. } = document;
  18255. const {
  18256. importNode
  18257. } = originalDocument;
  18258. let hooks = _createHooksMap();
  18259. /**
  18260. * Expose whether this browser supports running the full DOMPurify.
  18261. */
  18262. DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;
  18263. const {
  18264. MUSTACHE_EXPR,
  18265. ERB_EXPR,
  18266. TMPLIT_EXPR,
  18267. DATA_ATTR,
  18268. ARIA_ATTR,
  18269. IS_SCRIPT_OR_DATA,
  18270. ATTR_WHITESPACE,
  18271. CUSTOM_ELEMENT
  18272. } = EXPRESSIONS;
  18273. let {
  18274. IS_ALLOWED_URI: IS_ALLOWED_URI$1
  18275. } = EXPRESSIONS;
  18276. /**
  18277. * We consider the elements and attributes below to be safe. Ideally
  18278. * don't add any new ones but feel free to remove unwanted ones.
  18279. */
  18280. /* allowed element names */
  18281. let ALLOWED_TAGS = null;
  18282. const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]);
  18283. /* Allowed attribute names */
  18284. let ALLOWED_ATTR = null;
  18285. const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]);
  18286. /*
  18287. * Configure how DOMPurify should handle custom elements and their attributes as well as customized built-in elements.
  18288. * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)
  18289. * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)
  18290. * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.
  18291. */
  18292. let CUSTOM_ELEMENT_HANDLING = Object.seal(create$7(null, {
  18293. tagNameCheck: {
  18294. writable: true,
  18295. configurable: false,
  18296. enumerable: true,
  18297. value: null
  18298. },
  18299. attributeNameCheck: {
  18300. writable: true,
  18301. configurable: false,
  18302. enumerable: true,
  18303. value: null
  18304. },
  18305. allowCustomizedBuiltInElements: {
  18306. writable: true,
  18307. configurable: false,
  18308. enumerable: true,
  18309. value: false
  18310. }
  18311. }));
  18312. /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */
  18313. let FORBID_TAGS = null;
  18314. /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */
  18315. let FORBID_ATTR = null;
  18316. /* Decide if ARIA attributes are okay */
  18317. let ALLOW_ARIA_ATTR = true;
  18318. /* Decide if custom data attributes are okay */
  18319. let ALLOW_DATA_ATTR = true;
  18320. /* Decide if unknown protocols are okay */
  18321. let ALLOW_UNKNOWN_PROTOCOLS = false;
  18322. /* Decide if self-closing tags in attributes are allowed.
  18323. * Usually removed due to a mXSS issue in jQuery 3.0 */
  18324. let ALLOW_SELF_CLOSE_IN_ATTR = true;
  18325. /* Output should be safe for common template engines.
  18326. * This means, DOMPurify removes data attributes, mustaches and ERB
  18327. */
  18328. let SAFE_FOR_TEMPLATES = false;
  18329. /* Output should be safe even for XML used within HTML and alike.
  18330. * This means, DOMPurify removes comments when containing risky content.
  18331. */
  18332. let SAFE_FOR_XML = true;
  18333. /* Decide if document with <html>... should be returned */
  18334. let WHOLE_DOCUMENT = false;
  18335. /* Track whether config is already set on this instance of DOMPurify. */
  18336. let SET_CONFIG = false;
  18337. /* Decide if all elements (e.g. style, script) must be children of
  18338. * document.body. By default, browsers might move them to document.head */
  18339. let FORCE_BODY = false;
  18340. /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html
  18341. * string (or a TrustedHTML object if Trusted Types are supported).
  18342. * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead
  18343. */
  18344. let RETURN_DOM = false;
  18345. /* Decide if a DOM `DocumentFragment` should be returned, instead of a html
  18346. * string (or a TrustedHTML object if Trusted Types are supported) */
  18347. let RETURN_DOM_FRAGMENT = false;
  18348. /* Try to return a Trusted Type object instead of a string, return a string in
  18349. * case Trusted Types are not supported */
  18350. let RETURN_TRUSTED_TYPE = false;
  18351. /* Output should be free from DOM clobbering attacks?
  18352. * This sanitizes markups named with colliding, clobberable built-in DOM APIs.
  18353. */
  18354. let SANITIZE_DOM = true;
  18355. /* Achieve full DOM Clobbering protection by isolating the namespace of named
  18356. * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.
  18357. *
  18358. * HTML/DOM spec rules that enable DOM Clobbering:
  18359. * - Named Access on Window (§7.3.3)
  18360. * - DOM Tree Accessors (§3.1.5)
  18361. * - Form Element Parent-Child Relations (§4.10.3)
  18362. * - Iframe srcdoc / Nested WindowProxies (§4.8.5)
  18363. * - HTMLCollection (§4.2.10.2)
  18364. *
  18365. * Namespace isolation is implemented by prefixing `id` and `name` attributes
  18366. * with a constant string, i.e., `user-content-`
  18367. */
  18368. let SANITIZE_NAMED_PROPS = false;
  18369. const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';
  18370. /* Keep element content when removing element? */
  18371. let KEEP_CONTENT = true;
  18372. /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead
  18373. * of importing it into a new Document and returning a sanitized copy */
  18374. let IN_PLACE = false;
  18375. /* Allow usage of profiles like html, svg and mathMl */
  18376. let USE_PROFILES = {};
  18377. /* Tags to ignore content of when KEEP_CONTENT is true */
  18378. let FORBID_CONTENTS = null;
  18379. const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);
  18380. /* Tags that are safe for data: URIs */
  18381. let DATA_URI_TAGS = null;
  18382. const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);
  18383. /* Attributes safe for values like "javascript:" */
  18384. let URI_SAFE_ATTRIBUTES = null;
  18385. const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']);
  18386. const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
  18387. const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
  18388. const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
  18389. /* Document namespace */
  18390. let NAMESPACE = HTML_NAMESPACE;
  18391. let IS_EMPTY_INPUT = false;
  18392. /* Allowed XHTML+XML namespaces */
  18393. let ALLOWED_NAMESPACES = null;
  18394. const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
  18395. let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);
  18396. let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);
  18397. // Certain elements are allowed in both SVG and HTML
  18398. // namespace. We need to specify them explicitly
  18399. // so that they don't get erroneously deleted from
  18400. // HTML namespace.
  18401. const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']);
  18402. /* Parsing of strict XHTML documents */
  18403. let PARSER_MEDIA_TYPE = null;
  18404. const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html'];
  18405. const DEFAULT_PARSER_MEDIA_TYPE = 'text/html';
  18406. let transformCaseFunc = null;
  18407. /* Keep a reference to config to pass to hooks */
  18408. let CONFIG = null;
  18409. /* Ideally, do not touch anything below this line */
  18410. /* ______________________________________________ */
  18411. const formElement = document.createElement('form');
  18412. const isRegexOrFunction = function isRegexOrFunction(testValue) {
  18413. return testValue instanceof RegExp || testValue instanceof Function;
  18414. };
  18415. /**
  18416. * _parseConfig
  18417. *
  18418. * @param cfg optional config literal
  18419. */
  18420. // eslint-disable-next-line complexity
  18421. const _parseConfig = function _parseConfig() {
  18422. let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  18423. if (CONFIG && CONFIG === cfg) {
  18424. return;
  18425. }
  18426. /* Shield configuration object from tampering */
  18427. if (!cfg || typeof cfg !== 'object') {
  18428. cfg = {};
  18429. }
  18430. /* Shield configuration object from prototype pollution */
  18431. cfg = clone(cfg);
  18432. PARSER_MEDIA_TYPE =
  18433. // eslint-disable-next-line unicorn/prefer-includes
  18434. SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE;
  18435. // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
  18436. transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
  18437. /* Set configuration parameters */
  18438. ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
  18439. ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
  18440. ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
  18441. URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;
  18442. DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;
  18443. FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
  18444. FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});
  18445. FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});
  18446. USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false;
  18447. ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
  18448. ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
  18449. ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
  18450. ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true
  18451. SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false
  18452. SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true
  18453. WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false
  18454. RETURN_DOM = cfg.RETURN_DOM || false; // Default false
  18455. RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false
  18456. RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false
  18457. FORCE_BODY = cfg.FORCE_BODY || false; // Default false
  18458. SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true
  18459. SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false
  18460. KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
  18461. IN_PLACE = cfg.IN_PLACE || false; // Default false
  18462. IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;
  18463. NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;
  18464. MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS;
  18465. HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS;
  18466. CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};
  18467. if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {
  18468. CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;
  18469. }
  18470. if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {
  18471. CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;
  18472. }
  18473. if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {
  18474. CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;
  18475. }
  18476. if (SAFE_FOR_TEMPLATES) {
  18477. ALLOW_DATA_ATTR = false;
  18478. }
  18479. if (RETURN_DOM_FRAGMENT) {
  18480. RETURN_DOM = true;
  18481. }
  18482. /* Parse profile info */
  18483. if (USE_PROFILES) {
  18484. ALLOWED_TAGS = addToSet({}, text);
  18485. ALLOWED_ATTR = [];
  18486. if (USE_PROFILES.html === true) {
  18487. addToSet(ALLOWED_TAGS, html$1);
  18488. addToSet(ALLOWED_ATTR, html);
  18489. }
  18490. if (USE_PROFILES.svg === true) {
  18491. addToSet(ALLOWED_TAGS, svg$1);
  18492. addToSet(ALLOWED_ATTR, svg);
  18493. addToSet(ALLOWED_ATTR, xml);
  18494. }
  18495. if (USE_PROFILES.svgFilters === true) {
  18496. addToSet(ALLOWED_TAGS, svgFilters);
  18497. addToSet(ALLOWED_ATTR, svg);
  18498. addToSet(ALLOWED_ATTR, xml);
  18499. }
  18500. if (USE_PROFILES.mathMl === true) {
  18501. addToSet(ALLOWED_TAGS, mathMl$1);
  18502. addToSet(ALLOWED_ATTR, mathMl);
  18503. addToSet(ALLOWED_ATTR, xml);
  18504. }
  18505. }
  18506. /* Merge configuration parameters */
  18507. if (cfg.ADD_TAGS) {
  18508. if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
  18509. ALLOWED_TAGS = clone(ALLOWED_TAGS);
  18510. }
  18511. addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
  18512. }
  18513. if (cfg.ADD_ATTR) {
  18514. if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
  18515. ALLOWED_ATTR = clone(ALLOWED_ATTR);
  18516. }
  18517. addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
  18518. }
  18519. if (cfg.ADD_URI_SAFE_ATTR) {
  18520. addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
  18521. }
  18522. if (cfg.FORBID_CONTENTS) {
  18523. if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
  18524. FORBID_CONTENTS = clone(FORBID_CONTENTS);
  18525. }
  18526. addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
  18527. }
  18528. /* Add #text in case KEEP_CONTENT is set to true */
  18529. if (KEEP_CONTENT) {
  18530. ALLOWED_TAGS['#text'] = true;
  18531. }
  18532. /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */
  18533. if (WHOLE_DOCUMENT) {
  18534. addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);
  18535. }
  18536. /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */
  18537. if (ALLOWED_TAGS.table) {
  18538. addToSet(ALLOWED_TAGS, ['tbody']);
  18539. delete FORBID_TAGS.tbody;
  18540. }
  18541. if (cfg.TRUSTED_TYPES_POLICY) {
  18542. if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {
  18543. throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');
  18544. }
  18545. if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {
  18546. throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
  18547. }
  18548. // Overwrite existing TrustedTypes policy.
  18549. trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
  18550. // Sign local variables required by `sanitize`.
  18551. emptyHTML = trustedTypesPolicy.createHTML('');
  18552. } else {
  18553. // Uninitialized policy, attempt to initialize the internal dompurify policy.
  18554. if (trustedTypesPolicy === undefined) {
  18555. trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
  18556. }
  18557. // If creating the internal policy succeeded sign internal variables.
  18558. if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
  18559. emptyHTML = trustedTypesPolicy.createHTML('');
  18560. }
  18561. }
  18562. // Prevent further manipulation of configuration.
  18563. // Not available in IE8, Safari 5, etc.
  18564. if (freeze) {
  18565. freeze(cfg);
  18566. }
  18567. CONFIG = cfg;
  18568. };
  18569. /* Keep track of all possible SVG and MathML tags
  18570. * so that we can perform the namespace checks
  18571. * correctly. */
  18572. const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
  18573. const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
  18574. /**
  18575. * @param element a DOM element whose namespace is being checked
  18576. * @returns Return false if the element has a
  18577. * namespace that a spec-compliant parser would never
  18578. * return. Return true otherwise.
  18579. */
  18580. const _checkValidNamespace = function _checkValidNamespace(element) {
  18581. let parent = getParentNode(element);
  18582. // In JSDOM, if we're inside shadow DOM, then parentNode
  18583. // can be null. We just simulate parent in this case.
  18584. if (!parent || !parent.tagName) {
  18585. parent = {
  18586. namespaceURI: NAMESPACE,
  18587. tagName: 'template'
  18588. };
  18589. }
  18590. const tagName = stringToLowerCase(element.tagName);
  18591. const parentTagName = stringToLowerCase(parent.tagName);
  18592. if (!ALLOWED_NAMESPACES[element.namespaceURI]) {
  18593. return false;
  18594. }
  18595. if (element.namespaceURI === SVG_NAMESPACE) {
  18596. // The only way to switch from HTML namespace to SVG
  18597. // is via <svg>. If it happens via any other tag, then
  18598. // it should be killed.
  18599. if (parent.namespaceURI === HTML_NAMESPACE) {
  18600. return tagName === 'svg';
  18601. }
  18602. // The only way to switch from MathML to SVG is via`
  18603. // svg if parent is either <annotation-xml> or MathML
  18604. // text integration points.
  18605. if (parent.namespaceURI === MATHML_NAMESPACE) {
  18606. return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
  18607. }
  18608. // We only allow elements that are defined in SVG
  18609. // spec. All others are disallowed in SVG namespace.
  18610. return Boolean(ALL_SVG_TAGS[tagName]);
  18611. }
  18612. if (element.namespaceURI === MATHML_NAMESPACE) {
  18613. // The only way to switch from HTML namespace to MathML
  18614. // is via <math>. If it happens via any other tag, then
  18615. // it should be killed.
  18616. if (parent.namespaceURI === HTML_NAMESPACE) {
  18617. return tagName === 'math';
  18618. }
  18619. // The only way to switch from SVG to MathML is via
  18620. // <math> and HTML integration points
  18621. if (parent.namespaceURI === SVG_NAMESPACE) {
  18622. return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
  18623. }
  18624. // We only allow elements that are defined in MathML
  18625. // spec. All others are disallowed in MathML namespace.
  18626. return Boolean(ALL_MATHML_TAGS[tagName]);
  18627. }
  18628. if (element.namespaceURI === HTML_NAMESPACE) {
  18629. // The only way to switch from SVG to HTML is via
  18630. // HTML integration points, and from MathML to HTML
  18631. // is via MathML text integration points
  18632. if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
  18633. return false;
  18634. }
  18635. if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
  18636. return false;
  18637. }
  18638. // We disallow tags that are specific for MathML
  18639. // or SVG and should never appear in HTML namespace
  18640. return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
  18641. }
  18642. // For XHTML and XML documents that support custom namespaces
  18643. if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {
  18644. return true;
  18645. }
  18646. // The code should never reach this place (this means
  18647. // that the element somehow got namespace that is not
  18648. // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES).
  18649. // Return false just in case.
  18650. return false;
  18651. };
  18652. /**
  18653. * _forceRemove
  18654. *
  18655. * @param node a DOM node
  18656. */
  18657. const _forceRemove = function _forceRemove(node) {
  18658. arrayPush(DOMPurify.removed, {
  18659. element: node
  18660. });
  18661. try {
  18662. // eslint-disable-next-line unicorn/prefer-dom-node-remove
  18663. getParentNode(node).removeChild(node);
  18664. } catch (_) {
  18665. remove(node);
  18666. }
  18667. };
  18668. /**
  18669. * _removeAttribute
  18670. *
  18671. * @param name an Attribute name
  18672. * @param element a DOM node
  18673. */
  18674. const _removeAttribute = function _removeAttribute(name, element) {
  18675. try {
  18676. arrayPush(DOMPurify.removed, {
  18677. attribute: element.getAttributeNode(name),
  18678. from: element
  18679. });
  18680. } catch (_) {
  18681. arrayPush(DOMPurify.removed, {
  18682. attribute: null,
  18683. from: element
  18684. });
  18685. }
  18686. element.removeAttribute(name);
  18687. // We void attribute values for unremovable "is" attributes
  18688. if (name === 'is') {
  18689. if (RETURN_DOM || RETURN_DOM_FRAGMENT) {
  18690. try {
  18691. _forceRemove(element);
  18692. } catch (_) {}
  18693. } else {
  18694. try {
  18695. element.setAttribute(name, '');
  18696. } catch (_) {}
  18697. }
  18698. }
  18699. };
  18700. /**
  18701. * _initDocument
  18702. *
  18703. * @param dirty - a string of dirty markup
  18704. * @return a DOM, filled with the dirty markup
  18705. */
  18706. const _initDocument = function _initDocument(dirty) {
  18707. /* Create a HTML document */
  18708. let doc = null;
  18709. let leadingWhitespace = null;
  18710. if (FORCE_BODY) {
  18711. dirty = '<remove></remove>' + dirty;
  18712. } else {
  18713. /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */
  18714. const matches = stringMatch(dirty, /^[\r\n\t ]+/);
  18715. leadingWhitespace = matches && matches[0];
  18716. }
  18717. if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) {
  18718. // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
  18719. dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>';
  18720. }
  18721. const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
  18722. /*
  18723. * Use the DOMParser API by default, fallback later if needs be
  18724. * DOMParser not work for svg when has multiple root element.
  18725. */
  18726. if (NAMESPACE === HTML_NAMESPACE) {
  18727. try {
  18728. doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);
  18729. } catch (_) {}
  18730. }
  18731. /* Use createHTMLDocument in case DOMParser is not available */
  18732. if (!doc || !doc.documentElement) {
  18733. doc = implementation.createDocument(NAMESPACE, 'template', null);
  18734. try {
  18735. doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload;
  18736. } catch (_) {
  18737. // Syntax error if dirtyPayload is invalid xml
  18738. }
  18739. }
  18740. const body = doc.body || doc.documentElement;
  18741. if (dirty && leadingWhitespace) {
  18742. body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null);
  18743. }
  18744. /* Work on whole document or just its body */
  18745. if (NAMESPACE === HTML_NAMESPACE) {
  18746. return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0];
  18747. }
  18748. return WHOLE_DOCUMENT ? doc.documentElement : body;
  18749. };
  18750. /**
  18751. * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document.
  18752. *
  18753. * @param root The root element or node to start traversing on.
  18754. * @return The created NodeIterator
  18755. */
  18756. const _createNodeIterator = function _createNodeIterator(root) {
  18757. return createNodeIterator.call(root.ownerDocument || root, root,
  18758. // eslint-disable-next-line no-bitwise
  18759. NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
  18760. };
  18761. /**
  18762. * _isClobbered
  18763. *
  18764. * @param element element to check for clobbering attacks
  18765. * @return true if clobbered, false if safe
  18766. */
  18767. const _isClobbered = function _isClobbered(element) {
  18768. return element instanceof HTMLFormElement && (typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function');
  18769. };
  18770. /**
  18771. * Checks whether the given object is a DOM node.
  18772. *
  18773. * @param value object to check whether it's a DOM node
  18774. * @return true is object is a DOM node
  18775. */
  18776. const _isNode = function _isNode(value) {
  18777. return typeof Node === 'function' && value instanceof Node;
  18778. };
  18779. function _executeHooks(hooks, currentNode, data) {
  18780. arrayForEach(hooks, hook => {
  18781. hook.call(DOMPurify, currentNode, data, CONFIG);
  18782. });
  18783. }
  18784. /**
  18785. * _sanitizeElements
  18786. *
  18787. * @protect nodeName
  18788. * @protect textContent
  18789. * @protect removeChild
  18790. * @param currentNode to check for permission to exist
  18791. * @return true if node was killed, false if left alive
  18792. */
  18793. const _sanitizeElements = function _sanitizeElements(currentNode) {
  18794. let content = null;
  18795. /* Execute a hook if present */
  18796. _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
  18797. /* Check if element is clobbered or can clobber */
  18798. if (_isClobbered(currentNode)) {
  18799. _forceRemove(currentNode);
  18800. return true;
  18801. }
  18802. /* Now let's check the element's type and name */
  18803. const tagName = transformCaseFunc(currentNode.nodeName);
  18804. /* Execute a hook if present */
  18805. _executeHooks(hooks.uponSanitizeElement, currentNode, {
  18806. tagName,
  18807. allowedTags: ALLOWED_TAGS
  18808. });
  18809. /* Detect mXSS attempts abusing namespace confusion */
  18810. if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) {
  18811. _forceRemove(currentNode);
  18812. return true;
  18813. }
  18814. /* Remove any occurrence of processing instructions */
  18815. if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
  18816. _forceRemove(currentNode);
  18817. return true;
  18818. }
  18819. /* Remove any kind of possibly harmful comments */
  18820. if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) {
  18821. _forceRemove(currentNode);
  18822. return true;
  18823. }
  18824. /* Remove element if anything forbids its presence */
  18825. if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
  18826. /* Check if we have a custom element to handle */
  18827. if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
  18828. if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
  18829. return false;
  18830. }
  18831. if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
  18832. return false;
  18833. }
  18834. }
  18835. /* Keep content except for bad-listed elements */
  18836. if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
  18837. const parentNode = getParentNode(currentNode) || currentNode.parentNode;
  18838. const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
  18839. if (childNodes && parentNode) {
  18840. const childCount = childNodes.length;
  18841. for (let i = childCount - 1; i >= 0; --i) {
  18842. const childClone = cloneNode(childNodes[i], true);
  18843. childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
  18844. parentNode.insertBefore(childClone, getNextSibling(currentNode));
  18845. }
  18846. }
  18847. }
  18848. _forceRemove(currentNode);
  18849. return true;
  18850. }
  18851. /* Check whether element has a valid namespace */
  18852. if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
  18853. _forceRemove(currentNode);
  18854. return true;
  18855. }
  18856. /* Make sure that older browsers don't get fallback-tag mXSS */
  18857. if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) {
  18858. _forceRemove(currentNode);
  18859. return true;
  18860. }
  18861. /* Sanitize element content to be template-safe */
  18862. if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
  18863. /* Get the element's text content */
  18864. content = currentNode.textContent;
  18865. arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
  18866. content = stringReplace(content, expr, ' ');
  18867. });
  18868. if (currentNode.textContent !== content) {
  18869. arrayPush(DOMPurify.removed, {
  18870. element: currentNode.cloneNode()
  18871. });
  18872. currentNode.textContent = content;
  18873. }
  18874. }
  18875. /* Execute a hook if present */
  18876. _executeHooks(hooks.afterSanitizeElements, currentNode, null);
  18877. return false;
  18878. };
  18879. /**
  18880. * _isValidAttribute
  18881. *
  18882. * @param lcTag Lowercase tag name of containing element.
  18883. * @param lcName Lowercase attribute name.
  18884. * @param value Attribute value.
  18885. * @return Returns true if `value` is valid, otherwise false.
  18886. */
  18887. // eslint-disable-next-line complexity
  18888. const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {
  18889. /* Make sure attribute cannot clobber */
  18890. if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
  18891. return false;
  18892. }
  18893. /* Allow valid data-* attributes: At least one character after "-"
  18894. (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
  18895. XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
  18896. We don't need to check the value; it's always URI safe. */
  18897. if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
  18898. if (
  18899. // First condition does a very basic check if a) it's basically a valid custom element tagname AND
  18900. // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
  18901. // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck
  18902. _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)) ||
  18903. // Alternative, second condition checks if it's an `is`-attribute, AND
  18904. // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
  18905. lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else {
  18906. return false;
  18907. }
  18908. /* Check value is safe. First, is attr inert? If so, is safe */
  18909. } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) {
  18910. return false;
  18911. } else ;
  18912. return true;
  18913. };
  18914. /**
  18915. * _isBasicCustomElement
  18916. * checks if at least one dash is included in tagName, and it's not the first char
  18917. * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name
  18918. *
  18919. * @param tagName name of the tag of the node to sanitize
  18920. * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
  18921. */
  18922. const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
  18923. return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT);
  18924. };
  18925. /**
  18926. * _sanitizeAttributes
  18927. *
  18928. * @protect attributes
  18929. * @protect nodeName
  18930. * @protect removeAttribute
  18931. * @protect setAttribute
  18932. *
  18933. * @param currentNode to sanitize
  18934. */
  18935. const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
  18936. /* Execute a hook if present */
  18937. _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
  18938. const {
  18939. attributes
  18940. } = currentNode;
  18941. /* Check if we have attributes; if not we might have a text node */
  18942. if (!attributes || _isClobbered(currentNode)) {
  18943. return;
  18944. }
  18945. const hookEvent = {
  18946. attrName: '',
  18947. attrValue: '',
  18948. keepAttr: true,
  18949. allowedAttributes: ALLOWED_ATTR,
  18950. forceKeepAttr: undefined
  18951. };
  18952. let l = attributes.length;
  18953. /* Go backwards over all attributes; safely remove bad ones */
  18954. while (l--) {
  18955. const attr = attributes[l];
  18956. const {
  18957. name,
  18958. namespaceURI,
  18959. value: attrValue
  18960. } = attr;
  18961. const lcName = transformCaseFunc(name);
  18962. const initValue = attrValue;
  18963. let value = name === 'value' ? initValue : stringTrim(initValue);
  18964. /* Execute a hook if present */
  18965. hookEvent.attrName = lcName;
  18966. hookEvent.attrValue = value;
  18967. hookEvent.keepAttr = true;
  18968. hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set
  18969. _executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent);
  18970. value = hookEvent.attrValue;
  18971. /* Full DOM Clobbering protection via namespace isolation,
  18972. * Prefix id and name attributes with `user-content-`
  18973. */
  18974. if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {
  18975. // Remove the attribute with this value
  18976. _removeAttribute(name, currentNode);
  18977. // Prefix the value and later re-create the attribute with the sanitized value
  18978. value = SANITIZE_NAMED_PROPS_PREFIX + value;
  18979. }
  18980. /* Work around a security issue with comments inside attributes */
  18981. if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|title)/i, value)) {
  18982. _removeAttribute(name, currentNode);
  18983. continue;
  18984. }
  18985. /* Did the hooks approve of the attribute? */
  18986. if (hookEvent.forceKeepAttr) {
  18987. continue;
  18988. }
  18989. /* Did the hooks approve of the attribute? */
  18990. if (!hookEvent.keepAttr) {
  18991. _removeAttribute(name, currentNode);
  18992. continue;
  18993. }
  18994. /* Work around a security issue in jQuery 3.0 */
  18995. if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
  18996. _removeAttribute(name, currentNode);
  18997. continue;
  18998. }
  18999. /* Sanitize attribute content to be template-safe */
  19000. if (SAFE_FOR_TEMPLATES) {
  19001. arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
  19002. value = stringReplace(value, expr, ' ');
  19003. });
  19004. }
  19005. /* Is `value` valid for this attribute? */
  19006. const lcTag = transformCaseFunc(currentNode.nodeName);
  19007. if (!_isValidAttribute(lcTag, lcName, value)) {
  19008. _removeAttribute(name, currentNode);
  19009. continue;
  19010. }
  19011. /* Handle attributes that require Trusted Types */
  19012. if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {
  19013. if (namespaceURI) ; else {
  19014. switch (trustedTypes.getAttributeType(lcTag, lcName)) {
  19015. case 'TrustedHTML':
  19016. {
  19017. value = trustedTypesPolicy.createHTML(value);
  19018. break;
  19019. }
  19020. case 'TrustedScriptURL':
  19021. {
  19022. value = trustedTypesPolicy.createScriptURL(value);
  19023. break;
  19024. }
  19025. }
  19026. }
  19027. }
  19028. /* Handle invalid data-* attribute set by try-catching it */
  19029. if (value !== initValue) {
  19030. try {
  19031. if (namespaceURI) {
  19032. currentNode.setAttributeNS(namespaceURI, name, value);
  19033. } else {
  19034. /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
  19035. currentNode.setAttribute(name, value);
  19036. }
  19037. if (_isClobbered(currentNode)) {
  19038. _forceRemove(currentNode);
  19039. } else {
  19040. arrayPop(DOMPurify.removed);
  19041. }
  19042. } catch (_) {
  19043. _removeAttribute(name, currentNode);
  19044. }
  19045. }
  19046. }
  19047. /* Execute a hook if present */
  19048. _executeHooks(hooks.afterSanitizeAttributes, currentNode, null);
  19049. };
  19050. /**
  19051. * _sanitizeShadowDOM
  19052. *
  19053. * @param fragment to iterate over recursively
  19054. */
  19055. const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) {
  19056. let shadowNode = null;
  19057. const shadowIterator = _createNodeIterator(fragment);
  19058. /* Execute a hook if present */
  19059. _executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null);
  19060. while (shadowNode = shadowIterator.nextNode()) {
  19061. /* Execute a hook if present */
  19062. _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
  19063. /* Sanitize tags and elements */
  19064. _sanitizeElements(shadowNode);
  19065. /* Check attributes next */
  19066. _sanitizeAttributes(shadowNode);
  19067. /* Deep shadow DOM detected */
  19068. if (shadowNode.content instanceof DocumentFragment) {
  19069. _sanitizeShadowDOM(shadowNode.content);
  19070. }
  19071. }
  19072. /* Execute a hook if present */
  19073. _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
  19074. };
  19075. // eslint-disable-next-line complexity
  19076. DOMPurify.sanitize = function (dirty) {
  19077. let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  19078. let body = null;
  19079. let importedNode = null;
  19080. let currentNode = null;
  19081. let returnNode = null;
  19082. /* Make sure we have a string to sanitize.
  19083. DO NOT return early, as this will return the wrong type if
  19084. the user has requested a DOM object rather than a string */
  19085. IS_EMPTY_INPUT = !dirty;
  19086. if (IS_EMPTY_INPUT) {
  19087. dirty = '<!-->';
  19088. }
  19089. /* Stringify, in case dirty is an object */
  19090. if (typeof dirty !== 'string' && !_isNode(dirty)) {
  19091. if (typeof dirty.toString === 'function') {
  19092. dirty = dirty.toString();
  19093. if (typeof dirty !== 'string') {
  19094. throw typeErrorCreate('dirty is not a string, aborting');
  19095. }
  19096. } else {
  19097. throw typeErrorCreate('toString is not a function');
  19098. }
  19099. }
  19100. /* Return dirty HTML if DOMPurify cannot run */
  19101. if (!DOMPurify.isSupported) {
  19102. return dirty;
  19103. }
  19104. /* Assign config vars */
  19105. if (!SET_CONFIG) {
  19106. _parseConfig(cfg);
  19107. }
  19108. /* Clean up removed elements */
  19109. DOMPurify.removed = [];
  19110. /* Check if dirty is correctly typed for IN_PLACE */
  19111. if (typeof dirty === 'string') {
  19112. IN_PLACE = false;
  19113. }
  19114. if (IN_PLACE) {
  19115. /* Do some early pre-sanitization to avoid unsafe root nodes */
  19116. if (dirty.nodeName) {
  19117. const tagName = transformCaseFunc(dirty.nodeName);
  19118. if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
  19119. throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
  19120. }
  19121. }
  19122. } else if (dirty instanceof Node) {
  19123. /* If dirty is a DOM element, append to an empty document to avoid
  19124. elements being stripped by the parser */
  19125. body = _initDocument('<!---->');
  19126. importedNode = body.ownerDocument.importNode(dirty, true);
  19127. if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === 'BODY') {
  19128. /* Node is already a body, use as is */
  19129. body = importedNode;
  19130. } else if (importedNode.nodeName === 'HTML') {
  19131. body = importedNode;
  19132. } else {
  19133. // eslint-disable-next-line unicorn/prefer-dom-node-append
  19134. body.appendChild(importedNode);
  19135. }
  19136. } else {
  19137. /* Exit directly if we have nothing to do */
  19138. if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
  19139. // eslint-disable-next-line unicorn/prefer-includes
  19140. dirty.indexOf('<') === -1) {
  19141. return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
  19142. }
  19143. /* Initialize the document to work on */
  19144. body = _initDocument(dirty);
  19145. /* Check we have a DOM node from the data */
  19146. if (!body) {
  19147. return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : '';
  19148. }
  19149. }
  19150. /* Remove first element node (ours) if FORCE_BODY is set */
  19151. if (body && FORCE_BODY) {
  19152. _forceRemove(body.firstChild);
  19153. }
  19154. /* Get node iterator */
  19155. const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
  19156. /* Now start iterating over the created document */
  19157. while (currentNode = nodeIterator.nextNode()) {
  19158. /* Sanitize tags and elements */
  19159. _sanitizeElements(currentNode);
  19160. /* Check attributes next */
  19161. _sanitizeAttributes(currentNode);
  19162. /* Shadow DOM detected, sanitize it */
  19163. if (currentNode.content instanceof DocumentFragment) {
  19164. _sanitizeShadowDOM(currentNode.content);
  19165. }
  19166. }
  19167. /* If we sanitized `dirty` in-place, return it. */
  19168. if (IN_PLACE) {
  19169. return dirty;
  19170. }
  19171. /* Return sanitized string or DOM */
  19172. if (RETURN_DOM) {
  19173. if (RETURN_DOM_FRAGMENT) {
  19174. returnNode = createDocumentFragment.call(body.ownerDocument);
  19175. while (body.firstChild) {
  19176. // eslint-disable-next-line unicorn/prefer-dom-node-append
  19177. returnNode.appendChild(body.firstChild);
  19178. }
  19179. } else {
  19180. returnNode = body;
  19181. }
  19182. if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) {
  19183. /*
  19184. AdoptNode() is not used because internal state is not reset
  19185. (e.g. the past names map of a HTMLFormElement), this is safe
  19186. in theory but we would rather not risk another attack vector.
  19187. The state that is cloned by importNode() is explicitly defined
  19188. by the specs.
  19189. */
  19190. returnNode = importNode.call(originalDocument, returnNode, true);
  19191. }
  19192. return returnNode;
  19193. }
  19194. let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;
  19195. /* Serialize doctype if allowed */
  19196. if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) {
  19197. serializedHTML = '<!DOCTYPE ' + body.ownerDocument.doctype.name + '>\n' + serializedHTML;
  19198. }
  19199. /* Sanitize final string template-safe */
  19200. if (SAFE_FOR_TEMPLATES) {
  19201. arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
  19202. serializedHTML = stringReplace(serializedHTML, expr, ' ');
  19203. });
  19204. }
  19205. return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
  19206. };
  19207. DOMPurify.setConfig = function () {
  19208. let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  19209. _parseConfig(cfg);
  19210. SET_CONFIG = true;
  19211. };
  19212. DOMPurify.clearConfig = function () {
  19213. CONFIG = null;
  19214. SET_CONFIG = false;
  19215. };
  19216. DOMPurify.isValidAttribute = function (tag, attr, value) {
  19217. /* Initialize shared config vars if necessary. */
  19218. if (!CONFIG) {
  19219. _parseConfig({});
  19220. }
  19221. const lcTag = transformCaseFunc(tag);
  19222. const lcName = transformCaseFunc(attr);
  19223. return _isValidAttribute(lcTag, lcName, value);
  19224. };
  19225. DOMPurify.addHook = function (entryPoint, hookFunction) {
  19226. if (typeof hookFunction !== 'function') {
  19227. return;
  19228. }
  19229. arrayPush(hooks[entryPoint], hookFunction);
  19230. };
  19231. DOMPurify.removeHook = function (entryPoint, hookFunction) {
  19232. if (hookFunction !== undefined) {
  19233. const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
  19234. return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0];
  19235. }
  19236. return arrayPop(hooks[entryPoint]);
  19237. };
  19238. DOMPurify.removeHooks = function (entryPoint) {
  19239. hooks[entryPoint] = [];
  19240. };
  19241. DOMPurify.removeAllHooks = function () {
  19242. hooks = _createHooksMap();
  19243. };
  19244. return DOMPurify;
  19245. }
  19246. var purify = createDOMPurify();
  19247. /**
  19248. * This class handles parsing, modification and serialization of URI/URL strings.
  19249. * @class tinymce.util.URI
  19250. */
  19251. const each$6 = Tools.each, trim = Tools.trim;
  19252. const queryParts = [
  19253. 'source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host',
  19254. 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor'
  19255. ];
  19256. const DEFAULT_PORTS = {
  19257. ftp: 21,
  19258. http: 80,
  19259. https: 443,
  19260. mailto: 25
  19261. };
  19262. const safeSvgDataUrlElements = ['img', 'video'];
  19263. const blockSvgDataUris = (allowSvgDataUrls, tagName) => {
  19264. if (isNonNullable(allowSvgDataUrls)) {
  19265. return !allowSvgDataUrls;
  19266. }
  19267. else {
  19268. // Only allow SVGs by default on images/videos since the browser won't execute scripts on those elements
  19269. return isNonNullable(tagName) ? !contains$2(safeSvgDataUrlElements, tagName) : true;
  19270. }
  19271. };
  19272. const decodeUri = (encodedUri) => {
  19273. try {
  19274. // Might throw malformed URI sequence
  19275. return decodeURIComponent(encodedUri);
  19276. }
  19277. catch (_a) {
  19278. // Fallback to non UTF-8 decoder
  19279. return unescape(encodedUri);
  19280. }
  19281. };
  19282. const isInvalidUri = (settings, uri, tagName) => {
  19283. // remove all whitespaces from decoded uri to prevent impact on regex matching
  19284. const decodedUri = decodeUri(uri).replace(/\s/g, '');
  19285. if (settings.allow_script_urls) {
  19286. return false;
  19287. // Ensure we don't have a javascript URI, as that is not safe since it allows arbitrary JavaScript execution
  19288. }
  19289. else if (/((java|vb)script|mhtml):/i.test(decodedUri)) {
  19290. return true;
  19291. }
  19292. else if (settings.allow_html_data_urls) {
  19293. return false;
  19294. }
  19295. else if (/^data:image\//i.test(decodedUri)) {
  19296. return blockSvgDataUris(settings.allow_svg_data_urls, tagName) && /^data:image\/svg\+xml/i.test(decodedUri);
  19297. }
  19298. else {
  19299. return /^data:/i.test(decodedUri);
  19300. }
  19301. };
  19302. class URI {
  19303. static parseDataUri(uri) {
  19304. let type;
  19305. const uriComponents = decodeURIComponent(uri).split(',');
  19306. const matches = /data:([^;]+)/.exec(uriComponents[0]);
  19307. if (matches) {
  19308. type = matches[1];
  19309. }
  19310. return {
  19311. type,
  19312. data: uriComponents[1]
  19313. };
  19314. }
  19315. /**
  19316. * Check to see if a URI is safe to use in the Document Object Model (DOM). This will return
  19317. * true if the URI can be used in the DOM without potentially triggering a security issue.
  19318. *
  19319. * @method isDomSafe
  19320. * @static
  19321. * @param {String} uri The URI to be validated.
  19322. * @param {Object} context An optional HTML tag name where the element is being used.
  19323. * @param {Object} options An optional set of options to use when determining if the URI is safe.
  19324. * @return {Boolean} True if the URI is safe, otherwise false.
  19325. */
  19326. static isDomSafe(uri, context, options = {}) {
  19327. if (options.allow_script_urls) {
  19328. return true;
  19329. }
  19330. else {
  19331. const decodedUri = Entities.decode(uri).replace(/[\s\u0000-\u001F]+/g, '');
  19332. return !isInvalidUri(options, decodedUri, context);
  19333. }
  19334. }
  19335. static getDocumentBaseUrl(loc) {
  19336. var _a;
  19337. let baseUrl;
  19338. // Pass applewebdata:// and other non web protocols though
  19339. if (loc.protocol.indexOf('http') !== 0 && loc.protocol !== 'file:') {
  19340. baseUrl = (_a = loc.href) !== null && _a !== void 0 ? _a : '';
  19341. }
  19342. else {
  19343. baseUrl = loc.protocol + '//' + loc.host + loc.pathname;
  19344. }
  19345. if (/^[^:]+:\/\/\/?[^\/]+\//.test(baseUrl)) {
  19346. baseUrl = baseUrl.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
  19347. if (!/[\/\\]$/.test(baseUrl)) {
  19348. baseUrl += '/';
  19349. }
  19350. }
  19351. return baseUrl;
  19352. }
  19353. /**
  19354. * Constructs a new URI instance.
  19355. *
  19356. * @constructor
  19357. * @method URI
  19358. * @param {String} url URI string to parse.
  19359. * @param {Object} settings Optional settings object.
  19360. */
  19361. constructor(url, settings = {}) {
  19362. this.path = '';
  19363. this.directory = '';
  19364. url = trim(url);
  19365. this.settings = settings;
  19366. const baseUri = settings.base_uri;
  19367. const self = this;
  19368. // Strange app protocol that isn't http/https or local anchor
  19369. // For example: mailto,skype,tel etc.
  19370. if (/^([\w\-]+):([^\/]{2})/i.test(url) || /^\s*#/.test(url)) {
  19371. self.source = url;
  19372. return;
  19373. }
  19374. const isProtocolRelative = url.indexOf('//') === 0;
  19375. // Absolute path with no host, fake host and protocol
  19376. if (url.indexOf('/') === 0 && !isProtocolRelative) {
  19377. url = (baseUri ? baseUri.protocol || 'http' : 'http') + '://mce_host' + url;
  19378. }
  19379. // Relative path http:// or protocol relative //path
  19380. if (!/^[\w\-]*:?\/\//.test(url)) {
  19381. const baseUrl = baseUri ? baseUri.path : new URI(document.location.href).directory;
  19382. if ((baseUri === null || baseUri === void 0 ? void 0 : baseUri.protocol) === '') {
  19383. url = '//mce_host' + self.toAbsPath(baseUrl, url);
  19384. }
  19385. else {
  19386. const match = /([^#?]*)([#?]?.*)/.exec(url);
  19387. if (match) {
  19388. url = ((baseUri && baseUri.protocol) || 'http') + '://mce_host' + self.toAbsPath(baseUrl, match[1]) + match[2];
  19389. }
  19390. }
  19391. }
  19392. // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri)
  19393. url = url.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something
  19394. const urlMatch = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?(\[[a-zA-Z0-9:.%]+\]|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(url);
  19395. if (urlMatch) {
  19396. each$6(queryParts, (v, i) => {
  19397. let part = urlMatch[i];
  19398. // Zope 3 workaround, they use @@something
  19399. if (part) {
  19400. part = part.replace(/\(mce_at\)/g, '@@');
  19401. }
  19402. self[v] = part;
  19403. });
  19404. }
  19405. if (baseUri) {
  19406. if (!self.protocol) {
  19407. self.protocol = baseUri.protocol;
  19408. }
  19409. if (!self.userInfo) {
  19410. self.userInfo = baseUri.userInfo;
  19411. }
  19412. if (!self.port && self.host === 'mce_host') {
  19413. self.port = baseUri.port;
  19414. }
  19415. if (!self.host || self.host === 'mce_host') {
  19416. self.host = baseUri.host;
  19417. }
  19418. self.source = '';
  19419. }
  19420. if (isProtocolRelative) {
  19421. self.protocol = '';
  19422. }
  19423. }
  19424. /**
  19425. * Sets the internal path part of the URI.
  19426. *
  19427. * @method setPath
  19428. * @param {String} path Path string to set.
  19429. */
  19430. setPath(path) {
  19431. const pathMatch = /^(.*?)\/?(\w+)?$/.exec(path);
  19432. // Update path parts
  19433. if (pathMatch) {
  19434. this.path = pathMatch[0];
  19435. this.directory = pathMatch[1];
  19436. this.file = pathMatch[2];
  19437. }
  19438. // Rebuild source
  19439. this.source = '';
  19440. this.getURI();
  19441. }
  19442. /**
  19443. * Converts the specified URI into a relative URI based on the current URI instance location.
  19444. *
  19445. * @method toRelative
  19446. * @param {String} uri URI to convert into a relative path/URI.
  19447. * @return {String} Relative URI from the point specified in the current URI instance.
  19448. * @example
  19449. * // Converts an absolute URL to an relative URL url will be somedir/somefile.htm
  19450. * const url = new tinymce.util.URI('http://www.site.com/dir/').toRelative('http://www.site.com/dir/somedir/somefile.htm');
  19451. */
  19452. toRelative(uri) {
  19453. if (uri === './') {
  19454. return uri;
  19455. }
  19456. const relativeUri = new URI(uri, { base_uri: this });
  19457. // Not on same domain/port or protocol
  19458. if ((relativeUri.host !== 'mce_host' && this.host !== relativeUri.host && relativeUri.host) || this.port !== relativeUri.port ||
  19459. (this.protocol !== relativeUri.protocol && relativeUri.protocol !== '')) {
  19460. return relativeUri.getURI();
  19461. }
  19462. const tu = this.getURI(), uu = relativeUri.getURI();
  19463. // Allow usage of the base_uri when relative_urls = true
  19464. if (tu === uu || (tu.charAt(tu.length - 1) === '/' && tu.substr(0, tu.length - 1) === uu)) {
  19465. return tu;
  19466. }
  19467. let output = this.toRelPath(this.path, relativeUri.path);
  19468. // Add query
  19469. if (relativeUri.query) {
  19470. output += '?' + relativeUri.query;
  19471. }
  19472. // Add anchor
  19473. if (relativeUri.anchor) {
  19474. output += '#' + relativeUri.anchor;
  19475. }
  19476. return output;
  19477. }
  19478. /**
  19479. * Converts the specified URI into a absolute URI based on the current URI instance location.
  19480. *
  19481. * @method toAbsolute
  19482. * @param {String} uri URI to convert into a relative path/URI.
  19483. * @param {Boolean} noHost No host and protocol prefix.
  19484. * @return {String} Absolute URI from the point specified in the current URI instance.
  19485. * @example
  19486. * // Converts an relative URL to an absolute URL url will be http://www.site.com/dir/somedir/somefile.htm
  19487. * const url = new tinymce.util.URI('http://www.site.com/dir/').toAbsolute('somedir/somefile.htm');
  19488. */
  19489. toAbsolute(uri, noHost) {
  19490. const absoluteUri = new URI(uri, { base_uri: this });
  19491. return absoluteUri.getURI(noHost && this.isSameOrigin(absoluteUri));
  19492. }
  19493. /**
  19494. * Determine whether the given URI has the same origin as this URI. Based on RFC-6454.
  19495. * Supports default ports for protocols listed in DEFAULT_PORTS. Unsupported protocols will fail safe: they
  19496. * won't match, if the port specifications differ.
  19497. *
  19498. * @method isSameOrigin
  19499. * @param {tinymce.util.URI} uri Uri instance to compare.
  19500. * @returns {Boolean} True if the origins are the same.
  19501. */
  19502. isSameOrigin(uri) {
  19503. // eslint-disable-next-line eqeqeq
  19504. if (this.host == uri.host && this.protocol == uri.protocol) {
  19505. // eslint-disable-next-line eqeqeq
  19506. if (this.port == uri.port) {
  19507. return true;
  19508. }
  19509. const defaultPort = this.protocol ? DEFAULT_PORTS[this.protocol] : null;
  19510. // eslint-disable-next-line eqeqeq
  19511. if (defaultPort && ((this.port || defaultPort) == (uri.port || defaultPort))) {
  19512. return true;
  19513. }
  19514. }
  19515. return false;
  19516. }
  19517. /**
  19518. * Converts a absolute path into a relative path.
  19519. *
  19520. * @method toRelPath
  19521. * @param {String} base Base point to convert the path from.
  19522. * @param {String} path Absolute path to convert into a relative path.
  19523. */
  19524. toRelPath(base, path) {
  19525. let breakPoint = 0, out = '', i, l;
  19526. // Split the paths
  19527. const normalizedBase = base.substring(0, base.lastIndexOf('/')).split('/');
  19528. const items = path.split('/');
  19529. if (normalizedBase.length >= items.length) {
  19530. for (i = 0, l = normalizedBase.length; i < l; i++) {
  19531. if (i >= items.length || normalizedBase[i] !== items[i]) {
  19532. breakPoint = i + 1;
  19533. break;
  19534. }
  19535. }
  19536. }
  19537. if (normalizedBase.length < items.length) {
  19538. for (i = 0, l = items.length; i < l; i++) {
  19539. if (i >= normalizedBase.length || normalizedBase[i] !== items[i]) {
  19540. breakPoint = i + 1;
  19541. break;
  19542. }
  19543. }
  19544. }
  19545. if (breakPoint === 1) {
  19546. return path;
  19547. }
  19548. for (i = 0, l = normalizedBase.length - (breakPoint - 1); i < l; i++) {
  19549. out += '../';
  19550. }
  19551. for (i = breakPoint - 1, l = items.length; i < l; i++) {
  19552. if (i !== breakPoint - 1) {
  19553. out += '/' + items[i];
  19554. }
  19555. else {
  19556. out += items[i];
  19557. }
  19558. }
  19559. return out;
  19560. }
  19561. /**
  19562. * Converts a relative path into a absolute path.
  19563. *
  19564. * @method toAbsPath
  19565. * @param {String} base Base point to convert the path from.
  19566. * @param {String} path Relative path to convert into an absolute path.
  19567. */
  19568. toAbsPath(base, path) {
  19569. let nb = 0;
  19570. // Split paths
  19571. const tr = /\/$/.test(path) ? '/' : '';
  19572. const normalizedBase = base.split('/');
  19573. const normalizedPath = path.split('/');
  19574. // Remove empty chunks
  19575. const baseParts = [];
  19576. each$6(normalizedBase, (k) => {
  19577. if (k) {
  19578. baseParts.push(k);
  19579. }
  19580. });
  19581. // Merge relURLParts chunks
  19582. const pathParts = [];
  19583. for (let i = normalizedPath.length - 1; i >= 0; i--) {
  19584. // Ignore empty or .
  19585. if (normalizedPath[i].length === 0 || normalizedPath[i] === '.') {
  19586. continue;
  19587. }
  19588. // Is parent
  19589. if (normalizedPath[i] === '..') {
  19590. nb++;
  19591. continue;
  19592. }
  19593. // Move up
  19594. if (nb > 0) {
  19595. nb--;
  19596. continue;
  19597. }
  19598. pathParts.push(normalizedPath[i]);
  19599. }
  19600. const i = baseParts.length - nb;
  19601. // If /a/b/c or /
  19602. let outPath;
  19603. if (i <= 0) {
  19604. outPath = reverse(pathParts).join('/');
  19605. }
  19606. else {
  19607. outPath = baseParts.slice(0, i).join('/') + '/' + reverse(pathParts).join('/');
  19608. }
  19609. // Add front / if it's needed
  19610. if (outPath.indexOf('/') !== 0) {
  19611. outPath = '/' + outPath;
  19612. }
  19613. // Add trailing / if it's needed
  19614. if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) {
  19615. outPath += tr;
  19616. }
  19617. return outPath;
  19618. }
  19619. /**
  19620. * Returns the full URI of the internal structure.
  19621. *
  19622. * @method getURI
  19623. * @param {Boolean} noProtoHost Optional no host and protocol part. Defaults to false.
  19624. */
  19625. getURI(noProtoHost = false) {
  19626. let s;
  19627. // Rebuild source
  19628. if (!this.source || noProtoHost) {
  19629. s = '';
  19630. if (!noProtoHost) {
  19631. if (this.protocol) {
  19632. s += this.protocol + '://';
  19633. }
  19634. else {
  19635. s += '//';
  19636. }
  19637. if (this.userInfo) {
  19638. s += this.userInfo + '@';
  19639. }
  19640. if (this.host) {
  19641. s += this.host;
  19642. }
  19643. if (this.port) {
  19644. s += ':' + this.port;
  19645. }
  19646. }
  19647. if (this.path) {
  19648. s += this.path;
  19649. }
  19650. if (this.query) {
  19651. s += '?' + this.query;
  19652. }
  19653. if (this.anchor) {
  19654. s += '#' + this.anchor;
  19655. }
  19656. this.source = s;
  19657. }
  19658. return this.source;
  19659. }
  19660. }
  19661. // A list of attributes that should be filtered further based on the parser settings
  19662. const filteredUrlAttrs = Tools.makeMap('src,href,data,background,action,formaction,poster,xlink:href');
  19663. const internalElementAttr = 'data-mce-type';
  19664. let uid = 0;
  19665. const processNode = (node, settings, schema, scope, evt) => {
  19666. var _a, _b, _c, _d;
  19667. const validate = settings.validate;
  19668. const specialElements = schema.getSpecialElements();
  19669. if (node.nodeType === COMMENT) {
  19670. // Pad conditional comments if they aren't allowed
  19671. if (!settings.allow_conditional_comments && /^\[if/i.test((_a = node.nodeValue) !== null && _a !== void 0 ? _a : '')) {
  19672. node.nodeValue = ' ' + node.nodeValue;
  19673. }
  19674. if (settings.sanitize && settings.allow_html_in_comments && isString(node.nodeValue)) {
  19675. node.nodeValue = encodeData(node.nodeValue);
  19676. }
  19677. }
  19678. const lcTagName = (_b = evt === null || evt === void 0 ? void 0 : evt.tagName) !== null && _b !== void 0 ? _b : node.nodeName.toLowerCase();
  19679. if (scope !== 'html' && schema.isValid(scope)) {
  19680. if (isNonNullable(evt)) {
  19681. evt.allowedTags[lcTagName] = true;
  19682. }
  19683. return;
  19684. }
  19685. // Just leave non-elements such as text and comments up to dompurify
  19686. if (node.nodeType !== ELEMENT || lcTagName === 'body') {
  19687. return;
  19688. }
  19689. // Construct the sugar element wrapper
  19690. const element = SugarElement.fromDom(node);
  19691. // Determine if we're dealing with an internal attribute
  19692. const isInternalElement = has$1(element, internalElementAttr);
  19693. // Cleanup bogus elements
  19694. const bogus = get$9(element, 'data-mce-bogus');
  19695. if (!isInternalElement && isString(bogus)) {
  19696. if (bogus === 'all') {
  19697. remove$8(element);
  19698. }
  19699. else {
  19700. unwrap(element);
  19701. }
  19702. return;
  19703. }
  19704. // Determine if the schema allows the element and either add it or remove it
  19705. const rule = schema.getElementRule(lcTagName);
  19706. if (validate && !rule) {
  19707. // If a special element is invalid, then remove the entire element instead of unwrapping
  19708. if (has$2(specialElements, lcTagName)) {
  19709. remove$8(element);
  19710. }
  19711. else {
  19712. unwrap(element);
  19713. }
  19714. return;
  19715. }
  19716. else {
  19717. if (isNonNullable(evt)) {
  19718. evt.allowedTags[lcTagName] = true;
  19719. }
  19720. }
  19721. // Validate the element using the attribute rules
  19722. if (validate && rule && !isInternalElement) {
  19723. // Fix the attributes for the element, unwrapping it if we have to
  19724. each$e((_c = rule.attributesForced) !== null && _c !== void 0 ? _c : [], (attr) => {
  19725. set$4(element, attr.name, attr.value === '{$uid}' ? `mce_${uid++}` : attr.value);
  19726. });
  19727. each$e((_d = rule.attributesDefault) !== null && _d !== void 0 ? _d : [], (attr) => {
  19728. if (!has$1(element, attr.name)) {
  19729. set$4(element, attr.name, attr.value === '{$uid}' ? `mce_${uid++}` : attr.value);
  19730. }
  19731. });
  19732. // If none of the required attributes were found then remove
  19733. if (rule.attributesRequired && !exists(rule.attributesRequired, (attr) => has$1(element, attr))) {
  19734. unwrap(element);
  19735. return;
  19736. }
  19737. // If there are no attributes then remove
  19738. if (rule.removeEmptyAttrs && hasNone(element)) {
  19739. unwrap(element);
  19740. return;
  19741. }
  19742. // Change the node name if the schema says to
  19743. if (rule.outputName && rule.outputName !== lcTagName) {
  19744. mutate(element, rule.outputName);
  19745. }
  19746. }
  19747. };
  19748. const processAttr = (ele, settings, schema, scope, evt) => {
  19749. const tagName = ele.tagName.toLowerCase();
  19750. const { attrName, attrValue } = evt;
  19751. evt.keepAttr = shouldKeepAttribute(settings, schema, scope, tagName, attrName, attrValue);
  19752. if (evt.keepAttr) {
  19753. evt.allowedAttributes[attrName] = true;
  19754. if (isBooleanAttribute(attrName, schema)) {
  19755. evt.attrValue = attrName;
  19756. }
  19757. // We need to tell DOMPurify to forcibly keep the attribute if it's an SVG data URI and svg data URIs are allowed
  19758. if (settings.allow_svg_data_urls && startsWith(attrValue, 'data:image/svg+xml')) {
  19759. evt.forceKeepAttr = true;
  19760. }
  19761. // For internal elements always keep the attribute if the attribute name is id, class or style
  19762. }
  19763. else if (isRequiredAttributeOfInternalElement(ele, attrName)) {
  19764. evt.forceKeepAttr = true;
  19765. }
  19766. };
  19767. const shouldKeepAttribute = (settings, schema, scope, tagName, attrName, attrValue) => {
  19768. // All attributes within non HTML namespaces elements are considered valid
  19769. if (scope !== 'html' && !isNonHtmlElementRootName(tagName)) {
  19770. return true;
  19771. }
  19772. return !(attrName in filteredUrlAttrs && isInvalidUri(settings, attrValue, tagName)) &&
  19773. (!settings.validate || schema.isValid(tagName, attrName) || startsWith(attrName, 'data-') || startsWith(attrName, 'aria-'));
  19774. };
  19775. const isRequiredAttributeOfInternalElement = (ele, attrName) => ele.hasAttribute(internalElementAttr) && (attrName === 'id' || attrName === 'class' || attrName === 'style');
  19776. const isBooleanAttribute = (attrName, schema) => attrName in schema.getBoolAttrs();
  19777. const filterAttributes = (ele, settings, schema, scope) => {
  19778. const { attributes } = ele;
  19779. for (let i = attributes.length - 1; i >= 0; i--) {
  19780. const attr = attributes[i];
  19781. const attrName = attr.name;
  19782. const attrValue = attr.value;
  19783. if (!shouldKeepAttribute(settings, schema, scope, ele.tagName.toLowerCase(), attrName, attrValue) && !isRequiredAttributeOfInternalElement(ele, attrName)) {
  19784. ele.removeAttribute(attrName);
  19785. }
  19786. else if (isBooleanAttribute(attrName, schema)) {
  19787. ele.setAttribute(attrName, attrName);
  19788. }
  19789. }
  19790. };
  19791. const setupPurify = (settings, schema, namespaceTracker) => {
  19792. const purify$1 = purify();
  19793. // We use this to add new tags to the allow-list as we parse, if we notice that a tag has been banned but it's still in the schema
  19794. purify$1.addHook('uponSanitizeElement', (ele, evt) => {
  19795. processNode(ele, settings, schema, namespaceTracker.track(ele), evt);
  19796. });
  19797. // Let's do the same thing for attributes
  19798. purify$1.addHook('uponSanitizeAttribute', (ele, evt) => {
  19799. processAttr(ele, settings, schema, namespaceTracker.current(), evt);
  19800. });
  19801. return purify$1;
  19802. };
  19803. const getPurifyConfig = (settings, mimeType) => {
  19804. const basePurifyConfig = {
  19805. IN_PLACE: true,
  19806. ALLOW_UNKNOWN_PROTOCOLS: true,
  19807. // Deliberately ban all tags and attributes by default, and then un-ban them on demand in hooks
  19808. // #comment and #cdata-section are always allowed as they aren't controlled via the schema
  19809. // body is also allowed due to the DOMPurify checking the root node before sanitizing
  19810. ALLOWED_TAGS: ['#comment', '#cdata-section', 'body'],
  19811. ALLOWED_ATTR: []
  19812. };
  19813. const config = { ...basePurifyConfig };
  19814. // Set the relevant parser mimetype
  19815. config.PARSER_MEDIA_TYPE = mimeType;
  19816. // Allow any URI when allowing script urls
  19817. if (settings.allow_script_urls) {
  19818. config.ALLOWED_URI_REGEXP = /.*/;
  19819. // Allow anything except javascript (or similar) URIs if all html data urls are allowed
  19820. }
  19821. else if (settings.allow_html_data_urls) {
  19822. config.ALLOWED_URI_REGEXP = /^(?!(\w+script|mhtml):)/i;
  19823. }
  19824. return config;
  19825. };
  19826. const sanitizeSvgElement = (ele) => {
  19827. // xlink:href used to be the way to do links in SVG 1.x https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
  19828. const xlinkAttrs = ['type', 'href', 'role', 'arcrole', 'title', 'show', 'actuate', 'label', 'from', 'to'].map((name) => `xlink:${name}`);
  19829. const config = {
  19830. IN_PLACE: true,
  19831. USE_PROFILES: {
  19832. html: true,
  19833. svg: true,
  19834. svgFilters: true
  19835. },
  19836. ALLOWED_ATTR: xlinkAttrs
  19837. };
  19838. purify().sanitize(ele, config);
  19839. };
  19840. const sanitizeMathmlElement = (node, settings) => {
  19841. const config = {
  19842. IN_PLACE: true,
  19843. USE_PROFILES: {
  19844. mathMl: true
  19845. },
  19846. };
  19847. const purify$1 = purify();
  19848. const allowedEncodings = settings.allow_mathml_annotation_encodings;
  19849. const hasAllowedEncodings = isArray$1(allowedEncodings) && allowedEncodings.length > 0;
  19850. const hasValidEncoding = (el) => {
  19851. const encoding = el.getAttribute('encoding');
  19852. return hasAllowedEncodings && isString(encoding) && contains$2(allowedEncodings, encoding);
  19853. };
  19854. const isValidElementOpt = (node, lcTagName) => {
  19855. if (hasAllowedEncodings && lcTagName === 'semantics') {
  19856. return Optional.some(true);
  19857. }
  19858. else if (lcTagName === 'annotation') {
  19859. return Optional.some(isElement$7(node) && hasValidEncoding(node));
  19860. }
  19861. else if (isArray$1(settings.extended_mathml_elements)) {
  19862. if (settings.extended_mathml_elements.includes(lcTagName)) {
  19863. return Optional.from(true);
  19864. }
  19865. else {
  19866. return Optional.none();
  19867. }
  19868. }
  19869. else {
  19870. return Optional.none();
  19871. }
  19872. };
  19873. purify$1.addHook('uponSanitizeElement', (node, evt) => {
  19874. var _a;
  19875. // We know the node is an element as we have
  19876. // passed an element to the purify.sanitize function below
  19877. const lcTagName = (_a = evt.tagName) !== null && _a !== void 0 ? _a : node.nodeName.toLowerCase();
  19878. const keepElementOpt = isValidElementOpt(node, lcTagName);
  19879. keepElementOpt.each((keepElement) => {
  19880. evt.allowedTags[lcTagName] = keepElement;
  19881. if (!keepElement && settings.sanitize) {
  19882. if (isElement$7(node)) {
  19883. node.remove();
  19884. }
  19885. }
  19886. });
  19887. });
  19888. purify$1.addHook('uponSanitizeAttribute', (_node, event) => {
  19889. if (isArray$1(settings.extended_mathml_attributes)) {
  19890. const keepAttribute = settings.extended_mathml_attributes.includes(event.attrName);
  19891. if (keepAttribute) {
  19892. event.forceKeepAttr = true;
  19893. }
  19894. }
  19895. });
  19896. purify$1.sanitize(node, config);
  19897. };
  19898. const mkSanitizeNamespaceElement = (settings) => (ele) => {
  19899. const namespaceType = toScopeType(ele);
  19900. if (namespaceType === 'svg') {
  19901. sanitizeSvgElement(ele);
  19902. }
  19903. else if (namespaceType === 'math') {
  19904. sanitizeMathmlElement(ele, settings);
  19905. }
  19906. else {
  19907. throw new Error('Not a namespace element');
  19908. }
  19909. };
  19910. const getSanitizer = (settings, schema) => {
  19911. const namespaceTracker = createNamespaceTracker();
  19912. if (settings.sanitize) {
  19913. const purify = setupPurify(settings, schema, namespaceTracker);
  19914. const sanitizeHtmlElement = (body, mimeType) => {
  19915. purify.sanitize(body, getPurifyConfig(settings, mimeType));
  19916. purify.removed = [];
  19917. namespaceTracker.reset();
  19918. };
  19919. return {
  19920. sanitizeHtmlElement,
  19921. sanitizeNamespaceElement: mkSanitizeNamespaceElement(settings)
  19922. };
  19923. }
  19924. else {
  19925. const sanitizeHtmlElement = (body, _mimeType) => {
  19926. // eslint-disable-next-line no-bitwise
  19927. const nodeIterator = document.createNodeIterator(body, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
  19928. let node;
  19929. while ((node = nodeIterator.nextNode())) {
  19930. const currentScope = namespaceTracker.track(node);
  19931. processNode(node, settings, schema, currentScope);
  19932. if (isElement$7(node)) {
  19933. filterAttributes(node, settings, schema, currentScope);
  19934. }
  19935. }
  19936. namespaceTracker.reset();
  19937. };
  19938. const sanitizeNamespaceElement = noop;
  19939. return {
  19940. sanitizeHtmlElement,
  19941. sanitizeNamespaceElement
  19942. };
  19943. }
  19944. };
  19945. /**
  19946. * @summary
  19947. * This class parses HTML code into a DOM like structure of nodes it will remove redundant whitespace and make
  19948. * sure that the node tree is valid according to the specified schema.
  19949. * So for example: `<p>a<p>b</p>c</p>` will become `<p>a</p><p>b</p><p>c</p>`.
  19950. *
  19951. * @example
  19952. * const parser = tinymce.html.DomParser({ validate: true }, schema);
  19953. * const rootNode = parser.parse('<h1>content</h1>');
  19954. *
  19955. * @class tinymce.html.DomParser
  19956. * @version 3.4
  19957. */
  19958. const extraBlockLikeElements = ['script', 'style', 'template', 'param'];
  19959. const makeMap = Tools.makeMap, extend$1 = Tools.extend;
  19960. const transferChildren = (parent, nativeParent, specialElements, nsSanitizer, decodeComments) => {
  19961. const parentName = parent.name;
  19962. // Exclude the special elements where the content is RCDATA as their content needs to be parsed instead of being left as plain text
  19963. // See: https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments
  19964. const isSpecial = parentName in specialElements && parentName !== 'title' && parentName !== 'textarea' && parentName !== 'noscript';
  19965. const childNodes = nativeParent.childNodes;
  19966. for (let ni = 0, nl = childNodes.length; ni < nl; ni++) {
  19967. const nativeChild = childNodes[ni];
  19968. const child = new AstNode(nativeChild.nodeName.toLowerCase(), nativeChild.nodeType);
  19969. if (isElement$7(nativeChild)) {
  19970. const attributes = nativeChild.attributes;
  19971. for (let ai = 0, al = attributes.length; ai < al; ai++) {
  19972. const attr = attributes[ai];
  19973. child.attr(attr.name, attr.value);
  19974. }
  19975. if (isNonHtmlElementRootName(child.name)) {
  19976. nsSanitizer(nativeChild);
  19977. child.value = nativeChild.innerHTML;
  19978. }
  19979. }
  19980. else if (isText$b(nativeChild)) {
  19981. child.value = nativeChild.data;
  19982. if (isSpecial) {
  19983. child.raw = true;
  19984. }
  19985. }
  19986. else if (isComment(nativeChild)) {
  19987. child.value = decodeComments ? decodeData$1(nativeChild.data) : nativeChild.data;
  19988. }
  19989. else if (isCData(nativeChild) || isPi(nativeChild)) {
  19990. child.value = nativeChild.data;
  19991. }
  19992. if (isTemplate(nativeChild)) {
  19993. const content = AstNode.create('#text');
  19994. content.value = nativeChild.innerHTML;
  19995. content.raw = true;
  19996. child.append(content);
  19997. }
  19998. else if (!isNonHtmlElementRootName(child.name)) {
  19999. transferChildren(child, nativeChild, specialElements, nsSanitizer, decodeComments);
  20000. }
  20001. parent.append(child);
  20002. }
  20003. };
  20004. const walkTree = (root, preprocessors, postprocessors) => {
  20005. const traverseOrder = [];
  20006. for (let node = root, lastNode = node; node; lastNode = node, node = node.walk()) {
  20007. const tempNode = node;
  20008. each$e(preprocessors, (preprocess) => preprocess(tempNode));
  20009. if (isNullable(tempNode.parent) && tempNode !== root) {
  20010. // The node has been detached, so rewind a little and don't add it to our traversal
  20011. node = lastNode;
  20012. }
  20013. else {
  20014. traverseOrder.push(tempNode);
  20015. }
  20016. }
  20017. for (let i = traverseOrder.length - 1; i >= 0; i--) {
  20018. const node = traverseOrder[i];
  20019. each$e(postprocessors, (postprocess) => postprocess(node));
  20020. }
  20021. };
  20022. // All the dom operations we want to perform, regardless of whether we're trying to properly validate things
  20023. // e.g. removing excess whitespace
  20024. // e.g. removing empty nodes (or padding them with <br>)
  20025. //
  20026. // Returns [ preprocess, postprocess ]
  20027. const whitespaceCleaner = (root, schema, settings, args) => {
  20028. const validate = settings.validate;
  20029. const nonEmptyElements = schema.getNonEmptyElements();
  20030. const whitespaceElements = schema.getWhitespaceElements();
  20031. const blockElements = extend$1(makeMap(extraBlockLikeElements), schema.getBlockElements());
  20032. const textRootBlockElements = getTextRootBlockElements(schema);
  20033. const allWhiteSpaceRegExp = /[ \t\r\n]+/g;
  20034. const startWhiteSpaceRegExp = /^[ \t\r\n]+/;
  20035. const endWhiteSpaceRegExp = /[ \t\r\n]+$/;
  20036. const hasWhitespaceParent = (node) => {
  20037. let tempNode = node.parent;
  20038. while (isNonNullable(tempNode)) {
  20039. if (tempNode.name in whitespaceElements) {
  20040. return true;
  20041. }
  20042. else {
  20043. tempNode = tempNode.parent;
  20044. }
  20045. }
  20046. return false;
  20047. };
  20048. const isTextRootBlockEmpty = (node) => {
  20049. let tempNode = node;
  20050. while (isNonNullable(tempNode)) {
  20051. if (tempNode.name in textRootBlockElements) {
  20052. return isEmpty$2(schema, nonEmptyElements, whitespaceElements, tempNode);
  20053. }
  20054. else {
  20055. tempNode = tempNode.parent;
  20056. }
  20057. }
  20058. return false;
  20059. };
  20060. const isBlock = (node) => node.name in blockElements || isTransparentAstBlock(schema, node) || (isNonHtmlElementRootName(node.name) && node.parent === root);
  20061. const isAtEdgeOfBlock = (node, start) => {
  20062. const neighbour = start ? node.prev : node.next;
  20063. if (isNonNullable(neighbour) || isNullable(node.parent)) {
  20064. return false;
  20065. }
  20066. // Make sure our parent is actually a block, and also make sure it isn't a temporary "context" element
  20067. // that we're probably going to unwrap as soon as we insert this content into the editor
  20068. return isBlock(node.parent) && (node.parent !== root || args.isRootContent === true);
  20069. };
  20070. const preprocess = (node) => {
  20071. var _a;
  20072. if (node.type === 3) {
  20073. // Remove leading whitespace here, so that all whitespace in nodes to the left of us has already been fixed
  20074. if (!hasWhitespaceParent(node)) {
  20075. let text = (_a = node.value) !== null && _a !== void 0 ? _a : '';
  20076. text = text.replace(allWhiteSpaceRegExp, ' ');
  20077. if (isLineBreakNode(node.prev, isBlock) || isAtEdgeOfBlock(node, true)) {
  20078. text = text.replace(startWhiteSpaceRegExp, '');
  20079. }
  20080. if (text.length === 0) {
  20081. node.remove();
  20082. }
  20083. else if (text === ' ' && node.prev && node.prev.type === COMMENT && node.next && node.next.type === COMMENT) {
  20084. node.remove();
  20085. }
  20086. else {
  20087. node.value = text;
  20088. }
  20089. }
  20090. }
  20091. };
  20092. const postprocess = (node) => {
  20093. var _a;
  20094. if (node.type === 1) {
  20095. // Check for empty nodes here, because children will have been processed and (if necessary) emptied / removed already
  20096. const elementRule = schema.getElementRule(node.name);
  20097. if (validate && elementRule) {
  20098. const isNodeEmpty = isEmpty$2(schema, nonEmptyElements, whitespaceElements, node);
  20099. if (elementRule.paddInEmptyBlock && isNodeEmpty && isTextRootBlockEmpty(node)) {
  20100. paddEmptyNode(settings, args, isBlock, node);
  20101. }
  20102. else if (elementRule.removeEmpty && isNodeEmpty) {
  20103. if (isBlock(node)) {
  20104. node.remove();
  20105. }
  20106. else {
  20107. node.unwrap();
  20108. }
  20109. }
  20110. else if (elementRule.paddEmpty && (isNodeEmpty || isPaddedWithNbsp(node))) {
  20111. paddEmptyNode(settings, args, isBlock, node);
  20112. }
  20113. }
  20114. }
  20115. else if (node.type === 3) {
  20116. // Removing trailing whitespace here, so that all whitespace in nodes to the right of us has already been fixed
  20117. if (!hasWhitespaceParent(node)) {
  20118. let text = (_a = node.value) !== null && _a !== void 0 ? _a : '';
  20119. if (node.next && isBlock(node.next) || isAtEdgeOfBlock(node, false)) {
  20120. text = text.replace(endWhiteSpaceRegExp, '');
  20121. }
  20122. if (text.length === 0) {
  20123. node.remove();
  20124. }
  20125. else {
  20126. node.value = text;
  20127. }
  20128. }
  20129. }
  20130. };
  20131. return [preprocess, postprocess];
  20132. };
  20133. const getRootBlockName = (settings, args) => {
  20134. var _a;
  20135. const name = (_a = args.forced_root_block) !== null && _a !== void 0 ? _a : settings.forced_root_block;
  20136. if (name === false) {
  20137. return '';
  20138. }
  20139. else if (name === true) {
  20140. return 'p';
  20141. }
  20142. else {
  20143. return name;
  20144. }
  20145. };
  20146. const DomParser = (settings = {}, schema = Schema()) => {
  20147. const nodeFilterRegistry = create$8();
  20148. const attributeFilterRegistry = create$8();
  20149. // Apply setting defaults
  20150. const defaultedSettings = {
  20151. validate: true,
  20152. root_name: 'body',
  20153. sanitize: true,
  20154. allow_html_in_comments: false,
  20155. ...settings
  20156. };
  20157. const parser = new DOMParser();
  20158. const sanitizer = getSanitizer(defaultedSettings, schema);
  20159. const parseAndSanitizeWithContext = (html, rootName, format = 'html') => {
  20160. const mimeType = format === 'xhtml' ? 'application/xhtml+xml' : 'text/html';
  20161. // Determine the root element to wrap the HTML in when parsing. If we're dealing with a
  20162. // special element then we need to wrap it so the internal content is handled appropriately.
  20163. const isSpecialRoot = has$2(schema.getSpecialElements(), rootName.toLowerCase());
  20164. const content = isSpecialRoot ? `<${rootName}>${html}</${rootName}>` : html;
  20165. const makeWrap = () => {
  20166. if (format === 'xhtml') {
  20167. // If parsing XHTML then the content must contain the xmlns declaration, see https://www.w3.org/TR/xhtml1/normative.html#strict
  20168. return `<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>${content}</body></html>`;
  20169. }
  20170. else if (/^[\s]*<head/i.test(html) || /^[\s]*<html/i.test(html) || /^[\s]*<!DOCTYPE/i.test(html)) {
  20171. return `<html>${content}</html>`;
  20172. }
  20173. else {
  20174. return `<body>${content}</body>`;
  20175. }
  20176. };
  20177. const body = parser.parseFromString(makeWrap(), mimeType).body;
  20178. sanitizer.sanitizeHtmlElement(body, mimeType);
  20179. return isSpecialRoot ? body.firstChild : body;
  20180. };
  20181. /**
  20182. * Adds a node filter function to the parser, the parser will collect the specified nodes by name
  20183. * and then execute the callback once it has finished parsing the document.
  20184. *
  20185. * @method addNodeFilter
  20186. * @param {String} name Comma separated list of nodes to collect.
  20187. * @param {Function} callback Callback function to execute once it has collected nodes.
  20188. * @example
  20189. * parser.addNodeFilter('p,h1', (nodes, name) => {
  20190. * for (var i = 0; i < nodes.length; i++) {
  20191. * console.log(nodes[i].name);
  20192. * }
  20193. * });
  20194. */
  20195. const addNodeFilter = nodeFilterRegistry.addFilter;
  20196. const getNodeFilters = nodeFilterRegistry.getFilters;
  20197. /**
  20198. * Removes a node filter function or removes all filter functions from the parser for the node names provided.
  20199. *
  20200. * @method removeNodeFilter
  20201. * @param {String} name Comma separated list of node names to remove filters for.
  20202. * @param {Function} callback Optional callback function to only remove a specific callback.
  20203. * @example
  20204. * // Remove a single filter
  20205. * parser.removeNodeFilter('p,h1', someCallback);
  20206. *
  20207. * // Remove all filters
  20208. * parser.removeNodeFilter('p,h1');
  20209. */
  20210. const removeNodeFilter = nodeFilterRegistry.removeFilter;
  20211. /**
  20212. * Adds an attribute filter function to the parser, the parser will collect nodes that has the specified attributes
  20213. * and then execute the callback once it has finished parsing the document.
  20214. *
  20215. * @method addAttributeFilter
  20216. * @param {String} name Comma separated list of attributes to collect.
  20217. * @param {Function} callback Callback function to execute once it has collected nodes.
  20218. * @example
  20219. * parser.addAttributeFilter('src,href', (nodes, name) => {
  20220. * for (let i = 0; i < nodes.length; i++) {
  20221. * console.log(nodes[i].name);
  20222. * }
  20223. * });
  20224. */
  20225. const addAttributeFilter = attributeFilterRegistry.addFilter;
  20226. const getAttributeFilters = attributeFilterRegistry.getFilters;
  20227. /**
  20228. * Removes an attribute filter function or removes all filter functions from the parser for the attribute names provided.
  20229. *
  20230. * @method removeAttributeFilter
  20231. * @param {String} name Comma separated list of attribute names to remove filters for.
  20232. * @param {Function} callback Optional callback function to only remove a specific callback.
  20233. * @example
  20234. * // Remove a single filter
  20235. * parser.removeAttributeFilter('src,href', someCallback);
  20236. *
  20237. * // Remove all filters
  20238. * parser.removeAttributeFilter('src,href');
  20239. */
  20240. const removeAttributeFilter = attributeFilterRegistry.removeFilter;
  20241. const findInvalidChildren = (node, invalidChildren) => {
  20242. if (isInvalid(schema, node)) {
  20243. invalidChildren.push(node);
  20244. }
  20245. };
  20246. const isWrappableNode = (blockElements, node) => {
  20247. const isInternalElement = isString(node.attr(internalElementAttr));
  20248. const isInlineElement = node.type === 1 && (!has$2(blockElements, node.name) && !isTransparentAstBlock(schema, node)) && !isNonHtmlElementRootName(node.name);
  20249. return node.type === 3 || (isInlineElement && !isInternalElement);
  20250. };
  20251. const addRootBlocks = (rootNode, rootBlockName) => {
  20252. const blockElements = extend$1(makeMap(extraBlockLikeElements), schema.getBlockElements());
  20253. const startWhiteSpaceRegExp = /^[ \t\r\n]+/;
  20254. const endWhiteSpaceRegExp = /[ \t\r\n]+$/;
  20255. let node = rootNode.firstChild, rootBlockNode = null;
  20256. // Removes whitespace at beginning and end of block so:
  20257. // <p> x </p> -> <p>x</p>
  20258. const trim = (rootBlock) => {
  20259. var _a, _b;
  20260. if (rootBlock) {
  20261. node = rootBlock.firstChild;
  20262. if (node && node.type === 3) {
  20263. node.value = (_a = node.value) === null || _a === void 0 ? void 0 : _a.replace(startWhiteSpaceRegExp, '');
  20264. }
  20265. node = rootBlock.lastChild;
  20266. if (node && node.type === 3) {
  20267. node.value = (_b = node.value) === null || _b === void 0 ? void 0 : _b.replace(endWhiteSpaceRegExp, '');
  20268. }
  20269. }
  20270. };
  20271. // Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditable root
  20272. if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) {
  20273. return;
  20274. }
  20275. while (node) {
  20276. const next = node.next;
  20277. if (isWrappableNode(blockElements, node)) {
  20278. if (!rootBlockNode) {
  20279. // Create a new root block element
  20280. rootBlockNode = new AstNode(rootBlockName, 1);
  20281. rootBlockNode.attr(defaultedSettings.forced_root_block_attrs);
  20282. rootNode.insert(rootBlockNode, node);
  20283. rootBlockNode.append(node);
  20284. }
  20285. else {
  20286. rootBlockNode.append(node);
  20287. }
  20288. }
  20289. else {
  20290. trim(rootBlockNode);
  20291. rootBlockNode = null;
  20292. }
  20293. node = next;
  20294. }
  20295. trim(rootBlockNode);
  20296. };
  20297. /**
  20298. * Parses the specified HTML string into a DOM like node tree and returns the result.
  20299. *
  20300. * @method parse
  20301. * @param {String} html Html string to sax parse.
  20302. * @param {Object} args Optional args object that gets passed to all filter functions.
  20303. * @return {tinymce.html.Node} Root node containing the tree.
  20304. * @example
  20305. * const rootNode = tinymce.html.DomParser({...}).parse('<b>text</b>');
  20306. */
  20307. const parse = (html, args = {}) => {
  20308. var _a;
  20309. const validate = defaultedSettings.validate;
  20310. const rootName = (_a = args.context) !== null && _a !== void 0 ? _a : defaultedSettings.root_name;
  20311. // Parse and sanitize the content
  20312. const element = parseAndSanitizeWithContext(html, rootName, args.format);
  20313. updateChildren(schema, element);
  20314. // Create the AST representation
  20315. const rootNode = new AstNode(rootName, 11);
  20316. transferChildren(rootNode, element, schema.getSpecialElements(), sanitizer.sanitizeNamespaceElement, defaultedSettings.sanitize && defaultedSettings.allow_html_in_comments);
  20317. // This next line is needed to fix a memory leak in chrome and firefox.
  20318. // For more information see TINY-9186
  20319. element.innerHTML = '';
  20320. // Set up whitespace fixes
  20321. const [whitespacePre, whitespacePost] = whitespaceCleaner(rootNode, schema, defaultedSettings, args);
  20322. // Find the invalid children in the tree
  20323. const invalidChildren = [];
  20324. const invalidFinder = validate ? (node) => findInvalidChildren(node, invalidChildren) : noop;
  20325. // Set up attribute and node matching
  20326. const matches = { nodes: {}, attributes: {} };
  20327. const matchFinder = (node) => matchNode(getNodeFilters(), getAttributeFilters(), node, matches);
  20328. // Walk the dom, apply all of the above things
  20329. walkTree(rootNode, [whitespacePre, matchFinder], [whitespacePost, invalidFinder]);
  20330. // Because we collected invalid children while walking backwards, we need to reverse the list before operating on them
  20331. invalidChildren.reverse();
  20332. // Fix invalid children or report invalid children in a contextual parsing
  20333. if (validate && invalidChildren.length > 0) {
  20334. if (args.context) {
  20335. args.invalid = true;
  20336. }
  20337. else {
  20338. cleanInvalidNodes(invalidChildren, schema, rootNode, matchFinder);
  20339. }
  20340. }
  20341. // Wrap nodes in the root into block elements if the root is body
  20342. const rootBlockName = getRootBlockName(defaultedSettings, args);
  20343. if (rootBlockName && (rootNode.name === 'body' || args.isRootContent)) {
  20344. addRootBlocks(rootNode, rootBlockName);
  20345. }
  20346. // Run filters only when the contents is valid
  20347. if (!args.invalid) {
  20348. runFilters(matches, args);
  20349. }
  20350. return rootNode;
  20351. };
  20352. const exports = {
  20353. schema,
  20354. addAttributeFilter,
  20355. getAttributeFilters,
  20356. removeAttributeFilter,
  20357. addNodeFilter,
  20358. getNodeFilters,
  20359. removeNodeFilter,
  20360. parse
  20361. };
  20362. register$4(exports, defaultedSettings);
  20363. register$5(exports, defaultedSettings, schema);
  20364. return exports;
  20365. };
  20366. const isTreeNode = (content) => content instanceof AstNode;
  20367. const serializeContent = (content) => isTreeNode(content) ? HtmlSerializer({ validate: false }).serialize(content) : content;
  20368. const withSerializedContent = (content, fireEvent, parserSettings) => {
  20369. const serializedContent = serializeContent(content);
  20370. const eventArgs = fireEvent(serializedContent);
  20371. if (eventArgs.isDefaultPrevented()) {
  20372. return eventArgs;
  20373. }
  20374. else if (isTreeNode(content)) {
  20375. // Restore the content type back to being an AstNode. If the content has changed we need to
  20376. // re-parse the new content, otherwise we can return the input.
  20377. if (eventArgs.content !== serializedContent) {
  20378. const rootNode = DomParser({ validate: false, forced_root_block: false, ...parserSettings }).parse(eventArgs.content, { context: content.name });
  20379. return { ...eventArgs, content: rootNode };
  20380. }
  20381. else {
  20382. return { ...eventArgs, content };
  20383. }
  20384. }
  20385. else {
  20386. return eventArgs;
  20387. }
  20388. };
  20389. const makeParserSettings = (editor) => ({
  20390. sanitize: shouldSanitizeXss(editor),
  20391. sandbox_iframes: shouldSandboxIframes(editor),
  20392. sandbox_iframes_exclusions: getSandboxIframesExclusions(editor)
  20393. });
  20394. const preProcessGetContent = (editor, args) => {
  20395. if (args.no_events) {
  20396. return Result.value(args);
  20397. }
  20398. else {
  20399. const eventArgs = fireBeforeGetContent(editor, args);
  20400. if (eventArgs.isDefaultPrevented()) {
  20401. return Result.error(fireGetContent(editor, { content: '', ...eventArgs }).content);
  20402. }
  20403. else {
  20404. return Result.value(eventArgs);
  20405. }
  20406. }
  20407. };
  20408. const postProcessGetContent = (editor, content, args) => {
  20409. if (args.no_events) {
  20410. return content;
  20411. }
  20412. else {
  20413. const processedEventArgs = withSerializedContent(content, (content) => fireGetContent(editor, { ...args, content }), makeParserSettings(editor));
  20414. return processedEventArgs.content;
  20415. }
  20416. };
  20417. const preProcessSetContent = (editor, args) => {
  20418. if (args.no_events) {
  20419. return Result.value(args);
  20420. }
  20421. else {
  20422. const processedEventArgs = withSerializedContent(args.content, (content) => fireBeforeSetContent(editor, { ...args, content }), makeParserSettings(editor));
  20423. if (processedEventArgs.isDefaultPrevented()) {
  20424. fireSetContent(editor, processedEventArgs);
  20425. return Result.error(undefined);
  20426. }
  20427. else {
  20428. return Result.value(processedEventArgs);
  20429. }
  20430. }
  20431. };
  20432. const postProcessSetContent = (editor, content, args) => {
  20433. if (!args.no_events) {
  20434. fireSetContent(editor, { ...args, content });
  20435. }
  20436. };
  20437. const removedOptions = ('autoresize_on_init,content_editable_state,padd_empty_with_br,block_elements,' +
  20438. 'boolean_attributes,editor_deselector,editor_selector,elements,file_browser_callback_types,filepicker_validator_handler,' +
  20439. 'force_hex_style_colors,force_p_newlines,gecko_spellcheck,images_dataimg_filter,media_scripts,mode,move_caret_before_on_enter_elements,' +
  20440. 'non_empty_elements,self_closing_elements,short_ended_elements,special,spellchecker_select_languages,spellchecker_whitelist,' +
  20441. 'tab_focus,tabfocus_elements,table_responsive_width,text_block_elements,text_inline_elements,toolbar_drawer,types,validate,whitespace_elements,' +
  20442. 'paste_enable_default_filters,paste_filter_drop,paste_word_valid_elements,paste_retain_style_properties,paste_convert_word_fake_lists,' +
  20443. 'template_cdate_classes,template_mdate_classes,template_selected_content_classes,template_preview_replace_values,template_replace_values,templates,template_cdate_format,template_mdate_format').split(',');
  20444. // const deprecatedOptions: string[] = ('').split(',');
  20445. const deprecatedOptions = [];
  20446. const removedPlugins = 'bbcode,colorpicker,contextmenu,fullpage,legacyoutput,spellchecker,template,textcolor,rtc'.split(',');
  20447. const deprecatedPlugins = [
  20448. {
  20449. name: 'export',
  20450. replacedWith: 'Export to PDF'
  20451. },
  20452. ];
  20453. const getMatchingOptions = (options, searchingFor) => {
  20454. const settingNames = filter$5(searchingFor, (setting) => has$2(options, setting));
  20455. return sort(settingNames);
  20456. };
  20457. const getRemovedOptions = (options) => {
  20458. const settingNames = getMatchingOptions(options, removedOptions);
  20459. // Forced root block is a special case whereby only the empty/false value is deprecated
  20460. const forcedRootBlock = options.forced_root_block;
  20461. // Note: This cast is required for old configurations as forced root block used to allow a boolean
  20462. if (forcedRootBlock === false || forcedRootBlock === '') {
  20463. settingNames.push('forced_root_block (false only)');
  20464. }
  20465. return sort(settingNames);
  20466. };
  20467. const getDeprecatedOptions = (options) => getMatchingOptions(options, deprecatedOptions);
  20468. const getMatchingPlugins = (options, searchingFor) => {
  20469. const plugins = Tools.makeMap(options.plugins, ' ');
  20470. const hasPlugin = (plugin) => has$2(plugins, plugin);
  20471. const pluginNames = filter$5(searchingFor, hasPlugin);
  20472. return sort(pluginNames);
  20473. };
  20474. const getRemovedPlugins = (options) => getMatchingPlugins(options, removedPlugins);
  20475. const getDeprecatedPlugins = (options) => getMatchingPlugins(options, deprecatedPlugins.map((entry) => entry.name));
  20476. const logRemovedWarnings = (rawOptions, normalizedOptions) => {
  20477. // Note: Ensure we use the original user settings, not the final when logging
  20478. const removedOptions = getRemovedOptions(rawOptions);
  20479. const removedPlugins = getRemovedPlugins(normalizedOptions);
  20480. const hasRemovedPlugins = removedPlugins.length > 0;
  20481. const hasRemovedOptions = removedOptions.length > 0;
  20482. const isLegacyMobileTheme = normalizedOptions.theme === 'mobile';
  20483. if (hasRemovedPlugins || hasRemovedOptions || isLegacyMobileTheme) {
  20484. const listJoiner = '\n- ';
  20485. const themesMessage = isLegacyMobileTheme ? `\n\nThemes:${listJoiner}mobile` : '';
  20486. const pluginsMessage = hasRemovedPlugins ? `\n\nPlugins:${listJoiner}${removedPlugins.join(listJoiner)}` : '';
  20487. const optionsMessage = hasRemovedOptions ? `\n\nOptions:${listJoiner}${removedOptions.join(listJoiner)}` : '';
  20488. // eslint-disable-next-line no-console
  20489. console.warn('The following deprecated features are currently enabled and have been removed in TinyMCE 8.0. These features will no longer work and should be removed from the TinyMCE configuration. ' +
  20490. 'See https://www.tiny.cloud/docs/tinymce/8/migration-from-7x/ for more information.' +
  20491. themesMessage +
  20492. pluginsMessage +
  20493. optionsMessage);
  20494. }
  20495. };
  20496. const getPluginDescription = (name) => find$2(deprecatedPlugins, (entry) => entry.name === name).fold(() => name, (entry) => {
  20497. if (entry.replacedWith) {
  20498. return `${name}, replaced by ${entry.replacedWith}`;
  20499. }
  20500. else {
  20501. return name;
  20502. }
  20503. });
  20504. const logDeprecatedWarnings = (rawOptions, normalizedOptions) => {
  20505. // Note: Ensure we use the original user settings, not the final when logging
  20506. const deprecatedOptions = getDeprecatedOptions(rawOptions);
  20507. const deprecatedPlugins = getDeprecatedPlugins(normalizedOptions);
  20508. const hasDeprecatedPlugins = deprecatedPlugins.length > 0;
  20509. const hasDeprecatedOptions = deprecatedOptions.length > 0;
  20510. if (hasDeprecatedPlugins || hasDeprecatedOptions) {
  20511. const listJoiner = '\n- ';
  20512. const pluginsMessage = hasDeprecatedPlugins ? `\n\nPlugins:${listJoiner}${deprecatedPlugins.map(getPluginDescription).join(listJoiner)}` : '';
  20513. const optionsMessage = hasDeprecatedOptions ? `\n\nOptions:${listJoiner}${deprecatedOptions.join(listJoiner)}` : '';
  20514. // eslint-disable-next-line no-console
  20515. console.warn('The following deprecated features are currently enabled but will be removed soon.' +
  20516. pluginsMessage +
  20517. optionsMessage);
  20518. }
  20519. };
  20520. const logWarnings = (rawOptions, normalizedOptions) => {
  20521. logRemovedWarnings(rawOptions, normalizedOptions);
  20522. logDeprecatedWarnings(rawOptions, normalizedOptions);
  20523. };
  20524. const deprecatedFeatures = {
  20525. fire: 'The "fire" event api has been deprecated and will be removed in TinyMCE 9. Use "dispatch" instead.',
  20526. selectionSetContent: 'The "editor.selection.setContent" method has been deprecated and will be removed in TinyMCE 9. Use "editor.insertContent" instead.'
  20527. };
  20528. const logFeatureDeprecationWarning = (feature) => {
  20529. // eslint-disable-next-line no-console
  20530. console.warn(deprecatedFeatures[feature], new Error().stack);
  20531. };
  20532. const removeEmpty = (text) => {
  20533. if (text.dom.length === 0) {
  20534. remove$8(text);
  20535. return Optional.none();
  20536. }
  20537. else {
  20538. return Optional.some(text);
  20539. }
  20540. };
  20541. const walkPastBookmark = (node, start) => node.filter((elm) => BookmarkManager.isBookmarkNode(elm.dom))
  20542. .bind(start ? nextSibling : prevSibling);
  20543. const merge = (outer, inner, rng, start, schema) => {
  20544. const outerElm = outer.dom;
  20545. const innerElm = inner.dom;
  20546. const oldLength = start ? outerElm.length : innerElm.length;
  20547. if (start) {
  20548. mergeTextNodes(outerElm, innerElm, schema, false, !start);
  20549. rng.setStart(innerElm, oldLength);
  20550. }
  20551. else {
  20552. mergeTextNodes(innerElm, outerElm, schema, false, !start);
  20553. rng.setEnd(innerElm, oldLength);
  20554. }
  20555. };
  20556. const normalizeTextIfRequired = (inner, start, schema) => {
  20557. parent(inner).each((root) => {
  20558. const text = inner.dom;
  20559. if (start && needsToBeNbspLeft(root, CaretPosition(text, 0), schema)) {
  20560. normalizeWhitespaceAfter(text, 0, schema);
  20561. }
  20562. else if (!start && needsToBeNbspRight(root, CaretPosition(text, text.length), schema)) {
  20563. normalizeWhitespaceBefore(text, text.length, schema);
  20564. }
  20565. });
  20566. };
  20567. const mergeAndNormalizeText = (outerNode, innerNode, rng, start, schema) => {
  20568. outerNode.bind((outer) => {
  20569. // Normalize the text outside the inserted content
  20570. const normalizer = start ? normalizeWhitespaceBefore : normalizeWhitespaceAfter;
  20571. normalizer(outer.dom, start ? outer.dom.length : 0, schema);
  20572. // Merge the inserted content with other text nodes
  20573. return innerNode.filter(isText$c).map((inner) => merge(outer, inner, rng, start, schema));
  20574. }).orThunk(() => {
  20575. // Note: Attempt to leave the inserted/inner content as is and only adjust if absolutely required
  20576. const innerTextNode = walkPastBookmark(innerNode, start).or(innerNode).filter(isText$c);
  20577. return innerTextNode.map((inner) => normalizeTextIfRequired(inner, start, schema));
  20578. });
  20579. };
  20580. const rngSetContent = (rng, fragment, schema) => {
  20581. const firstChild = Optional.from(fragment.firstChild).map(SugarElement.fromDom);
  20582. const lastChild = Optional.from(fragment.lastChild).map(SugarElement.fromDom);
  20583. rng.deleteContents();
  20584. rng.insertNode(fragment);
  20585. const prevText = firstChild.bind(prevSibling).filter(isText$c).bind(removeEmpty);
  20586. const nextText = lastChild.bind(nextSibling).filter(isText$c).bind(removeEmpty);
  20587. // Join and normalize text
  20588. mergeAndNormalizeText(prevText, firstChild, rng, true, schema);
  20589. mergeAndNormalizeText(nextText, lastChild, rng, false, schema);
  20590. rng.collapse(false);
  20591. };
  20592. const setupArgs$3 = (args, content) => ({
  20593. format: 'html',
  20594. ...args,
  20595. set: true,
  20596. selection: true,
  20597. content
  20598. });
  20599. const cleanContent = (editor, args) => {
  20600. if (args.format !== 'raw') {
  20601. // Find which context to parse the content in
  20602. const rng = editor.selection.getRng();
  20603. const contextBlock = editor.dom.getParent(rng.commonAncestorContainer, editor.dom.isBlock);
  20604. const contextArgs = contextBlock ? { context: contextBlock.nodeName.toLowerCase() } : {};
  20605. const node = editor.parser.parse(args.content, { forced_root_block: false, ...contextArgs, ...args });
  20606. return HtmlSerializer({ validate: false }, editor.schema).serialize(node);
  20607. }
  20608. else {
  20609. return args.content;
  20610. }
  20611. };
  20612. const setContentInternal$1 = (editor, content, args = {}) => {
  20613. const defaultedArgs = setupArgs$3(args, content);
  20614. preProcessSetContent(editor, defaultedArgs).each((updatedArgs) => {
  20615. // Sanitize the content
  20616. const cleanedContent = cleanContent(editor, updatedArgs);
  20617. const rng = editor.selection.getRng();
  20618. rngSetContent(rng, rng.createContextualFragment(cleanedContent), editor.schema);
  20619. editor.selection.setRng(rng);
  20620. scrollRangeIntoView(editor, rng);
  20621. postProcessSetContent(editor, cleanedContent, updatedArgs);
  20622. });
  20623. };
  20624. const setContentExternal = (editor, content, args = {}) => {
  20625. logFeatureDeprecationWarning('selectionSetContent');
  20626. setContentInternal$1(editor, content, args);
  20627. };
  20628. /**
  20629. * Handles inserts of lists into the editor instance.
  20630. *
  20631. * @class tinymce.InsertList
  20632. * @private
  20633. */
  20634. const hasOnlyOneChild$1 = (node) => {
  20635. return isNonNullable(node.firstChild) && node.firstChild === node.lastChild;
  20636. };
  20637. const isPaddingNode = (node) => {
  20638. return node.name === 'br' || node.value === nbsp;
  20639. };
  20640. const isPaddedEmptyBlock = (schema, node) => {
  20641. const blockElements = schema.getBlockElements();
  20642. return blockElements[node.name] && hasOnlyOneChild$1(node) && isPaddingNode(node.firstChild);
  20643. };
  20644. const isEmptyFragmentElement = (schema, node) => {
  20645. const nonEmptyElements = schema.getNonEmptyElements();
  20646. return isNonNullable(node) && (node.isEmpty(nonEmptyElements) || isPaddedEmptyBlock(schema, node));
  20647. };
  20648. const isListFragment = (schema, fragment) => {
  20649. let firstChild = fragment.firstChild;
  20650. let lastChild = fragment.lastChild;
  20651. // Skip meta since it's likely <meta><ul>..</ul>
  20652. if (firstChild && firstChild.name === 'meta') {
  20653. firstChild = firstChild.next;
  20654. }
  20655. // Skip mce_marker since it's likely <ul>..</ul><span id="mce_marker"></span>
  20656. if (lastChild && lastChild.attr('id') === 'mce_marker') {
  20657. lastChild = lastChild.prev;
  20658. }
  20659. // Skip last child if it's an empty block
  20660. if (isEmptyFragmentElement(schema, lastChild)) {
  20661. lastChild = lastChild === null || lastChild === void 0 ? void 0 : lastChild.prev;
  20662. }
  20663. if (!firstChild || firstChild !== lastChild) {
  20664. return false;
  20665. }
  20666. return firstChild.name === 'ul' || firstChild.name === 'ol';
  20667. };
  20668. const cleanupDomFragment = (domFragment) => {
  20669. var _a, _b;
  20670. const firstChild = domFragment.firstChild;
  20671. const lastChild = domFragment.lastChild;
  20672. // TODO: remove the meta tag from paste logic
  20673. if (firstChild && firstChild.nodeName === 'META') {
  20674. (_a = firstChild.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(firstChild);
  20675. }
  20676. if (lastChild && lastChild.id === 'mce_marker') {
  20677. (_b = lastChild.parentNode) === null || _b === void 0 ? void 0 : _b.removeChild(lastChild);
  20678. }
  20679. return domFragment;
  20680. };
  20681. const toDomFragment = (dom, serializer, fragment) => {
  20682. const html = serializer.serialize(fragment);
  20683. const domFragment = dom.createFragment(html);
  20684. return cleanupDomFragment(domFragment);
  20685. };
  20686. const listItems = (elm) => {
  20687. var _a;
  20688. return filter$5((_a = elm === null || elm === void 0 ? void 0 : elm.childNodes) !== null && _a !== void 0 ? _a : [], (child) => {
  20689. return child.nodeName === 'LI';
  20690. });
  20691. };
  20692. const isPadding = (node) => {
  20693. return node.data === nbsp || isBr$7(node);
  20694. };
  20695. const isListItemPadded = (node) => {
  20696. return isNonNullable(node === null || node === void 0 ? void 0 : node.firstChild) && node.firstChild === node.lastChild && isPadding(node.firstChild);
  20697. };
  20698. const isEmptyOrPadded = (elm) => {
  20699. return !elm.firstChild || isListItemPadded(elm);
  20700. };
  20701. const trimListItems = (elms) => {
  20702. return elms.length > 0 && isEmptyOrPadded(elms[elms.length - 1]) ? elms.slice(0, -1) : elms;
  20703. };
  20704. const getParentLi = (dom, node) => {
  20705. const parentBlock = dom.getParent(node, dom.isBlock);
  20706. return parentBlock && parentBlock.nodeName === 'LI' ? parentBlock : null;
  20707. };
  20708. const isParentBlockLi = (dom, node) => {
  20709. return !!getParentLi(dom, node);
  20710. };
  20711. const getSplit = (parentNode, rng) => {
  20712. const beforeRng = rng.cloneRange();
  20713. const afterRng = rng.cloneRange();
  20714. beforeRng.setStartBefore(parentNode);
  20715. afterRng.setEndAfter(parentNode);
  20716. return [
  20717. beforeRng.cloneContents(),
  20718. afterRng.cloneContents()
  20719. ];
  20720. };
  20721. const findFirstIn = (node, rootNode) => {
  20722. const caretPos = CaretPosition.before(node);
  20723. const caretWalker = CaretWalker(rootNode);
  20724. const newCaretPos = caretWalker.next(caretPos);
  20725. return newCaretPos ? newCaretPos.toRange() : null;
  20726. };
  20727. const findLastOf = (node, rootNode) => {
  20728. const caretPos = CaretPosition.after(node);
  20729. const caretWalker = CaretWalker(rootNode);
  20730. const newCaretPos = caretWalker.prev(caretPos);
  20731. return newCaretPos ? newCaretPos.toRange() : null;
  20732. };
  20733. const insertMiddle = (target, elms, rootNode, rng) => {
  20734. const parts = getSplit(target, rng);
  20735. const parentElm = target.parentNode;
  20736. if (parentElm) {
  20737. parentElm.insertBefore(parts[0], target);
  20738. Tools.each(elms, (li) => {
  20739. parentElm.insertBefore(li, target);
  20740. });
  20741. parentElm.insertBefore(parts[1], target);
  20742. parentElm.removeChild(target);
  20743. }
  20744. return findLastOf(elms[elms.length - 1], rootNode);
  20745. };
  20746. const insertBefore$2 = (target, elms, rootNode) => {
  20747. const parentElm = target.parentNode;
  20748. if (parentElm) {
  20749. Tools.each(elms, (elm) => {
  20750. parentElm.insertBefore(elm, target);
  20751. });
  20752. }
  20753. return findFirstIn(target, rootNode);
  20754. };
  20755. const insertAfter$2 = (target, elms, rootNode, dom) => {
  20756. dom.insertAfter(elms.reverse(), target);
  20757. return findLastOf(elms[0], rootNode);
  20758. };
  20759. const insertAtCaret$1 = (serializer, dom, rng, fragment) => {
  20760. const domFragment = toDomFragment(dom, serializer, fragment);
  20761. const liTarget = getParentLi(dom, rng.startContainer);
  20762. const liElms = trimListItems(listItems(domFragment.firstChild));
  20763. const BEGINNING = 1, END = 2;
  20764. const rootNode = dom.getRoot();
  20765. const isAt = (location) => {
  20766. const caretPos = CaretPosition.fromRangeStart(rng);
  20767. const caretWalker = CaretWalker(dom.getRoot());
  20768. const newPos = location === BEGINNING ? caretWalker.prev(caretPos) : caretWalker.next(caretPos);
  20769. const newPosNode = newPos === null || newPos === void 0 ? void 0 : newPos.getNode();
  20770. return newPosNode ? getParentLi(dom, newPosNode) !== liTarget : true;
  20771. };
  20772. if (!liTarget) {
  20773. return null;
  20774. }
  20775. else if (isAt(BEGINNING)) {
  20776. return insertBefore$2(liTarget, liElms, rootNode);
  20777. }
  20778. else if (isAt(END)) {
  20779. return insertAfter$2(liTarget, liElms, rootNode, dom);
  20780. }
  20781. else {
  20782. return insertMiddle(liTarget, liElms, rootNode, rng);
  20783. }
  20784. };
  20785. const mergeableWrappedElements = ['pre'];
  20786. const shouldPasteContentOnly = (dom, fragment, parentNode, root) => {
  20787. var _a;
  20788. const firstNode = fragment.firstChild;
  20789. const lastNode = fragment.lastChild;
  20790. const last = lastNode.attr('data-mce-type') === 'bookmark' ? lastNode.prev : lastNode;
  20791. const isPastingSingleElement = firstNode === last;
  20792. const isWrappedElement = contains$2(mergeableWrappedElements, firstNode.name);
  20793. if (isPastingSingleElement && isWrappedElement) {
  20794. const isContentEditable = firstNode.attr('contenteditable') !== 'false';
  20795. const isPastingInTheSameBlockTag = ((_a = dom.getParent(parentNode, dom.isBlock)) === null || _a === void 0 ? void 0 : _a.nodeName.toLowerCase()) === firstNode.name;
  20796. const isPastingInContentEditable = Optional.from(getContentEditableRoot$1(root, parentNode)).forall(isContentEditableTrue$3);
  20797. return isContentEditable && isPastingInTheSameBlockTag && isPastingInContentEditable;
  20798. }
  20799. else {
  20800. return false;
  20801. }
  20802. };
  20803. const isTableCell = isTableCell$3;
  20804. const isTableCellContentSelected = (dom, rng, cell) => {
  20805. if (isNonNullable(cell)) {
  20806. const endCell = dom.getParent(rng.endContainer, isTableCell);
  20807. return cell === endCell && hasAllContentsSelected(SugarElement.fromDom(cell), rng);
  20808. }
  20809. else {
  20810. return false;
  20811. }
  20812. };
  20813. const isEditableEmptyBlock = (dom, node) => {
  20814. if (dom.isBlock(node) && dom.isEditable(node)) {
  20815. const childNodes = node.childNodes;
  20816. return (childNodes.length === 1 && isBr$7(childNodes[0])) || childNodes.length === 0;
  20817. }
  20818. else {
  20819. return false;
  20820. }
  20821. };
  20822. const validInsertion = (editor, value, parentNode) => {
  20823. var _a;
  20824. // Should never insert content into bogus elements, since these can
  20825. // be resize handles or similar
  20826. if (parentNode.getAttribute('data-mce-bogus') === 'all') {
  20827. (_a = parentNode.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(editor.dom.createFragment(value), parentNode);
  20828. }
  20829. else {
  20830. if (isEditableEmptyBlock(editor.dom, parentNode)) {
  20831. editor.dom.setHTML(parentNode, value);
  20832. }
  20833. else {
  20834. setContentInternal$1(editor, value, { no_events: true });
  20835. }
  20836. }
  20837. };
  20838. const trimBrsFromTableCell = (dom, elm, schema) => {
  20839. Optional.from(dom.getParent(elm, 'td,th')).map(SugarElement.fromDom).each((el) => trimBlockTrailingBr(el, schema));
  20840. };
  20841. // Remove children nodes that are exactly the same as a parent node - name, attributes, styles
  20842. const reduceInlineTextElements = (editor, merge) => {
  20843. const textInlineElements = editor.schema.getTextInlineElements();
  20844. const dom = editor.dom;
  20845. if (merge) {
  20846. const root = editor.getBody();
  20847. const elementUtils = ElementUtils(editor);
  20848. const fragmentSelector = '*[data-mce-fragment]';
  20849. const fragments = dom.select(fragmentSelector);
  20850. Tools.each(fragments, (node) => {
  20851. const isInline = (currentNode) => isNonNullable(textInlineElements[currentNode.nodeName.toLowerCase()]);
  20852. const hasOneChild = (currentNode) => currentNode.childNodes.length === 1;
  20853. const hasNoNonInheritableStyles = (currentNode) => !(hasNonInheritableStyles(dom, currentNode) || hasConditionalNonInheritableStyles(dom, currentNode));
  20854. if (hasNoNonInheritableStyles(node) && isInline(node) && hasOneChild(node)) {
  20855. const styles = getStyleProps(dom, node);
  20856. const isOverridden = (oldStyles, newStyles) => forall(oldStyles, (style) => contains$2(newStyles, style));
  20857. const overriddenByAllChildren = (childNode) => hasOneChild(node) && dom.is(childNode, fragmentSelector) && isInline(childNode) &&
  20858. (childNode.nodeName === node.nodeName && isOverridden(styles, getStyleProps(dom, childNode)) || overriddenByAllChildren(childNode.children[0]));
  20859. const identicalToParent = (parentNode) => isNonNullable(parentNode) && parentNode !== root
  20860. && (elementUtils.compare(node, parentNode) || identicalToParent(parentNode.parentElement));
  20861. const conflictWithInsertedParent = (parentNode) => isNonNullable(parentNode) && parentNode !== root
  20862. && dom.is(parentNode, fragmentSelector) && (hasStyleConflict(dom, node, parentNode) || conflictWithInsertedParent(parentNode.parentElement));
  20863. if (overriddenByAllChildren(node.children[0]) || (identicalToParent(node.parentElement) && !conflictWithInsertedParent(node.parentElement))) {
  20864. dom.remove(node, true);
  20865. }
  20866. }
  20867. });
  20868. normalizeElements(editor, fromDom$1(fragments));
  20869. }
  20870. };
  20871. const markFragmentElements = (fragment) => {
  20872. let node = fragment;
  20873. while ((node = node.walk())) {
  20874. if (node.type === 1) {
  20875. node.attr('data-mce-fragment', '1');
  20876. }
  20877. }
  20878. };
  20879. const unmarkFragmentElements = (elm) => {
  20880. Tools.each(elm.getElementsByTagName('*'), (elm) => {
  20881. elm.removeAttribute('data-mce-fragment');
  20882. });
  20883. };
  20884. const isPartOfFragment = (node) => {
  20885. return !!node.getAttribute('data-mce-fragment');
  20886. };
  20887. const canHaveChildren = (editor, node) => {
  20888. return isNonNullable(node) && !editor.schema.getVoidElements()[node.nodeName];
  20889. };
  20890. const moveSelectionToMarker = (editor, marker) => {
  20891. var _a, _b, _c;
  20892. let nextRng;
  20893. const dom = editor.dom;
  20894. const selection = editor.selection;
  20895. if (!marker) {
  20896. return;
  20897. }
  20898. selection.scrollIntoView(marker);
  20899. // If marker is in cE=false then move selection to that element instead
  20900. const parentEditableElm = getContentEditableRoot$1(editor.getBody(), marker);
  20901. if (parentEditableElm && dom.getContentEditable(parentEditableElm) === 'false') {
  20902. dom.remove(marker);
  20903. selection.select(parentEditableElm);
  20904. return;
  20905. }
  20906. // Move selection before marker and remove it
  20907. let rng = dom.createRng();
  20908. // If previous sibling is a text node set the selection to the end of that node
  20909. const node = marker.previousSibling;
  20910. if (isText$b(node)) {
  20911. rng.setStart(node, (_b = (_a = node.nodeValue) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0);
  20912. const node2 = marker.nextSibling;
  20913. if (isText$b(node2)) {
  20914. node.appendData(node2.data);
  20915. (_c = node2.parentNode) === null || _c === void 0 ? void 0 : _c.removeChild(node2);
  20916. }
  20917. }
  20918. else {
  20919. // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node
  20920. rng.setStartBefore(marker);
  20921. rng.setEndBefore(marker);
  20922. }
  20923. const findNextCaretRng = (rng) => {
  20924. let caretPos = CaretPosition.fromRangeStart(rng);
  20925. const caretWalker = CaretWalker(editor.getBody());
  20926. caretPos = caretWalker.next(caretPos);
  20927. return caretPos === null || caretPos === void 0 ? void 0 : caretPos.toRange();
  20928. };
  20929. // Remove the marker node and set the new range
  20930. const parentBlock = dom.getParent(marker, dom.isBlock);
  20931. dom.remove(marker);
  20932. if (parentBlock && dom.isEmpty(parentBlock)) {
  20933. const isCell = isTableCell(parentBlock);
  20934. empty(SugarElement.fromDom(parentBlock));
  20935. rng.setStart(parentBlock, 0);
  20936. rng.setEnd(parentBlock, 0);
  20937. if (!isCell && !isPartOfFragment(parentBlock) && (nextRng = findNextCaretRng(rng))) {
  20938. rng = nextRng;
  20939. dom.remove(parentBlock);
  20940. }
  20941. else {
  20942. // TINY-9860: If parentBlock is a table cell, add a br without 'data-mce-bogus' attribute.
  20943. dom.add(parentBlock, dom.create('br', isCell ? {} : { 'data-mce-bogus': '1' }));
  20944. }
  20945. }
  20946. selection.setRng(rng);
  20947. };
  20948. const deleteSelectedContent = (editor) => {
  20949. const dom = editor.dom;
  20950. // Fix for #2595 seems that delete removes one extra character on
  20951. // WebKit for some odd reason if you double click select a word
  20952. const rng = normalize(editor.selection.getRng());
  20953. editor.selection.setRng(rng);
  20954. // TINY-1044: Selecting all content in a single table cell will cause the entire table to be deleted
  20955. // when using the native delete command. As such we need to manually delete the cell content instead
  20956. const startCell = dom.getParent(rng.startContainer, isTableCell);
  20957. if (isTableCellContentSelected(dom, rng, startCell)) {
  20958. deleteCellContents(editor, rng, SugarElement.fromDom(startCell));
  20959. // TINY-9193: If the selection is over the whole text node in an element then Firefox incorrectly moves the caret to the previous line
  20960. // TINY-11953: If the selection is over the whole anchor node, then Chrome incorrectly removes parent node alongside with it's child - anchor
  20961. }
  20962. else if (isSelectionOverWholeAnchor(rng) || isSelectionOverWholeTextNode(rng)) {
  20963. rng.deleteContents();
  20964. }
  20965. else {
  20966. editor.getDoc().execCommand('Delete', false);
  20967. }
  20968. };
  20969. const findMarkerNode = (scope) => {
  20970. for (let markerNode = scope; markerNode; markerNode = markerNode.walk()) {
  20971. if (markerNode.attr('id') === 'mce_marker') {
  20972. return Optional.some(markerNode);
  20973. }
  20974. }
  20975. return Optional.none();
  20976. };
  20977. const notHeadingsInSummary = (dom, node, fragment) => {
  20978. var _a;
  20979. return exists(fragment.children(), isHeading) && ((_a = dom.getParent(node, dom.isBlock)) === null || _a === void 0 ? void 0 : _a.nodeName) === 'SUMMARY';
  20980. };
  20981. const insertHtmlAtCaret = (editor, value, details) => {
  20982. var _a;
  20983. const selection = editor.selection;
  20984. const dom = editor.dom;
  20985. // Setup parser and serializer
  20986. const parser = editor.parser;
  20987. const merge = details.merge;
  20988. const serializer = HtmlSerializer({
  20989. validate: true
  20990. }, editor.schema);
  20991. const bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">&#xFEFF;</span>';
  20992. // TINY-10305: Remove all user-input zwsp to avoid impacting caret removal from content.
  20993. if (!details.preserve_zwsp) {
  20994. value = trim$2(value);
  20995. }
  20996. // Add caret at end of contents if it's missing
  20997. if (value.indexOf('{$caret}') === -1) {
  20998. value += '{$caret}';
  20999. }
  21000. // Replace the caret marker with a span bookmark element
  21001. value = value.replace(/\{\$caret\}/, bookmarkHtml);
  21002. // If selection is at <body>|<p></p> then move it into <body><p>|</p>
  21003. let rng = selection.getRng();
  21004. const caretElement = rng.startContainer;
  21005. const body = editor.getBody();
  21006. if (caretElement === body && selection.isCollapsed()) {
  21007. if (dom.isBlock(body.firstChild) && canHaveChildren(editor, body.firstChild) && dom.isEmpty(body.firstChild)) {
  21008. rng = dom.createRng();
  21009. rng.setStart(body.firstChild, 0);
  21010. rng.setEnd(body.firstChild, 0);
  21011. selection.setRng(rng);
  21012. }
  21013. }
  21014. // Insert node maker where we will insert the new HTML and get it's parent
  21015. if (!selection.isCollapsed()) {
  21016. deleteSelectedContent(editor);
  21017. }
  21018. const parentNode = selection.getNode();
  21019. // Parse the fragment within the context of the parent node
  21020. const parserArgs = { context: parentNode.nodeName.toLowerCase(), data: details.data, insert: true };
  21021. const fragment = parser.parse(value, parserArgs);
  21022. // Custom handling of lists
  21023. if (details.paste === true && isListFragment(editor.schema, fragment) && isParentBlockLi(dom, parentNode)) {
  21024. rng = insertAtCaret$1(serializer, dom, selection.getRng(), fragment);
  21025. if (rng) {
  21026. selection.setRng(rng);
  21027. }
  21028. return value;
  21029. }
  21030. if (details.paste === true && shouldPasteContentOnly(dom, fragment, parentNode, editor.getBody())) {
  21031. (_a = fragment.firstChild) === null || _a === void 0 ? void 0 : _a.unwrap();
  21032. }
  21033. markFragmentElements(fragment);
  21034. // Move the caret to a more suitable location
  21035. let node = fragment.lastChild;
  21036. if (node && node.attr('id') === 'mce_marker') {
  21037. const marker = node;
  21038. for (node = node.prev; node; node = node.walk(true)) {
  21039. if (node.name === 'table') {
  21040. break;
  21041. }
  21042. if (node.type === 3 || !dom.isBlock(node.name)) {
  21043. if (node.parent && editor.schema.isValidChild(node.parent.name, 'span')) {
  21044. node.parent.insert(marker, node, node.name === 'br');
  21045. }
  21046. break;
  21047. }
  21048. }
  21049. }
  21050. editor._selectionOverrides.showBlockCaretContainer(parentNode);
  21051. // If parser says valid we can insert the contents into that parent
  21052. if (!parserArgs.invalid && !notHeadingsInSummary(dom, parentNode, fragment)) {
  21053. value = serializer.serialize(fragment);
  21054. validInsertion(editor, value, parentNode);
  21055. }
  21056. else {
  21057. // If the fragment was invalid within that context then we need
  21058. // to parse and process the parent it's inserted into
  21059. // Insert bookmark node and get the parent
  21060. setContentInternal$1(editor, bookmarkHtml);
  21061. let parentNode = selection.getNode();
  21062. let tempNode;
  21063. const rootNode = editor.getBody();
  21064. // Opera will return the document node when selection is in root
  21065. if (isDocument$1(parentNode)) {
  21066. parentNode = tempNode = rootNode;
  21067. }
  21068. else {
  21069. tempNode = parentNode;
  21070. }
  21071. // Find the ancestor just before the root element
  21072. while (tempNode && tempNode !== rootNode) {
  21073. parentNode = tempNode;
  21074. tempNode = tempNode.parentNode;
  21075. }
  21076. // Get the outer/inner HTML depending on if we are in the root and parser and serialize that
  21077. value = parentNode === rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode);
  21078. const root = parser.parse(value);
  21079. const markerNode = findMarkerNode(root);
  21080. const editingHost = markerNode.bind(findClosestEditingHost).getOr(root);
  21081. markerNode.each((marker) => marker.replace(fragment));
  21082. const fragmentNodes = getAllDescendants(fragment);
  21083. fragment.unwrap();
  21084. const invalidChildren = filter$5(fragmentNodes, (node) => isInvalid(editor.schema, node));
  21085. cleanInvalidNodes(invalidChildren, editor.schema, editingHost);
  21086. filter$1(parser.getNodeFilters(), parser.getAttributeFilters(), root);
  21087. value = serializer.serialize(root);
  21088. // Set the inner/outer HTML depending on if we are in the root or not
  21089. if (parentNode === rootNode) {
  21090. dom.setHTML(rootNode, value);
  21091. }
  21092. else {
  21093. dom.setOuterHTML(parentNode, value);
  21094. }
  21095. }
  21096. reduceInlineTextElements(editor, merge);
  21097. moveSelectionToMarker(editor, dom.get('mce_marker'));
  21098. unmarkFragmentElements(editor.getBody());
  21099. trimBrsFromTableCell(dom, selection.getStart(), editor.schema);
  21100. updateCaret(editor.schema, editor.getBody(), selection.getStart());
  21101. return value;
  21102. };
  21103. const moveSelection = (editor) => {
  21104. if (hasFocus(editor)) {
  21105. firstPositionIn(editor.getBody()).each((pos) => {
  21106. const node = pos.getNode();
  21107. const caretPos = isTable$2(node) ? firstPositionIn(node).getOr(pos) : pos;
  21108. editor.selection.setRng(caretPos.toRange());
  21109. });
  21110. }
  21111. };
  21112. const setEditorHtml = (editor, html, noSelection) => {
  21113. editor.dom.setHTML(editor.getBody(), html);
  21114. if (noSelection !== true) {
  21115. moveSelection(editor);
  21116. }
  21117. };
  21118. const setContentString = (editor, body, content, args) => {
  21119. // TINY-10305: Remove all user-input zwsp to avoid impacting caret removal from content.
  21120. content = trim$2(content);
  21121. // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content
  21122. // It will also be impossible to place the caret in the editor unless there is a BR element present
  21123. if (content.length === 0 || /^\s+$/.test(content)) {
  21124. const padd = '<br data-mce-bogus="1">';
  21125. // Todo: There is a lot more root elements that need special padding
  21126. // so separate this and add all of them at some point.
  21127. if (body.nodeName === 'TABLE') {
  21128. content = '<tr><td>' + padd + '</td></tr>';
  21129. }
  21130. else if (/^(UL|OL)$/.test(body.nodeName)) {
  21131. content = '<li>' + padd + '</li>';
  21132. }
  21133. const forcedRootBlockName = getForcedRootBlock(editor);
  21134. // Check if forcedRootBlock is a valid child of the body
  21135. if (editor.schema.isValidChild(body.nodeName.toLowerCase(), forcedRootBlockName.toLowerCase())) {
  21136. content = padd;
  21137. content = editor.dom.createHTML(forcedRootBlockName, getForcedRootBlockAttrs(editor), content);
  21138. }
  21139. else if (!content) {
  21140. content = padd;
  21141. }
  21142. setEditorHtml(editor, content, args.no_selection);
  21143. return { content, html: content };
  21144. }
  21145. else {
  21146. if (args.format !== 'raw') {
  21147. content = HtmlSerializer({ validate: false }, editor.schema).serialize(editor.parser.parse(content, { isRootContent: true, insert: true }));
  21148. }
  21149. const trimmedHtml = isWsPreserveElement(SugarElement.fromDom(body)) ? content : Tools.trim(content);
  21150. setEditorHtml(editor, trimmedHtml, args.no_selection);
  21151. return { content: trimmedHtml, html: trimmedHtml };
  21152. }
  21153. };
  21154. const setContentTree = (editor, body, content, args) => {
  21155. filter$1(editor.parser.getNodeFilters(), editor.parser.getAttributeFilters(), content);
  21156. const html = HtmlSerializer({ validate: false }, editor.schema).serialize(content);
  21157. // TINY-10305: Remove all user-input zwsp to avoid impacting caret removal from content.
  21158. const trimmedHtml = trim$2(isWsPreserveElement(SugarElement.fromDom(body)) ? html : Tools.trim(html));
  21159. setEditorHtml(editor, trimmedHtml, args.no_selection);
  21160. return { content, html: trimmedHtml };
  21161. };
  21162. const setContentInternal = (editor, content, args) => {
  21163. return Optional.from(editor.getBody()).map((body) => {
  21164. if (isTreeNode(content)) {
  21165. return setContentTree(editor, body, content, args);
  21166. }
  21167. else {
  21168. return setContentString(editor, body, content, args);
  21169. }
  21170. }).getOr({ content, html: isTreeNode(args.content) ? '' : args.content });
  21171. };
  21172. const postProcessHooks = {};
  21173. const isPre = matchNodeNames$1(['pre']);
  21174. const addPostProcessHook = (name, hook) => {
  21175. const hooks = postProcessHooks[name];
  21176. if (!hooks) {
  21177. postProcessHooks[name] = [];
  21178. }
  21179. postProcessHooks[name].push(hook);
  21180. };
  21181. const postProcess$1 = (name, editor) => {
  21182. if (has$2(postProcessHooks, name)) {
  21183. each$e(postProcessHooks[name], (hook) => {
  21184. hook(editor);
  21185. });
  21186. }
  21187. };
  21188. addPostProcessHook('pre', (editor) => {
  21189. const rng = editor.selection.getRng();
  21190. const hasPreSibling = (blocks) => (pre) => {
  21191. const prev = pre.previousSibling;
  21192. return isPre(prev) && contains$2(blocks, prev);
  21193. };
  21194. const joinPre = (pre1, pre2) => {
  21195. const sPre2 = SugarElement.fromDom(pre2);
  21196. const doc = documentOrOwner(sPre2).dom;
  21197. remove$8(sPre2);
  21198. append(SugarElement.fromDom(pre1), [
  21199. SugarElement.fromTag('br', doc),
  21200. SugarElement.fromTag('br', doc),
  21201. ...children$1(sPre2)
  21202. ]);
  21203. };
  21204. if (!rng.collapsed) {
  21205. const blocks = editor.selection.getSelectedBlocks();
  21206. const preBlocks = filter$5(filter$5(blocks, isPre), hasPreSibling(blocks));
  21207. each$e(preBlocks, (pre) => {
  21208. joinPre(pre.previousSibling, pre);
  21209. });
  21210. }
  21211. });
  21212. const each$5 = Tools.each;
  21213. const mergeTextDecorationsAndColor = (dom, format, vars, node) => {
  21214. const processTextDecorationsAndColor = (n) => {
  21215. if (isHTMLElement(n) && isElement$7(n.parentNode) && dom.isEditable(n)) {
  21216. const parentTextDecoration = getTextDecoration(dom, n.parentNode);
  21217. if (dom.getStyle(n, 'color') && parentTextDecoration) {
  21218. dom.setStyle(n, 'text-decoration', parentTextDecoration);
  21219. }
  21220. else if (dom.getStyle(n, 'text-decoration') === parentTextDecoration) {
  21221. dom.setStyle(n, 'text-decoration', null);
  21222. }
  21223. }
  21224. };
  21225. // Colored nodes should be underlined so that the color of the underline matches the text color.
  21226. if (format.styles && (format.styles.color || format.styles.textDecoration)) {
  21227. Tools.walk(node, processTextDecorationsAndColor, 'childNodes');
  21228. processTextDecorationsAndColor(node);
  21229. }
  21230. };
  21231. const mergeBackgroundColorAndFontSize = (dom, format, vars, node) => {
  21232. // nodes with font-size should have their own background color as well to fit the line-height (see TINY-882)
  21233. if (format.styles && format.styles.backgroundColor) {
  21234. const hasFontSize = hasStyle(dom, 'fontSize');
  21235. processChildElements(node, (elm) => hasFontSize(elm) && dom.isEditable(elm), applyStyle(dom, 'backgroundColor', replaceVars(format.styles.backgroundColor, vars)));
  21236. }
  21237. };
  21238. const mergeSubSup = (dom, format, vars, node) => {
  21239. // Remove font size on all descendants of a sub/sup and remove the inverse elements
  21240. if (isInlineFormat(format) && (format.inline === 'sub' || format.inline === 'sup')) {
  21241. const hasFontSize = hasStyle(dom, 'fontSize');
  21242. processChildElements(node, (elm) => hasFontSize(elm) && dom.isEditable(elm), applyStyle(dom, 'fontSize', ''));
  21243. const inverseTagDescendants = filter$5(dom.select(format.inline === 'sup' ? 'sub' : 'sup', node), dom.isEditable);
  21244. dom.remove(inverseTagDescendants, true);
  21245. }
  21246. };
  21247. const mergeWithChildren = (editor, formatList, vars, node) => {
  21248. // Remove/merge children
  21249. // Note: RemoveFormat.removeFormat will not remove formatting from noneditable nodes
  21250. each$5(formatList, (format) => {
  21251. // Merge all children of similar type will move styles from child to parent
  21252. // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span>
  21253. // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span>
  21254. if (isInlineFormat(format)) {
  21255. each$5(editor.dom.select(format.inline, node), (child) => {
  21256. if (isElementNode(child)) {
  21257. removeNodeFormat(editor, format, vars, child, format.exact ? child : null);
  21258. }
  21259. });
  21260. }
  21261. clearChildStyles(editor.dom, format, node);
  21262. });
  21263. };
  21264. const mergeWithParents = (editor, format, name, vars, node) => {
  21265. // Remove format if direct parent already has the same format
  21266. // Note: RemoveFormat.removeFormat will not remove formatting from noneditable nodes
  21267. const parentNode = node.parentNode;
  21268. if (matchNode$1(editor, parentNode, name, vars)) {
  21269. if (removeNodeFormat(editor, format, vars, node)) {
  21270. return;
  21271. }
  21272. }
  21273. // Remove format if any ancestor already has the same format
  21274. if (format.merge_with_parents && parentNode) {
  21275. editor.dom.getParent(parentNode, (parent) => {
  21276. if (matchNode$1(editor, parent, name, vars)) {
  21277. removeNodeFormat(editor, format, vars, node);
  21278. return true;
  21279. }
  21280. else {
  21281. return false;
  21282. }
  21283. });
  21284. }
  21285. };
  21286. const each$4 = Tools.each;
  21287. const canFormatBR = (editor, format, node, parentName) => {
  21288. // TINY-6483: Can format 'br' if it is contained in a valid empty block and an inline format is being applied
  21289. if (canFormatEmptyLines(editor) && isInlineFormat(format) && node.parentNode) {
  21290. const validBRParentElements = getTextRootBlockElements(editor.schema);
  21291. // If a caret node is present, the format should apply to that, not the br (applicable to collapsed selections)
  21292. const hasCaretNodeSibling = sibling(SugarElement.fromDom(node), (sibling) => isCaretNode(sibling.dom));
  21293. return hasNonNullableKey(validBRParentElements, parentName) && isEmptyNode(editor.schema, node.parentNode, { skipBogus: false, includeZwsp: true }) && !hasCaretNodeSibling;
  21294. }
  21295. else {
  21296. return false;
  21297. }
  21298. };
  21299. const applyFormatAction = (ed, name, vars, node) => {
  21300. const formatList = ed.formatter.get(name);
  21301. const format = formatList[0];
  21302. const isCollapsed = !node && ed.selection.isCollapsed();
  21303. const dom = ed.dom;
  21304. const selection = ed.selection;
  21305. const applyNodeStyle = (formatList, node) => {
  21306. let found = false;
  21307. // Look for matching formats
  21308. each$4(formatList, (format) => {
  21309. if (!isSelectorFormat(format)) {
  21310. return false;
  21311. }
  21312. // Check if the node is nonediatble and if the format can override noneditable node
  21313. if (dom.getContentEditable(node) === 'false' && !format.ceFalseOverride) {
  21314. return true;
  21315. }
  21316. // Check collapsed state if it exists
  21317. if (isNonNullable(format.collapsed) && format.collapsed !== isCollapsed) {
  21318. return true;
  21319. }
  21320. if (dom.is(node, format.selector) && !isCaretNode(node)) {
  21321. setElementFormat(ed, node, format, vars, node);
  21322. found = true;
  21323. return false;
  21324. }
  21325. return true;
  21326. });
  21327. return found;
  21328. };
  21329. const createWrapElement = (wrapName) => {
  21330. if (isString(wrapName)) {
  21331. const wrapElm = dom.create(wrapName);
  21332. setElementFormat(ed, wrapElm, format, vars, node);
  21333. return wrapElm;
  21334. }
  21335. else {
  21336. return null;
  21337. }
  21338. };
  21339. const applyRngStyle = (dom, rng, nodeSpecific) => {
  21340. const newWrappers = [];
  21341. let contentEditable = true;
  21342. // Setup wrapper element
  21343. const wrapName = format.inline || format.block;
  21344. const wrapElm = createWrapElement(wrapName);
  21345. const isMatchingWrappingBlock = (node) => isWrappingBlockFormat(format) && matchNode$1(ed, node, name, vars);
  21346. const canRenameBlock = (node, parentName, isEditableDescendant) => {
  21347. const isValidBlockFormatForNode = isNonWrappingBlockFormat(format) &&
  21348. isTextBlock$2(ed.schema, node) &&
  21349. isValid(ed, parentName, wrapName);
  21350. return isEditableDescendant && isValidBlockFormatForNode;
  21351. };
  21352. const canWrapNode = (node, parentName, isEditableDescendant, isWrappableNoneditableElm) => {
  21353. const nodeName = node.nodeName.toLowerCase();
  21354. const isValidWrapNode = isValid(ed, wrapName, nodeName) &&
  21355. isValid(ed, parentName, wrapName);
  21356. // If it is not node specific, it means that it was not passed into 'formatter.apply` and is within the editor selection
  21357. const isZwsp$1 = !nodeSpecific && isText$b(node) && isZwsp(node.data);
  21358. const isCaret = isCaretNode(node);
  21359. const isCorrectFormatForNode = !isInlineFormat(format) || !dom.isBlock(node);
  21360. return (isEditableDescendant || isWrappableNoneditableElm) && isValidWrapNode && !isZwsp$1 && !isCaret && isCorrectFormatForNode;
  21361. };
  21362. walk$3(dom, rng, (nodes) => {
  21363. let currentWrapElm;
  21364. /**
  21365. * Process a list of nodes wrap them.
  21366. */
  21367. const process = (node) => {
  21368. let hasContentEditableState = false;
  21369. let lastContentEditable = contentEditable;
  21370. let isWrappableNoneditableElm = false;
  21371. const parentNode = node.parentNode;
  21372. const parentName = parentNode.nodeName.toLowerCase();
  21373. // Node has a contentEditable value
  21374. const contentEditableValue = dom.getContentEditable(node);
  21375. if (isNonNullable(contentEditableValue)) {
  21376. lastContentEditable = contentEditable;
  21377. contentEditable = contentEditableValue === 'true';
  21378. // Unless the noneditable element is wrappable, we don't want to wrap the container, only it's editable children
  21379. hasContentEditableState = true;
  21380. isWrappableNoneditableElm = isWrappableNoneditable(ed, node);
  21381. }
  21382. const isEditableDescendant = contentEditable && !hasContentEditableState;
  21383. // Stop wrapping on br elements except when valid
  21384. if (isBr$7(node) && !canFormatBR(ed, format, node, parentName)) {
  21385. currentWrapElm = null;
  21386. // Remove any br elements when we wrap things
  21387. if (isBlockFormat(format)) {
  21388. dom.remove(node);
  21389. }
  21390. return;
  21391. }
  21392. if (isMatchingWrappingBlock(node)) {
  21393. currentWrapElm = null;
  21394. return;
  21395. }
  21396. if (canRenameBlock(node, parentName, isEditableDescendant)) {
  21397. const elm = dom.rename(node, wrapName);
  21398. setElementFormat(ed, elm, format, vars, node);
  21399. newWrappers.push(elm);
  21400. currentWrapElm = null;
  21401. return;
  21402. }
  21403. if (isSelectorFormat(format)) {
  21404. let found = applyNodeStyle(formatList, node);
  21405. // TINY-6567/TINY-7393: Include the parent if using an expanded selector format and no match was found for the current node
  21406. if (!found && isNonNullable(parentNode) && shouldExpandToSelector(format)) {
  21407. found = applyNodeStyle(formatList, parentNode);
  21408. }
  21409. // Continue processing if a selector match wasn't found and a inline element is defined
  21410. if (!isInlineFormat(format) || found) {
  21411. currentWrapElm = null;
  21412. return;
  21413. }
  21414. }
  21415. if (isNonNullable(wrapElm) && canWrapNode(node, parentName, isEditableDescendant, isWrappableNoneditableElm)) {
  21416. // Start wrapping
  21417. if (!currentWrapElm) {
  21418. // Wrap the node
  21419. currentWrapElm = dom.clone(wrapElm, false);
  21420. parentNode.insertBefore(currentWrapElm, node);
  21421. newWrappers.push(currentWrapElm);
  21422. }
  21423. // Wrappable noneditable element has been handled so go back to previous state
  21424. if (isWrappableNoneditableElm && hasContentEditableState) {
  21425. contentEditable = lastContentEditable;
  21426. }
  21427. currentWrapElm.appendChild(node);
  21428. }
  21429. else {
  21430. // Start a new wrapper for possible children
  21431. currentWrapElm = null;
  21432. each$e(from(node.childNodes), process);
  21433. if (hasContentEditableState) {
  21434. contentEditable = lastContentEditable; // Restore last contentEditable state from stack
  21435. }
  21436. // End the last wrapper
  21437. currentWrapElm = null;
  21438. }
  21439. };
  21440. each$e(nodes, process);
  21441. });
  21442. // Apply formats to links as well to get the color of the underline to change as well
  21443. if (format.links === true) {
  21444. each$e(newWrappers, (wrapper) => {
  21445. const process = (target) => {
  21446. if (target.nodeName === 'A') {
  21447. setElementFormat(ed, target, format, vars, node);
  21448. }
  21449. each$e(from(target.childNodes), process);
  21450. };
  21451. process(wrapper);
  21452. });
  21453. }
  21454. normalizeFontSizeElementsAfterApply(ed, name, fromDom$1(newWrappers));
  21455. // Cleanup
  21456. each$e(newWrappers, (node) => {
  21457. const getChildCount = (node) => {
  21458. let count = 0;
  21459. each$e(node.childNodes, (node) => {
  21460. if (!isEmptyTextNode$1(node) && !isBookmarkNode$1(node)) {
  21461. count++;
  21462. }
  21463. });
  21464. return count;
  21465. };
  21466. const mergeStyles = (node) => {
  21467. // Check if a child was found and of the same type as the current node
  21468. const childElement = find$2(node.childNodes, isElementNode$1)
  21469. .filter((child) => dom.getContentEditable(child) !== 'false' && matchName(dom, child, format));
  21470. return childElement.map((child) => {
  21471. const clone = dom.clone(child, false);
  21472. setElementFormat(ed, clone, format, vars, node);
  21473. dom.replace(clone, node, true);
  21474. dom.remove(child, true);
  21475. return clone;
  21476. }).getOr(node);
  21477. };
  21478. const childCount = getChildCount(node);
  21479. // Remove empty nodes but only if there is multiple wrappers and they are not block
  21480. // elements so never remove single <h1></h1> since that would remove the
  21481. // current empty block element where the caret is at
  21482. if ((newWrappers.length > 1 || !dom.isBlock(node)) && childCount === 0) {
  21483. dom.remove(node, true);
  21484. return;
  21485. }
  21486. if (isInlineFormat(format) || isBlockFormat(format) && format.wrapper) {
  21487. // Merges the current node with it's children of similar type to reduce the number of elements
  21488. if (!format.exact && childCount === 1) {
  21489. node = mergeStyles(node);
  21490. }
  21491. mergeWithChildren(ed, formatList, vars, node);
  21492. mergeWithParents(ed, format, name, vars, node);
  21493. mergeBackgroundColorAndFontSize(dom, format, vars, node);
  21494. mergeTextDecorationsAndColor(dom, format, vars, node);
  21495. mergeSubSup(dom, format, vars, node);
  21496. mergeSiblings(ed, format, vars, node);
  21497. }
  21498. });
  21499. };
  21500. // TODO: TINY-9142: Remove this to make nested noneditable formatting work
  21501. const targetNode = isNode(node) ? node : selection.getNode();
  21502. if (dom.getContentEditable(targetNode) === 'false' && !isWrappableNoneditable(ed, targetNode)) {
  21503. // node variable is used by other functions above in the same scope so need to set it here
  21504. node = targetNode;
  21505. applyNodeStyle(formatList, node);
  21506. fireFormatApply(ed, name, node, vars);
  21507. return;
  21508. }
  21509. if (format) {
  21510. if (node) {
  21511. if (isNode(node)) {
  21512. if (!applyNodeStyle(formatList, node)) {
  21513. const rng = dom.createRng();
  21514. rng.setStartBefore(node);
  21515. rng.setEndAfter(node);
  21516. applyRngStyle(dom, expandRng(dom, rng, formatList), true);
  21517. }
  21518. }
  21519. else {
  21520. applyRngStyle(dom, node, true);
  21521. }
  21522. }
  21523. else {
  21524. if (!isCollapsed || !isInlineFormat(format) || getCellsFromEditor(ed).length) {
  21525. // Apply formatting to selection
  21526. selection.setRng(normalize(selection.getRng()));
  21527. preserveSelection(ed, () => {
  21528. runOnRanges(ed, (selectionRng, fake) => {
  21529. const expandedRng = fake ? selectionRng : expandRng(dom, selectionRng, formatList);
  21530. applyRngStyle(dom, expandedRng, false);
  21531. });
  21532. }, always);
  21533. ed.nodeChanged();
  21534. }
  21535. else {
  21536. applyCaretFormat(ed, name, vars);
  21537. }
  21538. getExpandedListItemFormat(ed.formatter, name).each((liFmt) => {
  21539. each$e(getFullySelectedListItems(ed.selection), (li) => applyStyles(dom, li, liFmt, vars));
  21540. });
  21541. }
  21542. postProcess$1(name, ed);
  21543. }
  21544. fireFormatApply(ed, name, node, vars);
  21545. };
  21546. const applyFormat$1 = (editor, name, vars, node) => {
  21547. if (node || editor.selection.isEditable()) {
  21548. applyFormatAction(editor, name, vars, node);
  21549. }
  21550. };
  21551. const hasVars = (value) => has$2(value, 'vars');
  21552. const setup$A = (registeredFormatListeners, editor) => {
  21553. registeredFormatListeners.set({});
  21554. editor.on('NodeChange', (e) => {
  21555. updateAndFireChangeCallbacks(editor, e.element, registeredFormatListeners.get());
  21556. });
  21557. editor.on('FormatApply FormatRemove', (e) => {
  21558. const element = Optional.from(e.node)
  21559. .map((nodeOrRange) => isNode(nodeOrRange) ? nodeOrRange : nodeOrRange.startContainer)
  21560. .bind((node) => isElement$7(node) ? Optional.some(node) : Optional.from(node.parentElement))
  21561. .getOrThunk(() => fallbackElement(editor));
  21562. updateAndFireChangeCallbacks(editor, element, registeredFormatListeners.get());
  21563. });
  21564. };
  21565. const fallbackElement = (editor) => editor.selection.getStart();
  21566. const matchingNode = (editor, parents, format, similar, vars) => {
  21567. const isMatchingNode = (node) => {
  21568. const matchingFormat = editor.formatter.matchNode(node, format, vars !== null && vars !== void 0 ? vars : {}, similar);
  21569. return !isUndefined(matchingFormat);
  21570. };
  21571. const isUnableToMatch = (node) => {
  21572. if (matchesUnInheritedFormatSelector(editor, node, format)) {
  21573. return true;
  21574. }
  21575. else {
  21576. if (!similar) {
  21577. // If we want to find an exact match, then finding a similar match halfway up the parents tree is bad
  21578. return isNonNullable(editor.formatter.matchNode(node, format, vars, true));
  21579. }
  21580. else {
  21581. return false;
  21582. }
  21583. }
  21584. };
  21585. return findUntil$1(parents, isMatchingNode, isUnableToMatch);
  21586. };
  21587. const getParents = (editor, elm) => {
  21588. const element = elm !== null && elm !== void 0 ? elm : fallbackElement(editor);
  21589. return filter$5(getParents$2(editor.dom, element), (node) => isElement$7(node) && !isBogus$1(node));
  21590. };
  21591. const updateAndFireChangeCallbacks = (editor, elm, registeredCallbacks) => {
  21592. // Ignore bogus nodes like the <a> tag created by moveStart()
  21593. const parents = getParents(editor, elm);
  21594. each$d(registeredCallbacks, (data, format) => {
  21595. const runIfChanged = (spec) => {
  21596. const match = matchingNode(editor, parents, format, spec.similar, hasVars(spec) ? spec.vars : undefined);
  21597. const isSet = match.isSome();
  21598. if (spec.state.get() !== isSet) {
  21599. spec.state.set(isSet);
  21600. const node = match.getOr(elm);
  21601. if (hasVars(spec)) {
  21602. spec.callback(isSet, { node, format, parents });
  21603. }
  21604. else {
  21605. each$e(spec.callbacks, (callback) => callback(isSet, { node, format, parents }));
  21606. }
  21607. }
  21608. };
  21609. each$e([data.withSimilar, data.withoutSimilar], runIfChanged);
  21610. each$e(data.withVars, runIfChanged);
  21611. });
  21612. };
  21613. const addListeners = (editor, registeredFormatListeners, formats, callback, similar, vars) => {
  21614. const formatChangeItems = registeredFormatListeners.get();
  21615. each$e(formats.split(','), (format) => {
  21616. const group = get$a(formatChangeItems, format).getOrThunk(() => {
  21617. const base = {
  21618. withSimilar: {
  21619. state: Cell(false),
  21620. similar: true,
  21621. callbacks: []
  21622. },
  21623. withoutSimilar: {
  21624. state: Cell(false),
  21625. similar: false,
  21626. callbacks: []
  21627. },
  21628. withVars: []
  21629. };
  21630. formatChangeItems[format] = base;
  21631. return base;
  21632. });
  21633. const getCurrent = () => {
  21634. const parents = getParents(editor);
  21635. return matchingNode(editor, parents, format, similar, vars).isSome();
  21636. };
  21637. if (isUndefined(vars)) {
  21638. const toAppendTo = similar ? group.withSimilar : group.withoutSimilar;
  21639. toAppendTo.callbacks.push(callback);
  21640. if (toAppendTo.callbacks.length === 1) {
  21641. toAppendTo.state.set(getCurrent());
  21642. }
  21643. }
  21644. else {
  21645. group.withVars.push({
  21646. state: Cell(getCurrent()),
  21647. similar,
  21648. vars,
  21649. callback
  21650. });
  21651. }
  21652. });
  21653. registeredFormatListeners.set(formatChangeItems);
  21654. };
  21655. const removeListeners = (registeredFormatListeners, formats, callback) => {
  21656. const formatChangeItems = registeredFormatListeners.get();
  21657. each$e(formats.split(','), (format) => get$a(formatChangeItems, format).each((group) => {
  21658. formatChangeItems[format] = {
  21659. withSimilar: {
  21660. ...group.withSimilar,
  21661. callbacks: filter$5(group.withSimilar.callbacks, (cb) => cb !== callback),
  21662. },
  21663. withoutSimilar: {
  21664. ...group.withoutSimilar,
  21665. callbacks: filter$5(group.withoutSimilar.callbacks, (cb) => cb !== callback),
  21666. },
  21667. withVars: filter$5(group.withVars, (item) => item.callback !== callback),
  21668. };
  21669. }));
  21670. registeredFormatListeners.set(formatChangeItems);
  21671. };
  21672. const formatChangedInternal = (editor, registeredFormatListeners, formats, callback, similar, vars) => {
  21673. addListeners(editor, registeredFormatListeners, formats, callback, similar, vars);
  21674. return {
  21675. unbind: () => removeListeners(registeredFormatListeners, formats, callback)
  21676. };
  21677. };
  21678. const toggle = (editor, name, vars, node) => {
  21679. const fmt = editor.formatter.get(name);
  21680. if (fmt) {
  21681. if (match$2(editor, name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) {
  21682. removeFormat$1(editor, name, vars, node);
  21683. }
  21684. else {
  21685. applyFormat$1(editor, name, vars, node);
  21686. }
  21687. }
  21688. };
  21689. const tableModel = (element, width, rows) => ({
  21690. element,
  21691. width,
  21692. rows
  21693. });
  21694. const tableRow = (element, cells) => ({
  21695. element,
  21696. cells
  21697. });
  21698. const cellPosition = (x, y) => ({
  21699. x,
  21700. y
  21701. });
  21702. const getSpan = (td, key) => {
  21703. return getOpt(td, key).bind(toInt).getOr(1);
  21704. };
  21705. const fillout = (table, x, y, tr, td) => {
  21706. const rowspan = getSpan(td, 'rowspan');
  21707. const colspan = getSpan(td, 'colspan');
  21708. const rows = table.rows;
  21709. for (let y2 = y; y2 < y + rowspan; y2++) {
  21710. if (!rows[y2]) {
  21711. rows[y2] = tableRow(deep(tr), []);
  21712. }
  21713. for (let x2 = x; x2 < x + colspan; x2++) {
  21714. const cells = rows[y2].cells;
  21715. // not filler td:s are purposely not cloned so that we can
  21716. // find cells in the model by element object references
  21717. cells[x2] = y2 === y && x2 === x ? td : shallow(td);
  21718. }
  21719. }
  21720. };
  21721. const cellExists = (table, x, y) => {
  21722. const rows = table.rows;
  21723. const cells = rows[y] ? rows[y].cells : [];
  21724. return !!cells[x];
  21725. };
  21726. const skipCellsX = (table, x, y) => {
  21727. while (cellExists(table, x, y)) {
  21728. x++;
  21729. }
  21730. return x;
  21731. };
  21732. const getWidth = (rows) => {
  21733. return foldl(rows, (acc, row) => {
  21734. return row.cells.length > acc ? row.cells.length : acc;
  21735. }, 0);
  21736. };
  21737. const findElementPos = (table, element) => {
  21738. const rows = table.rows;
  21739. for (let y = 0; y < rows.length; y++) {
  21740. const cells = rows[y].cells;
  21741. for (let x = 0; x < cells.length; x++) {
  21742. if (eq(cells[x], element)) {
  21743. return Optional.some(cellPosition(x, y));
  21744. }
  21745. }
  21746. }
  21747. return Optional.none();
  21748. };
  21749. const extractRows = (table, sx, sy, ex, ey) => {
  21750. const newRows = [];
  21751. const rows = table.rows;
  21752. for (let y = sy; y <= ey; y++) {
  21753. const cells = rows[y].cells;
  21754. const slice = sx < ex ? cells.slice(sx, ex + 1) : cells.slice(ex, sx + 1);
  21755. newRows.push(tableRow(rows[y].element, slice));
  21756. }
  21757. return newRows;
  21758. };
  21759. const subTable = (table, startPos, endPos) => {
  21760. const sx = startPos.x, sy = startPos.y;
  21761. const ex = endPos.x, ey = endPos.y;
  21762. const newRows = sy < ey ? extractRows(table, sx, sy, ex, ey) : extractRows(table, sx, ey, ex, sy);
  21763. return tableModel(table.element, getWidth(newRows), newRows);
  21764. };
  21765. const createDomTable = (table, rows) => {
  21766. const tableElement = shallow(table.element);
  21767. const tableBody = SugarElement.fromTag('tbody');
  21768. append(tableBody, rows);
  21769. append$1(tableElement, tableBody);
  21770. return tableElement;
  21771. };
  21772. const modelRowsToDomRows = (table) => {
  21773. return map$3(table.rows, (row) => {
  21774. const cells = map$3(row.cells, (cell) => {
  21775. const td = deep(cell);
  21776. remove$9(td, 'colspan');
  21777. remove$9(td, 'rowspan');
  21778. return td;
  21779. });
  21780. const tr = shallow(row.element);
  21781. append(tr, cells);
  21782. return tr;
  21783. });
  21784. };
  21785. const fromDom = (tableElm) => {
  21786. const table = tableModel(shallow(tableElm), 0, []);
  21787. each$e(descendants(tableElm, 'tr'), (tr, y) => {
  21788. each$e(descendants(tr, 'td,th'), (td, x) => {
  21789. fillout(table, skipCellsX(table, x, y), y, tr, td);
  21790. });
  21791. });
  21792. return tableModel(table.element, getWidth(table.rows), table.rows);
  21793. };
  21794. const toDom = (table) => {
  21795. return createDomTable(table, modelRowsToDomRows(table));
  21796. };
  21797. const subsection = (table, startElement, endElement) => {
  21798. return findElementPos(table, startElement).bind((startPos) => {
  21799. return findElementPos(table, endElement).map((endPos) => {
  21800. return subTable(table, startPos, endPos);
  21801. });
  21802. });
  21803. };
  21804. const findParentListContainer = (parents) => find$2(parents, (elm) => name(elm) === 'ul' || name(elm) === 'ol');
  21805. const getFullySelectedListWrappers = (parents, rng) => find$2(parents, (elm) => name(elm) === 'li' && hasAllContentsSelected(elm, rng)).fold(constant([]), (_li) => findParentListContainer(parents).map((listCont) => {
  21806. const listElm = SugarElement.fromTag(name(listCont));
  21807. // Retain any list-style* styles when generating the new fragment
  21808. const listStyles = filter$4(getAllRaw(listCont), (_style, name) => startsWith(name, 'list-style'));
  21809. setAll(listElm, listStyles);
  21810. return [
  21811. SugarElement.fromTag('li'),
  21812. listElm
  21813. ];
  21814. }).getOr([]));
  21815. const wrap = (innerElm, elms) => {
  21816. const wrapped = foldl(elms, (acc, elm) => {
  21817. append$1(elm, acc);
  21818. return elm;
  21819. }, innerElm);
  21820. return elms.length > 0 ? fromElements([wrapped]) : wrapped;
  21821. };
  21822. const directListWrappers = (commonAnchorContainer) => {
  21823. if (isListItem$2(commonAnchorContainer)) {
  21824. return parent(commonAnchorContainer).filter(isList$1).fold(constant([]), (listElm) => [commonAnchorContainer, listElm]);
  21825. }
  21826. else {
  21827. return isList$1(commonAnchorContainer) ? [commonAnchorContainer] : [];
  21828. }
  21829. };
  21830. const getWrapElements = (rootNode, rng, schema) => {
  21831. const commonAnchorContainer = SugarElement.fromDom(rng.commonAncestorContainer);
  21832. const parents = parentsAndSelf(commonAnchorContainer, rootNode);
  21833. const wrapElements = filter$5(parents, (el) => schema.isWrapper(name(el)));
  21834. const listWrappers = getFullySelectedListWrappers(parents, rng);
  21835. const allWrappers = wrapElements.concat(listWrappers.length ? listWrappers : directListWrappers(commonAnchorContainer));
  21836. return map$3(allWrappers, shallow);
  21837. };
  21838. const emptyFragment = () => fromElements([]);
  21839. const getFragmentFromRange = (rootNode, rng, schema) => wrap(SugarElement.fromDom(rng.cloneContents()), getWrapElements(rootNode, rng, schema));
  21840. const getParentTable = (rootElm, cell) => ancestor$4(cell, 'table', curry(eq, rootElm));
  21841. const getTableFragment = (rootNode, selectedTableCells) => getParentTable(rootNode, selectedTableCells[0]).bind((tableElm) => {
  21842. const firstCell = selectedTableCells[0];
  21843. const lastCell = selectedTableCells[selectedTableCells.length - 1];
  21844. const fullTableModel = fromDom(tableElm);
  21845. return subsection(fullTableModel, firstCell, lastCell).map((sectionedTableModel) => fromElements([toDom(sectionedTableModel)]));
  21846. }).getOrThunk(emptyFragment);
  21847. const getSelectionFragment = (rootNode, ranges, schema) => ranges.length > 0 && ranges[0].collapsed ? emptyFragment() : getFragmentFromRange(rootNode, ranges[0], schema);
  21848. const read$3 = (rootNode, ranges, schema) => {
  21849. const selectedCells = getCellsFromElementOrRanges(ranges, rootNode);
  21850. return selectedCells.length > 0 ? getTableFragment(rootNode, selectedCells) : getSelectionFragment(rootNode, ranges, schema);
  21851. };
  21852. const isCollapsibleWhitespace = (text, index) => index >= 0 && index < text.length && isWhiteSpace(text.charAt(index));
  21853. const getInnerText = (bin) => {
  21854. return trim$2(bin.innerText);
  21855. };
  21856. const getContextNodeName = (parentBlockOpt) => parentBlockOpt.map((block) => block.nodeName).getOr('div').toLowerCase();
  21857. const getTextContent = (editor) => Optional.from(editor.selection.getRng()).map((rng) => {
  21858. var _a;
  21859. const parentBlockOpt = Optional.from(editor.dom.getParent(rng.commonAncestorContainer, editor.dom.isBlock));
  21860. const body = editor.getBody();
  21861. const contextNodeName = getContextNodeName(parentBlockOpt);
  21862. const rangeContentClone = SugarElement.fromDom(rng.cloneContents());
  21863. cleanupBogusElements(rangeContentClone);
  21864. cleanupInputNames(rangeContentClone);
  21865. const bin = editor.dom.add(body, contextNodeName, {
  21866. 'data-mce-bogus': 'all',
  21867. 'style': 'overflow: hidden; opacity: 0;'
  21868. }, rangeContentClone.dom);
  21869. const text = getInnerText(bin);
  21870. // textContent will not strip leading/trailing spaces since it doesn't consider how it'll render
  21871. const nonRenderedText = trim$2((_a = bin.textContent) !== null && _a !== void 0 ? _a : '');
  21872. editor.dom.remove(bin);
  21873. if (isCollapsibleWhitespace(nonRenderedText, 0) || isCollapsibleWhitespace(nonRenderedText, nonRenderedText.length - 1)) {
  21874. // If the bin contains a trailing/leading space, then we need to inspect the parent block to see if we should include the spaces
  21875. const parentBlock = parentBlockOpt.getOr(body);
  21876. const parentBlockText = getInnerText(parentBlock);
  21877. const textIndex = parentBlockText.indexOf(text);
  21878. if (textIndex === -1) {
  21879. return text;
  21880. }
  21881. else {
  21882. const hasProceedingSpace = isCollapsibleWhitespace(parentBlockText, textIndex - 1);
  21883. const hasTrailingSpace = isCollapsibleWhitespace(parentBlockText, textIndex + text.length);
  21884. return (hasProceedingSpace ? ' ' : '') + text + (hasTrailingSpace ? ' ' : '');
  21885. }
  21886. }
  21887. else {
  21888. return text;
  21889. }
  21890. }).getOr('');
  21891. const getSerializedContent = (editor, args) => {
  21892. const rng = editor.selection.getRng(), tmpElm = editor.dom.create('body');
  21893. const sel = editor.selection.getSel();
  21894. const ranges = processRanges(editor, getRanges(sel));
  21895. const fragment = args.contextual ? read$3(SugarElement.fromDom(editor.getBody()), ranges, editor.schema).dom : rng.cloneContents();
  21896. if (fragment) {
  21897. tmpElm.appendChild(fragment);
  21898. }
  21899. return editor.selection.serializer.serialize(tmpElm, args);
  21900. };
  21901. const extractSelectedContent = (editor, args) => {
  21902. if (args.format === 'text') {
  21903. return getTextContent(editor);
  21904. }
  21905. else {
  21906. const content = getSerializedContent(editor, args);
  21907. if (args.format === 'tree') {
  21908. return content;
  21909. }
  21910. else {
  21911. return editor.selection.isCollapsed() ? '' : content;
  21912. }
  21913. }
  21914. };
  21915. const setupArgs$2 = (args, format) => ({
  21916. ...args,
  21917. format,
  21918. get: true,
  21919. selection: true,
  21920. getInner: true
  21921. });
  21922. const getSelectedContentInternal = (editor, format, args = {}) => {
  21923. const defaultedArgs = setupArgs$2(args, format);
  21924. return preProcessGetContent(editor, defaultedArgs).fold(identity, (updatedArgs) => {
  21925. const content = extractSelectedContent(editor, updatedArgs);
  21926. return postProcessGetContent(editor, content, updatedArgs);
  21927. });
  21928. };
  21929. /**
  21930. * JS Implementation of the O(ND) Difference Algorithm by Eugene W. Myers.
  21931. *
  21932. * @class tinymce.undo.Diff
  21933. * @private
  21934. */
  21935. const KEEP = 0, INSERT = 1, DELETE = 2;
  21936. const diff = (left, right) => {
  21937. const size = left.length + right.length + 2;
  21938. const vDown = new Array(size);
  21939. const vUp = new Array(size);
  21940. const snake = (start, end, diag) => {
  21941. return {
  21942. start,
  21943. end,
  21944. diag
  21945. };
  21946. };
  21947. const buildScript = (start1, end1, start2, end2, script) => {
  21948. const middle = getMiddleSnake(start1, end1, start2, end2);
  21949. if (middle === null || middle.start === end1 && middle.diag === end1 - end2 ||
  21950. middle.end === start1 && middle.diag === start1 - start2) {
  21951. let i = start1;
  21952. let j = start2;
  21953. while (i < end1 || j < end2) {
  21954. if (i < end1 && j < end2 && left[i] === right[j]) {
  21955. script.push([KEEP, left[i]]);
  21956. ++i;
  21957. ++j;
  21958. }
  21959. else {
  21960. if (end1 - start1 > end2 - start2) {
  21961. script.push([DELETE, left[i]]);
  21962. ++i;
  21963. }
  21964. else {
  21965. script.push([INSERT, right[j]]);
  21966. ++j;
  21967. }
  21968. }
  21969. }
  21970. }
  21971. else {
  21972. buildScript(start1, middle.start, start2, middle.start - middle.diag, script);
  21973. for (let i2 = middle.start; i2 < middle.end; ++i2) {
  21974. script.push([KEEP, left[i2]]);
  21975. }
  21976. buildScript(middle.end, end1, middle.end - middle.diag, end2, script);
  21977. }
  21978. };
  21979. const buildSnake = (start, diag, end1, end2) => {
  21980. let end = start;
  21981. while (end - diag < end2 && end < end1 && left[end] === right[end - diag]) {
  21982. ++end;
  21983. }
  21984. return snake(start, end, diag);
  21985. };
  21986. const getMiddleSnake = (start1, end1, start2, end2) => {
  21987. // Myers Algorithm
  21988. // Initialisations
  21989. const m = end1 - start1;
  21990. const n = end2 - start2;
  21991. if (m === 0 || n === 0) {
  21992. return null;
  21993. }
  21994. const delta = m - n;
  21995. const sum = n + m;
  21996. const offset = (sum % 2 === 0 ? sum : sum + 1) / 2;
  21997. vDown[1 + offset] = start1;
  21998. vUp[1 + offset] = end1 + 1;
  21999. let d, k, i, x, y;
  22000. for (d = 0; d <= offset; ++d) {
  22001. // Down
  22002. for (k = -d; k <= d; k += 2) {
  22003. // First step
  22004. i = k + offset;
  22005. if (k === -d || k !== d && vDown[i - 1] < vDown[i + 1]) {
  22006. vDown[i] = vDown[i + 1];
  22007. }
  22008. else {
  22009. vDown[i] = vDown[i - 1] + 1;
  22010. }
  22011. x = vDown[i];
  22012. y = x - start1 + start2 - k;
  22013. while (x < end1 && y < end2 && left[x] === right[y]) {
  22014. vDown[i] = ++x;
  22015. ++y;
  22016. }
  22017. // Second step
  22018. if (delta % 2 !== 0 && delta - d <= k && k <= delta + d) {
  22019. if (vUp[i - delta] <= vDown[i]) {
  22020. return buildSnake(vUp[i - delta], k + start1 - start2, end1, end2);
  22021. }
  22022. }
  22023. }
  22024. // Up
  22025. for (k = delta - d; k <= delta + d; k += 2) {
  22026. // First step
  22027. i = k + offset - delta;
  22028. if (k === delta - d || k !== delta + d && vUp[i + 1] <= vUp[i - 1]) {
  22029. vUp[i] = vUp[i + 1] - 1;
  22030. }
  22031. else {
  22032. vUp[i] = vUp[i - 1];
  22033. }
  22034. x = vUp[i] - 1;
  22035. y = x - start1 + start2 - k;
  22036. while (x >= start1 && y >= start2 && left[x] === right[y]) {
  22037. vUp[i] = x--;
  22038. y--;
  22039. }
  22040. // Second step
  22041. if (delta % 2 === 0 && -d <= k && k <= d) {
  22042. if (vUp[i] <= vDown[i + delta]) {
  22043. return buildSnake(vUp[i], k + start1 - start2, end1, end2);
  22044. }
  22045. }
  22046. }
  22047. }
  22048. return null;
  22049. };
  22050. const script = [];
  22051. buildScript(0, left.length, 0, right.length, script);
  22052. return script;
  22053. };
  22054. /**
  22055. * This module reads and applies html fragments from/to dom nodes.
  22056. *
  22057. * @class tinymce.undo.Fragments
  22058. * @private
  22059. */
  22060. const getOuterHtml = (elm) => {
  22061. if (isElement$7(elm)) {
  22062. return elm.outerHTML;
  22063. }
  22064. else if (isText$b(elm)) {
  22065. return Entities.encodeRaw(elm.data, false);
  22066. }
  22067. else if (isComment(elm)) {
  22068. return '<!--' + elm.data + '-->';
  22069. }
  22070. return '';
  22071. };
  22072. const createFragment = (html) => {
  22073. let node;
  22074. const container = document.createElement('div');
  22075. const frag = document.createDocumentFragment();
  22076. if (html) {
  22077. container.innerHTML = html;
  22078. }
  22079. while ((node = container.firstChild)) {
  22080. frag.appendChild(node);
  22081. }
  22082. return frag;
  22083. };
  22084. const insertAt = (elm, html, index) => {
  22085. const fragment = createFragment(html);
  22086. if (elm.hasChildNodes() && index < elm.childNodes.length) {
  22087. const target = elm.childNodes[index];
  22088. elm.insertBefore(fragment, target);
  22089. }
  22090. else {
  22091. elm.appendChild(fragment);
  22092. }
  22093. };
  22094. const removeAt = (elm, index) => {
  22095. if (elm.hasChildNodes() && index < elm.childNodes.length) {
  22096. const target = elm.childNodes[index];
  22097. elm.removeChild(target);
  22098. }
  22099. };
  22100. const applyDiff = (diff, elm) => {
  22101. let index = 0;
  22102. each$e(diff, (action) => {
  22103. if (action[0] === KEEP) {
  22104. index++;
  22105. }
  22106. else if (action[0] === INSERT) {
  22107. insertAt(elm, action[1], index);
  22108. index++;
  22109. }
  22110. else if (action[0] === DELETE) {
  22111. removeAt(elm, index);
  22112. }
  22113. });
  22114. };
  22115. const read$2 = (elm, trimZwsp) => filter$5(map$3(from(elm.childNodes), trimZwsp ? compose(trim$2, getOuterHtml) : getOuterHtml), (item) => {
  22116. return item.length > 0;
  22117. });
  22118. const write = (fragments, elm) => {
  22119. const currentFragments = map$3(from(elm.childNodes), getOuterHtml);
  22120. applyDiff(diff(currentFragments, fragments), elm);
  22121. return elm;
  22122. };
  22123. // We need to create a temporary document instead of using the global document since
  22124. // innerHTML on a detached element will still make http requests to the images
  22125. const lazyTempDocument = cached(() => document.implementation.createHTMLDocument('undo'));
  22126. const hasIframes = (body) => body.querySelector('iframe') !== null;
  22127. const createFragmentedLevel = (fragments) => {
  22128. return {
  22129. type: 'fragmented',
  22130. fragments,
  22131. content: '',
  22132. bookmark: null,
  22133. beforeBookmark: null
  22134. };
  22135. };
  22136. const createCompleteLevel = (content) => {
  22137. return {
  22138. type: 'complete',
  22139. fragments: null,
  22140. content,
  22141. bookmark: null,
  22142. beforeBookmark: null
  22143. };
  22144. };
  22145. const createFromEditor = (editor) => {
  22146. const tempAttrs = editor.serializer.getTempAttrs();
  22147. const body = trim$1(editor.getBody(), tempAttrs);
  22148. return hasIframes(body) ? createFragmentedLevel(read$2(body, true)) : createCompleteLevel(trim$2(body.innerHTML));
  22149. };
  22150. const applyToEditor = (editor, level, before) => {
  22151. const bookmark = before ? level.beforeBookmark : level.bookmark;
  22152. if (level.type === 'fragmented') {
  22153. write(level.fragments, editor.getBody());
  22154. }
  22155. else {
  22156. editor.setContent(level.content, {
  22157. format: 'raw',
  22158. // If we have a path bookmark, we need to check if the bookmark location was a fake caret.
  22159. // If the bookmark was not a fake caret, then we need to ensure that setContent does not move the selection
  22160. // as this can create a new fake caret - particularly if the first element in the body is contenteditable=false.
  22161. // The creation of this new fake caret will cause our path offset to be off by one when restoring the original selection.
  22162. no_selection: isNonNullable(bookmark) && isPathBookmark(bookmark) ? !bookmark.isFakeCaret : true
  22163. });
  22164. }
  22165. if (bookmark) {
  22166. editor.selection.moveToBookmark(bookmark);
  22167. editor.selection.scrollIntoView();
  22168. }
  22169. };
  22170. const getLevelContent = (level) => {
  22171. return level.type === 'fragmented' ? level.fragments.join('') : level.content;
  22172. };
  22173. const getCleanLevelContent = (level) => {
  22174. const elm = SugarElement.fromTag('body', lazyTempDocument());
  22175. set$3(elm, getLevelContent(level));
  22176. each$e(descendants(elm, '*[data-mce-bogus]'), unwrap);
  22177. return get$8(elm);
  22178. };
  22179. const hasEqualContent = (level1, level2) => getLevelContent(level1) === getLevelContent(level2);
  22180. const hasEqualCleanedContent = (level1, level2) => getCleanLevelContent(level1) === getCleanLevelContent(level2);
  22181. // Most of the time the contents is equal so it's faster to first check that using strings then fallback to a cleaned dom comparison
  22182. const isEq$1 = (level1, level2) => {
  22183. if (!level1 || !level2) {
  22184. return false;
  22185. }
  22186. else if (hasEqualContent(level1, level2)) {
  22187. return true;
  22188. }
  22189. else {
  22190. return hasEqualCleanedContent(level1, level2);
  22191. }
  22192. };
  22193. const isUnlocked = (locks) => locks.get() === 0;
  22194. const setTyping = (undoManager, typing, locks) => {
  22195. if (isUnlocked(locks)) {
  22196. undoManager.typing = typing;
  22197. }
  22198. };
  22199. const endTyping = (undoManager, locks) => {
  22200. if (undoManager.typing) {
  22201. setTyping(undoManager, false, locks);
  22202. undoManager.add();
  22203. }
  22204. };
  22205. const endTypingLevelIgnoreLocks = (undoManager) => {
  22206. if (undoManager.typing) {
  22207. undoManager.typing = false;
  22208. undoManager.add();
  22209. }
  22210. };
  22211. const beforeChange$1 = (editor, locks, beforeBookmark) => {
  22212. if (isUnlocked(locks)) {
  22213. beforeBookmark.set(getUndoBookmark(editor.selection));
  22214. }
  22215. };
  22216. const addUndoLevel$1 = (editor, undoManager, index, locks, beforeBookmark, level, event) => {
  22217. const currentLevel = createFromEditor(editor);
  22218. const newLevel = Tools.extend(level || {}, currentLevel);
  22219. if (!isUnlocked(locks) || editor.removed) {
  22220. return null;
  22221. }
  22222. const lastLevel = undoManager.data[index.get()];
  22223. if (editor.dispatch('BeforeAddUndo', { level: newLevel, lastLevel, originalEvent: event }).isDefaultPrevented()) {
  22224. return null;
  22225. }
  22226. // Add undo level if needed
  22227. if (lastLevel && isEq$1(lastLevel, newLevel)) {
  22228. return null;
  22229. }
  22230. // Set before bookmark on previous level
  22231. if (undoManager.data[index.get()]) {
  22232. beforeBookmark.get().each((bm) => {
  22233. undoManager.data[index.get()].beforeBookmark = bm;
  22234. });
  22235. }
  22236. // Time to compress
  22237. const customUndoRedoLevels = getCustomUndoRedoLevels(editor);
  22238. if (customUndoRedoLevels) {
  22239. if (undoManager.data.length > customUndoRedoLevels) {
  22240. for (let i = 0; i < undoManager.data.length - 1; i++) {
  22241. undoManager.data[i] = undoManager.data[i + 1];
  22242. }
  22243. undoManager.data.length--;
  22244. index.set(undoManager.data.length);
  22245. }
  22246. }
  22247. // Get a non intrusive normalized bookmark
  22248. newLevel.bookmark = getUndoBookmark(editor.selection);
  22249. // Crop array if needed
  22250. if (index.get() < undoManager.data.length - 1) {
  22251. undoManager.data.length = index.get() + 1;
  22252. }
  22253. undoManager.data.push(newLevel);
  22254. index.set(undoManager.data.length - 1);
  22255. const args = { level: newLevel, lastLevel, originalEvent: event };
  22256. if (index.get() > 0) {
  22257. editor.setDirty(true);
  22258. editor.dispatch('AddUndo', args);
  22259. editor.dispatch('change', args);
  22260. }
  22261. else {
  22262. editor.dispatch('AddUndo', args);
  22263. }
  22264. return newLevel;
  22265. };
  22266. const clear$1 = (editor, undoManager, index) => {
  22267. undoManager.data = [];
  22268. index.set(0);
  22269. undoManager.typing = false;
  22270. editor.dispatch('ClearUndos');
  22271. };
  22272. const extra$1 = (editor, undoManager, index, callback1, callback2) => {
  22273. if (undoManager.transact(callback1)) {
  22274. const bookmark = undoManager.data[index.get()].bookmark;
  22275. const lastLevel = undoManager.data[index.get() - 1];
  22276. applyToEditor(editor, lastLevel, true);
  22277. if (undoManager.transact(callback2)) {
  22278. undoManager.data[index.get() - 1].beforeBookmark = bookmark;
  22279. }
  22280. }
  22281. };
  22282. const redo$1 = (editor, index, data) => {
  22283. let level;
  22284. if (index.get() < data.length - 1) {
  22285. index.set(index.get() + 1);
  22286. level = data[index.get()];
  22287. applyToEditor(editor, level, false);
  22288. editor.setDirty(true);
  22289. editor.dispatch('Redo', { level });
  22290. }
  22291. return level;
  22292. };
  22293. const undo$1 = (editor, undoManager, locks, index) => {
  22294. let level;
  22295. if (undoManager.typing) {
  22296. undoManager.add();
  22297. undoManager.typing = false;
  22298. setTyping(undoManager, false, locks);
  22299. }
  22300. if (index.get() > 0) {
  22301. index.set(index.get() - 1);
  22302. level = undoManager.data[index.get()];
  22303. applyToEditor(editor, level, true);
  22304. editor.setDirty(true);
  22305. editor.dispatch('Undo', { level });
  22306. }
  22307. return level;
  22308. };
  22309. const reset$1 = (undoManager) => {
  22310. undoManager.clear();
  22311. undoManager.add();
  22312. };
  22313. const hasUndo$1 = (editor, undoManager, index) =>
  22314. // Has undo levels or typing and content isn't the same as the initial level
  22315. index.get() > 0 || (undoManager.typing && undoManager.data[0] && !isEq$1(createFromEditor(editor), undoManager.data[0]));
  22316. const hasRedo$1 = (undoManager, index) => index.get() < undoManager.data.length - 1 && !undoManager.typing;
  22317. const transact$1 = (undoManager, locks, callback) => {
  22318. endTyping(undoManager, locks);
  22319. undoManager.beforeChange();
  22320. undoManager.ignore(callback);
  22321. return undoManager.add();
  22322. };
  22323. const ignore$1 = (locks, callback) => {
  22324. try {
  22325. locks.set(locks.get() + 1);
  22326. callback();
  22327. }
  22328. finally {
  22329. locks.set(locks.get() - 1);
  22330. }
  22331. };
  22332. const addVisualInternal = (editor, elm) => {
  22333. const dom = editor.dom;
  22334. const scope = isNonNullable(elm) ? elm : editor.getBody();
  22335. each$e(dom.select('table,a', scope), (matchedElm) => {
  22336. switch (matchedElm.nodeName) {
  22337. case 'TABLE':
  22338. const cls = getVisualAidsTableClass(editor);
  22339. const value = dom.getAttrib(matchedElm, 'border');
  22340. if ((!value || value === '0') && editor.hasVisual) {
  22341. dom.addClass(matchedElm, cls);
  22342. }
  22343. else {
  22344. dom.removeClass(matchedElm, cls);
  22345. }
  22346. break;
  22347. case 'A':
  22348. if (!dom.getAttrib(matchedElm, 'href')) {
  22349. const value = dom.getAttrib(matchedElm, 'name') || matchedElm.id;
  22350. const cls = getVisualAidsAnchorClass(editor);
  22351. if (value && editor.hasVisual) {
  22352. dom.addClass(matchedElm, cls);
  22353. }
  22354. else {
  22355. dom.removeClass(matchedElm, cls);
  22356. }
  22357. }
  22358. break;
  22359. }
  22360. });
  22361. editor.dispatch('VisualAid', { element: elm, hasVisual: editor.hasVisual });
  22362. };
  22363. const makePlainAdaptor = (editor) => ({
  22364. init: {
  22365. bindEvents: noop
  22366. },
  22367. undoManager: {
  22368. beforeChange: (locks, beforeBookmark) => beforeChange$1(editor, locks, beforeBookmark),
  22369. add: (undoManager, index, locks, beforeBookmark, level, event) => addUndoLevel$1(editor, undoManager, index, locks, beforeBookmark, level, event),
  22370. undo: (undoManager, locks, index) => undo$1(editor, undoManager, locks, index),
  22371. redo: (index, data) => redo$1(editor, index, data),
  22372. clear: (undoManager, index) => clear$1(editor, undoManager, index),
  22373. reset: (undoManager) => reset$1(undoManager),
  22374. hasUndo: (undoManager, index) => hasUndo$1(editor, undoManager, index),
  22375. hasRedo: (undoManager, index) => hasRedo$1(undoManager, index),
  22376. transact: (undoManager, locks, callback) => transact$1(undoManager, locks, callback),
  22377. ignore: (locks, callback) => ignore$1(locks, callback),
  22378. extra: (undoManager, index, callback1, callback2) => extra$1(editor, undoManager, index, callback1, callback2)
  22379. },
  22380. formatter: {
  22381. match: (name, vars, node, similar) => match$2(editor, name, vars, node, similar),
  22382. matchAll: (names, vars) => matchAll(editor, names, vars),
  22383. matchNode: (node, name, vars, similar) => matchNode$1(editor, node, name, vars, similar),
  22384. canApply: (name) => canApply(editor, name),
  22385. closest: (names) => closest(editor, names),
  22386. apply: (name, vars, node) => applyFormat$1(editor, name, vars, node),
  22387. remove: (name, vars, node, similar) => removeFormat$1(editor, name, vars, node, similar),
  22388. toggle: (name, vars, node) => toggle(editor, name, vars, node),
  22389. formatChanged: (registeredFormatListeners, formats, callback, similar, vars) => formatChangedInternal(editor, registeredFormatListeners, formats, callback, similar, vars)
  22390. },
  22391. editor: {
  22392. getContent: (args) => getContentInternal(editor, args),
  22393. setContent: (content, args) => setContentInternal(editor, content, args),
  22394. insertContent: (value, details) => insertHtmlAtCaret(editor, value, details),
  22395. addVisual: (elm) => addVisualInternal(editor, elm)
  22396. },
  22397. selection: {
  22398. getContent: (format, args) => getSelectedContentInternal(editor, format, args)
  22399. },
  22400. autocompleter: {
  22401. addDecoration: noop, // This was never fully implemented in RTC
  22402. removeDecoration: noop, // This was never fully implemented in RTC
  22403. },
  22404. raw: {
  22405. getModel: () => Optional.none()
  22406. }
  22407. });
  22408. const makeRtcAdaptor = (rtcEditor) => {
  22409. const defaultVars = (vars) => isObject(vars) ? vars : {};
  22410. const { init, undoManager, formatter, editor, selection, autocompleter, raw } = rtcEditor;
  22411. return {
  22412. init: {
  22413. bindEvents: init.bindEvents
  22414. },
  22415. undoManager: {
  22416. beforeChange: undoManager.beforeChange,
  22417. add: undoManager.add,
  22418. undo: undoManager.undo,
  22419. redo: undoManager.redo,
  22420. clear: undoManager.clear,
  22421. reset: undoManager.reset,
  22422. hasUndo: undoManager.hasUndo,
  22423. hasRedo: undoManager.hasRedo,
  22424. transact: (_undoManager, _locks, fn) => undoManager.transact(fn),
  22425. ignore: (_locks, callback) => undoManager.ignore(callback),
  22426. extra: (_undoManager, _index, callback1, callback2) => undoManager.extra(callback1, callback2)
  22427. },
  22428. formatter: {
  22429. match: (name, vars, _node, similar) => formatter.match(name, defaultVars(vars), similar),
  22430. matchAll: formatter.matchAll,
  22431. matchNode: formatter.matchNode,
  22432. canApply: (name) => formatter.canApply(name),
  22433. closest: (names) => formatter.closest(names),
  22434. apply: (name, vars, _node) => formatter.apply(name, defaultVars(vars)),
  22435. remove: (name, vars, _node, _similar) => formatter.remove(name, defaultVars(vars)),
  22436. toggle: (name, vars, _node) => formatter.toggle(name, defaultVars(vars)),
  22437. formatChanged: (_rfl, formats, callback, similar, vars) => formatter.formatChanged(formats, callback, similar, vars)
  22438. },
  22439. editor: {
  22440. getContent: (args) => editor.getContent(args),
  22441. setContent: (content, args) => {
  22442. return { content: editor.setContent(content, args), html: '' };
  22443. },
  22444. insertContent: (content, _details) => {
  22445. editor.insertContent(content);
  22446. return '';
  22447. },
  22448. addVisual: editor.addVisual
  22449. },
  22450. selection: {
  22451. getContent: (_format, args) => selection.getContent(args)
  22452. },
  22453. autocompleter: {
  22454. addDecoration: autocompleter.addDecoration,
  22455. removeDecoration: autocompleter.removeDecoration
  22456. },
  22457. raw: {
  22458. getModel: () => Optional.some(raw.getRawModel())
  22459. }
  22460. };
  22461. };
  22462. const makeNoopAdaptor = () => {
  22463. // Cast as any since this will never match the implementations
  22464. const nul = constant(null);
  22465. const empty = constant('');
  22466. return {
  22467. init: {
  22468. bindEvents: noop
  22469. },
  22470. undoManager: {
  22471. beforeChange: noop,
  22472. add: nul,
  22473. undo: nul,
  22474. redo: nul,
  22475. clear: noop,
  22476. reset: noop,
  22477. hasUndo: never,
  22478. hasRedo: never,
  22479. transact: nul,
  22480. ignore: noop,
  22481. extra: noop
  22482. },
  22483. formatter: {
  22484. match: never,
  22485. matchAll: constant([]),
  22486. matchNode: constant(undefined),
  22487. canApply: never,
  22488. closest: empty,
  22489. apply: noop,
  22490. remove: noop,
  22491. toggle: noop,
  22492. formatChanged: constant({ unbind: noop })
  22493. },
  22494. editor: {
  22495. getContent: empty,
  22496. setContent: constant({ content: '', html: '' }),
  22497. insertContent: constant(''),
  22498. addVisual: noop
  22499. },
  22500. selection: {
  22501. getContent: empty
  22502. },
  22503. autocompleter: {
  22504. addDecoration: noop,
  22505. removeDecoration: noop
  22506. },
  22507. raw: {
  22508. getModel: constant(Optional.none())
  22509. }
  22510. };
  22511. };
  22512. const isRtc = (editor) => has$2(editor.plugins, 'rtc');
  22513. const getRtcSetup = (editor) => get$a(editor.plugins, 'rtc').bind((rtcPlugin) =>
  22514. // This might not exist if the stub plugin is loaded on cloud
  22515. Optional.from(rtcPlugin.setup));
  22516. const setup$z = (editor) => {
  22517. const editorCast = editor;
  22518. return getRtcSetup(editor).fold(() => {
  22519. editorCast.rtcInstance = makePlainAdaptor(editor);
  22520. return Optional.none();
  22521. }, (setup) => {
  22522. // We need to provide a noop adaptor while initializing since any call by the theme or plugins to say undoManager.hasUndo would throw errors
  22523. editorCast.rtcInstance = makeNoopAdaptor();
  22524. return Optional.some(() => setup().then((rtcEditor) => {
  22525. editorCast.rtcInstance = makeRtcAdaptor(rtcEditor);
  22526. return rtcEditor.rtc.isRemote;
  22527. }));
  22528. });
  22529. };
  22530. const getRtcInstanceWithFallback = (editor) =>
  22531. // Calls to editor.getContent/editor.setContent should still work even if the rtcInstance is not yet available
  22532. editor.rtcInstance ? editor.rtcInstance : makePlainAdaptor(editor);
  22533. const getRtcInstanceWithError = (editor) => {
  22534. const rtcInstance = editor.rtcInstance;
  22535. if (!rtcInstance) {
  22536. throw new Error('Failed to get RTC instance not yet initialized.');
  22537. }
  22538. else {
  22539. return rtcInstance;
  22540. }
  22541. };
  22542. /** In theory these could all be inlined but having them here makes it clear what is overridden */
  22543. const beforeChange = (editor, locks, beforeBookmark) => {
  22544. getRtcInstanceWithError(editor).undoManager.beforeChange(locks, beforeBookmark);
  22545. };
  22546. const addUndoLevel = (editor, undoManager, index, locks, beforeBookmark, level, event) => getRtcInstanceWithError(editor).undoManager.add(undoManager, index, locks, beforeBookmark, level, event);
  22547. const undo = (editor, undoManager, locks, index) => getRtcInstanceWithError(editor).undoManager.undo(undoManager, locks, index);
  22548. const redo = (editor, index, data) => getRtcInstanceWithError(editor).undoManager.redo(index, data);
  22549. const clear = (editor, undoManager, index) => {
  22550. getRtcInstanceWithError(editor).undoManager.clear(undoManager, index);
  22551. };
  22552. const reset = (editor, undoManager) => {
  22553. getRtcInstanceWithError(editor).undoManager.reset(undoManager);
  22554. };
  22555. const hasUndo = (editor, undoManager, index) => getRtcInstanceWithError(editor).undoManager.hasUndo(undoManager, index);
  22556. const hasRedo = (editor, undoManager, index) => getRtcInstanceWithError(editor).undoManager.hasRedo(undoManager, index);
  22557. const transact = (editor, undoManager, locks, callback) => getRtcInstanceWithError(editor).undoManager.transact(undoManager, locks, callback);
  22558. const ignore = (editor, locks, callback) => {
  22559. getRtcInstanceWithError(editor).undoManager.ignore(locks, callback);
  22560. };
  22561. const extra = (editor, undoManager, index, callback1, callback2) => {
  22562. getRtcInstanceWithError(editor).undoManager.extra(undoManager, index, callback1, callback2);
  22563. };
  22564. const matchFormat = (editor, name, vars, node, similar) => getRtcInstanceWithError(editor).formatter.match(name, vars, node, similar);
  22565. const matchAllFormats = (editor, names, vars) => getRtcInstanceWithError(editor).formatter.matchAll(names, vars);
  22566. const matchNodeFormat = (editor, node, name, vars, similar) => getRtcInstanceWithError(editor).formatter.matchNode(node, name, vars, similar);
  22567. const canApplyFormat = (editor, name) => getRtcInstanceWithError(editor).formatter.canApply(name);
  22568. const closestFormat = (editor, names) => getRtcInstanceWithError(editor).formatter.closest(names);
  22569. const applyFormat = (editor, name, vars, node) => {
  22570. getRtcInstanceWithError(editor).formatter.apply(name, vars, node);
  22571. };
  22572. const removeFormat = (editor, name, vars, node, similar) => {
  22573. getRtcInstanceWithError(editor).formatter.remove(name, vars, node, similar);
  22574. };
  22575. const toggleFormat = (editor, name, vars, node) => {
  22576. getRtcInstanceWithError(editor).formatter.toggle(name, vars, node);
  22577. };
  22578. const formatChanged = (editor, registeredFormatListeners, formats, callback, similar, vars) => getRtcInstanceWithError(editor).formatter.formatChanged(registeredFormatListeners, formats, callback, similar, vars);
  22579. const getContent$2 = (editor, args) => getRtcInstanceWithFallback(editor).editor.getContent(args);
  22580. const setContent$1 = (editor, content, args) => getRtcInstanceWithFallback(editor).editor.setContent(content, args);
  22581. const insertContent$1 = (editor, value, details) => getRtcInstanceWithFallback(editor).editor.insertContent(value, details);
  22582. const getSelectedContent = (editor, format, args) => getRtcInstanceWithError(editor).selection.getContent(format, args);
  22583. const addVisual$1 = (editor, elm) => getRtcInstanceWithError(editor).editor.addVisual(elm);
  22584. const bindEvents = (editor) => getRtcInstanceWithError(editor).init.bindEvents();
  22585. const getContent$1 = (editor, args = {}) => {
  22586. const format = args.format ? args.format : 'html';
  22587. return getSelectedContent(editor, format, args);
  22588. };
  22589. const deleteFromCallbackMap = (callbackMap, selector, callback) => {
  22590. if (has$2(callbackMap, selector)) {
  22591. const newCallbacks = filter$5(callbackMap[selector], (cb) => cb !== callback);
  22592. if (newCallbacks.length === 0) {
  22593. delete callbackMap[selector];
  22594. }
  22595. else {
  22596. callbackMap[selector] = newCallbacks;
  22597. }
  22598. }
  22599. };
  22600. var SelectorChanged = (dom, editor) => {
  22601. let selectorChangedData;
  22602. let currentSelectors;
  22603. const findMatchingNode = (selector, nodes) => find$2(nodes, (node) => dom.is(node, selector));
  22604. const getParents = (elem) => dom.getParents(elem, undefined, dom.getRoot());
  22605. const setup = () => {
  22606. selectorChangedData = {};
  22607. currentSelectors = {};
  22608. editor.on('NodeChange', (e) => {
  22609. const node = e.element;
  22610. const parents = getParents(node);
  22611. const matchedSelectors = {};
  22612. // Check for new matching selectors
  22613. each$d(selectorChangedData, (callbacks, selector) => {
  22614. findMatchingNode(selector, parents).each((node) => {
  22615. if (!currentSelectors[selector]) {
  22616. // Execute callbacks
  22617. each$e(callbacks, (callback) => {
  22618. callback(true, { node, selector, parents });
  22619. });
  22620. currentSelectors[selector] = callbacks;
  22621. }
  22622. matchedSelectors[selector] = callbacks;
  22623. });
  22624. });
  22625. // Check if current selectors still match
  22626. each$d(currentSelectors, (callbacks, selector) => {
  22627. if (!matchedSelectors[selector]) {
  22628. delete currentSelectors[selector];
  22629. each$e(callbacks, (callback) => {
  22630. callback(false, { node, selector, parents });
  22631. });
  22632. }
  22633. });
  22634. });
  22635. };
  22636. return {
  22637. selectorChangedWithUnbind: (selector, callback) => {
  22638. if (!selectorChangedData) {
  22639. setup();
  22640. }
  22641. // Add selector listeners
  22642. if (!selectorChangedData[selector]) {
  22643. selectorChangedData[selector] = [];
  22644. }
  22645. selectorChangedData[selector].push(callback);
  22646. // Setup the initial state if selected already
  22647. findMatchingNode(selector, getParents(editor.selection.getStart())).each(() => {
  22648. currentSelectors[selector] = selectorChangedData[selector];
  22649. });
  22650. return {
  22651. unbind: () => {
  22652. deleteFromCallbackMap(selectorChangedData, selector, callback);
  22653. deleteFromCallbackMap(currentSelectors, selector, callback);
  22654. }
  22655. };
  22656. }
  22657. };
  22658. };
  22659. /**
  22660. * This class handles text and control selection it's an crossbrowser utility class.
  22661. * Consult the TinyMCE API Documentation for more details and examples on how to use this class.
  22662. *
  22663. * @class tinymce.dom.Selection
  22664. * @example
  22665. * // Getting the currently selected node for the active editor
  22666. * alert(tinymce.activeEditor.selection.getNode().nodeName);
  22667. */
  22668. const isAttachedToDom = (node) => {
  22669. return !!(node && node.ownerDocument) && contains(SugarElement.fromDom(node.ownerDocument), SugarElement.fromDom(node));
  22670. };
  22671. const isValidRange = (rng) => {
  22672. if (!rng) {
  22673. return false;
  22674. }
  22675. else {
  22676. return isAttachedToDom(rng.startContainer) && isAttachedToDom(rng.endContainer);
  22677. }
  22678. };
  22679. /**
  22680. * Constructs a new selection instance.
  22681. *
  22682. * @constructor
  22683. * @method Selection
  22684. * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference.
  22685. * @param {Window} win Window to bind the selection object to.
  22686. * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent.
  22687. * @param {tinymce.Editor} editor Editor instance of the selection.
  22688. */
  22689. const EditorSelection = (dom, win, serializer, editor) => {
  22690. let selectedRange;
  22691. let explicitRange;
  22692. const { selectorChangedWithUnbind } = SelectorChanged(dom, editor);
  22693. /**
  22694. * Move the selection cursor range to the specified node and offset.
  22695. * If there is no node specified it will move it to the first suitable location within the body.
  22696. *
  22697. * @method setCursorLocation
  22698. * @param {Node} node Optional node to put the cursor in.
  22699. * @param {Number} offset Optional offset from the start of the node to put the cursor at.
  22700. */
  22701. const setCursorLocation = (node, offset) => {
  22702. const rng = dom.createRng();
  22703. if (isNonNullable(node) && isNonNullable(offset)) {
  22704. rng.setStart(node, offset);
  22705. rng.setEnd(node, offset);
  22706. setRng(rng);
  22707. collapse(false);
  22708. }
  22709. else {
  22710. moveEndPoint(dom, rng, editor.getBody(), true);
  22711. setRng(rng);
  22712. }
  22713. };
  22714. /**
  22715. * Returns the selected contents using the DOM serializer passed in to this class.
  22716. *
  22717. * @method getContent
  22718. * @param {Object} args Optional settings class with for example output format text or html.
  22719. * @return {String} Selected contents in for example HTML format.
  22720. * @example
  22721. * // Alerts the currently selected contents
  22722. * alert(tinymce.activeEditor.selection.getContent());
  22723. *
  22724. * // Alerts the currently selected contents as plain text
  22725. * alert(tinymce.activeEditor.selection.getContent({ format: 'text' }));
  22726. */
  22727. const getContent = (args) => getContent$1(editor, args);
  22728. /**
  22729. * This method has been deprecated. Use "editor.insertContent" instead.
  22730. *
  22731. * Sets the current selection to the specified content. If any contents is selected it will be replaced
  22732. * with the contents passed in to this function. If there is no selection the contents will be inserted
  22733. * where the caret is placed in the editor/page.
  22734. *
  22735. * @method setContent
  22736. * @param {String} content HTML contents to set could also be other formats depending on settings.
  22737. * @param {Object} args Optional settings object with for example data format.
  22738. * @example
  22739. * // Inserts some HTML contents at the current selection
  22740. * tinymce.activeEditor.selection.setContent('<strong>Some contents</strong>');
  22741. */
  22742. const setContent = (content, args) => setContentExternal(editor, content, args);
  22743. /**
  22744. * Returns the start element of a selection range. If the start is in a text
  22745. * node the parent element will be returned.
  22746. *
  22747. * @method getStart
  22748. * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element.
  22749. * @return {Element} Start element of selection range.
  22750. */
  22751. const getStart$1 = (real) => getStart(editor.getBody(), getRng$1(), real);
  22752. /**
  22753. * Returns the end element of a selection range. If the end is in a text
  22754. * node the parent element will be returned.
  22755. *
  22756. * @method getEnd
  22757. * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element.
  22758. * @return {Element} End element of selection range.
  22759. */
  22760. const getEnd$1 = (real) => getEnd(editor.getBody(), getRng$1(), real);
  22761. /**
  22762. * Returns a bookmark location for the current selection. This bookmark object
  22763. * can then be used to restore the selection after some content modification to the document.
  22764. *
  22765. * @method getBookmark
  22766. * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
  22767. * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
  22768. * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
  22769. * @example
  22770. * // Stores a bookmark of the current selection
  22771. * const bm = tinymce.activeEditor.selection.getBookmark();
  22772. *
  22773. * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
  22774. *
  22775. * // Restore the selection bookmark
  22776. * tinymce.activeEditor.selection.moveToBookmark(bm);
  22777. */
  22778. const getBookmark = (type, normalized) => bookmarkManager.getBookmark(type, normalized);
  22779. /**
  22780. * Restores the selection to the specified bookmark.
  22781. *
  22782. * @method moveToBookmark
  22783. * @param {Object} bookmark Bookmark to restore selection from.
  22784. * @example
  22785. * // Stores a bookmark of the current selection
  22786. * const bm = tinymce.activeEditor.selection.getBookmark();
  22787. *
  22788. * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
  22789. *
  22790. * // Restore the selection bookmark
  22791. * tinymce.activeEditor.selection.moveToBookmark(bm);
  22792. */
  22793. const moveToBookmark = (bookmark) => bookmarkManager.moveToBookmark(bookmark);
  22794. /**
  22795. * Selects the specified element. This will place the start and end of the selection range around the element.
  22796. *
  22797. * @method select
  22798. * @param {Element} node HTML DOM element to select.
  22799. * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser.
  22800. * @return {Element} Selected element the same element as the one that got passed in.
  22801. * @example
  22802. * // Select the first paragraph in the active editor
  22803. * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]);
  22804. */
  22805. const select$1 = (node, content) => {
  22806. select(dom, node, content).each(setRng);
  22807. return node;
  22808. };
  22809. /**
  22810. * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
  22811. *
  22812. * @method isCollapsed
  22813. * @return {Boolean} true/false state if the selection range is collapsed or not.
  22814. * Collapsed means if it's a caret or a larger selection.
  22815. */
  22816. const isCollapsed = () => {
  22817. const rng = getRng$1(), sel = getSel();
  22818. if (!rng || rng.item) {
  22819. return false;
  22820. }
  22821. if (rng.compareEndPoints) {
  22822. return rng.compareEndPoints('StartToEnd', rng) === 0;
  22823. }
  22824. return !sel || rng.collapsed;
  22825. };
  22826. /**
  22827. * Checks if the current selection’s start and end containers are editable within their parent’s contexts.
  22828. *
  22829. * @method isEditable
  22830. * @return {Boolean} Will be true if the selection is editable and false if it's not editable.
  22831. */
  22832. const isEditable = () => {
  22833. if (editor.mode.isReadOnly()) {
  22834. return false;
  22835. }
  22836. const rng = getRng$1();
  22837. const fakeSelectedElements = editor.getBody().querySelectorAll('[data-mce-selected="1"]');
  22838. if (fakeSelectedElements.length > 0) {
  22839. return forall(fakeSelectedElements, (el) => dom.isEditable(el.parentElement));
  22840. }
  22841. else {
  22842. return isEditableRange(dom, rng);
  22843. }
  22844. };
  22845. /**
  22846. * Collapse the selection to start or end of range.
  22847. *
  22848. * @method collapse
  22849. * @param {Boolean} toStart Optional boolean state if to collapse to end or not. Defaults to false.
  22850. */
  22851. const collapse = (toStart) => {
  22852. const rng = getRng$1();
  22853. rng.collapse(!!toStart);
  22854. setRng(rng);
  22855. };
  22856. /**
  22857. * Returns the browsers internal selection object.
  22858. *
  22859. * @method getSel
  22860. * @return {Selection} Internal browser selection object.
  22861. */
  22862. const getSel = () => win.getSelection ? win.getSelection() : win.document.selection;
  22863. /**
  22864. * Returns the browsers internal range object.
  22865. *
  22866. * @method getRng
  22867. * @return {Range} Internal browser range object.
  22868. * @see http://www.quirksmode.org/dom/range_intro.html
  22869. * @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/
  22870. */
  22871. const getRng$1 = () => {
  22872. let rng;
  22873. const tryCompareBoundaryPoints = (how, sourceRange, destinationRange) => {
  22874. try {
  22875. return sourceRange.compareBoundaryPoints(how, destinationRange);
  22876. }
  22877. catch (_a) {
  22878. // Gecko throws wrong document exception if the range points
  22879. // to nodes that where removed from the dom #6690
  22880. // Browsers should mutate existing DOMRange instances so that they always point
  22881. // to something in the document this is not the case in Gecko works fine in IE/WebKit/Blink
  22882. // For performance reasons just return -1
  22883. return -1;
  22884. }
  22885. };
  22886. const doc = win.document;
  22887. if (isNonNullable(editor.bookmark) && !hasFocus(editor)) {
  22888. const bookmark = getRng(editor);
  22889. if (bookmark.isSome()) {
  22890. return bookmark.map((r) => processRanges(editor, [r])[0]).getOr(doc.createRange());
  22891. }
  22892. }
  22893. try {
  22894. const selection = getSel();
  22895. if (selection && !isRestrictedNode(selection.anchorNode)) {
  22896. if (selection.rangeCount > 0) {
  22897. rng = selection.getRangeAt(0);
  22898. }
  22899. else {
  22900. rng = doc.createRange();
  22901. }
  22902. rng = processRanges(editor, [rng])[0];
  22903. }
  22904. }
  22905. catch (_a) {
  22906. // IE throws unspecified error here if TinyMCE is placed in a frame/iframe
  22907. }
  22908. // No range found then create an empty one
  22909. // This can occur when the editor is placed in a hidden container element on Gecko
  22910. if (!rng) {
  22911. rng = doc.createRange();
  22912. }
  22913. // If range is at start of document then move it to start of body
  22914. if (isDocument$1(rng.startContainer) && rng.collapsed) {
  22915. const elm = dom.getRoot();
  22916. rng.setStart(elm, 0);
  22917. rng.setEnd(elm, 0);
  22918. }
  22919. if (selectedRange && explicitRange) {
  22920. if (tryCompareBoundaryPoints(rng.START_TO_START, rng, selectedRange) === 0 &&
  22921. tryCompareBoundaryPoints(rng.END_TO_END, rng, selectedRange) === 0) {
  22922. // Safari, Opera and Chrome only ever select text which causes the range to change.
  22923. // This lets us use the originally set range if the selection hasn't been changed by the user.
  22924. rng = explicitRange;
  22925. }
  22926. else {
  22927. selectedRange = null;
  22928. explicitRange = null;
  22929. }
  22930. }
  22931. return rng;
  22932. };
  22933. /**
  22934. * Changes the selection to the specified DOM range.
  22935. *
  22936. * @method setRng
  22937. * @param {Range} rng Range to select.
  22938. * @param {Boolean} forward Optional boolean if the selection is forwards or backwards.
  22939. */
  22940. const setRng = (rng, forward) => {
  22941. if (!isValidRange(rng)) {
  22942. return;
  22943. }
  22944. const sel = getSel();
  22945. const evt = editor.dispatch('SetSelectionRange', { range: rng, forward });
  22946. rng = evt.range;
  22947. if (sel) {
  22948. explicitRange = rng;
  22949. try {
  22950. sel.removeAllRanges();
  22951. sel.addRange(rng);
  22952. }
  22953. catch (_a) {
  22954. // IE might throw errors here if the editor is within a hidden container and selection is changed
  22955. }
  22956. // Forward is set to false and we have an extend function
  22957. if (forward === false && sel.extend) {
  22958. sel.collapse(rng.endContainer, rng.endOffset);
  22959. sel.extend(rng.startContainer, rng.startOffset);
  22960. }
  22961. // adding range isn't always successful so we need to check range count otherwise an exception can occur
  22962. selectedRange = sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
  22963. }
  22964. // WebKit edge case selecting images works better using setBaseAndExtent when the image is floated
  22965. if (!rng.collapsed && rng.startContainer === rng.endContainer && (sel === null || sel === void 0 ? void 0 : sel.setBaseAndExtent)) {
  22966. if (rng.endOffset - rng.startOffset < 2) {
  22967. if (rng.startContainer.hasChildNodes()) {
  22968. const node = rng.startContainer.childNodes[rng.startOffset];
  22969. if (node && node.nodeName === 'IMG') {
  22970. sel.setBaseAndExtent(rng.startContainer, rng.startOffset, rng.endContainer, rng.endOffset);
  22971. // Since the setBaseAndExtent is fixed in more recent Blink versions we
  22972. // need to detect if it's doing the wrong thing and falling back to the
  22973. // crazy incorrect behavior api call since that seems to be the only way
  22974. // to get it to work on Safari WebKit as of 2017-02-23
  22975. if (sel.anchorNode !== rng.startContainer || sel.focusNode !== rng.endContainer) {
  22976. sel.setBaseAndExtent(node, 0, node, 1);
  22977. }
  22978. }
  22979. }
  22980. }
  22981. }
  22982. editor.dispatch('AfterSetSelectionRange', { range: rng, forward });
  22983. };
  22984. /**
  22985. * Sets the current selection to the specified DOM element.
  22986. *
  22987. * @method setNode
  22988. * @param {Element} elm Element to set as the contents of the selection.
  22989. * @return {Element} Returns the element that got passed in.
  22990. * @example
  22991. * // Inserts a DOM node at current selection/caret location
  22992. * tinymce.activeEditor.selection.setNode(tinymce.activeEditor.dom.create('img', { src: 'some.gif', title: 'some title' }));
  22993. */
  22994. const setNode = (elm) => {
  22995. setContentInternal$1(editor, dom.getOuterHTML(elm));
  22996. return elm;
  22997. };
  22998. /**
  22999. * Returns the currently selected element or the common ancestor element for both start and end of the selection.
  23000. *
  23001. * @method getNode
  23002. * @return {Element} Currently selected element or common ancestor element.
  23003. * @example
  23004. * // Alerts the currently selected elements node name
  23005. * alert(tinymce.activeEditor.selection.getNode().nodeName);
  23006. */
  23007. const getNode$1 = () => getNode(editor.getBody(), getRng$1());
  23008. const getSelectedBlocks$1 = (startElm, endElm) => getSelectedBlocks(dom, getRng$1(), startElm, endElm);
  23009. const isForward = () => {
  23010. const sel = getSel();
  23011. const anchorNode = sel === null || sel === void 0 ? void 0 : sel.anchorNode;
  23012. const focusNode = sel === null || sel === void 0 ? void 0 : sel.focusNode;
  23013. // No support for selection direction then always return true
  23014. if (!sel || !anchorNode || !focusNode || isRestrictedNode(anchorNode) || isRestrictedNode(focusNode)) {
  23015. return true;
  23016. }
  23017. const anchorRange = dom.createRng();
  23018. const focusRange = dom.createRng();
  23019. try {
  23020. anchorRange.setStart(anchorNode, sel.anchorOffset);
  23021. anchorRange.collapse(true);
  23022. focusRange.setStart(focusNode, sel.focusOffset);
  23023. focusRange.collapse(true);
  23024. }
  23025. catch (_a) {
  23026. // Safari can generate an invalid selection and error. Silently handle it and default to forward.
  23027. // See https://bugs.webkit.org/show_bug.cgi?id=230594.
  23028. return true;
  23029. }
  23030. return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0;
  23031. };
  23032. const normalize = () => {
  23033. const rng = getRng$1();
  23034. const sel = getSel();
  23035. if (!hasMultipleRanges(sel) && hasAnyRanges(editor)) {
  23036. const normRng = normalize$2(dom, rng);
  23037. normRng.each((normRng) => {
  23038. setRng(normRng, isForward());
  23039. });
  23040. return normRng.getOr(rng);
  23041. }
  23042. return rng;
  23043. };
  23044. /**
  23045. * Executes callback when the current selection starts/stops matching the specified selector. The current
  23046. * state will be passed to the callback as it's first argument.
  23047. *
  23048. * @method selectorChanged
  23049. * @param {String} selector CSS selector to check for.
  23050. * @param {Function} callback Callback with state and args when the selector is matches or not.
  23051. */
  23052. // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types
  23053. const selectorChanged = (selector, callback) => {
  23054. selectorChangedWithUnbind(selector, callback);
  23055. return exports;
  23056. };
  23057. const getScrollContainer = () => {
  23058. let scrollContainer;
  23059. let node = dom.getRoot();
  23060. while (node && node.nodeName !== 'BODY') {
  23061. if (node.scrollHeight > node.clientHeight) {
  23062. scrollContainer = node;
  23063. break;
  23064. }
  23065. node = node.parentNode;
  23066. }
  23067. return scrollContainer;
  23068. };
  23069. const scrollIntoView = (elm, alignToTop) => {
  23070. if (isNonNullable(elm)) {
  23071. scrollElementIntoView(editor, elm, alignToTop);
  23072. }
  23073. else {
  23074. scrollRangeIntoView(editor, getRng$1(), alignToTop);
  23075. }
  23076. };
  23077. const placeCaretAt = (clientX, clientY) => setRng(fromPoint(clientX, clientY, editor.getDoc()));
  23078. const getBoundingClientRect = () => {
  23079. const rng = getRng$1();
  23080. return rng.collapsed ? CaretPosition.fromRangeStart(rng).getClientRects()[0] : rng.getBoundingClientRect();
  23081. };
  23082. const destroy = () => {
  23083. win = selectedRange = explicitRange = null;
  23084. controlSelection.destroy();
  23085. };
  23086. /**
  23087. * Expands the selection range to contain the entire word when the selection is collapsed within the word.
  23088. *
  23089. * @method expand
  23090. * @param {Object} options Optional options provided to the expansion. Defaults to { type: 'word' }
  23091. */
  23092. const expand = (options = { type: 'word' }) => setRng(RangeUtils(dom).expand(getRng$1(), options));
  23093. const exports = {
  23094. dom,
  23095. win,
  23096. serializer,
  23097. editor,
  23098. expand,
  23099. collapse,
  23100. setCursorLocation,
  23101. getContent,
  23102. setContent,
  23103. getBookmark,
  23104. moveToBookmark,
  23105. select: select$1,
  23106. isCollapsed,
  23107. isEditable,
  23108. isForward,
  23109. setNode,
  23110. getNode: getNode$1,
  23111. getSel,
  23112. setRng,
  23113. getRng: getRng$1,
  23114. getStart: getStart$1,
  23115. getEnd: getEnd$1,
  23116. getSelectedBlocks: getSelectedBlocks$1,
  23117. normalize,
  23118. selectorChanged,
  23119. selectorChangedWithUnbind,
  23120. getScrollContainer,
  23121. scrollIntoView,
  23122. placeCaretAt,
  23123. getBoundingClientRect,
  23124. destroy
  23125. };
  23126. const bookmarkManager = BookmarkManager(exports);
  23127. const controlSelection = ControlSelection(exports, editor);
  23128. exports.bookmarkManager = bookmarkManager;
  23129. exports.controlSelection = controlSelection;
  23130. return exports;
  23131. };
  23132. const addNodeFilter = (settings, htmlParser, schema) => {
  23133. htmlParser.addNodeFilter('br', (nodes, _, args) => {
  23134. const blockElements = Tools.extend({}, schema.getBlockElements());
  23135. const nonEmptyElements = schema.getNonEmptyElements();
  23136. const whitespaceElements = schema.getWhitespaceElements();
  23137. // Remove brs from body element as well
  23138. blockElements.body = 1;
  23139. const isBlock = (node) => node.name in blockElements || isTransparentAstBlock(schema, node);
  23140. // Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p>
  23141. for (let i = 0, l = nodes.length; i < l; i++) {
  23142. let node = nodes[i];
  23143. let parent = node.parent;
  23144. if (parent && isBlock(parent) && node === parent.lastChild) {
  23145. // Loop all nodes to the left of the current node and check for other BR elements
  23146. // excluding bookmarks since they are invisible
  23147. let prev = node.prev;
  23148. while (prev) {
  23149. const prevName = prev.name;
  23150. // Ignore bookmarks
  23151. if (prevName !== 'span' || prev.attr('data-mce-type') !== 'bookmark') {
  23152. // Found another br it's a <br><br> structure then don't remove anything
  23153. if (prevName === 'br') {
  23154. node = null;
  23155. }
  23156. break;
  23157. }
  23158. prev = prev.prev;
  23159. }
  23160. if (node) {
  23161. node.remove();
  23162. // Is the parent to be considered empty after we removed the BR
  23163. if (isEmpty$2(schema, nonEmptyElements, whitespaceElements, parent)) {
  23164. const elementRule = schema.getElementRule(parent.name);
  23165. // Remove or padd the element depending on schema rule
  23166. if (elementRule) {
  23167. if (elementRule.removeEmpty) {
  23168. parent.remove();
  23169. }
  23170. else if (elementRule.paddEmpty) {
  23171. paddEmptyNode(settings, args, isBlock, parent);
  23172. }
  23173. }
  23174. }
  23175. }
  23176. }
  23177. else {
  23178. // Replaces BR elements inside inline elements like <p><b><i><br></i></b></p>
  23179. // so they become <p><b><i>&nbsp;</i></b></p>
  23180. let lastParent = node;
  23181. while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) {
  23182. lastParent = parent;
  23183. if (blockElements[parent.name]) {
  23184. break;
  23185. }
  23186. parent = parent.parent;
  23187. }
  23188. if (lastParent === parent) {
  23189. const textNode = new AstNode('#text', 3);
  23190. textNode.value = nbsp;
  23191. node.replace(textNode);
  23192. }
  23193. }
  23194. }
  23195. });
  23196. };
  23197. const register$3 = (htmlParser, settings, dom) => {
  23198. // Convert tabindex back to elements when serializing contents
  23199. htmlParser.addAttributeFilter('data-mce-tabindex', (nodes, name) => {
  23200. let i = nodes.length;
  23201. while (i--) {
  23202. const node = nodes[i];
  23203. node.attr('tabindex', node.attr('data-mce-tabindex'));
  23204. node.attr(name, null);
  23205. }
  23206. });
  23207. // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed
  23208. htmlParser.addAttributeFilter('src,href,style', (nodes, name) => {
  23209. const internalName = 'data-mce-' + name;
  23210. const urlConverter = settings.url_converter;
  23211. const urlConverterScope = settings.url_converter_scope;
  23212. let i = nodes.length;
  23213. while (i--) {
  23214. const node = nodes[i];
  23215. let value = node.attr(internalName);
  23216. if (value !== undefined) {
  23217. // Set external name to internal value and remove internal
  23218. node.attr(name, value.length > 0 ? value : null);
  23219. node.attr(internalName, null);
  23220. }
  23221. else {
  23222. // No internal attribute found then convert the value we have in the DOM
  23223. value = node.attr(name);
  23224. if (name === 'style') {
  23225. value = dom.serializeStyle(dom.parseStyle(value), node.name);
  23226. }
  23227. else if (urlConverter) {
  23228. value = urlConverter.call(urlConverterScope, value, name, node.name);
  23229. }
  23230. node.attr(name, value.length > 0 ? value : null);
  23231. }
  23232. }
  23233. });
  23234. // Remove internal classes mceItem<..> or mceSelected
  23235. htmlParser.addAttributeFilter('class', (nodes) => {
  23236. let i = nodes.length;
  23237. while (i--) {
  23238. const node = nodes[i];
  23239. let value = node.attr('class');
  23240. if (value) {
  23241. value = value.replace(/(?:^|\s)mce-item-\w+(?!\S)/g, '');
  23242. node.attr('class', value.length > 0 ? value : null);
  23243. }
  23244. }
  23245. });
  23246. // Remove bookmark elements
  23247. htmlParser.addAttributeFilter('data-mce-type', (nodes, name, args) => {
  23248. let i = nodes.length;
  23249. while (i--) {
  23250. const node = nodes[i];
  23251. if (node.attr('data-mce-type') === 'bookmark' && !args.cleanup) {
  23252. // We maybe dealing with a "filled" bookmark. If so just remove the node, otherwise unwrap it
  23253. const hasChildren = Optional.from(node.firstChild).exists((firstChild) => { var _a; return !isZwsp((_a = firstChild.value) !== null && _a !== void 0 ? _a : ''); });
  23254. if (hasChildren) {
  23255. node.unwrap();
  23256. }
  23257. else {
  23258. node.remove();
  23259. }
  23260. }
  23261. }
  23262. });
  23263. // Force script into CDATA sections and remove the mce- prefix also add comments around styles
  23264. htmlParser.addNodeFilter('script,style', (nodes, name) => {
  23265. var _a;
  23266. const trim = (value) => {
  23267. /* jshint maxlen:255 */
  23268. /* eslint max-len:0 */
  23269. return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n')
  23270. .replace(/^[\r\n]*|[\r\n]*$/g, '')
  23271. .replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '')
  23272. .replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, '');
  23273. };
  23274. let i = nodes.length;
  23275. while (i--) {
  23276. const node = nodes[i];
  23277. const firstChild = node.firstChild;
  23278. const value = (_a = firstChild === null || firstChild === void 0 ? void 0 : firstChild.value) !== null && _a !== void 0 ? _a : '';
  23279. if (name === 'script') {
  23280. // Remove mce- prefix from script elements and remove default type since the user specified
  23281. // a script element without type attribute
  23282. const type = node.attr('type');
  23283. if (type) {
  23284. node.attr('type', type === 'mce-no/type' ? null : type.replace(/^mce\-/, ''));
  23285. }
  23286. if (settings.element_format === 'xhtml' && firstChild && value.length > 0) {
  23287. firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>';
  23288. }
  23289. }
  23290. else {
  23291. if (settings.element_format === 'xhtml' && firstChild && value.length > 0) {
  23292. firstChild.value = '<!--\n' + trim(value) + '\n-->';
  23293. }
  23294. }
  23295. }
  23296. });
  23297. // Convert comments to cdata and handle protected comments
  23298. htmlParser.addNodeFilter('#comment', (nodes) => {
  23299. let i = nodes.length;
  23300. while (i--) {
  23301. const node = nodes[i];
  23302. const value = node.value;
  23303. if (settings.preserve_cdata && (value === null || value === void 0 ? void 0 : value.indexOf('[CDATA[')) === 0) {
  23304. node.name = '#cdata';
  23305. node.type = 4;
  23306. node.value = dom.decode(value.replace(/^\[CDATA\[|\]\]$/g, ''));
  23307. }
  23308. else if ((value === null || value === void 0 ? void 0 : value.indexOf('mce:protected ')) === 0) {
  23309. node.name = '#text';
  23310. node.type = 3;
  23311. node.raw = true;
  23312. node.value = unescape(value).substr(14);
  23313. }
  23314. }
  23315. });
  23316. htmlParser.addNodeFilter('xml:namespace,input', (nodes, name) => {
  23317. let i = nodes.length;
  23318. while (i--) {
  23319. const node = nodes[i];
  23320. if (node.type === 7) {
  23321. node.remove();
  23322. }
  23323. else if (node.type === 1) {
  23324. if (name === 'input' && !node.attr('type')) {
  23325. node.attr('type', 'text');
  23326. }
  23327. }
  23328. }
  23329. });
  23330. htmlParser.addAttributeFilter('data-mce-type', (nodes) => {
  23331. each$e(nodes, (node) => {
  23332. if (node.attr('data-mce-type') === 'format-caret') {
  23333. if (node.isEmpty(htmlParser.schema.getNonEmptyElements())) {
  23334. node.remove();
  23335. }
  23336. else {
  23337. node.unwrap();
  23338. }
  23339. }
  23340. });
  23341. });
  23342. // Remove internal data attributes
  23343. htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style,' +
  23344. 'data-mce-selected,data-mce-expando,data-mce-block,' +
  23345. 'data-mce-type,data-mce-resize,data-mce-placeholder', (nodes, name) => {
  23346. let i = nodes.length;
  23347. while (i--) {
  23348. nodes[i].attr(name, null);
  23349. }
  23350. });
  23351. // Remove <br> at end of block elements Gecko and WebKit injects BR elements to
  23352. // make it possible to place the caret inside empty blocks. This logic tries to remove
  23353. // these elements and keep br elements that where intended to be there intact
  23354. if (settings.remove_trailing_brs) {
  23355. addNodeFilter(settings, htmlParser, htmlParser.schema);
  23356. }
  23357. };
  23358. /**
  23359. * IE 11 has a fantastic bug where it will produce two trailing BR elements to iframe bodies when
  23360. * the iframe is hidden by display: none on a parent container. The DOM is actually out of sync
  23361. * with innerHTML in this case. It's like IE adds shadow DOM BR elements that appears on innerHTML
  23362. * but not as the lastChild of the body. So this fix simply removes the last two
  23363. * BR elements at the end of the document.
  23364. *
  23365. * Example of what happens: <body>text</body> becomes <body>text<br><br></body>
  23366. */
  23367. const trimTrailingBr = (rootNode) => {
  23368. const isBr = (node) => {
  23369. return (node === null || node === void 0 ? void 0 : node.name) === 'br';
  23370. };
  23371. const brNode1 = rootNode.lastChild;
  23372. if (isBr(brNode1)) {
  23373. const brNode2 = brNode1.prev;
  23374. if (isBr(brNode2)) {
  23375. brNode1.remove();
  23376. brNode2.remove();
  23377. }
  23378. }
  23379. };
  23380. const preProcess$1 = (editor, node, args) => {
  23381. let oldDoc;
  23382. const dom = editor.dom;
  23383. let clonedNode = node.cloneNode(true);
  23384. // Nodes needs to be attached to something in WebKit/Opera
  23385. // This fix will make DOM ranges and make Sizzle happy!
  23386. const impl = document.implementation;
  23387. if (impl.createHTMLDocument) {
  23388. // Create an empty HTML document
  23389. const doc = impl.createHTMLDocument('');
  23390. // Add the element or it's children if it's a body element to the new document
  23391. Tools.each(clonedNode.nodeName === 'BODY' ? clonedNode.childNodes : [clonedNode], (node) => {
  23392. doc.body.appendChild(doc.importNode(node, true));
  23393. });
  23394. // Grab first child or body element for serialization
  23395. if (clonedNode.nodeName !== 'BODY') {
  23396. // We cast to a Element here, as this will be the cloned node imported and appended above
  23397. clonedNode = doc.body.firstChild;
  23398. }
  23399. else {
  23400. clonedNode = doc.body;
  23401. }
  23402. // set the new document in DOMUtils so createElement etc works
  23403. oldDoc = dom.doc;
  23404. dom.doc = doc;
  23405. }
  23406. firePreProcess(editor, { ...args, node: clonedNode });
  23407. if (oldDoc) {
  23408. dom.doc = oldDoc;
  23409. }
  23410. return clonedNode;
  23411. };
  23412. const shouldFireEvent = (editor, args) => {
  23413. return isNonNullable(editor) && editor.hasEventListeners('PreProcess') && !args.no_events;
  23414. };
  23415. const process$1 = (editor, node, args) => {
  23416. return shouldFireEvent(editor, args) ? preProcess$1(editor, node, args) : node;
  23417. };
  23418. const addTempAttr = (htmlParser, tempAttrs, name) => {
  23419. if (Tools.inArray(tempAttrs, name) === -1) {
  23420. htmlParser.addAttributeFilter(name, (nodes, name) => {
  23421. let i = nodes.length;
  23422. while (i--) {
  23423. nodes[i].attr(name, null);
  23424. }
  23425. });
  23426. tempAttrs.push(name);
  23427. }
  23428. };
  23429. const postProcess = (editor, args, content) => {
  23430. if (!args.no_events && editor) {
  23431. const outArgs = firePostProcess(editor, { ...args, content });
  23432. return outArgs.content;
  23433. }
  23434. else {
  23435. return content;
  23436. }
  23437. };
  23438. const getHtmlFromNode = (dom, node, args) => {
  23439. // TODO: Investigate if using `innerHTML` is correct as DomSerializerPreProcess definitely returns a Node
  23440. const html = trim$2(args.getInner ? node.innerHTML : dom.getOuterHTML(node));
  23441. return args.selection || isWsPreserveElement(SugarElement.fromDom(node)) ? html : Tools.trim(html);
  23442. };
  23443. const parseHtml = (htmlParser, html, args) => {
  23444. const parserArgs = args.selection ? { forced_root_block: false, ...args } : args;
  23445. const rootNode = htmlParser.parse(html, parserArgs);
  23446. trimTrailingBr(rootNode);
  23447. return rootNode;
  23448. };
  23449. const serializeNode = (settings, schema, node) => {
  23450. const htmlSerializer = HtmlSerializer(settings, schema);
  23451. return htmlSerializer.serialize(node);
  23452. };
  23453. const toHtml = (editor, settings, schema, rootNode, args) => {
  23454. const content = serializeNode(settings, schema, rootNode);
  23455. return postProcess(editor, args, content);
  23456. };
  23457. const DomSerializerImpl = (settings, editor) => {
  23458. const tempAttrs = ['data-mce-selected'];
  23459. const defaultedSettings = {
  23460. entity_encoding: 'named',
  23461. remove_trailing_brs: true,
  23462. pad_empty_with_br: false,
  23463. ...settings
  23464. };
  23465. const dom = editor && editor.dom ? editor.dom : DOMUtils.DOM;
  23466. const schema = editor && editor.schema ? editor.schema : Schema(defaultedSettings);
  23467. const htmlParser = DomParser(defaultedSettings, schema);
  23468. register$3(htmlParser, defaultedSettings, dom);
  23469. const serialize = (node, parserArgs = {}) => {
  23470. const args = { format: 'html', ...parserArgs };
  23471. const targetNode = process$1(editor, node, args);
  23472. const html = getHtmlFromNode(dom, targetNode, args);
  23473. const rootNode = parseHtml(htmlParser, html, args);
  23474. return args.format === 'tree' ? rootNode : toHtml(editor, defaultedSettings, schema, rootNode, args);
  23475. };
  23476. return {
  23477. schema,
  23478. addNodeFilter: htmlParser.addNodeFilter,
  23479. addAttributeFilter: htmlParser.addAttributeFilter,
  23480. serialize: serialize,
  23481. addRules: schema.addValidElements,
  23482. setRules: schema.setValidElements,
  23483. addTempAttr: curry(addTempAttr, htmlParser, tempAttrs),
  23484. getTempAttrs: constant(tempAttrs),
  23485. getNodeFilters: htmlParser.getNodeFilters,
  23486. getAttributeFilters: htmlParser.getAttributeFilters,
  23487. removeNodeFilter: htmlParser.removeNodeFilter,
  23488. removeAttributeFilter: htmlParser.removeAttributeFilter
  23489. };
  23490. };
  23491. /**
  23492. * This class is used to serialize DOM trees into a string. Consult the TinyMCE API Documentation for
  23493. * more details and examples on how to use this class.
  23494. *
  23495. * @class tinymce.dom.Serializer
  23496. */
  23497. const DomSerializer = (settings, editor) => {
  23498. const domSerializer = DomSerializerImpl(settings, editor);
  23499. // Return public methods
  23500. return {
  23501. /**
  23502. * Schema instance that was used to when the Serializer was constructed.
  23503. *
  23504. * @field {tinymce.html.Schema} schema
  23505. */
  23506. schema: domSerializer.schema,
  23507. /**
  23508. * Adds a node filter function to the parser used by the serializer, the parser will collect the specified nodes by name
  23509. * and then execute the callback once it has finished parsing the document.
  23510. *
  23511. * @method addNodeFilter
  23512. * @param {String} name Comma separated list of nodes to collect.
  23513. * @param {Function} callback Callback function to execute once it has collected nodes.
  23514. * @example
  23515. * serializer.addNodeFilter('p,h1', (nodes, name) => {
  23516. * for (let i = 0; i < nodes.length; i++) {
  23517. * console.log(nodes[i].name);
  23518. * }
  23519. * });
  23520. */
  23521. addNodeFilter: domSerializer.addNodeFilter,
  23522. /**
  23523. * Adds an attribute filter function to the parser used by the serializer, the parser will
  23524. * collect nodes that has the specified attributes
  23525. * and then execute the callback once it has finished parsing the document.
  23526. *
  23527. * @method addAttributeFilter
  23528. * @param {String} name Comma separated list of attributes to collect.
  23529. * @param {Function} callback Callback function to execute once it has collected nodes.
  23530. * @example
  23531. * serializer.addAttributeFilter('src,href', (nodes, name) => {
  23532. * for (let i = 0; i < nodes.length; i++) {
  23533. * console.log(nodes[i].name);
  23534. * }
  23535. * });
  23536. */
  23537. addAttributeFilter: domSerializer.addAttributeFilter,
  23538. /**
  23539. * Serializes the specified browser DOM node into a HTML string.
  23540. *
  23541. * @method serialize
  23542. * @param {DOMNode} node DOM node to serialize.
  23543. * @param {Object} args Arguments option that gets passed to event handlers.
  23544. */
  23545. serialize: domSerializer.serialize,
  23546. /**
  23547. * Adds valid elements rules to the serializers schema instance this enables you to specify things
  23548. * like what elements should be outputted and what attributes specific elements might have.
  23549. * Consult the TinyMCE Documentation for more details on this format.
  23550. *
  23551. * @method addRules
  23552. * @param {String} rules Valid elements rules string to add to schema.
  23553. */
  23554. addRules: domSerializer.addRules,
  23555. /**
  23556. * Sets the valid elements rules to the serializers schema instance this enables you to specify things
  23557. * like what elements should be outputted and what attributes specific elements might have.
  23558. * Consult the TinyMCE Documentation for more details on this format.
  23559. *
  23560. * @method setRules
  23561. * @param {String} rules Valid elements rules string.
  23562. */
  23563. setRules: domSerializer.setRules,
  23564. /**
  23565. * Adds a temporary internal attribute these attributes will get removed on undo and
  23566. * when getting contents out of the editor.
  23567. *
  23568. * @method addTempAttr
  23569. * @param {String} name string
  23570. */
  23571. addTempAttr: domSerializer.addTempAttr,
  23572. /**
  23573. * Returns an array of all added temp attrs names.
  23574. *
  23575. * @method getTempAttrs
  23576. * @return {String[]} Array with attribute names.
  23577. */
  23578. getTempAttrs: domSerializer.getTempAttrs,
  23579. getNodeFilters: domSerializer.getNodeFilters,
  23580. getAttributeFilters: domSerializer.getAttributeFilters,
  23581. /**
  23582. * Removes a node filter function or removes all filter functions from the parser used by the serializer for the node names provided.
  23583. *
  23584. * @method removeNodeFilter
  23585. * @param {String} name Comma separated list of node names to remove filters for.
  23586. * @param {Function} callback Optional callback function to only remove a specific callback.
  23587. * @example
  23588. * // Remove a single filter
  23589. * serializer.removeNodeFilter('p,h1', someCallback);
  23590. *
  23591. * // Remove all filters
  23592. * serializer.removeNodeFilter('p,h1');
  23593. */
  23594. removeNodeFilter: domSerializer.removeNodeFilter,
  23595. /**
  23596. * Removes an attribute filter function or removes all filter functions from the parser used by the serializer for the attribute names provided.
  23597. *
  23598. * @method removeAttributeFilter
  23599. * @param {String} name Comma separated list of attribute names to remove filters for.
  23600. * @param {Function} callback Optional callback function to only remove a specific callback.
  23601. * @example
  23602. * // Remove a single filter
  23603. * serializer.removeAttributeFilter('src,href', someCallback);
  23604. *
  23605. * // Remove all filters
  23606. * serializer.removeAttributeFilter('src,href');
  23607. */
  23608. removeAttributeFilter: domSerializer.removeAttributeFilter
  23609. };
  23610. };
  23611. const defaultFormat$1 = 'html';
  23612. const setupArgs$1 = (args, format) => ({
  23613. ...args,
  23614. format,
  23615. get: true,
  23616. getInner: true
  23617. });
  23618. const getContent = (editor, args = {}) => {
  23619. const format = args.format ? args.format : defaultFormat$1;
  23620. const defaultedArgs = setupArgs$1(args, format);
  23621. return preProcessGetContent(editor, defaultedArgs).fold(identity, (updatedArgs) => {
  23622. const content = getContent$2(editor, updatedArgs);
  23623. return postProcessGetContent(editor, content, updatedArgs);
  23624. });
  23625. };
  23626. const defaultFormat = 'html';
  23627. const setupArgs = (args, content) => ({
  23628. format: defaultFormat,
  23629. ...args,
  23630. set: true,
  23631. content
  23632. });
  23633. const setContent = (editor, content, args = {}) => {
  23634. const defaultedArgs = setupArgs(args, content);
  23635. preProcessSetContent(editor, defaultedArgs).each((updatedArgs) => {
  23636. const result = setContent$1(editor, updatedArgs.content, updatedArgs);
  23637. postProcessSetContent(editor, result.html, updatedArgs);
  23638. });
  23639. };
  23640. const DOM$b = DOMUtils.DOM;
  23641. const restoreOriginalStyles = (editor) => {
  23642. DOM$b.setStyle(editor.id, 'display', editor.orgDisplay);
  23643. };
  23644. const safeDestroy = (x) => Optional.from(x).each((x) => x.destroy());
  23645. const clearDomReferences = (editor) => {
  23646. const ed = editor;
  23647. ed.contentAreaContainer = ed.formElement = ed.container = ed.editorContainer = null;
  23648. ed.bodyElement = ed.contentDocument = ed.contentWindow = null;
  23649. ed.iframeElement = ed.targetElm = null;
  23650. const selection = editor.selection;
  23651. if (selection) {
  23652. const dom = selection.dom;
  23653. ed.selection = selection.win = selection.dom = dom.doc = null;
  23654. }
  23655. };
  23656. const restoreForm = (editor) => {
  23657. const form = editor.formElement;
  23658. if (form) {
  23659. if (form._mceOldSubmit) {
  23660. form.submit = form._mceOldSubmit;
  23661. delete form._mceOldSubmit;
  23662. }
  23663. DOM$b.unbind(form, 'submit reset', editor.formEventDelegate);
  23664. }
  23665. };
  23666. const remove$1 = (editor) => {
  23667. if (!editor.removed) {
  23668. const { _selectionOverrides, editorUpload } = editor;
  23669. const body = editor.getBody();
  23670. const element = editor.getElement();
  23671. if (body) {
  23672. editor.save({ is_removing: true });
  23673. }
  23674. editor.removed = true;
  23675. editor.unbindAllNativeEvents();
  23676. // Remove any hidden input
  23677. if (editor.hasHiddenInput && isNonNullable(element === null || element === void 0 ? void 0 : element.nextSibling)) {
  23678. DOM$b.remove(element.nextSibling);
  23679. }
  23680. fireRemove(editor);
  23681. editor.editorManager.remove(editor);
  23682. if (!editor.inline && body) {
  23683. restoreOriginalStyles(editor);
  23684. }
  23685. fireDetach(editor);
  23686. DOM$b.remove(editor.getContainer());
  23687. safeDestroy(_selectionOverrides);
  23688. safeDestroy(editorUpload);
  23689. editor.destroy();
  23690. }
  23691. };
  23692. const destroy = (editor, automatic) => {
  23693. const { selection, dom } = editor;
  23694. if (editor.destroyed) {
  23695. return;
  23696. }
  23697. // If user manually calls destroy and not remove
  23698. // Users seems to have logic that calls destroy instead of remove
  23699. if (!automatic && !editor.removed) {
  23700. editor.remove();
  23701. return;
  23702. }
  23703. if (!automatic) {
  23704. editor.editorManager.off('beforeunload', editor._beforeUnload);
  23705. // Manual destroy
  23706. if (editor.theme && editor.theme.destroy) {
  23707. editor.theme.destroy();
  23708. }
  23709. safeDestroy(selection);
  23710. safeDestroy(dom);
  23711. }
  23712. restoreForm(editor);
  23713. clearDomReferences(editor);
  23714. editor.destroyed = true;
  23715. };
  23716. const CreateIconManager = () => {
  23717. const lookup = {};
  23718. const add = (id, iconPack) => {
  23719. lookup[id] = iconPack;
  23720. };
  23721. const get = (id) => {
  23722. if (lookup[id]) {
  23723. return lookup[id];
  23724. }
  23725. else {
  23726. return { icons: {} };
  23727. }
  23728. };
  23729. const has = (id) => has$2(lookup, id);
  23730. return {
  23731. add,
  23732. get,
  23733. has
  23734. };
  23735. };
  23736. const IconManager = CreateIconManager();
  23737. const ModelManager = AddOnManager.ModelManager;
  23738. const getProp = (propName, elm) => {
  23739. const rawElm = elm.dom;
  23740. return rawElm[propName];
  23741. };
  23742. const getComputedSizeProp = (propName, elm) => parseInt(get$7(elm, propName), 10);
  23743. const getClientWidth = curry(getProp, 'clientWidth');
  23744. const getClientHeight = curry(getProp, 'clientHeight');
  23745. const getMarginTop = curry(getComputedSizeProp, 'margin-top');
  23746. const getMarginLeft = curry(getComputedSizeProp, 'margin-left');
  23747. const getBoundingClientRect = (elm) => elm.dom.getBoundingClientRect();
  23748. const isInsideElementContentArea = (bodyElm, clientX, clientY) => {
  23749. const clientWidth = getClientWidth(bodyElm);
  23750. const clientHeight = getClientHeight(bodyElm);
  23751. return clientX >= 0 && clientY >= 0 && clientX <= clientWidth && clientY <= clientHeight;
  23752. };
  23753. const transpose = (inline, elm, clientX, clientY) => {
  23754. const clientRect = getBoundingClientRect(elm);
  23755. const deltaX = inline ? clientRect.left + elm.dom.clientLeft + getMarginLeft(elm) : 0;
  23756. const deltaY = inline ? clientRect.top + elm.dom.clientTop + getMarginTop(elm) : 0;
  23757. const x = clientX - deltaX;
  23758. const y = clientY - deltaY;
  23759. return { x, y };
  23760. };
  23761. // Checks if the specified coordinate is within the visual content area excluding the scrollbars
  23762. const isXYInContentArea = (editor, clientX, clientY) => {
  23763. const bodyElm = SugarElement.fromDom(editor.getBody());
  23764. const targetElm = editor.inline ? bodyElm : documentElement(bodyElm);
  23765. const transposedPoint = transpose(editor.inline, targetElm, clientX, clientY);
  23766. return isInsideElementContentArea(targetElm, transposedPoint.x, transposedPoint.y);
  23767. };
  23768. const fromDomSafe = (node) => Optional.from(node).map(SugarElement.fromDom);
  23769. const isEditorAttachedToDom = (editor) => {
  23770. const rawContainer = editor.inline ? editor.getBody() : editor.getContentAreaContainer();
  23771. return fromDomSafe(rawContainer).map(inBody).getOr(false);
  23772. };
  23773. var NotificationManagerImpl = () => {
  23774. const unimplemented = () => {
  23775. throw new Error('Theme did not provide a NotificationManager implementation.');
  23776. };
  23777. return {
  23778. open: unimplemented,
  23779. close: unimplemented,
  23780. getArgs: unimplemented
  23781. };
  23782. };
  23783. /**
  23784. * This class handles the creation of TinyMCE's notifications.
  23785. *
  23786. * @class tinymce.NotificationManager
  23787. * @example
  23788. * // Opens a new notification of type "error" with text "An error occurred."
  23789. * tinymce.activeEditor.notificationManager.open({
  23790. * text: 'An error occurred.',
  23791. * type: 'error'
  23792. * });
  23793. */
  23794. const NotificationManager = (editor) => {
  23795. const notifications = [];
  23796. const getImplementation = () => {
  23797. const theme = editor.theme;
  23798. return theme && theme.getNotificationManagerImpl ? theme.getNotificationManagerImpl() : NotificationManagerImpl();
  23799. };
  23800. const getTopNotification = () => {
  23801. return Optional.from(notifications[0]);
  23802. };
  23803. const isEqual = (a, b) => {
  23804. return a.type === b.type && a.text === b.text && !a.progressBar && !a.timeout && !b.progressBar && !b.timeout;
  23805. };
  23806. const reposition = () => {
  23807. getTopNotification().each((notification) => {
  23808. notification.reposition();
  23809. });
  23810. };
  23811. const addNotification = (notification) => {
  23812. notifications.push(notification);
  23813. };
  23814. const closeNotification = (notification) => {
  23815. findIndex$2(notifications, (otherNotification) => {
  23816. return otherNotification === notification;
  23817. }).each((index) => {
  23818. // Mutate here since third party might have stored away the window array
  23819. // TODO: Consider breaking this api
  23820. notifications.splice(index, 1);
  23821. });
  23822. };
  23823. const open = (spec, fireEvent = true) => {
  23824. // Never open notification if editor has been removed.
  23825. if (editor.removed || !isEditorAttachedToDom(editor)) {
  23826. return {};
  23827. }
  23828. // fire event to allow notification spec to be mutated before display
  23829. if (fireEvent) {
  23830. editor.dispatch('BeforeOpenNotification', { notification: spec });
  23831. }
  23832. return find$2(notifications, (notification) => {
  23833. return isEqual(getImplementation().getArgs(notification), spec);
  23834. }).getOrThunk(() => {
  23835. editor.editorManager.setActive(editor);
  23836. const notification = getImplementation().open(spec, () => {
  23837. closeNotification(notification);
  23838. }, () => hasEditorOrUiFocus(editor));
  23839. addNotification(notification);
  23840. reposition();
  23841. // Ensure notification is not passed by reference to prevent mutation
  23842. editor.dispatch('OpenNotification', { notification: { ...notification } });
  23843. return notification;
  23844. });
  23845. };
  23846. const close = () => {
  23847. getTopNotification().each((notification) => {
  23848. getImplementation().close(notification);
  23849. closeNotification(notification);
  23850. reposition();
  23851. });
  23852. };
  23853. const getNotifications = constant(notifications);
  23854. const registerEvents = (editor) => {
  23855. editor.on('SkinLoaded', () => {
  23856. const serviceMessage = getServiceMessage(editor);
  23857. if (serviceMessage) {
  23858. // Ensure we pass false for fireEvent so that service message cannot be altered.
  23859. open({
  23860. text: serviceMessage,
  23861. type: 'warning',
  23862. timeout: 0
  23863. }, false);
  23864. }
  23865. // Ensure the notifications are repositioned once the skin has loaded, as otherwise
  23866. // any notifications rendered before then may have wrapped and been in the wrong place
  23867. reposition();
  23868. });
  23869. // NodeChange is needed for inline mode and autoresize as the positioning is done
  23870. // from the bottom up, which changes when the content in the editor changes.
  23871. editor.on('show ResizeEditor ResizeWindow NodeChange ToggleView FullscreenStateChanged', () => {
  23872. requestAnimationFrame(reposition);
  23873. });
  23874. editor.on('remove', () => {
  23875. each$e(notifications.slice(), (notification) => {
  23876. getImplementation().close(notification);
  23877. });
  23878. });
  23879. editor.on('keydown', (e) => {
  23880. var _a;
  23881. // TODO: TINY-11429 Remove this once we remove the use of keycodes
  23882. const isF12 = ((_a = e.key) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'f12' || e.keyCode === 123;
  23883. if (e.altKey && isF12) {
  23884. e.preventDefault();
  23885. getTopNotification()
  23886. .map((notificationApi) => SugarElement.fromDom(notificationApi.getEl()))
  23887. .each((elm) => focus$1(elm));
  23888. }
  23889. });
  23890. };
  23891. registerEvents(editor);
  23892. return {
  23893. /**
  23894. * Opens a new notification.
  23895. *
  23896. * @method open
  23897. * @param {Object} args A <code>name: value</code> collection containing settings such as: <code>timeout</code>, <code>type</code>, and message (<code>text</code>).
  23898. * <br /><br />
  23899. * For information on the available settings, see: <a href="https://www.tiny.cloud/docs/tinymce/8/creating-custom-notifications/">Create custom notifications</a>.
  23900. */
  23901. open,
  23902. /**
  23903. * Closes the top most notification.
  23904. *
  23905. * @method close
  23906. */
  23907. close,
  23908. /**
  23909. * Returns the currently opened notification objects.
  23910. *
  23911. * @method getNotifications
  23912. * @return {Array} Array of the currently opened notifications.
  23913. */
  23914. getNotifications
  23915. };
  23916. };
  23917. const PluginManager = AddOnManager.PluginManager;
  23918. const ThemeManager = AddOnManager.ThemeManager;
  23919. var WindowManagerImpl = () => {
  23920. const unimplemented = () => {
  23921. throw new Error('Theme did not provide a WindowManager implementation.');
  23922. };
  23923. return {
  23924. open: unimplemented,
  23925. openUrl: unimplemented,
  23926. alert: unimplemented,
  23927. confirm: unimplemented,
  23928. close: unimplemented
  23929. };
  23930. };
  23931. const WindowManager = (editor) => {
  23932. let dialogs = [];
  23933. const getImplementation = () => {
  23934. const theme = editor.theme;
  23935. return theme && theme.getWindowManagerImpl ? theme.getWindowManagerImpl() : WindowManagerImpl();
  23936. };
  23937. const funcBind = (scope, f) => {
  23938. return (...args) => {
  23939. return f ? f.apply(scope, args) : undefined;
  23940. };
  23941. };
  23942. const fireOpenEvent = (dialog) => {
  23943. editor.dispatch('OpenWindow', {
  23944. dialog
  23945. });
  23946. };
  23947. const fireCloseEvent = (dialog) => {
  23948. editor.dispatch('CloseWindow', {
  23949. dialog
  23950. });
  23951. };
  23952. const addDialog = (dialog, triggerElement) => {
  23953. dialogs.push({ instanceApi: dialog, triggerElement });
  23954. fireOpenEvent(dialog);
  23955. };
  23956. const closeDialog = (dialog) => {
  23957. fireCloseEvent(dialog);
  23958. const dialogTriggerElement = findMap(dialogs, ({ instanceApi, triggerElement }) => instanceApi === dialog ? triggerElement : Optional.none());
  23959. dialogs = filter$5(dialogs, ({ instanceApi }) => instanceApi !== dialog);
  23960. // Move focus back to editor when the last window is closed
  23961. if (dialogs.length === 0) {
  23962. editor.focus();
  23963. }
  23964. else {
  23965. // Move focus to the element that was active before the dialog was opened
  23966. dialogTriggerElement.filter(inBody).each(focus$1);
  23967. }
  23968. };
  23969. const getTopDialog = () => {
  23970. return Optional.from(dialogs[dialogs.length - 1]);
  23971. };
  23972. const storeSelectionAndOpenDialog = (openDialog) => {
  23973. editor.editorManager.setActive(editor);
  23974. store(editor);
  23975. const activeEl = active();
  23976. editor.ui.show();
  23977. const dialog = openDialog();
  23978. addDialog(dialog, activeEl);
  23979. return dialog;
  23980. };
  23981. const open = (args, params) => {
  23982. return storeSelectionAndOpenDialog(() => getImplementation().open(args, params, closeDialog));
  23983. };
  23984. const openUrl = (args) => {
  23985. return storeSelectionAndOpenDialog(() => getImplementation().openUrl(args, closeDialog));
  23986. };
  23987. const restoreFocus = (activeEl) => {
  23988. if (dialogs.length !== 0) {
  23989. // If there are some dialogs, the confirm/alert was probably triggered from the dialog
  23990. // Move focus to the element that was active before the confirm/alert was opened
  23991. activeEl.each((el) => focus$1(el));
  23992. }
  23993. };
  23994. const alert = (message, callback, scope) => {
  23995. const activeEl = active();
  23996. const windowManagerImpl = getImplementation();
  23997. windowManagerImpl.alert(message, funcBind(scope ? scope : windowManagerImpl, () => {
  23998. restoreFocus(activeEl);
  23999. callback === null || callback === void 0 ? void 0 : callback();
  24000. }));
  24001. };
  24002. const confirm = (message, callback, scope) => {
  24003. const activeEl = active();
  24004. const windowManagerImpl = getImplementation();
  24005. windowManagerImpl.confirm(message, funcBind(scope ? scope : windowManagerImpl, (state) => {
  24006. restoreFocus(activeEl);
  24007. callback === null || callback === void 0 ? void 0 : callback(state);
  24008. }));
  24009. };
  24010. const close = () => {
  24011. getTopDialog().each(({ instanceApi: dialog }) => {
  24012. getImplementation().close(dialog);
  24013. closeDialog(dialog);
  24014. });
  24015. };
  24016. editor.on('remove', () => {
  24017. each$e(dialogs, ({ instanceApi: dialog }) => {
  24018. getImplementation().close(dialog);
  24019. });
  24020. });
  24021. return {
  24022. /**
  24023. * Opens a new window.
  24024. *
  24025. * @method open
  24026. * @param {Object} config For information on the available options, see: <a href="https://www.tiny.cloud/docs/tinymce/8/dialog-configuration/#options">Dialog - Configuration options</a>.
  24027. * @param {Object} params (Optional) For information on the available options, see: <a href="https://www.tiny.cloud/docs/tinymce/8/dialog-configuration/#configuration-parameters">Dialog - Configuration parameters</a>.
  24028. * @returns {WindowManager.DialogInstanceApi} A new dialog instance.
  24029. */
  24030. open,
  24031. /**
  24032. * Opens a new window for the specified url.
  24033. *
  24034. * @method openUrl
  24035. * @param {Object} config For information on the available options, see: <a href="https://www.tiny.cloud/docs/tinymce/8/urldialog/#configuration">URL dialog - Configuration</a>.
  24036. * @returns {WindowManager.UrlDialogInstanceApi} A new URL dialog instance.
  24037. */
  24038. openUrl,
  24039. /**
  24040. * Creates an alert dialog. Do not use the blocking behavior of this
  24041. * native version. Use the callback method instead; then it can be extended.
  24042. *
  24043. * @method alert
  24044. * @param {String} message Text to display in the new alert dialog.
  24045. * @param {Function} callback (Optional) Callback function to be executed after the user has selected ok.
  24046. * @param {Object} scope (Optional) Scope to execute the callback in.
  24047. * @example
  24048. * // Displays an alert box using the active editors window manager instance
  24049. * tinymce.activeEditor.windowManager.alert('Hello world!');
  24050. */
  24051. alert,
  24052. /**
  24053. * Creates an alert dialog. Do not use the blocking behavior of this
  24054. * native version. Use the callback method instead; then it can be extended.
  24055. *
  24056. * @method confirm
  24057. * @param {String} message Text to display in the new confirm dialog.
  24058. * @param {Function} callback (Optional) Callback function to be executed after the user has selected ok or cancel.
  24059. * @param {Object} scope (Optional) Scope to execute the callback in.
  24060. * @example
  24061. * // Displays a confirm box and an alert message will be displayed depending on what you choose in the confirm
  24062. * tinymce.activeEditor.windowManager.confirm('Do you want to do something?', (state) => {
  24063. * const message = state ? 'Ok' : 'Cancel';
  24064. * tinymce.activeEditor.windowManager.alert(message);
  24065. * });
  24066. */
  24067. confirm,
  24068. /**
  24069. * Closes the top most window.
  24070. *
  24071. * @method close
  24072. */
  24073. close
  24074. };
  24075. };
  24076. const displayNotification$1 = (editor, message) => {
  24077. editor.notificationManager.open({
  24078. type: 'error',
  24079. text: message
  24080. });
  24081. };
  24082. const displayError = (editor, message) => {
  24083. if (editor._skinLoaded) {
  24084. displayNotification$1(editor, message);
  24085. }
  24086. else {
  24087. editor.on('SkinLoaded', () => {
  24088. displayNotification$1(editor, message);
  24089. });
  24090. }
  24091. };
  24092. const uploadError = (editor, message) => {
  24093. displayError(editor, I18n.translate(['Failed to upload image: {0}', message]));
  24094. };
  24095. const logError = (editor, errorType, msg) => {
  24096. fireError(editor, errorType, { message: msg });
  24097. // eslint-disable-next-line no-console
  24098. console.error(msg);
  24099. };
  24100. const createLoadError = (type, url, name) => name ?
  24101. `Failed to load ${type}: ${name} from url ${url}` :
  24102. `Failed to load ${type} url: ${url}`;
  24103. const pluginLoadError = (editor, url, name) => {
  24104. logError(editor, 'PluginLoadError', createLoadError('plugin', url, name));
  24105. };
  24106. const iconsLoadError = (editor, url, name) => {
  24107. logError(editor, 'IconsLoadError', createLoadError('icons', url, name));
  24108. };
  24109. const languageLoadError = (editor, url, name) => {
  24110. logError(editor, 'LanguageLoadError', createLoadError('language', url, name));
  24111. };
  24112. const themeLoadError = (editor, url, name) => {
  24113. logError(editor, 'ThemeLoadError', createLoadError('theme', url, name));
  24114. };
  24115. const modelLoadError = (editor, url, name) => {
  24116. logError(editor, 'ModelLoadError', createLoadError('model', url, name));
  24117. };
  24118. const licenseKeyManagerLoadError = (editor, url) => {
  24119. logError(editor, 'LicenseKeyManagerLoadError', createLoadError('license key manager', url));
  24120. };
  24121. const pluginInitError = (editor, name, err) => {
  24122. const message = I18n.translate(['Failed to initialize plugin: {0}', name]);
  24123. fireError(editor, 'PluginLoadError', { message });
  24124. initError(message, err);
  24125. displayError(editor, message);
  24126. };
  24127. const initError = (message, ...x) => {
  24128. const console = window.console;
  24129. if (console) { // Skip test env
  24130. if (console.error) {
  24131. console.error(message, ...x);
  24132. }
  24133. else {
  24134. console.log(message, ...x);
  24135. }
  24136. }
  24137. };
  24138. // Map to track which editors have already been processed and disabled
  24139. const processedEditors = new WeakMap();
  24140. const forceDisable = (editor) => {
  24141. // Check if we've already disabled the editor
  24142. if (processedEditors.has(editor)) {
  24143. return;
  24144. }
  24145. // Mark this editor as processed
  24146. processedEditors.set(editor, true);
  24147. const switchModeListener = () => {
  24148. editor.on('SwitchMode', (e) => {
  24149. const { mode } = e;
  24150. if (mode !== 'readonly') {
  24151. editor.mode.set('readonly');
  24152. }
  24153. });
  24154. };
  24155. const disabledStateChangeListener = () => {
  24156. editor.on('DisabledStateChange', (e) => {
  24157. const { state } = e;
  24158. if (!state) {
  24159. e.preventDefault();
  24160. }
  24161. }, true);
  24162. };
  24163. if (editor.initialized) {
  24164. // Set readonly before setting disabled as disabling editor prevents mode from being changed
  24165. if (!editor.removed) {
  24166. editor.mode.set('readonly');
  24167. }
  24168. editor.options.set('disabled', true);
  24169. }
  24170. else {
  24171. editor.on('init', () => {
  24172. // Set readonly before setting disabled as disabling editor prevents mode from being changed
  24173. if (!editor.removed) {
  24174. editor.mode.set('readonly');
  24175. }
  24176. editor.options.set('disabled', true);
  24177. });
  24178. }
  24179. disabledStateChangeListener();
  24180. switchModeListener();
  24181. };
  24182. /* eslint-disable no-console */
  24183. const displayNotification = (editor, messageData) => {
  24184. const { type, message } = messageData;
  24185. editor.notificationManager.open({
  24186. type,
  24187. text: message
  24188. });
  24189. };
  24190. const getConsoleFn = (type) => {
  24191. switch (type) {
  24192. case 'error':
  24193. return console.error;
  24194. case 'info':
  24195. return console.info;
  24196. case 'warn':
  24197. return console.warn;
  24198. case 'log':
  24199. default:
  24200. return console.log;
  24201. }
  24202. };
  24203. const displayConsoleMessage = (messageData) => {
  24204. const consoleFn = getConsoleFn(messageData.type);
  24205. consoleFn(messageData.message);
  24206. };
  24207. const reportMessage = (editor, message) => {
  24208. const { console, editor: editorUi } = message;
  24209. if (isNonNullable(editorUi)) {
  24210. if (editor._skinLoaded) {
  24211. displayNotification(editor, editorUi);
  24212. }
  24213. else {
  24214. editor.on('SkinLoaded', () => {
  24215. displayNotification(editor, editorUi);
  24216. });
  24217. }
  24218. }
  24219. if (isNonNullable(console)) {
  24220. displayConsoleMessage(console);
  24221. }
  24222. };
  24223. const DOCS_URL = 'https://www.tiny.cloud/docs/tinymce/latest/license-key/';
  24224. const DOCS_URL_MESSAGE = `Read more: ${DOCS_URL}`;
  24225. const PROVIDE_LICENSE_KEY_MESSAGE = `Make sure to provide a valid license key or add license_key: 'gpl' to the init config to agree to the open source license terms.`;
  24226. const reportNoKeyError = (editor) => {
  24227. const baseMessage = 'The editor is disabled because a TinyMCE license key has not been provided.';
  24228. reportMessage(editor, {
  24229. console: {
  24230. type: 'error',
  24231. message: [
  24232. `${baseMessage}`,
  24233. PROVIDE_LICENSE_KEY_MESSAGE,
  24234. DOCS_URL_MESSAGE
  24235. ].join(' ')
  24236. },
  24237. editor: {
  24238. type: 'warning',
  24239. message: `${baseMessage}`
  24240. }
  24241. });
  24242. };
  24243. const reportLoadError = (editor, onlineStatus) => {
  24244. const key = `${onlineStatus === 'online' ? 'API' : 'license'} key`;
  24245. const baseMessage = `The editor is disabled because the TinyMCE ${key} could not be validated.`;
  24246. reportMessage(editor, {
  24247. console: {
  24248. type: 'error',
  24249. message: [
  24250. `${baseMessage}`,
  24251. `The TinyMCE Commercial License Key Manager plugin is required for the provided ${key} to be validated but could not be loaded.`,
  24252. DOCS_URL_MESSAGE
  24253. ].join(' ')
  24254. },
  24255. editor: {
  24256. type: 'warning',
  24257. message: `${baseMessage}`
  24258. }
  24259. });
  24260. };
  24261. const reportInvalidPlugin = (editor, pluginCode) => {
  24262. const baseMessage = `The "${pluginCode}" plugin requires a valid TinyMCE license key.`;
  24263. reportMessage(editor, {
  24264. console: {
  24265. type: 'error',
  24266. message: [
  24267. `${baseMessage}`,
  24268. DOCS_URL_MESSAGE
  24269. ].join(' ')
  24270. }
  24271. });
  24272. };
  24273. const PLUGIN_CODE$1 = 'licensekeymanager';
  24274. const getOnlineStatus = (editor) => {
  24275. const hasApiKey = isString(getApiKey(editor));
  24276. return hasApiKey ? 'online' : 'offline';
  24277. };
  24278. const getLicenseKeyType = (editor) => {
  24279. var _a;
  24280. const licenseKey = (_a = getLicenseKey(editor)) === null || _a === void 0 ? void 0 : _a.toLowerCase();
  24281. if (licenseKey === 'gpl') {
  24282. return 'gpl';
  24283. }
  24284. else if (isNullable(licenseKey)) {
  24285. return 'no_key';
  24286. }
  24287. else {
  24288. return 'non_gpl';
  24289. }
  24290. };
  24291. const determineStrategy = (editor) => {
  24292. const onlineStatus = getOnlineStatus(editor);
  24293. const licenseKeyType = getLicenseKeyType(editor);
  24294. const forcePlugin = (new Set(getPlugins(editor))).has(PLUGIN_CODE$1);
  24295. if (licenseKeyType !== 'gpl' || onlineStatus === 'online' || forcePlugin) {
  24296. return {
  24297. type: 'use_plugin',
  24298. onlineStatus,
  24299. licenseKeyType,
  24300. forcePlugin
  24301. };
  24302. }
  24303. else {
  24304. return {
  24305. type: 'use_gpl',
  24306. onlineStatus,
  24307. licenseKeyType,
  24308. forcePlugin
  24309. };
  24310. }
  24311. };
  24312. const NoLicenseKeyManager = (editor) => ({
  24313. validate: (data) => {
  24314. const { plugin } = data;
  24315. const hasPlugin = isString(plugin);
  24316. // Premium plugins are not allowed
  24317. if (hasPlugin) {
  24318. reportInvalidPlugin(editor, plugin);
  24319. }
  24320. return Promise.resolve(false);
  24321. },
  24322. });
  24323. const GplLicenseKeyManager = (editor) => ({
  24324. validate: (data) => {
  24325. const { plugin } = data;
  24326. const hasPlugin = isString(plugin);
  24327. // Premium plugins are not allowed if 'gpl' is given as the license_key
  24328. if (hasPlugin) {
  24329. reportInvalidPlugin(editor, plugin);
  24330. }
  24331. return Promise.resolve(!hasPlugin);
  24332. },
  24333. });
  24334. const ADDON_KEY = 'manager';
  24335. const PLUGIN_CODE = PLUGIN_CODE$1;
  24336. const setup$y = () => {
  24337. const addOnManager = AddOnManager();
  24338. const add = (addOn) => {
  24339. addOnManager.add(ADDON_KEY, addOn);
  24340. };
  24341. const load = (editor, suffix) => {
  24342. const strategy = determineStrategy(editor);
  24343. if (strategy.type === 'use_plugin') {
  24344. const url = `plugins/${PLUGIN_CODE}/plugin${suffix}.js`;
  24345. addOnManager.load(ADDON_KEY, url).catch(() => {
  24346. licenseKeyManagerLoadError(editor, url);
  24347. });
  24348. }
  24349. };
  24350. const init = (editor) => {
  24351. const setLicenseKeyManager = (licenseKeyManager) => {
  24352. Object.defineProperty(editor, 'licenseKeyManager', {
  24353. value: licenseKeyManager,
  24354. writable: false,
  24355. configurable: false,
  24356. enumerable: true,
  24357. });
  24358. };
  24359. const strategy = determineStrategy(editor);
  24360. const LicenseKeyManager = addOnManager.get(ADDON_KEY);
  24361. // Use plugin if it is already loaded as it can handle all license key types
  24362. if (isNonNullable(LicenseKeyManager)) {
  24363. const licenseKeyManagerApi = LicenseKeyManager(editor, addOnManager.urls[ADDON_KEY]);
  24364. setLicenseKeyManager(licenseKeyManagerApi);
  24365. }
  24366. else {
  24367. switch (strategy.type) {
  24368. case 'use_gpl': {
  24369. setLicenseKeyManager(GplLicenseKeyManager(editor));
  24370. break;
  24371. }
  24372. case 'use_plugin': {
  24373. // We know the plugin hasn't loaded and it is required
  24374. forceDisable(editor);
  24375. setLicenseKeyManager(NoLicenseKeyManager(editor));
  24376. if (strategy.onlineStatus === 'offline' && strategy.licenseKeyType === 'no_key') {
  24377. reportNoKeyError(editor);
  24378. }
  24379. else {
  24380. reportLoadError(editor, strategy.onlineStatus);
  24381. }
  24382. break;
  24383. }
  24384. }
  24385. }
  24386. // Validation of the license key is done asynchronously and does
  24387. // not block initialization of the editor
  24388. // The validate function is expected to set the editor to the correct
  24389. // state depending on if the license key is valid or not
  24390. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  24391. editor.licenseKeyManager.validate({});
  24392. };
  24393. return {
  24394. load,
  24395. add,
  24396. init
  24397. };
  24398. };
  24399. const LicenseKeyManagerLoader = setup$y();
  24400. const removeFakeSelection = (editor) => {
  24401. Optional.from(editor.selection.getNode()).each((elm) => {
  24402. elm.removeAttribute('data-mce-selected');
  24403. });
  24404. };
  24405. const setEditorCommandState = (editor, cmd, state) => {
  24406. try {
  24407. // execCommand needs a string for the value, so convert the boolean to a string
  24408. // See: https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand#Parameters
  24409. editor.getDoc().execCommand(cmd, false, String(state));
  24410. }
  24411. catch (_a) {
  24412. // Ignore
  24413. }
  24414. };
  24415. const setCommonEditorCommands = (editor, state) => {
  24416. setEditorCommandState(editor, 'StyleWithCSS', state);
  24417. setEditorCommandState(editor, 'enableInlineTableEditing', state);
  24418. setEditorCommandState(editor, 'enableObjectResizing', state);
  24419. };
  24420. const restoreFakeSelection = (editor) => {
  24421. editor.selection.setRng(editor.selection.getRng());
  24422. };
  24423. // Not quite sugar Class.toggle, it's more of a Class.set
  24424. const toggleClass = (elm, cls, state) => {
  24425. if (has(elm, cls) && !state) {
  24426. remove$4(elm, cls);
  24427. }
  24428. else if (state) {
  24429. add$2(elm, cls);
  24430. }
  24431. };
  24432. const disableEditor = (editor) => {
  24433. const body = SugarElement.fromDom(editor.getBody());
  24434. toggleClass(body, 'mce-content-readonly', true);
  24435. editor.selection.controlSelection.hideResizeRect();
  24436. editor._selectionOverrides.hideFakeCaret();
  24437. removeFakeSelection(editor);
  24438. };
  24439. const enableEditor = (editor) => {
  24440. const body = SugarElement.fromDom(editor.getBody());
  24441. toggleClass(body, 'mce-content-readonly', false);
  24442. if (editor.hasEditableRoot()) {
  24443. set(body, true);
  24444. }
  24445. setCommonEditorCommands(editor, false);
  24446. if (hasEditorOrUiFocus(editor)) {
  24447. editor.focus();
  24448. }
  24449. restoreFakeSelection(editor);
  24450. editor.nodeChanged();
  24451. };
  24452. const isDisabled = (editor) => isDisabled$1(editor);
  24453. const internalContentEditableAttr = 'data-mce-contenteditable';
  24454. const switchOffContentEditableTrue = (elm) => {
  24455. each$e(descendants(elm, '*[contenteditable="true"]'), (elm) => {
  24456. set$4(elm, internalContentEditableAttr, 'true');
  24457. set(elm, false);
  24458. });
  24459. };
  24460. const switchOnContentEditableTrue = (elm) => {
  24461. each$e(descendants(elm, `*[${internalContentEditableAttr}="true"]`), (elm) => {
  24462. remove$9(elm, internalContentEditableAttr);
  24463. set(elm, true);
  24464. });
  24465. };
  24466. const toggleDisabled = (editor, state) => {
  24467. const body = SugarElement.fromDom(editor.getBody());
  24468. if (state) {
  24469. disableEditor(editor);
  24470. set(body, false);
  24471. switchOffContentEditableTrue(body);
  24472. }
  24473. else {
  24474. switchOnContentEditableTrue(body);
  24475. enableEditor(editor);
  24476. }
  24477. };
  24478. const registerDisabledContentFilters = (editor) => {
  24479. if (editor.serializer) {
  24480. registerFilters(editor);
  24481. }
  24482. else {
  24483. editor.on('PreInit', () => {
  24484. registerFilters(editor);
  24485. });
  24486. }
  24487. };
  24488. const registerFilters = (editor) => {
  24489. editor.parser.addAttributeFilter('contenteditable', (nodes) => {
  24490. if (isDisabled(editor)) {
  24491. each$e(nodes, (node) => {
  24492. node.attr(internalContentEditableAttr, node.attr('contenteditable'));
  24493. node.attr('contenteditable', 'false');
  24494. });
  24495. }
  24496. });
  24497. editor.serializer.addAttributeFilter(internalContentEditableAttr, (nodes) => {
  24498. if (isDisabled(editor)) {
  24499. each$e(nodes, (node) => {
  24500. node.attr('contenteditable', node.attr(internalContentEditableAttr));
  24501. });
  24502. }
  24503. });
  24504. editor.serializer.addTempAttr(internalContentEditableAttr);
  24505. };
  24506. const isClickEvent = (e) => e.type === 'click';
  24507. const allowedEvents = ['copy'];
  24508. const isAllowedEventInDisabledMode = (e) => contains$2(allowedEvents, e.type);
  24509. const getAnchorHrefOpt = (editor, elm) => {
  24510. const isRoot = (elm) => eq(elm, SugarElement.fromDom(editor.getBody()));
  24511. return closest$3(elm, 'a', isRoot).bind((a) => getOpt(a, 'href'));
  24512. };
  24513. const processDisabledEvents = (editor, e) => {
  24514. /*
  24515. If an event is a click event on or within an anchor, and the CMD/CTRL key is
  24516. not held, then we want to prevent default behaviour and either:
  24517. a) scroll to the relevant bookmark
  24518. b) open the link using default browser behaviour
  24519. */
  24520. if (isClickEvent(e) && !VK.metaKeyPressed(e)) {
  24521. const elm = SugarElement.fromDom(e.target);
  24522. getAnchorHrefOpt(editor, elm).each((href) => {
  24523. e.preventDefault();
  24524. if (/^#/.test(href)) {
  24525. const targetEl = editor.dom.select(`${href},[name="${removeLeading(href, '#')}"]`);
  24526. if (targetEl.length) {
  24527. editor.selection.scrollIntoView(targetEl[0], true);
  24528. }
  24529. }
  24530. else {
  24531. window.open(href, '_blank', 'rel=noopener noreferrer,menubar=yes,toolbar=yes,location=yes,status=yes,resizable=yes,scrollbars=yes');
  24532. }
  24533. });
  24534. }
  24535. else if (isAllowedEventInDisabledMode(e)) {
  24536. editor.dispatch(e.type, e);
  24537. }
  24538. };
  24539. const registerDisabledModeEventHandlers = (editor) => {
  24540. editor.on('ShowCaret ObjectSelected', (e) => {
  24541. if (isDisabled(editor)) {
  24542. e.preventDefault();
  24543. }
  24544. });
  24545. // Preprend to the handlers as this should be the first to fire
  24546. editor.on('DisabledStateChange', (e) => {
  24547. if (!e.isDefaultPrevented()) {
  24548. toggleDisabled(editor, e.state);
  24549. }
  24550. });
  24551. };
  24552. const registerEventsAndFilters$1 = (editor) => {
  24553. registerDisabledContentFilters(editor);
  24554. registerDisabledModeEventHandlers(editor);
  24555. };
  24556. const isContentCssSkinName = (url) => /^[a-z0-9\-]+$/i.test(url);
  24557. const toContentSkinResourceName = (url) => 'content/' + url + '/content.css';
  24558. const isBundledCssSkinName = (url) => tinymce.Resource.has(toContentSkinResourceName(url));
  24559. const getContentCssUrls = (editor) => {
  24560. return transformToUrls(editor, getContentCss(editor));
  24561. };
  24562. const getFontCssUrls = (editor) => {
  24563. return transformToUrls(editor, getFontCss(editor));
  24564. };
  24565. const transformToUrls = (editor, cssLinks) => {
  24566. const skinUrl = editor.editorManager.baseURL + '/skins/content';
  24567. const suffix = editor.editorManager.suffix;
  24568. const contentCssFile = `content${suffix}.css`;
  24569. return map$3(cssLinks, (url) => {
  24570. if (isBundledCssSkinName(url)) {
  24571. return url;
  24572. }
  24573. else if (isContentCssSkinName(url) && !editor.inline) {
  24574. return `${skinUrl}/${url}/${contentCssFile}`;
  24575. }
  24576. else {
  24577. return editor.documentBaseURI.toAbsolute(url);
  24578. }
  24579. });
  24580. };
  24581. const appendContentCssFromSettings = (editor) => {
  24582. editor.contentCSS = editor.contentCSS.concat(getContentCssUrls(editor), getFontCssUrls(editor));
  24583. };
  24584. /**
  24585. * Finds images with data uris or blob uris. If data uris are found it will convert them into blob uris.
  24586. *
  24587. * @private
  24588. * @class tinymce.file.ImageScanner
  24589. */
  24590. const getAllImages = (elm) => {
  24591. return elm ? from(elm.getElementsByTagName('img')) : [];
  24592. };
  24593. const ImageScanner = (uploadStatus, blobCache) => {
  24594. const cachedPromises = {};
  24595. const findAll = (elm, predicate = always) => {
  24596. const images = filter$5(getAllImages(elm), (img) => {
  24597. const src = img.src;
  24598. if (img.hasAttribute('data-mce-bogus')) {
  24599. return false;
  24600. }
  24601. if (img.hasAttribute('data-mce-placeholder')) {
  24602. return false;
  24603. }
  24604. if (!src || src === Env.transparentSrc) {
  24605. return false;
  24606. }
  24607. if (startsWith(src, 'blob:')) {
  24608. return !uploadStatus.isUploaded(src) && predicate(img);
  24609. }
  24610. if (startsWith(src, 'data:')) {
  24611. return predicate(img);
  24612. }
  24613. return false;
  24614. });
  24615. const promises = map$3(images, (img) => {
  24616. const imageSrc = img.src;
  24617. if (has$2(cachedPromises, imageSrc)) {
  24618. // Since the cached promise will return the cached image
  24619. // We need to wrap it and resolve with the actual image
  24620. return cachedPromises[imageSrc].then((imageInfo) => {
  24621. if (isString(imageInfo)) { // error apparently
  24622. return imageInfo;
  24623. }
  24624. else {
  24625. return {
  24626. image: img,
  24627. blobInfo: imageInfo.blobInfo
  24628. };
  24629. }
  24630. });
  24631. }
  24632. else {
  24633. const newPromise = imageToBlobInfo(blobCache, imageSrc)
  24634. .then((blobInfo) => {
  24635. delete cachedPromises[imageSrc];
  24636. return { image: img, blobInfo };
  24637. }).catch((error) => {
  24638. delete cachedPromises[imageSrc];
  24639. return error;
  24640. });
  24641. cachedPromises[imageSrc] = newPromise;
  24642. return newPromise;
  24643. }
  24644. });
  24645. return Promise.all(promises);
  24646. };
  24647. return {
  24648. findAll
  24649. };
  24650. };
  24651. /**
  24652. * Holds the current status of a blob uri, if it's pending or uploaded and what the result urls was.
  24653. *
  24654. * @private
  24655. * @class tinymce.file.UploadStatus
  24656. */
  24657. const UploadStatus = () => {
  24658. const PENDING = 1, UPLOADED = 2;
  24659. let blobUriStatuses = {};
  24660. const createStatus = (status, resultUri) => {
  24661. return {
  24662. status,
  24663. resultUri
  24664. };
  24665. };
  24666. const hasBlobUri = (blobUri) => {
  24667. return blobUri in blobUriStatuses;
  24668. };
  24669. const getResultUri = (blobUri) => {
  24670. const result = blobUriStatuses[blobUri];
  24671. return result ? result.resultUri : null;
  24672. };
  24673. const isPending = (blobUri) => {
  24674. return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === PENDING : false;
  24675. };
  24676. const isUploaded = (blobUri) => {
  24677. return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === UPLOADED : false;
  24678. };
  24679. const markPending = (blobUri) => {
  24680. blobUriStatuses[blobUri] = createStatus(PENDING, null);
  24681. };
  24682. const markUploaded = (blobUri, resultUri) => {
  24683. blobUriStatuses[blobUri] = createStatus(UPLOADED, resultUri);
  24684. };
  24685. const removeFailed = (blobUri) => {
  24686. delete blobUriStatuses[blobUri];
  24687. };
  24688. const destroy = () => {
  24689. blobUriStatuses = {};
  24690. };
  24691. return {
  24692. hasBlobUri,
  24693. getResultUri,
  24694. isPending,
  24695. isUploaded,
  24696. markPending,
  24697. markUploaded,
  24698. removeFailed,
  24699. destroy
  24700. };
  24701. };
  24702. /**
  24703. * Generates unique ids.
  24704. *
  24705. * @class tinymce.util.Uuid
  24706. * @private
  24707. */
  24708. let count = 0;
  24709. const seed = () => {
  24710. const rnd = () => {
  24711. return Math.round(random() * 0xFFFFFFFF).toString(36);
  24712. };
  24713. const now = new Date().getTime();
  24714. return 's' + now.toString(36) + rnd() + rnd() + rnd();
  24715. };
  24716. const uuid = (prefix) => {
  24717. return prefix + (count++) + seed();
  24718. };
  24719. const BlobCache = () => {
  24720. let cache = [];
  24721. const mimeToExt = (mime) => {
  24722. const mimes = {
  24723. 'image/jpeg': 'jpg',
  24724. 'image/jpg': 'jpg',
  24725. 'image/gif': 'gif',
  24726. 'image/png': 'png',
  24727. 'image/apng': 'apng',
  24728. 'image/avif': 'avif',
  24729. 'image/svg+xml': 'svg',
  24730. 'image/webp': 'webp',
  24731. 'image/bmp': 'bmp',
  24732. 'image/tiff': 'tiff'
  24733. };
  24734. return mimes[mime.toLowerCase()] || 'dat';
  24735. };
  24736. const create = (o, blob, base64, name, filename) => {
  24737. if (isString(o)) {
  24738. const id = o;
  24739. return toBlobInfo({
  24740. id,
  24741. name,
  24742. filename,
  24743. blob: blob,
  24744. base64: base64
  24745. });
  24746. }
  24747. else if (isObject(o)) {
  24748. return toBlobInfo(o);
  24749. }
  24750. else {
  24751. throw new Error('Unknown input type');
  24752. }
  24753. };
  24754. const toBlobInfo = (o) => {
  24755. if (!o.blob || !o.base64) {
  24756. throw new Error('blob and base64 representations of the image are required for BlobInfo to be created');
  24757. }
  24758. const id = o.id || uuid('blobid');
  24759. const name = o.name || id;
  24760. const blob = o.blob;
  24761. return {
  24762. id: constant(id),
  24763. name: constant(name),
  24764. filename: constant(o.filename || name + '.' + mimeToExt(blob.type)),
  24765. blob: constant(blob),
  24766. base64: constant(o.base64),
  24767. blobUri: constant(o.blobUri || URL.createObjectURL(blob)),
  24768. uri: constant(o.uri)
  24769. };
  24770. };
  24771. const add = (blobInfo) => {
  24772. if (!get(blobInfo.id())) {
  24773. cache.push(blobInfo);
  24774. }
  24775. };
  24776. const findFirst = (predicate) => find$2(cache, predicate).getOrUndefined();
  24777. const get = (id) => findFirst((cachedBlobInfo) => cachedBlobInfo.id() === id);
  24778. const getByUri = (blobUri) => findFirst((blobInfo) => blobInfo.blobUri() === blobUri);
  24779. const getByData = (base64, type) => findFirst((blobInfo) => blobInfo.base64() === base64 && blobInfo.blob().type === type);
  24780. const removeByUri = (blobUri) => {
  24781. cache = filter$5(cache, (blobInfo) => {
  24782. if (blobInfo.blobUri() === blobUri) {
  24783. URL.revokeObjectURL(blobInfo.blobUri());
  24784. return false;
  24785. }
  24786. return true;
  24787. });
  24788. };
  24789. const destroy = () => {
  24790. each$e(cache, (cachedBlobInfo) => {
  24791. URL.revokeObjectURL(cachedBlobInfo.blobUri());
  24792. });
  24793. cache = [];
  24794. };
  24795. return {
  24796. create,
  24797. add,
  24798. get,
  24799. getByUri,
  24800. getByData,
  24801. findFirst,
  24802. removeByUri,
  24803. destroy
  24804. };
  24805. };
  24806. const Uploader = (uploadStatus, settings) => {
  24807. const pendingPromises = {};
  24808. const pathJoin = (path1, path2) => {
  24809. if (path1) {
  24810. return path1.replace(/\/$/, '') + '/' + path2.replace(/^\//, '');
  24811. }
  24812. return path2;
  24813. };
  24814. const defaultHandler = (blobInfo, progress) => new Promise((success, failure) => {
  24815. const xhr = new XMLHttpRequest();
  24816. xhr.open('POST', settings.url);
  24817. xhr.withCredentials = settings.credentials;
  24818. xhr.upload.onprogress = (e) => {
  24819. progress(e.loaded / e.total * 100);
  24820. };
  24821. xhr.onerror = () => {
  24822. failure('Image upload failed due to a XHR Transport error. Code: ' + xhr.status);
  24823. };
  24824. xhr.onload = () => {
  24825. if (xhr.status < 200 || xhr.status >= 300) {
  24826. failure('HTTP Error: ' + xhr.status);
  24827. return;
  24828. }
  24829. const json = JSON.parse(xhr.responseText);
  24830. if (!json || !isString(json.location)) {
  24831. failure('Invalid JSON: ' + xhr.responseText);
  24832. return;
  24833. }
  24834. success(pathJoin(settings.basePath, json.location));
  24835. };
  24836. const formData = new FormData();
  24837. formData.append('file', blobInfo.blob(), blobInfo.filename());
  24838. xhr.send(formData);
  24839. });
  24840. const uploadHandler = isFunction(settings.handler) ? settings.handler : defaultHandler;
  24841. const noUpload = () => new Promise((resolve) => {
  24842. resolve([]);
  24843. });
  24844. const handlerSuccess = (blobInfo, url) => ({
  24845. url,
  24846. blobInfo,
  24847. status: true
  24848. });
  24849. const handlerFailure = (blobInfo, error) => ({
  24850. url: '',
  24851. blobInfo,
  24852. status: false,
  24853. error
  24854. });
  24855. const resolvePending = (blobUri, result) => {
  24856. Tools.each(pendingPromises[blobUri], (resolve) => {
  24857. resolve(result);
  24858. });
  24859. delete pendingPromises[blobUri];
  24860. };
  24861. const uploadBlobInfo = (blobInfo, handler, openNotification) => {
  24862. uploadStatus.markPending(blobInfo.blobUri());
  24863. return new Promise((resolve) => {
  24864. let notification;
  24865. let progress;
  24866. try {
  24867. const closeNotification = () => {
  24868. if (notification) {
  24869. notification.close();
  24870. progress = noop; // Once it's closed it's closed
  24871. }
  24872. };
  24873. const success = (url) => {
  24874. closeNotification();
  24875. uploadStatus.markUploaded(blobInfo.blobUri(), url);
  24876. resolvePending(blobInfo.blobUri(), handlerSuccess(blobInfo, url));
  24877. resolve(handlerSuccess(blobInfo, url));
  24878. };
  24879. const failure = (error) => {
  24880. closeNotification();
  24881. uploadStatus.removeFailed(blobInfo.blobUri());
  24882. resolvePending(blobInfo.blobUri(), handlerFailure(blobInfo, error));
  24883. resolve(handlerFailure(blobInfo, error));
  24884. };
  24885. progress = (percent) => {
  24886. if (percent < 0 || percent > 100) {
  24887. return;
  24888. }
  24889. Optional.from(notification)
  24890. .orThunk(() => Optional.from(openNotification).map(apply$1))
  24891. .each((n) => {
  24892. notification = n;
  24893. n.progressBar.value(percent);
  24894. });
  24895. };
  24896. handler(blobInfo, progress).then(success, (err) => {
  24897. failure(isString(err) ? { message: err } : err);
  24898. });
  24899. }
  24900. catch (ex) {
  24901. resolve(handlerFailure(blobInfo, ex));
  24902. }
  24903. });
  24904. };
  24905. const isDefaultHandler = (handler) => handler === defaultHandler;
  24906. const pendingUploadBlobInfo = (blobInfo) => {
  24907. const blobUri = blobInfo.blobUri();
  24908. return new Promise((resolve) => {
  24909. pendingPromises[blobUri] = pendingPromises[blobUri] || [];
  24910. pendingPromises[blobUri].push(resolve);
  24911. });
  24912. };
  24913. const uploadBlobs = (blobInfos, openNotification) => {
  24914. blobInfos = Tools.grep(blobInfos, (blobInfo) => !uploadStatus.isUploaded(blobInfo.blobUri()));
  24915. return Promise.all(Tools.map(blobInfos, (blobInfo) => uploadStatus.isPending(blobInfo.blobUri()) ?
  24916. pendingUploadBlobInfo(blobInfo) : uploadBlobInfo(blobInfo, uploadHandler, openNotification)));
  24917. };
  24918. const upload = (blobInfos, openNotification) => (!settings.url && isDefaultHandler(uploadHandler)) ? noUpload() : uploadBlobs(blobInfos, openNotification);
  24919. return {
  24920. upload
  24921. };
  24922. };
  24923. const openNotification = (editor) => () => editor.notificationManager.open({
  24924. text: editor.translate('Image uploading...'),
  24925. type: 'info',
  24926. timeout: -1,
  24927. progressBar: true
  24928. });
  24929. const createUploader = (editor, uploadStatus) => Uploader(uploadStatus, {
  24930. url: getImageUploadUrl(editor),
  24931. basePath: getImageUploadBasePath(editor),
  24932. credentials: getImagesUploadCredentials(editor),
  24933. handler: getImagesUploadHandler(editor)
  24934. });
  24935. /**
  24936. * This class handles uploading images to a back-end server.
  24937. *
  24938. * @class tinymce.util.ImageUploader
  24939. */
  24940. const ImageUploader = (editor) => {
  24941. const uploadStatus = UploadStatus();
  24942. const uploader = createUploader(editor, uploadStatus);
  24943. return {
  24944. /**
  24945. * Uploads images to the configured image upload URL (`images_upload_url`) or passes the images to the defined image upload handler function (`images_upload_handler`).
  24946. *
  24947. * @method upload
  24948. * @param {Array} blobInfos A BlobInfo array containing the image data to upload. A BlobInfo can be created by calling `editor.editorUpload.blobCache.create()`.
  24949. * @param {Boolean} showNotification (Optional) When set to true, a notification with a progress bar will be shown during image uploads.
  24950. */
  24951. upload: (blobInfos, showNotification = true) => uploader.upload(blobInfos, showNotification ? openNotification(editor) : undefined)
  24952. };
  24953. };
  24954. const isEmptyForPadding = (editor, element) => editor.dom.isEmpty(element.dom) && isNonNullable(editor.schema.getTextBlockElements()[name(element)]);
  24955. const addPaddingToEmpty = (editor) => (element) => {
  24956. if (isEmptyForPadding(editor, element)) {
  24957. append$1(element, SugarElement.fromHtml('<br data-mce-bogus="1" />'));
  24958. }
  24959. };
  24960. const EditorUpload = (editor) => {
  24961. const blobCache = BlobCache();
  24962. let uploader, imageScanner;
  24963. const uploadStatus = UploadStatus();
  24964. const urlFilters = [];
  24965. const aliveGuard = (callback) => {
  24966. return (result) => {
  24967. if (editor.selection) {
  24968. return callback(result);
  24969. }
  24970. return [];
  24971. };
  24972. };
  24973. const cacheInvalidator = (url) => url + (url.indexOf('?') === -1 ? '?' : '&') + (new Date()).getTime();
  24974. // Replaces strings without regexps to avoid FF regexp to big issue
  24975. const replaceString = (content, search, replace) => {
  24976. let index = 0;
  24977. do {
  24978. index = content.indexOf(search, index);
  24979. if (index !== -1) {
  24980. content = content.substring(0, index) + replace + content.substr(index + search.length);
  24981. index += replace.length - search.length + 1;
  24982. }
  24983. } while (index !== -1);
  24984. return content;
  24985. };
  24986. const replaceImageUrl = (content, targetUrl, replacementUrl) => {
  24987. const replacementString = `src="${replacementUrl}"${replacementUrl === Env.transparentSrc ? ' data-mce-placeholder="1"' : ''}`;
  24988. content = replaceString(content, `src="${targetUrl}"`, replacementString);
  24989. content = replaceString(content, 'data-mce-src="' + targetUrl + '"', 'data-mce-src="' + replacementUrl + '"');
  24990. return content;
  24991. };
  24992. const replaceUrlInUndoStack = (targetUrl, replacementUrl) => {
  24993. each$e(editor.undoManager.data, (level) => {
  24994. if (level.type === 'fragmented') {
  24995. level.fragments = map$3(level.fragments, (fragment) => replaceImageUrl(fragment, targetUrl, replacementUrl));
  24996. }
  24997. else {
  24998. level.content = replaceImageUrl(level.content, targetUrl, replacementUrl);
  24999. }
  25000. });
  25001. };
  25002. const replaceImageUriInView = (image, resultUri) => {
  25003. const src = editor.convertURL(resultUri, 'src');
  25004. replaceUrlInUndoStack(image.src, resultUri);
  25005. setAll$1(SugarElement.fromDom(image), {
  25006. 'src': shouldReuseFileName(editor) ? cacheInvalidator(resultUri) : resultUri,
  25007. 'data-mce-src': src
  25008. });
  25009. };
  25010. const uploadImages = () => {
  25011. if (!uploader) {
  25012. uploader = createUploader(editor, uploadStatus);
  25013. }
  25014. return scanForImages().then(aliveGuard((imageInfos) => {
  25015. const blobInfos = map$3(imageInfos, (imageInfo) => imageInfo.blobInfo);
  25016. return uploader.upload(blobInfos, openNotification(editor)).then(aliveGuard((result) => {
  25017. const imagesToRemove = [];
  25018. let shouldDispatchChange = false;
  25019. const filteredResult = map$3(result, (uploadInfo, index) => {
  25020. const { blobInfo, image } = imageInfos[index];
  25021. let removed = false;
  25022. if (uploadInfo.status && shouldReplaceBlobUris(editor)) {
  25023. if (uploadInfo.url && !contains$1(image.src, uploadInfo.url)) {
  25024. shouldDispatchChange = true;
  25025. }
  25026. blobCache.removeByUri(image.src);
  25027. if (isRtc(editor)) ;
  25028. else {
  25029. replaceImageUriInView(image, uploadInfo.url);
  25030. }
  25031. }
  25032. else if (uploadInfo.error) {
  25033. if (uploadInfo.error.remove) {
  25034. replaceUrlInUndoStack(image.src, Env.transparentSrc);
  25035. imagesToRemove.push(image);
  25036. removed = true;
  25037. }
  25038. uploadError(editor, uploadInfo.error.message);
  25039. }
  25040. return {
  25041. element: image,
  25042. status: uploadInfo.status,
  25043. uploadUri: uploadInfo.url,
  25044. blobInfo,
  25045. removed
  25046. };
  25047. });
  25048. if (imagesToRemove.length > 0 && !isRtc(editor)) {
  25049. editor.undoManager.transact(() => {
  25050. each$e(fromDom$1(imagesToRemove), (sugarElement) => {
  25051. const parentOpt = parent(sugarElement);
  25052. remove$8(sugarElement);
  25053. // This needs a more editor-wide fix, see issue TINY-9802. Short version: Removing the image resulted in empty <p> elements, which confused the editor.
  25054. parentOpt.each(addPaddingToEmpty(editor));
  25055. blobCache.removeByUri(sugarElement.dom.src);
  25056. });
  25057. });
  25058. }
  25059. else if (shouldDispatchChange) {
  25060. editor.undoManager.dispatchChange();
  25061. }
  25062. return filteredResult;
  25063. }));
  25064. }));
  25065. };
  25066. const uploadImagesAuto = () => isAutomaticUploadsEnabled(editor) ? uploadImages() : Promise.resolve([]);
  25067. const isValidDataUriImage = (imgElm) => forall(urlFilters, (filter) => filter(imgElm));
  25068. const addFilter = (filter) => {
  25069. urlFilters.push(filter);
  25070. };
  25071. const scanForImages = () => {
  25072. if (!imageScanner) {
  25073. imageScanner = ImageScanner(uploadStatus, blobCache);
  25074. }
  25075. return imageScanner.findAll(editor.getBody(), isValidDataUriImage).then(aliveGuard((result) => {
  25076. const filteredResult = filter$5(result, (resultItem) => {
  25077. // ImageScanner internally converts images that it finds, but it may fail to do so if image source is inaccessible.
  25078. // In such case resultItem will contain appropriate text error message, instead of image data.
  25079. if (isString(resultItem)) {
  25080. displayError(editor, resultItem);
  25081. return false;
  25082. }
  25083. else if (resultItem.uriType === 'blob') {
  25084. return false;
  25085. }
  25086. else {
  25087. return true;
  25088. }
  25089. });
  25090. if (isRtc(editor)) ;
  25091. else {
  25092. each$e(filteredResult, (resultItem) => {
  25093. replaceUrlInUndoStack(resultItem.image.src, resultItem.blobInfo.blobUri());
  25094. resultItem.image.src = resultItem.blobInfo.blobUri();
  25095. resultItem.image.removeAttribute('data-mce-src');
  25096. });
  25097. }
  25098. return filteredResult;
  25099. }));
  25100. };
  25101. const destroy = () => {
  25102. blobCache.destroy();
  25103. uploadStatus.destroy();
  25104. imageScanner = uploader = null;
  25105. };
  25106. const replaceBlobUris = (content) => {
  25107. return content.replace(/src="(blob:[^"]+)"/g, (match, blobUri) => {
  25108. const resultUri = uploadStatus.getResultUri(blobUri);
  25109. if (resultUri) {
  25110. return 'src="' + resultUri + '"';
  25111. }
  25112. let blobInfo = blobCache.getByUri(blobUri);
  25113. if (!blobInfo) {
  25114. blobInfo = foldl(editor.editorManager.get(), (result, editor) => {
  25115. return result || editor.editorUpload && editor.editorUpload.blobCache.getByUri(blobUri);
  25116. }, undefined);
  25117. }
  25118. if (blobInfo) {
  25119. const blob = blobInfo.blob();
  25120. return 'src="data:' + blob.type + ';base64,' + blobInfo.base64() + '"';
  25121. }
  25122. return match;
  25123. });
  25124. };
  25125. editor.on('SetContent', () => {
  25126. if (isAutomaticUploadsEnabled(editor)) {
  25127. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  25128. uploadImagesAuto();
  25129. }
  25130. else {
  25131. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  25132. scanForImages();
  25133. }
  25134. });
  25135. editor.on('RawSaveContent', (e) => {
  25136. e.content = replaceBlobUris(e.content);
  25137. });
  25138. editor.on('GetContent', (e) => {
  25139. if (e.source_view || e.format === 'raw' || e.format === 'tree') {
  25140. return;
  25141. }
  25142. e.content = replaceBlobUris(e.content);
  25143. });
  25144. editor.on('PostRender', () => {
  25145. editor.parser.addNodeFilter('img', (images) => {
  25146. each$e(images, (img) => {
  25147. const src = img.attr('src');
  25148. if (!src || blobCache.getByUri(src)) {
  25149. return;
  25150. }
  25151. const resultUri = uploadStatus.getResultUri(src);
  25152. if (resultUri) {
  25153. img.attr('src', resultUri);
  25154. }
  25155. });
  25156. });
  25157. });
  25158. return {
  25159. blobCache,
  25160. addFilter,
  25161. uploadImages,
  25162. uploadImagesAuto,
  25163. scanForImages,
  25164. destroy
  25165. };
  25166. };
  25167. const get$1 = (editor) => {
  25168. const dom = editor.dom;
  25169. const schemaType = editor.schema.type;
  25170. const formats = {
  25171. valigntop: [
  25172. { selector: 'td,th', styles: { verticalAlign: 'top' } }
  25173. ],
  25174. valignmiddle: [
  25175. { selector: 'td,th', styles: { verticalAlign: 'middle' } }
  25176. ],
  25177. valignbottom: [
  25178. { selector: 'td,th', styles: { verticalAlign: 'bottom' } }
  25179. ],
  25180. alignleft: [
  25181. {
  25182. selector: 'figure.image',
  25183. collapsed: false,
  25184. classes: 'align-left',
  25185. ceFalseOverride: true,
  25186. preview: 'font-family font-size'
  25187. },
  25188. {
  25189. selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li,pre',
  25190. styles: {
  25191. textAlign: 'left'
  25192. },
  25193. inherit: false,
  25194. preview: false
  25195. },
  25196. {
  25197. selector: 'img,audio,video',
  25198. collapsed: false,
  25199. styles: {
  25200. float: 'left',
  25201. },
  25202. preview: 'font-family font-size'
  25203. },
  25204. {
  25205. selector: '.mce-placeholder',
  25206. styles: {
  25207. float: 'left',
  25208. },
  25209. ceFalseOverride: true
  25210. },
  25211. {
  25212. selector: 'table',
  25213. collapsed: false,
  25214. styles: {
  25215. marginLeft: '0px',
  25216. marginRight: 'auto',
  25217. },
  25218. onformat: (table) => {
  25219. // Remove conflicting float style
  25220. dom.setStyle(table, 'float', null);
  25221. },
  25222. preview: 'font-family font-size'
  25223. },
  25224. {
  25225. selector: '.mce-preview-object,[data-ephox-embed-iri]',
  25226. ceFalseOverride: true,
  25227. styles: {
  25228. float: 'left'
  25229. }
  25230. }
  25231. ],
  25232. aligncenter: [
  25233. {
  25234. selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li,pre',
  25235. styles: {
  25236. textAlign: 'center'
  25237. },
  25238. inherit: false,
  25239. preview: 'font-family font-size'
  25240. },
  25241. {
  25242. selector: 'figure.image',
  25243. collapsed: false,
  25244. classes: 'align-center',
  25245. ceFalseOverride: true,
  25246. preview: 'font-family font-size'
  25247. },
  25248. {
  25249. selector: 'img,audio,video',
  25250. collapsed: false,
  25251. styles: {
  25252. display: 'block',
  25253. marginLeft: 'auto',
  25254. marginRight: 'auto'
  25255. },
  25256. preview: false
  25257. },
  25258. {
  25259. selector: '.mce-placeholder',
  25260. styles: {
  25261. display: 'block',
  25262. marginLeft: 'auto',
  25263. marginRight: 'auto',
  25264. },
  25265. ceFalseOverride: true
  25266. },
  25267. {
  25268. selector: 'table',
  25269. collapsed: false,
  25270. styles: {
  25271. marginLeft: 'auto',
  25272. marginRight: 'auto'
  25273. },
  25274. preview: 'font-family font-size'
  25275. },
  25276. {
  25277. selector: '.mce-preview-object',
  25278. ceFalseOverride: true,
  25279. styles: {
  25280. display: 'table', // Needs to be `table` to properly render while editing
  25281. marginLeft: 'auto',
  25282. marginRight: 'auto'
  25283. },
  25284. preview: false
  25285. },
  25286. {
  25287. selector: '[data-ephox-embed-iri]',
  25288. ceFalseOverride: true,
  25289. styles: {
  25290. marginLeft: 'auto',
  25291. marginRight: 'auto'
  25292. },
  25293. preview: false
  25294. }
  25295. ],
  25296. alignright: [
  25297. {
  25298. selector: 'figure.image',
  25299. collapsed: false,
  25300. classes: 'align-right',
  25301. ceFalseOverride: true,
  25302. preview: 'font-family font-size'
  25303. },
  25304. {
  25305. selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li,pre',
  25306. styles: {
  25307. textAlign: 'right'
  25308. },
  25309. inherit: false,
  25310. preview: 'font-family font-size'
  25311. },
  25312. {
  25313. selector: 'img,audio,video',
  25314. collapsed: false,
  25315. styles: {
  25316. float: 'right'
  25317. },
  25318. preview: 'font-family font-size'
  25319. },
  25320. {
  25321. selector: '.mce-placeholder',
  25322. styles: {
  25323. float: 'right'
  25324. },
  25325. ceFalseOverride: true
  25326. },
  25327. {
  25328. selector: 'table',
  25329. collapsed: false,
  25330. styles: {
  25331. marginRight: '0px',
  25332. marginLeft: 'auto',
  25333. },
  25334. onformat: (table) => {
  25335. // Remove conflicting float style
  25336. dom.setStyle(table, 'float', null);
  25337. },
  25338. preview: 'font-family font-size'
  25339. },
  25340. {
  25341. selector: '.mce-preview-object,[data-ephox-embed-iri]',
  25342. ceFalseOverride: true,
  25343. styles: {
  25344. float: 'right'
  25345. },
  25346. preview: false
  25347. }
  25348. ],
  25349. alignjustify: [
  25350. {
  25351. selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li,pre',
  25352. styles: {
  25353. textAlign: 'justify'
  25354. },
  25355. inherit: false,
  25356. preview: 'font-family font-size'
  25357. }
  25358. ],
  25359. bold: [
  25360. { inline: 'strong', remove: 'all', preserve_attributes: ['class', 'style'] },
  25361. { inline: 'span', styles: { fontWeight: 'bold' } },
  25362. { inline: 'b', remove: 'all', preserve_attributes: ['class', 'style'] }
  25363. ],
  25364. italic: [
  25365. { inline: 'em', remove: 'all', preserve_attributes: ['class', 'style'] },
  25366. { inline: 'span', styles: { fontStyle: 'italic' } },
  25367. { inline: 'i', remove: 'all', preserve_attributes: ['class', 'style'] }
  25368. ],
  25369. underline: [
  25370. { inline: 'span', styles: { textDecoration: 'underline' }, exact: true },
  25371. { inline: 'u', remove: 'all', preserve_attributes: ['class', 'style'] }
  25372. ],
  25373. strikethrough: (() => {
  25374. const span = { inline: 'span', styles: { textDecoration: 'line-through' }, exact: true };
  25375. const strike = { inline: 'strike', remove: 'all', preserve_attributes: ['class', 'style'] };
  25376. const s = { inline: 's', remove: 'all', preserve_attributes: ['class', 'style'] };
  25377. return schemaType !== 'html4' ? [s, span, strike] : [span, s, strike];
  25378. })(),
  25379. forecolor: { inline: 'span', styles: { color: '%value' }, links: true, remove_similar: true, clear_child_styles: true },
  25380. hilitecolor: { inline: 'span', styles: { backgroundColor: '%value' }, links: true, remove_similar: true, clear_child_styles: true },
  25381. fontname: { inline: 'span', toggle: false, styles: { fontFamily: '%value' }, clear_child_styles: true },
  25382. fontsize: { inline: 'span', toggle: false, styles: { fontSize: '%value' }, clear_child_styles: true },
  25383. lineheight: { selector: 'h1,h2,h3,h4,h5,h6,p,li,td,th,div', styles: { lineHeight: '%value' } },
  25384. fontsize_class: { inline: 'span', attributes: { class: '%value' } },
  25385. blockquote: { block: 'blockquote', wrapper: true, remove: 'all' },
  25386. subscript: { inline: 'sub' },
  25387. superscript: { inline: 'sup' },
  25388. code: { inline: 'code' },
  25389. samp: { inline: 'samp' },
  25390. link: {
  25391. inline: 'a', selector: 'a', remove: 'all', split: true, deep: true,
  25392. onmatch: (node, _fmt, _itemName) => {
  25393. return isElement$7(node) && node.hasAttribute('href');
  25394. },
  25395. onformat: (elm, _fmt, vars) => {
  25396. Tools.each(vars, (value, key) => {
  25397. dom.setAttrib(elm, key, value);
  25398. });
  25399. }
  25400. },
  25401. lang: {
  25402. inline: 'span',
  25403. clear_child_styles: true,
  25404. remove_similar: true,
  25405. attributes: {
  25406. 'lang': '%value',
  25407. 'data-mce-lang': (vars) => { var _a; return (_a = vars === null || vars === void 0 ? void 0 : vars.customValue) !== null && _a !== void 0 ? _a : null; }
  25408. }
  25409. },
  25410. removeformat: [
  25411. {
  25412. selector: 'b,strong,em,i,font,u,strike,s,sub,sup,dfn,code,samp,kbd,var,cite,mark,q,del,ins,small',
  25413. remove: 'all',
  25414. split: true,
  25415. expand: false,
  25416. block_expand: true,
  25417. deep: true
  25418. },
  25419. { selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true },
  25420. { selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true }
  25421. ]
  25422. };
  25423. Tools.each('p h1 h2 h3 h4 h5 h6 div address pre dt dd'.split(/\s/), (name) => {
  25424. formats[name] = { block: name, remove: 'all' };
  25425. });
  25426. return formats;
  25427. };
  25428. const genericBase = {
  25429. remove_similar: true,
  25430. inherit: false
  25431. };
  25432. const cellBase = {
  25433. selector: 'td,th',
  25434. ...genericBase
  25435. };
  25436. const cellFormats = {
  25437. tablecellbackgroundcolor: {
  25438. styles: { backgroundColor: '%value' },
  25439. ...cellBase
  25440. },
  25441. tablecellverticalalign: {
  25442. styles: {
  25443. 'vertical-align': '%value'
  25444. },
  25445. ...cellBase
  25446. },
  25447. tablecellbordercolor: {
  25448. styles: { borderColor: '%value' },
  25449. ...cellBase
  25450. },
  25451. tablecellclass: {
  25452. classes: ['%value'],
  25453. ...cellBase
  25454. },
  25455. tableclass: {
  25456. selector: 'table',
  25457. classes: ['%value'],
  25458. ...genericBase
  25459. },
  25460. tablecellborderstyle: {
  25461. styles: { borderStyle: '%value' },
  25462. ...cellBase
  25463. },
  25464. tablecellborderwidth: {
  25465. styles: { borderWidth: '%value' },
  25466. ...cellBase
  25467. }
  25468. };
  25469. const get = constant(cellFormats);
  25470. const FormatRegistry = (editor) => {
  25471. const formats = {};
  25472. const get$2 = (name) => isNonNullable(name) ? formats[name] : formats;
  25473. const has = (name) => has$2(formats, name);
  25474. const register = (name, format) => {
  25475. if (name) {
  25476. if (!isString(name)) {
  25477. each$d(name, (format, name) => {
  25478. register(name, format);
  25479. });
  25480. }
  25481. else {
  25482. // Force format into array and add it to internal collection
  25483. if (!isArray$1(format)) {
  25484. format = [format];
  25485. }
  25486. each$e(format, (format) => {
  25487. // Set deep to false by default on selector formats this to avoid removing
  25488. // alignment on images inside paragraphs when alignment is changed on paragraphs
  25489. if (isUndefined(format.deep)) {
  25490. format.deep = !isSelectorFormat(format);
  25491. }
  25492. // Default to true
  25493. if (isUndefined(format.split)) {
  25494. format.split = !isSelectorFormat(format) || isInlineFormat(format);
  25495. }
  25496. // Default to true
  25497. if (isUndefined(format.remove) && isSelectorFormat(format) && !isInlineFormat(format)) {
  25498. format.remove = 'none';
  25499. }
  25500. // Mark format as a mixed format inline + block level
  25501. if (isSelectorFormat(format) && isInlineFormat(format)) {
  25502. format.mixed = true;
  25503. format.block_expand = true;
  25504. }
  25505. // Split classes if needed
  25506. if (isString(format.classes)) {
  25507. format.classes = format.classes.split(/\s+/);
  25508. }
  25509. });
  25510. formats[name] = format;
  25511. }
  25512. }
  25513. };
  25514. const unregister = (name) => {
  25515. if (name && formats[name]) {
  25516. delete formats[name];
  25517. }
  25518. return formats;
  25519. };
  25520. register(get$1(editor));
  25521. register(get());
  25522. register(getFormats(editor));
  25523. return {
  25524. get: get$2,
  25525. has,
  25526. register,
  25527. unregister
  25528. };
  25529. };
  25530. const each$3 = Tools.each;
  25531. const dom = DOMUtils.DOM;
  25532. const isPreviewItem = (item) => isNonNullable(item) && isObject(item);
  25533. const parsedSelectorToHtml = (ancestry, editor) => {
  25534. const schema = editor && editor.schema || Schema({});
  25535. const decorate = (elm, item) => {
  25536. if (item.classes.length > 0) {
  25537. dom.addClass(elm, item.classes.join(' '));
  25538. }
  25539. dom.setAttribs(elm, item.attrs);
  25540. };
  25541. const createElement = (sItem) => {
  25542. const item = isString(sItem) ? {
  25543. name: sItem,
  25544. classes: [],
  25545. attrs: {}
  25546. } : sItem;
  25547. const elm = dom.create(item.name);
  25548. decorate(elm, item);
  25549. return elm;
  25550. };
  25551. const getRequiredParent = (elm, candidate) => {
  25552. const elmRule = schema.getElementRule(elm.nodeName.toLowerCase());
  25553. const parentsRequired = elmRule === null || elmRule === void 0 ? void 0 : elmRule.parentsRequired;
  25554. if (parentsRequired && parentsRequired.length) {
  25555. return candidate && contains$2(parentsRequired, candidate) ? candidate : parentsRequired[0];
  25556. }
  25557. else {
  25558. return false;
  25559. }
  25560. };
  25561. const wrapInHtml = (elm, ancestors, siblings) => {
  25562. let parentCandidate;
  25563. const ancestor = ancestors[0];
  25564. const ancestorName = isPreviewItem(ancestor) ? ancestor.name : undefined;
  25565. const parentRequired = getRequiredParent(elm, ancestorName);
  25566. if (parentRequired) {
  25567. if (ancestorName === parentRequired) {
  25568. parentCandidate = ancestor;
  25569. ancestors = ancestors.slice(1);
  25570. }
  25571. else {
  25572. parentCandidate = parentRequired;
  25573. }
  25574. }
  25575. else if (ancestor) {
  25576. parentCandidate = ancestor;
  25577. ancestors = ancestors.slice(1);
  25578. }
  25579. else if (!siblings) {
  25580. return elm;
  25581. }
  25582. // if no more ancestry, wrap in generic div
  25583. const parent = parentCandidate ? createElement(parentCandidate) : dom.create('div');
  25584. parent.appendChild(elm);
  25585. if (siblings) {
  25586. Tools.each(siblings, (sibling) => {
  25587. const siblingElm = createElement(sibling);
  25588. parent.insertBefore(siblingElm, elm);
  25589. });
  25590. }
  25591. const parentSiblings = isPreviewItem(parentCandidate) ? parentCandidate.siblings : undefined;
  25592. return wrapInHtml(parent, ancestors, parentSiblings);
  25593. };
  25594. const fragment = dom.create('div');
  25595. if (ancestry.length > 0) {
  25596. const item = ancestry[0];
  25597. const elm = createElement(item);
  25598. const siblings = isPreviewItem(item) ? item.siblings : undefined;
  25599. fragment.appendChild(wrapInHtml(elm, ancestry.slice(1), siblings));
  25600. }
  25601. return fragment;
  25602. };
  25603. const parseSelectorItem = (item) => {
  25604. item = Tools.trim(item);
  25605. let tagName = 'div';
  25606. const obj = {
  25607. name: tagName,
  25608. classes: [],
  25609. attrs: {},
  25610. selector: item
  25611. };
  25612. if (item !== '*') {
  25613. // matching IDs, CLASSes, ATTRIBUTES and PSEUDOs
  25614. tagName = item.replace(/(?:([#\.]|::?)([\w\-]+)|(\[)([^\]]+)\]?)/g, ($0, $1, $2, $3, $4) => {
  25615. switch ($1) {
  25616. case '#':
  25617. obj.attrs.id = $2;
  25618. break;
  25619. case '.':
  25620. obj.classes.push($2);
  25621. break;
  25622. case ':':
  25623. if (Tools.inArray('checked disabled enabled read-only required'.split(' '), $2) !== -1) {
  25624. obj.attrs[$2] = $2;
  25625. }
  25626. break;
  25627. }
  25628. // attribute matched
  25629. if ($3 === '[') {
  25630. const m = $4.match(/([\w\-]+)(?:\=\"([^\"]+))?/);
  25631. if (m) {
  25632. obj.attrs[m[1]] = m[2];
  25633. }
  25634. }
  25635. return '';
  25636. });
  25637. }
  25638. obj.name = tagName || 'div';
  25639. return obj;
  25640. };
  25641. const parseSelector = (selector) => {
  25642. if (!isString(selector)) {
  25643. return [];
  25644. }
  25645. // take into account only first one
  25646. selector = selector.split(/\s*,\s*/)[0];
  25647. // tighten
  25648. selector = selector.replace(/\s*(~\+|~|\+|>)\s*/g, '$1');
  25649. // split either on > or on space, but not the one inside brackets
  25650. return Tools.map(selector.split(/(?:>|\s+(?![^\[\]]+\]))/), (item) => {
  25651. // process each sibling selector separately
  25652. const siblings = Tools.map(item.split(/(?:~\+|~|\+)/), parseSelectorItem);
  25653. const obj = siblings.pop(); // the last one is our real target
  25654. if (siblings.length) {
  25655. obj.siblings = siblings;
  25656. }
  25657. return obj;
  25658. }).reverse();
  25659. };
  25660. const getCssText = (editor, format) => {
  25661. let previewCss = '';
  25662. let previewStyles = getPreviewStyles(editor);
  25663. // No preview forced
  25664. if (previewStyles === '') {
  25665. return '';
  25666. }
  25667. // Removes any variables since these can't be previewed
  25668. const removeVars = (val) => {
  25669. return isString(val) ? val.replace(/%(\w+)/g, '') : '';
  25670. };
  25671. const getComputedStyle = (name, elm) => {
  25672. return dom.getStyle(elm !== null && elm !== void 0 ? elm : editor.getBody(), name, true);
  25673. };
  25674. // Create block/inline element to use for preview
  25675. if (isString(format)) {
  25676. const formats = editor.formatter.get(format);
  25677. if (!formats) {
  25678. return '';
  25679. }
  25680. format = formats[0];
  25681. }
  25682. // Format specific preview override
  25683. // TODO: This should probably be further reduced by the previewStyles option
  25684. if ('preview' in format) {
  25685. const preview = format.preview;
  25686. if (preview === false) {
  25687. return '';
  25688. }
  25689. else {
  25690. previewStyles = preview || previewStyles;
  25691. }
  25692. }
  25693. let name = format.block || format.inline || 'span';
  25694. let previewFrag;
  25695. const items = parseSelector(format.selector);
  25696. if (items.length > 0) {
  25697. if (!items[0].name) { // e.g. something like ul > .someClass was provided
  25698. items[0].name = name;
  25699. }
  25700. name = format.selector;
  25701. previewFrag = parsedSelectorToHtml(items, editor);
  25702. }
  25703. else {
  25704. previewFrag = parsedSelectorToHtml([name], editor);
  25705. }
  25706. const previewElm = dom.select(name, previewFrag)[0] || previewFrag.firstChild;
  25707. // Add format styles to preview element
  25708. each$3(format.styles, (value, name) => {
  25709. const newValue = removeVars(value);
  25710. if (newValue) {
  25711. dom.setStyle(previewElm, name, newValue);
  25712. }
  25713. });
  25714. // Add attributes to preview element
  25715. each$3(format.attributes, (value, name) => {
  25716. const newValue = removeVars(value);
  25717. if (newValue) {
  25718. dom.setAttrib(previewElm, name, newValue);
  25719. }
  25720. });
  25721. // Add classes to preview element
  25722. each$3(format.classes, (value) => {
  25723. const newValue = removeVars(value);
  25724. if (!dom.hasClass(previewElm, newValue)) {
  25725. dom.addClass(previewElm, newValue);
  25726. }
  25727. });
  25728. editor.dispatch('PreviewFormats');
  25729. // Add the previewElm outside the visual area
  25730. dom.setStyles(previewFrag, { position: 'absolute', left: -0xFFFF });
  25731. editor.getBody().appendChild(previewFrag);
  25732. // Get parent container font size so we can compute px values out of em/% for older IE:s
  25733. const rawParentFontSize = getComputedStyle('fontSize');
  25734. const parentFontSize = /px$/.test(rawParentFontSize) ? parseInt(rawParentFontSize, 10) : 0;
  25735. each$3(previewStyles.split(' '), (name) => {
  25736. let value = getComputedStyle(name, previewElm);
  25737. // If background is transparent then check if the body has a background color we can use
  25738. if (name === 'background-color' && /transparent|rgba\s*\([^)]+,\s*0\)/.test(value)) {
  25739. value = getComputedStyle(name);
  25740. // Ignore white since it's the default color, not the nicest fix
  25741. // TODO: Fix this by detecting runtime style
  25742. if (rgbaToHexString(value).toLowerCase() === '#ffffff') {
  25743. return;
  25744. }
  25745. }
  25746. if (name === 'color') {
  25747. // Ignore black since it's the default color, not the nicest fix
  25748. // TODO: Fix this by detecting runtime style
  25749. if (rgbaToHexString(value).toLowerCase() === '#000000') {
  25750. return;
  25751. }
  25752. }
  25753. // Old IE won't calculate the font size so we need to do that manually
  25754. if (name === 'font-size') {
  25755. if (/em|%$/.test(value)) {
  25756. if (parentFontSize === 0) {
  25757. return;
  25758. }
  25759. // Convert font size from em/% to px
  25760. const numValue = parseFloat(value) / (/%$/.test(value) ? 100 : 1);
  25761. value = (numValue * parentFontSize) + 'px';
  25762. }
  25763. }
  25764. if (name === 'border' && value) {
  25765. previewCss += 'padding:0 2px;';
  25766. }
  25767. previewCss += name + ':' + value + ';';
  25768. });
  25769. editor.dispatch('AfterPreviewFormats');
  25770. // previewCss += 'line-height:normal';
  25771. dom.remove(previewFrag);
  25772. return previewCss;
  25773. };
  25774. const setup$x = (editor) => {
  25775. // Add some inline shortcuts
  25776. editor.addShortcut('meta+b', '', 'Bold');
  25777. editor.addShortcut('meta+i', '', 'Italic');
  25778. editor.addShortcut('meta+u', '', 'Underline');
  25779. // BlockFormat shortcuts keys
  25780. for (let i = 1; i <= 6; i++) {
  25781. editor.addShortcut('access+' + i, '', ['FormatBlock', false, 'h' + i]);
  25782. }
  25783. editor.addShortcut('access+7', '', ['FormatBlock', false, 'p']);
  25784. editor.addShortcut('access+8', '', ['FormatBlock', false, 'div']);
  25785. editor.addShortcut('access+9', '', ['FormatBlock', false, 'address']);
  25786. };
  25787. const Formatter = (editor) => {
  25788. const formats = FormatRegistry(editor);
  25789. const formatChangeState = Cell({});
  25790. setup$x(editor);
  25791. setup$B(editor);
  25792. if (!isRtc(editor)) {
  25793. setup$A(formatChangeState, editor);
  25794. }
  25795. return {
  25796. /**
  25797. * Returns the format by name or all formats if no name is specified.
  25798. *
  25799. * @method get
  25800. * @param {String} name Optional name to retrieve by.
  25801. * @return {Array/Object} Array/Object with all registered formats or a specific format.
  25802. */
  25803. get: formats.get,
  25804. /**
  25805. * Returns true or false if a format is registered for the specified name.
  25806. *
  25807. * @method has
  25808. * @param {String} name Format name to check if a format exists.
  25809. * @return {Boolean} True/False if a format for the specified name exists.
  25810. */
  25811. has: formats.has,
  25812. /**
  25813. * Registers a specific format by name.
  25814. *
  25815. * @method register
  25816. * @param {Object/String} name Name of the format for example "bold".
  25817. * @param {Object/Array} format Optional format object or array of format variants
  25818. * can only be omitted if the first arg is an object.
  25819. */
  25820. register: formats.register,
  25821. /**
  25822. * Unregister a specific format by name.
  25823. *
  25824. * @method unregister
  25825. * @param {String} name Name of the format for example "bold".
  25826. */
  25827. unregister: formats.unregister,
  25828. /**
  25829. * Applies the specified format to the current selection or specified node.
  25830. *
  25831. * @method apply
  25832. * @param {String} name Name of format to apply.
  25833. * @param {Object} vars Optional list of variables to replace within format before applying it.
  25834. * @param {Node} node Optional node to apply the format to defaults to current selection.
  25835. */
  25836. apply: (name, vars, node) => {
  25837. applyFormat(editor, name, vars, node);
  25838. },
  25839. /**
  25840. * Removes the specified format from the current selection or specified node.
  25841. *
  25842. * @method remove
  25843. * @param {String} name Name of format to remove.
  25844. * @param {Object} vars Optional list of variables to replace within format before removing it.
  25845. * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection.
  25846. */
  25847. remove: (name, vars, node, similar) => {
  25848. removeFormat(editor, name, vars, node, similar);
  25849. },
  25850. /**
  25851. * Toggles the specified format on/off.
  25852. *
  25853. * @method toggle
  25854. * @param {String} name Name of format to apply/remove.
  25855. * @param {Object} vars Optional list of variables to replace within format before applying/removing it.
  25856. * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection.
  25857. */
  25858. toggle: (name, vars, node) => {
  25859. toggleFormat(editor, name, vars, node);
  25860. },
  25861. /**
  25862. * Matches the current selection or specified node against the specified format name.
  25863. *
  25864. * @method match
  25865. * @param {String} name Name of format to match.
  25866. * @param {Object} vars Optional list of variables to replace before checking it.
  25867. * @param {Node} node Optional node to check.
  25868. * @param {Boolean} similar Optional argument to specify that similar formats should be checked instead of only exact formats.
  25869. * @return {Boolean} true/false if the specified selection/node matches the format.
  25870. */
  25871. match: (name, vars, node, similar) => matchFormat(editor, name, vars, node, similar),
  25872. /**
  25873. * Finds the closest matching format from a set of formats for the current selection.
  25874. *
  25875. * @method closest
  25876. * @param {Array} names Format names to check for.
  25877. * @return {String} The closest matching format name or null.
  25878. */
  25879. closest: (names) => closestFormat(editor, names),
  25880. /**
  25881. * Matches the current selection against the array of formats and returns a new array with matching formats.
  25882. *
  25883. * @method matchAll
  25884. * @param {Array} names Name of format to match.
  25885. * @param {Object} vars Optional list of variables to replace before checking it.
  25886. * @return {Array} Array with matched formats.
  25887. */
  25888. matchAll: (names, vars) => matchAllFormats(editor, names, vars),
  25889. /**
  25890. * Return true/false if the specified node has the specified format.
  25891. *
  25892. * @method matchNode
  25893. * @param {Node} node Node to check the format on.
  25894. * @param {String} name Format name to check.
  25895. * @param {Object} vars Optional list of variables to replace before checking it.
  25896. * @param {Boolean} similar Match format that has similar properties.
  25897. * @return {Object} Returns the format object it matches or undefined if it doesn't match.
  25898. */
  25899. matchNode: (node, name, vars, similar) => matchNodeFormat(editor, node, name, vars, similar),
  25900. /**
  25901. * Returns true/false if the specified format can be applied to the current selection or not. It
  25902. * will currently only check the state for selector formats, it returns true on all other format types.
  25903. *
  25904. * @method canApply
  25905. * @param {String} name Name of format to check.
  25906. * @return {Boolean} true/false if the specified format can be applied to the current selection/node.
  25907. */
  25908. canApply: (name) => canApplyFormat(editor, name),
  25909. /**
  25910. * Executes the specified callback when the current selection matches the formats or not.
  25911. *
  25912. * @method formatChanged
  25913. * @param {String} formats Comma separated list of formats to check for.
  25914. * @param {Function} callback Callback with state and args when the format is changed/toggled on/off.
  25915. * @param {Boolean} similar True/false state if the match should handle similar or exact formats.
  25916. * @param {Object} vars Restrict the format being watched to only match if the variables applied are equal to vars.
  25917. */
  25918. formatChanged: (formats, callback, similar, vars) => formatChanged(editor, formatChangeState, formats, callback, similar, vars),
  25919. /**
  25920. * Returns a preview css text for the specified format.
  25921. *
  25922. * @method getCssText
  25923. * @param {String/Object} format Format to generate preview css text for.
  25924. * @return {String} Css text for the specified format.
  25925. * @example
  25926. * const cssText1 = editor.formatter.getCssText('bold');
  25927. * const cssText2 = editor.formatter.getCssText({ inline: 'b' });
  25928. */
  25929. getCssText: curry(getCssText, editor)
  25930. };
  25931. };
  25932. // Avoid adding non-typing undo levels for commands that could cause duplicate undo levels to be created
  25933. // or do not alter the editor content or selection in any way
  25934. const shouldIgnoreCommand = (cmd) => {
  25935. switch (cmd.toLowerCase()) {
  25936. case 'undo':
  25937. case 'redo':
  25938. case 'mcefocus':
  25939. return true;
  25940. default:
  25941. return false;
  25942. }
  25943. };
  25944. const registerEvents = (editor, undoManager, locks) => {
  25945. const isFirstTypedCharacter = Cell(false);
  25946. const addNonTypingUndoLevel = (e) => {
  25947. setTyping(undoManager, false, locks);
  25948. undoManager.add({}, e);
  25949. };
  25950. // Add initial undo level when the editor is initialized
  25951. editor.on('init', () => {
  25952. undoManager.add();
  25953. });
  25954. // Get position before an execCommand is processed
  25955. editor.on('BeforeExecCommand', (e) => {
  25956. const cmd = e.command;
  25957. if (!shouldIgnoreCommand(cmd)) {
  25958. endTyping(undoManager, locks);
  25959. undoManager.beforeChange();
  25960. }
  25961. });
  25962. // Add undo level after an execCommand call was made
  25963. editor.on('ExecCommand', (e) => {
  25964. const cmd = e.command;
  25965. if (!shouldIgnoreCommand(cmd)) {
  25966. addNonTypingUndoLevel(e);
  25967. }
  25968. });
  25969. editor.on('ObjectResizeStart cut', () => {
  25970. undoManager.beforeChange();
  25971. });
  25972. editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel);
  25973. editor.on('dragend', addNonTypingUndoLevel);
  25974. editor.on('keyup', (e) => {
  25975. const keyCode = e.keyCode;
  25976. // If key is prevented then don't add undo level
  25977. // This would happen on keyboard shortcuts for example
  25978. if (e.isDefaultPrevented()) {
  25979. return;
  25980. }
  25981. const isMeta = Env.os.isMacOS() && e.key === 'Meta';
  25982. if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45 || e.ctrlKey || isMeta) {
  25983. addNonTypingUndoLevel();
  25984. editor.nodeChanged();
  25985. }
  25986. if (keyCode === 46 || keyCode === 8) {
  25987. editor.nodeChanged();
  25988. }
  25989. // Fire a TypingUndo event on the first character entered
  25990. if (isFirstTypedCharacter.get() && undoManager.typing && !isEq$1(createFromEditor(editor), undoManager.data[0])) {
  25991. if (!editor.isDirty()) {
  25992. editor.setDirty(true);
  25993. }
  25994. editor.dispatch('TypingUndo');
  25995. isFirstTypedCharacter.set(false);
  25996. editor.nodeChanged();
  25997. }
  25998. });
  25999. editor.on('keydown', (e) => {
  26000. const keyCode = e.keyCode;
  26001. // If key is prevented then don't add undo level
  26002. // This would happen on keyboard shortcuts for example
  26003. if (e.isDefaultPrevented()) {
  26004. return;
  26005. }
  26006. // Is character position keys left,right,up,down,home,end,pgdown,pgup,enter
  26007. if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45) {
  26008. if (undoManager.typing) {
  26009. addNonTypingUndoLevel(e);
  26010. }
  26011. return;
  26012. }
  26013. // If key isn't Ctrl+Alt/AltGr
  26014. const modKey = (e.ctrlKey && !e.altKey) || e.metaKey;
  26015. if ((keyCode < 16 || keyCode > 20) && keyCode !== 224 && keyCode !== 91 && !undoManager.typing && !modKey) {
  26016. undoManager.beforeChange();
  26017. setTyping(undoManager, true, locks);
  26018. undoManager.add({}, e);
  26019. isFirstTypedCharacter.set(true);
  26020. return;
  26021. }
  26022. const hasOnlyMetaOrCtrlModifier = Env.os.isMacOS() ? e.metaKey : e.ctrlKey && !e.altKey;
  26023. if (hasOnlyMetaOrCtrlModifier) {
  26024. undoManager.beforeChange();
  26025. }
  26026. });
  26027. editor.on('mousedown', (e) => {
  26028. if (undoManager.typing) {
  26029. addNonTypingUndoLevel(e);
  26030. }
  26031. });
  26032. // Special inputType, currently only Chrome implements this: https://www.w3.org/TR/input-events-2/#x5.1.2-attributes
  26033. const isInsertReplacementText = (event) => event.inputType === 'insertReplacementText';
  26034. // Safari just shows inputType `insertText` but with data set to null so we can use that
  26035. const isInsertTextDataNull = (event) => event.inputType === 'insertText' && event.data === null;
  26036. const isInsertFromPasteOrDrop = (event) => event.inputType === 'insertFromPaste' || event.inputType === 'insertFromDrop';
  26037. // For detecting when user has replaced text using the browser built-in spell checker or paste/drop events
  26038. editor.on('input', (e) => {
  26039. if (e.inputType && (isInsertReplacementText(e) || isInsertTextDataNull(e) || isInsertFromPasteOrDrop(e))) {
  26040. addNonTypingUndoLevel(e);
  26041. }
  26042. });
  26043. editor.on('AddUndo Undo Redo ClearUndos', (e) => {
  26044. if (!e.isDefaultPrevented()) {
  26045. editor.nodeChanged();
  26046. }
  26047. });
  26048. };
  26049. const addKeyboardShortcuts = (editor) => {
  26050. editor.addShortcut('meta+z', '', 'Undo');
  26051. editor.addShortcut('meta+y,meta+shift+z', '', 'Redo');
  26052. };
  26053. /**
  26054. * This class handles the undo/redo history levels for the editor. Since the built-in undo/redo has major drawbacks a custom one was needed.
  26055. *
  26056. * @class tinymce.UndoManager
  26057. */
  26058. const UndoManager = (editor) => {
  26059. const beforeBookmark = value$1();
  26060. const locks = Cell(0);
  26061. const index = Cell(0);
  26062. /* eslint consistent-this:0 */
  26063. const undoManager = {
  26064. data: [], // Gets mutated both internally and externally by plugins like remark, not documented
  26065. /**
  26066. * State if the user is currently typing or not. This will add a typing operation into one undo
  26067. * level instead of one new level for each keystroke.
  26068. *
  26069. * @field {Boolean} typing
  26070. */
  26071. typing: false,
  26072. /**
  26073. * Stores away a bookmark to be used when performing an undo action so that the selection is before
  26074. * the change has been made.
  26075. *
  26076. * @method beforeChange
  26077. */
  26078. beforeChange: () => {
  26079. beforeChange(editor, locks, beforeBookmark);
  26080. },
  26081. /**
  26082. * Adds a new undo level/snapshot to the undo list.
  26083. *
  26084. * @method add
  26085. * @param {Object} level Optional undo level object to add.
  26086. * @param {DOMEvent} event Optional event responsible for the creation of the undo level.
  26087. * @return {Object} Undo level that got added or null if a level wasn't needed.
  26088. */
  26089. add: (level, event) => {
  26090. return addUndoLevel(editor, undoManager, index, locks, beforeBookmark, level, event);
  26091. },
  26092. /**
  26093. * Dispatch a change event with current editor status as level and current undoManager layer as lastLevel
  26094. *
  26095. * @method dispatchChange
  26096. */
  26097. dispatchChange: () => {
  26098. editor.setDirty(true);
  26099. const level = createFromEditor(editor);
  26100. level.bookmark = getUndoBookmark(editor.selection);
  26101. editor.dispatch('change', {
  26102. level,
  26103. lastLevel: get$b(undoManager.data, index.get()).getOrUndefined()
  26104. });
  26105. },
  26106. /**
  26107. * Undoes the last action.
  26108. *
  26109. * @method undo
  26110. * @return {Object} Undo level or null if no undo was performed.
  26111. */
  26112. undo: () => {
  26113. return undo(editor, undoManager, locks, index);
  26114. },
  26115. /**
  26116. * Redoes the last action.
  26117. *
  26118. * @method redo
  26119. * @return {Object} Redo level or null if no redo was performed.
  26120. */
  26121. redo: () => {
  26122. return redo(editor, index, undoManager.data);
  26123. },
  26124. /**
  26125. * Removes all undo levels.
  26126. *
  26127. * @method clear
  26128. */
  26129. clear: () => {
  26130. clear(editor, undoManager, index);
  26131. },
  26132. /**
  26133. * Resets the undo manager levels by clearing all levels and then adding an initial level.
  26134. *
  26135. * @method reset
  26136. */
  26137. reset: () => {
  26138. reset(editor, undoManager);
  26139. },
  26140. /**
  26141. * Returns true/false if the undo manager has any undo levels.
  26142. *
  26143. * @method hasUndo
  26144. * @return {Boolean} true/false if the undo manager has any undo levels.
  26145. */
  26146. hasUndo: () => {
  26147. return hasUndo(editor, undoManager, index);
  26148. },
  26149. /**
  26150. * Returns true/false if the undo manager has any redo levels.
  26151. *
  26152. * @method hasRedo
  26153. * @return {Boolean} true/false if the undo manager has any redo levels.
  26154. */
  26155. hasRedo: () => {
  26156. return hasRedo(editor, undoManager, index);
  26157. },
  26158. /**
  26159. * Executes the specified mutator function as an undo transaction. The selection
  26160. * before the modification will be stored to the undo stack and if the DOM changes
  26161. * it will add a new undo level. Any logic within the translation that adds undo levels will
  26162. * be ignored. So a translation can include calls to execCommand or editor.insertContent.
  26163. *
  26164. * @method transact
  26165. * @param {Function} callback Function that gets executed and has dom manipulation logic in it.
  26166. * @return {Object} Undo level that got added or null it a level wasn't needed.
  26167. */
  26168. transact: (callback) => {
  26169. return transact(editor, undoManager, locks, callback);
  26170. },
  26171. /**
  26172. * Executes the specified mutator function as an undo transaction. But without adding an undo level.
  26173. * Any logic within the translation that adds undo levels will be ignored. So a translation can
  26174. * include calls to execCommand or editor.insertContent.
  26175. *
  26176. * @method ignore
  26177. * @param {Function} callback Function that gets executed and has dom manipulation logic in it.
  26178. */
  26179. ignore: (callback) => {
  26180. ignore(editor, locks, callback);
  26181. },
  26182. /**
  26183. * Adds an extra "hidden" undo level by first applying the first mutation and store that to the undo stack
  26184. * then roll back that change and do the second mutation on top of the stack. This will produce an extra
  26185. * undo level that the user doesn't see until they undo.
  26186. *
  26187. * @method extra
  26188. * @param {Function} callback1 Function that does mutation but gets stored as a "hidden" extra undo level.
  26189. * @param {Function} callback2 Function that does mutation but gets displayed to the user.
  26190. */
  26191. extra: (callback1, callback2) => {
  26192. extra(editor, undoManager, index, callback1, callback2);
  26193. }
  26194. };
  26195. if (!isRtc(editor)) {
  26196. registerEvents(editor, undoManager, locks);
  26197. }
  26198. addKeyboardShortcuts(editor);
  26199. return undoManager;
  26200. };
  26201. const nonTypingKeycodes = [
  26202. // tab, esc, home, end
  26203. 9, 27, VK.HOME, VK.END,
  26204. // pause, capslock, print screen, numlock, scroll lock
  26205. 19, 20, 44, 144, 145,
  26206. // page up/down, insert
  26207. 33, 34, 45,
  26208. // alt, shift, ctrl
  26209. 16, 17, 18,
  26210. // meta/windows key
  26211. 91, 92, 93,
  26212. // direction
  26213. VK.DOWN, VK.UP, VK.LEFT, VK.RIGHT
  26214. ].concat(
  26215. // Meta key on firefox is different
  26216. Env.browser.isFirefox() ? [224] : []);
  26217. const placeholderAttr = 'data-mce-placeholder';
  26218. const isKeyboardEvent = (e) => e.type === 'keydown' || e.type === 'keyup';
  26219. const isDeleteEvent = (e) => {
  26220. const keyCode = e.keyCode;
  26221. return keyCode === VK.BACKSPACE || keyCode === VK.DELETE;
  26222. };
  26223. const isNonTypingKeyboardEvent = (e) => {
  26224. if (isKeyboardEvent(e)) {
  26225. const keyCode = e.keyCode;
  26226. // Ctrl/Meta/Alt key pressed, F1-12 or non typing keycode
  26227. return !isDeleteEvent(e) && (VK.metaKeyPressed(e) || e.altKey || keyCode >= 112 && keyCode <= 123 || contains$2(nonTypingKeycodes, keyCode));
  26228. }
  26229. else {
  26230. return false;
  26231. }
  26232. };
  26233. const isTypingKeyboardEvent = (e) =>
  26234. // 229 === Unidentified, so since we don't know what it is treat it as a non typing event on keyup but as a typing event on keydown
  26235. // Android will generally always send a 229 keycode since it uses an IME to input text
  26236. isKeyboardEvent(e) && !(isDeleteEvent(e) || e.type === 'keyup' && e.keyCode === 229);
  26237. const isVisuallyEmpty = (dom, rootElm, forcedRootBlock) => {
  26238. if (dom.isEmpty(rootElm, undefined, { skipBogus: false, includeZwsp: true })) {
  26239. // Ensure the node matches the forced_root_block setting, as the content could be an empty list, etc...
  26240. // and also check that the content isn't indented
  26241. const firstElement = rootElm.firstElementChild;
  26242. if (!firstElement) {
  26243. return true;
  26244. }
  26245. else if (dom.getStyle(rootElm.firstElementChild, 'padding-left') || dom.getStyle(rootElm.firstElementChild, 'padding-right')) {
  26246. return false;
  26247. }
  26248. else {
  26249. return forcedRootBlock === firstElement.nodeName.toLowerCase();
  26250. }
  26251. }
  26252. else {
  26253. return false;
  26254. }
  26255. };
  26256. const setup$w = (editor) => {
  26257. var _a;
  26258. const dom = editor.dom;
  26259. const rootBlock = getForcedRootBlock(editor);
  26260. const placeholder = (_a = getPlaceholder(editor)) !== null && _a !== void 0 ? _a : '';
  26261. const updatePlaceholder = (e, initial) => {
  26262. if (isNonTypingKeyboardEvent(e)) {
  26263. return;
  26264. }
  26265. // Check to see if we should show the placeholder
  26266. const body = editor.getBody();
  26267. const showPlaceholder = isTypingKeyboardEvent(e) ? false : isVisuallyEmpty(dom, body, rootBlock);
  26268. // Update the attribute as required
  26269. const isPlaceholderShown = dom.getAttrib(body, placeholderAttr) !== '';
  26270. if (isPlaceholderShown !== showPlaceholder || initial) {
  26271. dom.setAttrib(body, placeholderAttr, showPlaceholder ? placeholder : null);
  26272. firePlaceholderToggle(editor, showPlaceholder);
  26273. // Swap the key listener state
  26274. editor.on(showPlaceholder ? 'keydown' : 'keyup', updatePlaceholder);
  26275. editor.off(showPlaceholder ? 'keyup' : 'keydown', updatePlaceholder);
  26276. }
  26277. };
  26278. if (isNotEmpty(placeholder)) {
  26279. editor.on('init', (e) => {
  26280. // Setup the initial state
  26281. updatePlaceholder(e, true);
  26282. editor.on('change SetContent ExecCommand', updatePlaceholder);
  26283. // TINY-4828: Update the placeholder after pasting content. This needs to use a timeout as
  26284. // the browser doesn't update the dom until after the paste event has fired
  26285. editor.on('paste', (e) => Delay.setEditorTimeout(editor, () => updatePlaceholder(e)));
  26286. });
  26287. }
  26288. };
  26289. const matchNodeName = (name) => (node) => isNonNullable(node) && node.nodeName.toLowerCase() === name;
  26290. const matchNodeNames = (regex) => (node) => isNonNullable(node) && regex.test(node.nodeName);
  26291. const isTextNode$1 = (node) => isNonNullable(node) && node.nodeType === 3;
  26292. const isElement$1 = (node) => isNonNullable(node) && node.nodeType === 1;
  26293. const isListNode = matchNodeNames(/^(OL|UL|DL)$/);
  26294. const isOlUlNode = matchNodeNames(/^(OL|UL)$/);
  26295. const isListItemNode = matchNodeNames(/^(LI|DT|DD)$/);
  26296. const isDlItemNode = matchNodeNames(/^(DT|DD)$/);
  26297. const isBr$1 = matchNodeName('br');
  26298. const isFirstChild$1 = (node) => { var _a; return ((_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.firstChild) === node; };
  26299. const isTextBlock = (editor, node) => isNonNullable(node) && node.nodeName in editor.schema.getTextBlockElements();
  26300. const isBlock = (node, blockElements) => isNonNullable(node) && node.nodeName in blockElements;
  26301. const isVoid = (editor, node) => isNonNullable(node) && node.nodeName in editor.schema.getVoidElements();
  26302. const isBogusBr = (dom, node) => {
  26303. if (!isBr$1(node)) {
  26304. return false;
  26305. }
  26306. return dom.isBlock(node.nextSibling) && !isBr$1(node.previousSibling);
  26307. };
  26308. const isEmpty$1 = (dom, elm, keepBookmarks) => {
  26309. const empty = dom.isEmpty(elm);
  26310. if (keepBookmarks && dom.select('span[data-mce-type=bookmark]', elm).length > 0) {
  26311. return false;
  26312. }
  26313. return empty;
  26314. };
  26315. const isChildOfBody = (dom, elm) => dom.isChildOf(elm, dom.getRoot());
  26316. const getNormalizedPoint = (container, offset) => {
  26317. if (isTextNode$1(container)) {
  26318. return { container, offset };
  26319. }
  26320. const node = RangeUtils.getNode(container, offset);
  26321. if (isTextNode$1(node)) {
  26322. return {
  26323. container: node,
  26324. offset: offset >= container.childNodes.length ? node.data.length : 0
  26325. };
  26326. }
  26327. else if (node.previousSibling && isTextNode$1(node.previousSibling)) {
  26328. return {
  26329. container: node.previousSibling,
  26330. offset: node.previousSibling.data.length
  26331. };
  26332. }
  26333. else if (node.nextSibling && isTextNode$1(node.nextSibling)) {
  26334. return {
  26335. container: node.nextSibling,
  26336. offset: 0
  26337. };
  26338. }
  26339. return { container, offset };
  26340. };
  26341. const normalizeRange = (rng) => {
  26342. const outRng = rng.cloneRange();
  26343. const rangeStart = getNormalizedPoint(rng.startContainer, rng.startOffset);
  26344. outRng.setStart(rangeStart.container, rangeStart.offset);
  26345. const rangeEnd = getNormalizedPoint(rng.endContainer, rng.endOffset);
  26346. outRng.setEnd(rangeEnd.container, rangeEnd.offset);
  26347. return outRng;
  26348. };
  26349. const DOM$a = DOMUtils.DOM;
  26350. /**
  26351. * Returns a range bookmark. This will convert indexed bookmarks into temporary span elements with
  26352. * index 0 so that they can be restored properly after the DOM has been modified. Text bookmarks will not have spans
  26353. * added to them since they can be restored after a dom operation.
  26354. *
  26355. * So this: <p><b>|</b><b>|</b></p>
  26356. * becomes: <p><b><span data-mce-type="bookmark">|</span></b><b data-mce-type="bookmark">|</span></b></p>
  26357. *
  26358. */
  26359. const createBookmark = (rng) => {
  26360. const bookmark = {};
  26361. const setupEndPoint = (start) => {
  26362. let container = rng[start ? 'startContainer' : 'endContainer'];
  26363. let offset = rng[start ? 'startOffset' : 'endOffset'];
  26364. if (isElement$1(container)) {
  26365. const offsetNode = DOM$a.create('span', { 'data-mce-type': 'bookmark' });
  26366. if (container.hasChildNodes()) {
  26367. offset = Math.min(offset, container.childNodes.length - 1);
  26368. if (start) {
  26369. container.insertBefore(offsetNode, container.childNodes[offset]);
  26370. }
  26371. else {
  26372. DOM$a.insertAfter(offsetNode, container.childNodes[offset]);
  26373. }
  26374. }
  26375. else {
  26376. container.appendChild(offsetNode);
  26377. }
  26378. container = offsetNode;
  26379. offset = 0;
  26380. }
  26381. bookmark[start ? 'startContainer' : 'endContainer'] = container;
  26382. bookmark[start ? 'startOffset' : 'endOffset'] = offset;
  26383. };
  26384. setupEndPoint(true);
  26385. if (!rng.collapsed) {
  26386. setupEndPoint();
  26387. }
  26388. return bookmark;
  26389. };
  26390. const resolveBookmark$1 = (bookmark) => {
  26391. const restoreEndPoint = (start) => {
  26392. const nodeIndex = (container) => {
  26393. var _a;
  26394. let node = (_a = container.parentNode) === null || _a === void 0 ? void 0 : _a.firstChild;
  26395. let idx = 0;
  26396. while (node) {
  26397. if (node === container) {
  26398. return idx;
  26399. }
  26400. // Skip data-mce-type=bookmark nodes
  26401. if (!isElement$1(node) || node.getAttribute('data-mce-type') !== 'bookmark') {
  26402. idx++;
  26403. }
  26404. node = node.nextSibling;
  26405. }
  26406. return -1;
  26407. };
  26408. let container = bookmark[start ? 'startContainer' : 'endContainer'];
  26409. let offset = bookmark[start ? 'startOffset' : 'endOffset'];
  26410. if (!container) {
  26411. return;
  26412. }
  26413. if (isElement$1(container) && container.parentNode) {
  26414. const node = container;
  26415. offset = nodeIndex(container);
  26416. container = container.parentNode;
  26417. DOM$a.remove(node);
  26418. if (!container.hasChildNodes() && DOM$a.isBlock(container)) {
  26419. container.appendChild(DOM$a.create('br'));
  26420. }
  26421. }
  26422. bookmark[start ? 'startContainer' : 'endContainer'] = container;
  26423. bookmark[start ? 'startOffset' : 'endOffset'] = offset;
  26424. };
  26425. restoreEndPoint(true);
  26426. restoreEndPoint();
  26427. const rng = DOM$a.createRng();
  26428. rng.setStart(bookmark.startContainer, bookmark.startOffset);
  26429. if (bookmark.endContainer) {
  26430. rng.setEnd(bookmark.endContainer, bookmark.endOffset);
  26431. }
  26432. return normalizeRange(rng);
  26433. };
  26434. const DOM$9 = DOMUtils.DOM;
  26435. const normalizeList = (dom, list) => {
  26436. const parentNode = list.parentElement;
  26437. // Move UL/OL to previous LI if it's the only child of a LI
  26438. if (parentNode && parentNode.nodeName === 'LI' && parentNode.firstChild === list) {
  26439. const sibling = parentNode.previousSibling;
  26440. if (sibling && sibling.nodeName === 'LI') {
  26441. sibling.appendChild(list);
  26442. if (isEmpty$1(dom, parentNode)) {
  26443. DOM$9.remove(parentNode);
  26444. }
  26445. }
  26446. else {
  26447. DOM$9.setStyle(parentNode, 'listStyleType', 'none');
  26448. }
  26449. }
  26450. // Append OL/UL to previous LI if it's in a parent OL/UL i.e. old HTML4
  26451. if (isListNode(parentNode)) {
  26452. const sibling = parentNode.previousSibling;
  26453. if (sibling && sibling.nodeName === 'LI') {
  26454. sibling.appendChild(list);
  26455. }
  26456. }
  26457. };
  26458. const normalizeLists = (dom, element) => {
  26459. const lists = Tools.grep(dom.select('ol,ul', element));
  26460. Tools.each(lists, (list) => {
  26461. normalizeList(dom, list);
  26462. });
  26463. };
  26464. const listNames = ['OL', 'UL', 'DL'];
  26465. const listSelector = listNames.join(',');
  26466. const getParentList = (editor, node) => {
  26467. const selectionStart = node || editor.selection.getStart(true);
  26468. return editor.dom.getParent(selectionStart, listSelector, getClosestListHost(editor, selectionStart));
  26469. };
  26470. const isParentListSelected = (parentList, selectedBlocks) => isNonNullable(parentList) && selectedBlocks.length === 1 && selectedBlocks[0] === parentList;
  26471. const findSubLists = (parentList) => filter$5(parentList.querySelectorAll(listSelector), isListNode);
  26472. const getSelectedSubLists = (editor) => {
  26473. const parentList = getParentList(editor);
  26474. const selectedBlocks = editor.selection.getSelectedBlocks();
  26475. if (isParentListSelected(parentList, selectedBlocks)) {
  26476. return findSubLists(parentList);
  26477. }
  26478. else {
  26479. return filter$5(selectedBlocks, (elm) => {
  26480. return isListNode(elm) && parentList !== elm;
  26481. });
  26482. }
  26483. };
  26484. const findParentListItemsNodes = (editor, elms) => {
  26485. const listItemsElms = Tools.map(elms, (elm) => {
  26486. const parentLi = editor.dom.getParent(elm, 'li,dd,dt', getClosestListHost(editor, elm));
  26487. return parentLi ? parentLi : elm;
  26488. });
  26489. return unique$1(listItemsElms);
  26490. };
  26491. const getSelectedListItems = (editor) => {
  26492. const selectedBlocks = editor.selection.getSelectedBlocks();
  26493. return filter$5(findParentListItemsNodes(editor, selectedBlocks), isListItemNode);
  26494. };
  26495. const getSelectedDlItems = (editor) => filter$5(getSelectedListItems(editor), isDlItemNode);
  26496. const getClosestEditingHost = (editor, elm) => {
  26497. const parentTableCell = editor.dom.getParents(elm, 'TD,TH');
  26498. return parentTableCell.length > 0 ? parentTableCell[0] : editor.getBody();
  26499. };
  26500. const isListHost = (schema, node) => !isListNode(node) && !isListItemNode(node) && exists(listNames, (listName) => schema.isValidChild(node.nodeName, listName));
  26501. const getClosestListHost = (editor, elm) => {
  26502. const parentBlocks = editor.dom.getParents(elm, editor.dom.isBlock);
  26503. const isNotForcedRootBlock = (elm) => elm.nodeName.toLowerCase() !== getForcedRootBlock(editor);
  26504. const parentBlock = find$2(parentBlocks, (elm) => isNotForcedRootBlock(elm) && isListHost(editor.schema, elm));
  26505. return parentBlock.getOr(editor.getBody());
  26506. };
  26507. const isListInsideAnLiWithFirstAndLastNotListElement = (list) => parent(list).exists((parent) => isListItemNode(parent.dom)
  26508. && firstChild(parent).exists((firstChild) => !isListNode(firstChild.dom))
  26509. && lastChild(parent).exists((lastChild) => !isListNode(lastChild.dom)));
  26510. const findLastParentListNode = (editor, elm) => {
  26511. const parentLists = editor.dom.getParents(elm, 'ol,ul', getClosestListHost(editor, elm));
  26512. return last$2(parentLists);
  26513. };
  26514. const getSelectedLists = (editor) => {
  26515. const firstList = findLastParentListNode(editor, editor.selection.getStart());
  26516. const subsequentLists = filter$5(editor.selection.getSelectedBlocks(), isOlUlNode);
  26517. return firstList.toArray().concat(subsequentLists);
  26518. };
  26519. const getParentLists = (editor) => {
  26520. const elm = editor.selection.getStart();
  26521. return editor.dom.getParents(elm, 'ol,ul', getClosestListHost(editor, elm));
  26522. };
  26523. const getSelectedListRoots = (editor) => {
  26524. const selectedLists = getSelectedLists(editor);
  26525. const parentLists = getParentLists(editor);
  26526. return find$2(parentLists, (p) => isListInsideAnLiWithFirstAndLastNotListElement(SugarElement.fromDom(p))).fold(() => getUniqueListRoots(editor, selectedLists), (l) => [l]);
  26527. };
  26528. const getUniqueListRoots = (editor, lists) => {
  26529. const listRoots = map$3(lists, (list) => findLastParentListNode(editor, list).getOr(list));
  26530. return unique$1(listRoots);
  26531. };
  26532. const isCustomList = (list) => /\btox\-/.test(list.className);
  26533. // Advlist/core/ListUtils.ts - Duplicated in Advlist plugin
  26534. const isWithinNonEditable = (editor, element) => element !== null && !editor.dom.isEditable(element);
  26535. const selectionIsWithinNonEditableList = (editor) => {
  26536. const parentList = getParentList(editor);
  26537. return isWithinNonEditable(editor, parentList) || !editor.selection.isEditable();
  26538. };
  26539. const isWithinNonEditableList$1 = (editor, element) => {
  26540. const parentList = editor.dom.getParent(element, 'ol,ul,dl');
  26541. return isWithinNonEditable(editor, parentList) || !editor.selection.isEditable();
  26542. };
  26543. const fireListEvent = (editor, action, element) => editor.dispatch('ListMutation', { action, element });
  26544. const createTextBlock$1 = (editor, contentNode, attrs = {}) => {
  26545. const dom = editor.dom;
  26546. const blockElements = editor.schema.getBlockElements();
  26547. const fragment = dom.createFragment();
  26548. const blockName = getForcedRootBlock(editor);
  26549. const blockAttrs = getForcedRootBlockAttrs(editor);
  26550. let node;
  26551. let textBlock;
  26552. let hasContentNode = false;
  26553. textBlock = dom.create(blockName, {
  26554. ...blockAttrs,
  26555. ...(attrs.style ? { style: attrs.style } : {})
  26556. });
  26557. if (!isBlock(contentNode.firstChild, blockElements)) {
  26558. fragment.appendChild(textBlock);
  26559. }
  26560. while ((node = contentNode.firstChild)) {
  26561. const nodeName = node.nodeName;
  26562. if (!hasContentNode && (nodeName !== 'SPAN' || node.getAttribute('data-mce-type') !== 'bookmark')) {
  26563. hasContentNode = true;
  26564. }
  26565. if (isBlock(node, blockElements)) {
  26566. fragment.appendChild(node);
  26567. textBlock = null;
  26568. }
  26569. else {
  26570. if (!textBlock) {
  26571. textBlock = dom.create(blockName, blockAttrs);
  26572. fragment.appendChild(textBlock);
  26573. }
  26574. textBlock.appendChild(node);
  26575. }
  26576. }
  26577. // BR is needed in empty blocks
  26578. if (!hasContentNode && textBlock) {
  26579. textBlock.appendChild(dom.create('br', { 'data-mce-bogus': '1' }));
  26580. }
  26581. return fragment;
  26582. };
  26583. const isList = (el) => is$1(el, 'OL,UL');
  26584. const isListItem$1 = (el) => is$1(el, 'LI');
  26585. const hasFirstChildList = (el) => firstChild(el).exists(isList);
  26586. const hasLastChildList = (el) => lastChild(el).exists(isList);
  26587. const canIncreaseDepthOfList = (editor, amount) => {
  26588. return getListMaxDepth(editor).map((max) => max >= amount).getOr(true);
  26589. };
  26590. const isEntryList = (entry) => 'listAttributes' in entry;
  26591. const isEntryComment = (entry) => 'isComment' in entry;
  26592. const isEntryFragment = (entry) => 'isFragment' in entry;
  26593. const isIndented = (entry) => entry.depth > 0;
  26594. const isSelected = (entry) => entry.isSelected;
  26595. const cloneItemContent = (li) => {
  26596. const children = children$1(li);
  26597. const content = hasLastChildList(li) ? children.slice(0, -1) : children;
  26598. return map$3(content, deep);
  26599. };
  26600. const createEntry = (li, depth, isSelected) => parent(li).filter(isElement$8).map((list) => ({
  26601. depth,
  26602. dirty: false,
  26603. isSelected,
  26604. content: cloneItemContent(li),
  26605. itemAttributes: clone$4(li),
  26606. listAttributes: clone$4(list),
  26607. listType: name(list),
  26608. isInPreviousLi: false
  26609. }));
  26610. const joinSegment = (parent, child) => {
  26611. append$1(parent.item, child.list);
  26612. };
  26613. const joinSegments = (segments) => {
  26614. for (let i = 1; i < segments.length; i++) {
  26615. joinSegment(segments[i - 1], segments[i]);
  26616. }
  26617. };
  26618. const appendSegments = (head$1, tail) => {
  26619. lift2(last$2(head$1), head(tail), joinSegment);
  26620. };
  26621. const createSegment = (scope, listType) => {
  26622. const segment = {
  26623. list: SugarElement.fromTag(listType, scope),
  26624. item: SugarElement.fromTag('li', scope)
  26625. };
  26626. append$1(segment.list, segment.item);
  26627. return segment;
  26628. };
  26629. const createSegments = (scope, entry, size) => {
  26630. const segments = [];
  26631. for (let i = 0; i < size; i++) {
  26632. segments.push(createSegment(scope, isEntryList(entry) ? entry.listType : entry.parentListType));
  26633. }
  26634. return segments;
  26635. };
  26636. const populateSegments = (segments, entry) => {
  26637. for (let i = 0; i < segments.length - 1; i++) {
  26638. set$2(segments[i].item, 'list-style-type', 'none');
  26639. }
  26640. last$2(segments).each((segment) => {
  26641. if (isEntryList(entry)) {
  26642. setAll$1(segment.list, entry.listAttributes);
  26643. setAll$1(segment.item, entry.itemAttributes);
  26644. }
  26645. append(segment.item, entry.content);
  26646. });
  26647. };
  26648. const normalizeSegment = (segment, entry) => {
  26649. if (name(segment.list) !== entry.listType) {
  26650. segment.list = mutate(segment.list, entry.listType);
  26651. }
  26652. setAll$1(segment.list, entry.listAttributes);
  26653. };
  26654. const createItem = (scope, attr, content) => {
  26655. const item = SugarElement.fromTag('li', scope);
  26656. setAll$1(item, attr);
  26657. append(item, content);
  26658. return item;
  26659. };
  26660. const appendItem = (segment, item) => {
  26661. append$1(segment.list, item);
  26662. segment.item = item;
  26663. };
  26664. const writeShallow = (scope, cast, entry) => {
  26665. const newCast = cast.slice(0, entry.depth);
  26666. last$2(newCast).each((segment) => {
  26667. if (isEntryList(entry)) {
  26668. const item = createItem(scope, entry.itemAttributes, entry.content);
  26669. appendItem(segment, item);
  26670. normalizeSegment(segment, entry);
  26671. }
  26672. else if (isEntryFragment(entry)) {
  26673. append(segment.item, entry.content);
  26674. }
  26675. else {
  26676. const item = SugarElement.fromHtml(`<!--${entry.content}-->`);
  26677. append$1(segment.list, item);
  26678. }
  26679. });
  26680. return newCast;
  26681. };
  26682. const writeDeep = (scope, cast, entry) => {
  26683. const segments = createSegments(scope, entry, entry.depth - cast.length);
  26684. joinSegments(segments);
  26685. populateSegments(segments, entry);
  26686. appendSegments(cast, segments);
  26687. return cast.concat(segments);
  26688. };
  26689. const composeList = (scope, entries) => {
  26690. let firstCommentEntryOpt = Optional.none();
  26691. const cast = foldl(entries, (cast, entry, i) => {
  26692. if (!isEntryComment(entry)) {
  26693. return entry.depth > cast.length ? writeDeep(scope, cast, entry) : writeShallow(scope, cast, entry);
  26694. }
  26695. else {
  26696. // this is needed becuase if the first element of the list is a comment we would not have the data to create the new list
  26697. if (i === 0) {
  26698. firstCommentEntryOpt = Optional.some(entry);
  26699. return cast;
  26700. }
  26701. return writeShallow(scope, cast, entry);
  26702. }
  26703. }, []);
  26704. firstCommentEntryOpt.each((firstCommentEntry) => {
  26705. const item = SugarElement.fromHtml(`<!--${firstCommentEntry.content}-->`);
  26706. head(cast).each((fistCast) => {
  26707. prepend(fistCast.list, item);
  26708. });
  26709. });
  26710. return head(cast).map((segment) => segment.list);
  26711. };
  26712. const indentEntry = (editor, indentation, entry) => {
  26713. switch (indentation) {
  26714. case "Indent" /* Indentation.Indent */:
  26715. if (canIncreaseDepthOfList(editor, entry.depth)) {
  26716. entry.depth++;
  26717. }
  26718. else {
  26719. return;
  26720. }
  26721. break;
  26722. case "Outdent" /* Indentation.Outdent */:
  26723. entry.depth--;
  26724. break;
  26725. case "Flatten" /* Indentation.Flatten */:
  26726. entry.depth = 0;
  26727. }
  26728. entry.dirty = true;
  26729. };
  26730. const cloneListProperties = (target, source) => {
  26731. if (isEntryList(target) && isEntryList(source)) {
  26732. target.listType = source.listType;
  26733. target.listAttributes = { ...source.listAttributes };
  26734. }
  26735. };
  26736. const cleanListProperties = (entry) => {
  26737. // Remove the start attribute if generating a new list
  26738. entry.listAttributes = filter$4(entry.listAttributes, (_value, key) => key !== 'start');
  26739. };
  26740. // Closest entry above/below in the same list
  26741. const closestSiblingEntry = (entries, start) => {
  26742. const depth = entries[start].depth;
  26743. // Ignore dirty items as they've been moved and won't have the right list data yet
  26744. const matches = (entry) => entry.depth === depth && !entry.dirty;
  26745. const until = (entry) => entry.depth < depth;
  26746. // Check in reverse to see if there's an entry as the same depth before the current entry
  26747. // but if not, then try to walk forwards as well
  26748. return findUntil$1(reverse(entries.slice(0, start)), matches, until)
  26749. .orThunk(() => findUntil$1(entries.slice(start + 1), matches, until));
  26750. };
  26751. const normalizeEntries = (entries) => {
  26752. each$e(entries, (entry, i) => {
  26753. closestSiblingEntry(entries, i).fold(() => {
  26754. if (entry.dirty && isEntryList(entry)) {
  26755. cleanListProperties(entry);
  26756. }
  26757. }, (matchingEntry) => cloneListProperties(entry, matchingEntry));
  26758. });
  26759. return entries;
  26760. };
  26761. const parseSingleItem = (depth, itemSelection, selectionState, item) => {
  26762. var _a;
  26763. if (isComment$1(item)) {
  26764. return [{
  26765. depth: depth + 1,
  26766. content: (_a = item.dom.nodeValue) !== null && _a !== void 0 ? _a : '',
  26767. dirty: false,
  26768. isSelected: false,
  26769. isComment: true
  26770. }];
  26771. }
  26772. itemSelection.each((selection) => {
  26773. if (eq(selection.start, item)) {
  26774. selectionState.set(true);
  26775. }
  26776. });
  26777. const currentItemEntry = createEntry(item, depth, selectionState.get());
  26778. // Update selectionState (end)
  26779. itemSelection.each((selection) => {
  26780. if (eq(selection.end, item)) {
  26781. selectionState.set(false);
  26782. }
  26783. });
  26784. const childListEntries = lastChild(item)
  26785. .filter(isList)
  26786. .map((list) => parseList(depth, itemSelection, selectionState, list))
  26787. .getOr([]);
  26788. return currentItemEntry.toArray().concat(childListEntries);
  26789. };
  26790. const parseItem = (depth, itemSelection, selectionState, item) => firstChild(item).filter(isList).fold(() => parseSingleItem(depth, itemSelection, selectionState, item), (list) => {
  26791. const parsedSiblings = foldl(children$1(item), (acc, liChild, i) => {
  26792. if (i === 0) {
  26793. return acc;
  26794. }
  26795. else {
  26796. if (isListItem$1(liChild)) {
  26797. return acc.concat(parseSingleItem(depth, itemSelection, selectionState, liChild));
  26798. }
  26799. else {
  26800. const fragment = {
  26801. isFragment: true,
  26802. depth,
  26803. content: [liChild],
  26804. isSelected: false,
  26805. dirty: false,
  26806. parentListType: name(list)
  26807. };
  26808. return acc.concat(fragment);
  26809. }
  26810. }
  26811. }, []);
  26812. return parseList(depth, itemSelection, selectionState, list).concat(parsedSiblings);
  26813. });
  26814. const parseList = (depth, itemSelection, selectionState, list) => bind$3(children$1(list), (element) => {
  26815. const parser = isList(element) ? parseList : parseItem;
  26816. const newDepth = depth + 1;
  26817. return parser(newDepth, itemSelection, selectionState, element);
  26818. });
  26819. const parseLists = (lists, itemSelection) => {
  26820. const selectionState = Cell(false);
  26821. const initialDepth = 0;
  26822. return map$3(lists, (list) => ({
  26823. sourceList: list,
  26824. entries: parseList(initialDepth, itemSelection, selectionState, list)
  26825. }));
  26826. };
  26827. const outdentedComposer = (editor, entries) => {
  26828. const normalizedEntries = normalizeEntries(entries);
  26829. return map$3(normalizedEntries, (entry) => {
  26830. const content = !isEntryComment(entry)
  26831. ? fromElements(entry.content)
  26832. : fromElements([SugarElement.fromHtml(`<!--${entry.content}-->`)]);
  26833. const listItemAttrs = isEntryList(entry) ? entry.itemAttributes : {};
  26834. return SugarElement.fromDom(createTextBlock$1(editor, content.dom, listItemAttrs));
  26835. });
  26836. };
  26837. const indentedComposer = (editor, entries) => {
  26838. const normalizedEntries = normalizeEntries(entries);
  26839. return composeList(editor.contentDocument, normalizedEntries).toArray();
  26840. };
  26841. const composeEntries = (editor, entries) => bind$3(groupBy(entries, isIndented), (entries) => {
  26842. const groupIsIndented = head(entries).exists(isIndented);
  26843. return groupIsIndented ? indentedComposer(editor, entries) : outdentedComposer(editor, entries);
  26844. });
  26845. const indentSelectedEntries = (editor, entries, indentation) => {
  26846. each$e(filter$5(entries, isSelected), (entry) => indentEntry(editor, indentation, entry));
  26847. };
  26848. const getItemSelection = (editor) => {
  26849. const selectedListItems = map$3(getSelectedListItems(editor), SugarElement.fromDom);
  26850. return lift2(find$2(selectedListItems, not(hasFirstChildList)), find$2(reverse(selectedListItems), not(hasFirstChildList)), (start, end) => ({ start, end }));
  26851. };
  26852. const listIndentation = (editor, lists, indentation) => {
  26853. const entrySets = parseLists(lists, getItemSelection(editor));
  26854. each$e(entrySets, (entrySet) => {
  26855. indentSelectedEntries(editor, entrySet.entries, indentation);
  26856. const composedLists = composeEntries(editor, entrySet.entries);
  26857. each$e(composedLists, (composedList) => {
  26858. fireListEvent(editor, indentation === "Indent" /* Indentation.Indent */ ? "IndentList" /* ListAction.IndentList */ : "OutdentList" /* ListAction.OutdentList */, composedList.dom);
  26859. });
  26860. before$3(entrySet.sourceList, composedLists);
  26861. remove$8(entrySet.sourceList);
  26862. });
  26863. };
  26864. const canIndent$1 = (editor) => getListMaxDepth(editor).forall((max) => {
  26865. const blocks = editor.selection.getSelectedBlocks();
  26866. return exists(blocks, (element) => {
  26867. return closest$3(SugarElement.fromDom(element), 'li').forall((sugarElement) => ancestors(sugarElement, 'ol,ul').length <= max);
  26868. });
  26869. });
  26870. const DOM$8 = DOMUtils.DOM;
  26871. const splitList = (editor, list, li) => {
  26872. const removeAndKeepBookmarks = (targetNode) => {
  26873. const parent = targetNode.parentNode;
  26874. if (parent) {
  26875. Tools.each(bookmarks, (node) => {
  26876. parent.insertBefore(node, li.parentNode);
  26877. });
  26878. }
  26879. DOM$8.remove(targetNode);
  26880. };
  26881. const bookmarks = DOM$8.select('span[data-mce-type="bookmark"]', list);
  26882. const newBlock = createTextBlock$1(editor, li);
  26883. const tmpRng = DOM$8.createRng();
  26884. tmpRng.setStartAfter(li);
  26885. tmpRng.setEndAfter(list);
  26886. const fragment = tmpRng.extractContents();
  26887. for (let node = fragment.firstChild; node; node = node.firstChild) {
  26888. if (node.nodeName === 'LI' && editor.dom.isEmpty(node)) {
  26889. DOM$8.remove(node);
  26890. break;
  26891. }
  26892. }
  26893. if (!editor.dom.isEmpty(fragment)) {
  26894. DOM$8.insertAfter(fragment, list);
  26895. }
  26896. DOM$8.insertAfter(newBlock, list);
  26897. const parent = li.parentElement;
  26898. if (parent && isEmpty$1(editor.dom, parent)) {
  26899. removeAndKeepBookmarks(parent);
  26900. }
  26901. DOM$8.remove(li);
  26902. if (isEmpty$1(editor.dom, list)) {
  26903. DOM$8.remove(list);
  26904. }
  26905. };
  26906. const isDescriptionDetail = isTag('dd');
  26907. const isDescriptionTerm = isTag('dt');
  26908. const outdentDlItem = (editor, item) => {
  26909. if (isDescriptionDetail(item)) {
  26910. mutate(item, 'dt');
  26911. }
  26912. else if (isDescriptionTerm(item)) {
  26913. parentElement(item).each((dl) => splitList(editor, dl.dom, item.dom));
  26914. }
  26915. };
  26916. const indentDlItem = (item) => {
  26917. if (isDescriptionTerm(item)) {
  26918. mutate(item, 'dd');
  26919. }
  26920. };
  26921. const dlIndentation = (editor, indentation, dlItems) => {
  26922. if (indentation === "Indent" /* Indentation.Indent */) {
  26923. each$e(dlItems, indentDlItem);
  26924. }
  26925. else {
  26926. each$e(dlItems, (item) => outdentDlItem(editor, item));
  26927. }
  26928. };
  26929. const selectionIndentation = (editor, indentation) => {
  26930. const lists = fromDom$1(getSelectedListRoots(editor));
  26931. const dlItems = fromDom$1(getSelectedDlItems(editor));
  26932. let isHandled = false;
  26933. if (lists.length || dlItems.length) {
  26934. const bookmark = editor.selection.getBookmark();
  26935. listIndentation(editor, lists, indentation);
  26936. dlIndentation(editor, indentation, dlItems);
  26937. editor.selection.moveToBookmark(bookmark);
  26938. editor.selection.setRng(normalizeRange(editor.selection.getRng()));
  26939. editor.nodeChanged();
  26940. isHandled = true;
  26941. }
  26942. return isHandled;
  26943. };
  26944. const handleIndentation = (editor, indentation) => !selectionIsWithinNonEditableList(editor) && selectionIndentation(editor, indentation);
  26945. const indentListSelection = (editor) => handleIndentation(editor, "Indent" /* Indentation.Indent */);
  26946. const outdentListSelection = (editor) => handleIndentation(editor, "Outdent" /* Indentation.Outdent */);
  26947. const flattenListSelection = (editor) => handleIndentation(editor, "Flatten" /* Indentation.Flatten */);
  26948. const listToggleActionFromListName = (listName) => {
  26949. switch (listName) {
  26950. case 'UL': return "ToggleUlList" /* ListAction.ToggleUlList */;
  26951. case 'OL': return "ToggleOlList" /* ListAction.ToggleOlList */;
  26952. case 'DL': return "ToggleDLList" /* ListAction.ToggleDLList */;
  26953. }
  26954. };
  26955. const updateListStyle = (dom, el, detail) => {
  26956. const type = detail['list-style-type'] ? detail['list-style-type'] : null;
  26957. dom.setStyle(el, 'list-style-type', type);
  26958. };
  26959. const setAttribs = (elm, attrs) => {
  26960. Tools.each(attrs, (value, key) => {
  26961. elm.setAttribute(key, value);
  26962. });
  26963. };
  26964. const updateListAttrs = (dom, el, detail) => {
  26965. setAttribs(el, detail['list-attributes']);
  26966. Tools.each(dom.select('li', el), (li) => {
  26967. setAttribs(li, detail['list-item-attributes']);
  26968. });
  26969. };
  26970. const updateListWithDetails = (dom, el, detail) => {
  26971. updateListStyle(dom, el, detail);
  26972. updateListAttrs(dom, el, detail);
  26973. };
  26974. const removeStyles = (dom, element, styles) => {
  26975. Tools.each(styles, (style) => dom.setStyle(element, style, ''));
  26976. };
  26977. const isInline$1 = (editor, node) => isNonNullable(node) && !isBlock(node, editor.schema.getBlockElements());
  26978. const getEndPointNode = (editor, rng, start, root) => {
  26979. let container = rng[start ? 'startContainer' : 'endContainer'];
  26980. const offset = rng[start ? 'startOffset' : 'endOffset'];
  26981. // Resolve node index
  26982. if (isElement$1(container)) {
  26983. container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
  26984. }
  26985. if (!start && isBr$1(container.nextSibling)) {
  26986. container = container.nextSibling;
  26987. }
  26988. const findBlockAncestor = (node) => {
  26989. while (!editor.dom.isBlock(node) && node.parentNode && root !== node) {
  26990. node = node.parentNode;
  26991. }
  26992. return node;
  26993. };
  26994. // The reason why the next two if statements exist is because when the root node is a table cell (possibly some other node types)
  26995. // then the highest we can go up the dom hierarchy is one level below the table cell.
  26996. // So what happens when we have a bunch of inline nodes and text nodes in the table cell
  26997. // and when the selection is collapsed inside one of the inline nodes then only that inline node (or text node) will be included
  26998. // in the created list because that would be one level below td node and the other inline nodes won't be included.
  26999. // So the fix proposed is to traverse left when looking for start node (and traverse right when looking for end node)
  27000. // and keep traversing as long as we have an inline or text node (same for traversing right).
  27001. // This way we end up including all the inline elements in the created list.
  27002. // For more info look at #TINY-6853
  27003. const findBetterContainer = (container, forward) => {
  27004. var _a;
  27005. const walker = new DomTreeWalker(container, findBlockAncestor(container));
  27006. const dir = forward ? 'next' : 'prev';
  27007. let node;
  27008. while ((node = walker[dir]())) {
  27009. if (!(isVoid(editor, node) || isZwsp$2(node.textContent) || ((_a = node.textContent) === null || _a === void 0 ? void 0 : _a.length) === 0)) {
  27010. return Optional.some(node);
  27011. }
  27012. }
  27013. return Optional.none();
  27014. };
  27015. // Traverse left to include inline/text nodes
  27016. if (start && isTextNode$1(container)) {
  27017. if (isZwsp$2(container.textContent)) {
  27018. container = findBetterContainer(container, false).getOr(container);
  27019. }
  27020. else {
  27021. if (container.parentNode !== null && isInline$1(editor, container.parentNode)) {
  27022. container = container.parentNode;
  27023. }
  27024. while (container.previousSibling !== null && (isInline$1(editor, container.previousSibling) || isTextNode$1(container.previousSibling))) {
  27025. container = container.previousSibling;
  27026. }
  27027. }
  27028. }
  27029. // Traverse right to include inline/text nodes
  27030. if (!start && isTextNode$1(container)) {
  27031. if (isZwsp$2(container.textContent)) {
  27032. container = findBetterContainer(container, true).getOr(container);
  27033. }
  27034. else {
  27035. if (container.parentNode !== null && isInline$1(editor, container.parentNode)) {
  27036. container = container.parentNode;
  27037. }
  27038. while (container.nextSibling !== null && (isInline$1(editor, container.nextSibling) || isTextNode$1(container.nextSibling))) {
  27039. container = container.nextSibling;
  27040. }
  27041. }
  27042. }
  27043. while (container.parentNode !== root) {
  27044. const parent = container.parentNode;
  27045. if (isTextBlock(editor, container)) {
  27046. return container;
  27047. }
  27048. if (/^(TD|TH)$/.test(parent.nodeName)) {
  27049. return container;
  27050. }
  27051. container = parent;
  27052. }
  27053. return container;
  27054. };
  27055. const getSelectedTextBlocks = (editor, rng, root) => {
  27056. const textBlocks = [];
  27057. const dom = editor.dom;
  27058. const startNode = getEndPointNode(editor, rng, true, root);
  27059. const endNode = getEndPointNode(editor, rng, false, root);
  27060. let block;
  27061. const siblings = [];
  27062. for (let node = startNode; node; node = node.nextSibling) {
  27063. siblings.push(node);
  27064. if (node === endNode) {
  27065. break;
  27066. }
  27067. }
  27068. Tools.each(siblings, (node) => {
  27069. var _a;
  27070. if (isTextBlock(editor, node)) {
  27071. textBlocks.push(node);
  27072. block = null;
  27073. return;
  27074. }
  27075. if (dom.isBlock(node) || isBr$1(node)) {
  27076. if (isBr$1(node)) {
  27077. dom.remove(node);
  27078. }
  27079. block = null;
  27080. return;
  27081. }
  27082. const nextSibling = node.nextSibling;
  27083. if (BookmarkManager.isBookmarkNode(node)) {
  27084. if (isListNode(nextSibling) || isTextBlock(editor, nextSibling) || (!nextSibling && node.parentNode === root)) {
  27085. block = null;
  27086. return;
  27087. }
  27088. }
  27089. if (!block) {
  27090. block = dom.create('p');
  27091. (_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(block, node);
  27092. textBlocks.push(block);
  27093. }
  27094. block.appendChild(node);
  27095. });
  27096. return textBlocks;
  27097. };
  27098. const hasCompatibleStyle = (dom, sib, detail) => {
  27099. const sibStyle = dom.getStyle(sib, 'list-style-type');
  27100. let detailStyle = detail ? detail['list-style-type'] : '';
  27101. detailStyle = detailStyle === null ? '' : detailStyle;
  27102. return sibStyle === detailStyle;
  27103. };
  27104. /*
  27105. Find the first element we would transform into a li-element if given no constraints.
  27106. If the common ancestor is higher up than that provide it as the starting-point for the search for the root instead of the first selected element.
  27107. This helps avoid issues with divs that should become li-elements are detected as the root when they should not be.
  27108. */
  27109. const getRootSearchStart = (editor, range) => {
  27110. const start = editor.selection.getStart(true);
  27111. const startPoint = getEndPointNode(editor, range, true, editor.getBody());
  27112. if (ancestor$2(SugarElement.fromDom(startPoint), SugarElement.fromDom(range.commonAncestorContainer))) {
  27113. return range.commonAncestorContainer;
  27114. }
  27115. else {
  27116. return start;
  27117. }
  27118. };
  27119. const applyList = (editor, listName, detail) => {
  27120. const rng = editor.selection.getRng();
  27121. let listItemName = 'LI';
  27122. const root = getClosestListHost(editor, getRootSearchStart(editor, rng));
  27123. const dom = editor.dom;
  27124. if (dom.getContentEditable(editor.selection.getNode()) === 'false') {
  27125. return;
  27126. }
  27127. listName = listName.toUpperCase();
  27128. if (listName === 'DL') {
  27129. listItemName = 'DT';
  27130. }
  27131. const bookmark = createBookmark(rng);
  27132. const selectedTextBlocks = filter$5(getSelectedTextBlocks(editor, rng, root), editor.dom.isEditable);
  27133. Tools.each(selectedTextBlocks, (block) => {
  27134. let listBlock;
  27135. const sibling = block.previousSibling;
  27136. const parent = block.parentNode;
  27137. if (!isListItemNode(parent)) {
  27138. if (sibling && isListNode(sibling) && sibling.nodeName === listName && hasCompatibleStyle(dom, sibling, detail)) {
  27139. listBlock = sibling;
  27140. block = dom.rename(block, listItemName);
  27141. sibling.appendChild(block);
  27142. }
  27143. else {
  27144. listBlock = dom.create(listName);
  27145. parent.insertBefore(listBlock, block);
  27146. listBlock.appendChild(block);
  27147. block = dom.rename(block, listItemName);
  27148. }
  27149. removeStyles(dom, block, [
  27150. 'margin', 'margin-right', 'margin-bottom', 'margin-left', 'margin-top',
  27151. 'padding', 'padding-right', 'padding-bottom', 'padding-left', 'padding-top'
  27152. ]);
  27153. updateListWithDetails(dom, listBlock, detail);
  27154. mergeWithAdjacentLists(editor.dom, listBlock);
  27155. }
  27156. });
  27157. editor.selection.setRng(resolveBookmark$1(bookmark));
  27158. };
  27159. const isValidLists = (list1, list2) => {
  27160. return isListNode(list1) && list1.nodeName === (list2 === null || list2 === void 0 ? void 0 : list2.nodeName);
  27161. };
  27162. const hasSameListStyle = (dom, list1, list2) => {
  27163. const targetStyle = dom.getStyle(list1, 'list-style-type', true);
  27164. const style = dom.getStyle(list2, 'list-style-type', true);
  27165. return targetStyle === style;
  27166. };
  27167. const hasSameClasses = (elm1, elm2) => {
  27168. return elm1.className === elm2.className;
  27169. };
  27170. const shouldMerge = (dom, list1, list2) => {
  27171. return isValidLists(list1, list2) &&
  27172. // Note: isValidLists will ensure list1 and list2 are a HTMLElement. Unfortunately TypeScript doesn't
  27173. // support type guards on multiple variables. See https://github.com/microsoft/TypeScript/issues/26916
  27174. hasSameListStyle(dom, list1, list2) &&
  27175. hasSameClasses(list1, list2);
  27176. };
  27177. const mergeWithAdjacentLists = (dom, listBlock) => {
  27178. let node;
  27179. let sibling = listBlock.nextSibling;
  27180. if (shouldMerge(dom, listBlock, sibling)) {
  27181. const liSibling = sibling;
  27182. while ((node = liSibling.firstChild)) {
  27183. listBlock.appendChild(node);
  27184. }
  27185. dom.remove(liSibling);
  27186. }
  27187. sibling = listBlock.previousSibling;
  27188. if (shouldMerge(dom, listBlock, sibling)) {
  27189. const liSibling = sibling;
  27190. while ((node = liSibling.lastChild)) {
  27191. listBlock.insertBefore(node, listBlock.firstChild);
  27192. }
  27193. dom.remove(liSibling);
  27194. }
  27195. };
  27196. const updateList$1 = (editor, list, listName, detail) => {
  27197. if (list.nodeName !== listName) {
  27198. const newList = editor.dom.rename(list, listName);
  27199. updateListWithDetails(editor.dom, newList, detail);
  27200. fireListEvent(editor, listToggleActionFromListName(listName), newList);
  27201. }
  27202. else {
  27203. updateListWithDetails(editor.dom, list, detail);
  27204. fireListEvent(editor, listToggleActionFromListName(listName), list);
  27205. }
  27206. };
  27207. const updateCustomList = (editor, list, listName, detail) => {
  27208. list.classList.forEach((cls, _, classList) => {
  27209. if (cls.startsWith('tox-')) {
  27210. classList.remove(cls);
  27211. if (classList.length === 0) {
  27212. list.removeAttribute('class');
  27213. }
  27214. }
  27215. });
  27216. if (list.nodeName !== listName) {
  27217. const newList = editor.dom.rename(list, listName);
  27218. updateListWithDetails(editor.dom, newList, detail);
  27219. fireListEvent(editor, listToggleActionFromListName(listName), newList);
  27220. }
  27221. else {
  27222. updateListWithDetails(editor.dom, list, detail);
  27223. fireListEvent(editor, listToggleActionFromListName(listName), list);
  27224. }
  27225. };
  27226. const toggleMultipleLists = (editor, parentList, lists, listName, detail) => {
  27227. const parentIsList = isListNode(parentList);
  27228. if (parentIsList && parentList.nodeName === listName && !hasListStyleDetail(detail) && !isCustomList(parentList)) {
  27229. flattenListSelection(editor);
  27230. }
  27231. else {
  27232. applyList(editor, listName, detail);
  27233. const bookmark = createBookmark(editor.selection.getRng());
  27234. const allLists = parentIsList ? [parentList, ...lists] : lists;
  27235. const updateFunction = (parentIsList && isCustomList(parentList)) ? updateCustomList : updateList$1;
  27236. Tools.each(allLists, (elm) => {
  27237. updateFunction(editor, elm, listName, detail);
  27238. });
  27239. editor.selection.setRng(resolveBookmark$1(bookmark));
  27240. }
  27241. };
  27242. const hasListStyleDetail = (detail) => {
  27243. return 'list-style-type' in detail;
  27244. };
  27245. const toggleSingleList = (editor, parentList, listName, detail) => {
  27246. if (parentList === editor.getBody()) {
  27247. return;
  27248. }
  27249. if (parentList) {
  27250. if (parentList.nodeName === listName && !hasListStyleDetail(detail) && !isCustomList(parentList)) {
  27251. flattenListSelection(editor);
  27252. }
  27253. else {
  27254. const bookmark = createBookmark(editor.selection.getRng());
  27255. if (isCustomList(parentList)) {
  27256. parentList.classList.forEach((cls, _, classList) => {
  27257. if (cls.startsWith('tox-')) {
  27258. classList.remove(cls);
  27259. if (classList.length === 0) {
  27260. parentList.removeAttribute('class');
  27261. }
  27262. }
  27263. });
  27264. }
  27265. updateListWithDetails(editor.dom, parentList, detail);
  27266. const newList = editor.dom.rename(parentList, listName);
  27267. mergeWithAdjacentLists(editor.dom, newList);
  27268. editor.selection.setRng(resolveBookmark$1(bookmark));
  27269. applyList(editor, listName, detail);
  27270. fireListEvent(editor, listToggleActionFromListName(listName), newList);
  27271. }
  27272. }
  27273. else {
  27274. applyList(editor, listName, detail);
  27275. fireListEvent(editor, listToggleActionFromListName(listName), parentList);
  27276. }
  27277. };
  27278. const toggleList = (editor, listName, _detail) => {
  27279. const parentList = getParentList(editor);
  27280. if (isWithinNonEditableList$1(editor, parentList)) {
  27281. return;
  27282. }
  27283. const selectedSubLists = getSelectedSubLists(editor);
  27284. const detail = isObject(_detail) ? _detail : {};
  27285. if (selectedSubLists.length > 0) {
  27286. toggleMultipleLists(editor, parentList, selectedSubLists, listName, detail);
  27287. }
  27288. else {
  27289. toggleSingleList(editor, parentList, listName, detail);
  27290. }
  27291. };
  27292. const findNextCaretContainer = (editor, rng, isForward, root) => {
  27293. let node = rng.startContainer;
  27294. const offset = rng.startOffset;
  27295. if (isTextNode$1(node) && (isForward ? offset < node.data.length : offset > 0)) {
  27296. return node;
  27297. }
  27298. const nonEmptyBlocks = editor.schema.getNonEmptyElements();
  27299. if (isElement$1(node)) {
  27300. node = RangeUtils.getNode(node, offset);
  27301. }
  27302. const walker = new DomTreeWalker(node, root);
  27303. // Delete at <li>|<br></li> then jump over the bogus br
  27304. if (isForward) {
  27305. if (isBogusBr(editor.dom, node)) {
  27306. walker.next();
  27307. }
  27308. }
  27309. const walkFn = isForward ? walker.next.bind(walker) : walker.prev2.bind(walker);
  27310. while ((node = walkFn())) {
  27311. if (node.nodeName === 'LI' && !node.hasChildNodes()) {
  27312. return node;
  27313. }
  27314. if (nonEmptyBlocks[node.nodeName]) {
  27315. return node;
  27316. }
  27317. if (isTextNode$1(node) && node.data.length > 0) {
  27318. return node;
  27319. }
  27320. }
  27321. return null;
  27322. };
  27323. const hasOnlyOneBlockChild = (dom, elm) => {
  27324. const childNodes = elm.childNodes;
  27325. return childNodes.length === 1 && !isListNode(childNodes[0]) && dom.isBlock(childNodes[0]);
  27326. };
  27327. const isUnwrappable = (node) => Optional.from(node)
  27328. .map(SugarElement.fromDom)
  27329. .filter(isHTMLElement$1)
  27330. .exists((el) => isEditable$2(el) && !contains$2(['details'], name(el)));
  27331. const unwrapSingleBlockChild = (dom, elm) => {
  27332. if (hasOnlyOneBlockChild(dom, elm) && isUnwrappable(elm.firstChild)) {
  27333. dom.remove(elm.firstChild, true);
  27334. }
  27335. };
  27336. const moveChildren = (dom, fromElm, toElm) => {
  27337. let node;
  27338. const targetElm = hasOnlyOneBlockChild(dom, toElm) ? toElm.firstChild : toElm;
  27339. unwrapSingleBlockChild(dom, fromElm);
  27340. if (!isEmpty$1(dom, fromElm, true)) {
  27341. while ((node = fromElm.firstChild)) {
  27342. targetElm.appendChild(node);
  27343. }
  27344. }
  27345. };
  27346. const mergeLiElements = (dom, fromElm, toElm) => {
  27347. let listNode;
  27348. const ul = fromElm.parentNode;
  27349. if (!isChildOfBody(dom, fromElm) || !isChildOfBody(dom, toElm)) {
  27350. return;
  27351. }
  27352. if (isListNode(toElm.lastChild)) {
  27353. listNode = toElm.lastChild;
  27354. }
  27355. if (ul === toElm.lastChild) {
  27356. if (isBr$1(ul.previousSibling)) {
  27357. dom.remove(ul.previousSibling);
  27358. }
  27359. }
  27360. const node = toElm.lastChild;
  27361. if (node && isBr$1(node) && fromElm.hasChildNodes()) {
  27362. dom.remove(node);
  27363. }
  27364. if (isEmpty$1(dom, toElm, true)) {
  27365. empty(SugarElement.fromDom(toElm));
  27366. }
  27367. moveChildren(dom, fromElm, toElm);
  27368. if (listNode) {
  27369. toElm.appendChild(listNode);
  27370. }
  27371. const contains$1 = contains(SugarElement.fromDom(toElm), SugarElement.fromDom(fromElm));
  27372. const nestedLists = contains$1 ? dom.getParents(fromElm, isListNode, toElm) : [];
  27373. dom.remove(fromElm);
  27374. each$e(nestedLists, (list) => {
  27375. if (isEmpty$1(dom, list) && list !== dom.getRoot()) {
  27376. dom.remove(list);
  27377. }
  27378. });
  27379. };
  27380. const mergeIntoEmptyLi = (editor, fromLi, toLi) => {
  27381. empty(SugarElement.fromDom(toLi));
  27382. mergeLiElements(editor.dom, fromLi, toLi);
  27383. editor.selection.setCursorLocation(toLi, 0);
  27384. };
  27385. const mergeForward = (editor, rng, fromLi, toLi) => {
  27386. const dom = editor.dom;
  27387. if (dom.isEmpty(toLi)) {
  27388. mergeIntoEmptyLi(editor, fromLi, toLi);
  27389. }
  27390. else {
  27391. const bookmark = createBookmark(rng);
  27392. mergeLiElements(dom, fromLi, toLi);
  27393. editor.selection.setRng(resolveBookmark$1(bookmark));
  27394. }
  27395. };
  27396. const mergeBackward = (editor, rng, fromLi, toLi) => {
  27397. const bookmark = createBookmark(rng);
  27398. mergeLiElements(editor.dom, fromLi, toLi);
  27399. const resolvedBookmark = resolveBookmark$1(bookmark);
  27400. editor.selection.setRng(resolvedBookmark);
  27401. };
  27402. const backspaceDeleteFromListToListCaret = (editor, isForward) => {
  27403. const dom = editor.dom, selection = editor.selection;
  27404. const selectionStartElm = selection.getStart();
  27405. const root = getClosestEditingHost(editor, selectionStartElm);
  27406. const li = dom.getParent(selection.getStart(), 'LI', root);
  27407. if (li) {
  27408. const ul = li.parentElement;
  27409. if (ul === editor.getBody() && isEmpty$1(dom, ul)) {
  27410. return true;
  27411. }
  27412. const rng = normalizeRange(selection.getRng());
  27413. const otherLi = dom.getParent(findNextCaretContainer(editor, rng, isForward, root), 'LI', root);
  27414. const willMergeParentIntoChild = otherLi && (isForward ? dom.isChildOf(li, otherLi) : dom.isChildOf(otherLi, li));
  27415. if (otherLi && otherLi !== li && !willMergeParentIntoChild) {
  27416. editor.undoManager.transact(() => {
  27417. if (isForward) {
  27418. mergeForward(editor, rng, otherLi, li);
  27419. }
  27420. else {
  27421. if (isFirstChild$1(li)) {
  27422. outdentListSelection(editor);
  27423. }
  27424. else {
  27425. mergeBackward(editor, rng, li, otherLi);
  27426. }
  27427. }
  27428. });
  27429. return true;
  27430. }
  27431. else if (willMergeParentIntoChild && !isForward && otherLi !== li) {
  27432. const commonAncestorParent = rng.commonAncestorContainer.parentElement;
  27433. if (!commonAncestorParent || dom.isChildOf(otherLi, commonAncestorParent)) {
  27434. return false;
  27435. }
  27436. editor.undoManager.transact(() => {
  27437. const bookmark = createBookmark(rng);
  27438. moveChildren(dom, commonAncestorParent, otherLi);
  27439. commonAncestorParent.remove();
  27440. const resolvedBookmark = resolveBookmark$1(bookmark);
  27441. editor.selection.setRng(resolvedBookmark);
  27442. });
  27443. return true;
  27444. }
  27445. else if (!otherLi) {
  27446. if (!isForward && rng.startOffset === 0 && rng.endOffset === 0) {
  27447. editor.undoManager.transact(() => {
  27448. flattenListSelection(editor);
  27449. });
  27450. return true;
  27451. }
  27452. }
  27453. }
  27454. return false;
  27455. };
  27456. const removeBlock = (dom, block, root) => {
  27457. const parentBlock = dom.getParent(block.parentNode, dom.isBlock, root);
  27458. dom.remove(block);
  27459. if (parentBlock && dom.isEmpty(parentBlock)) {
  27460. dom.remove(parentBlock);
  27461. }
  27462. };
  27463. const backspaceDeleteIntoListCaret = (editor, isForward) => {
  27464. const dom = editor.dom;
  27465. const selectionStartElm = editor.selection.getStart();
  27466. const root = getClosestEditingHost(editor, selectionStartElm);
  27467. const block = dom.getParent(selectionStartElm, dom.isBlock, root);
  27468. if (block && dom.isEmpty(block, undefined, { checkRootAsContent: true })) {
  27469. const rng = normalizeRange(editor.selection.getRng());
  27470. const nextCaretContainer = findNextCaretContainer(editor, rng, isForward, root);
  27471. const otherLi = dom.getParent(nextCaretContainer, 'LI', root);
  27472. if (nextCaretContainer && otherLi) {
  27473. const findValidElement = (element) => contains$2(['td', 'th', 'caption'], name(element));
  27474. const findRoot = (node) => node.dom === root;
  27475. const otherLiCell = closest$4(SugarElement.fromDom(otherLi), findValidElement, findRoot);
  27476. const caretCell = closest$4(SugarElement.fromDom(rng.startContainer), findValidElement, findRoot);
  27477. if (!equals(otherLiCell, caretCell, eq)) {
  27478. return false;
  27479. }
  27480. editor.undoManager.transact(() => {
  27481. const parentNode = otherLi.parentNode;
  27482. removeBlock(dom, block, root);
  27483. mergeWithAdjacentLists(dom, parentNode);
  27484. editor.selection.select(nextCaretContainer, true);
  27485. editor.selection.collapse(isForward);
  27486. });
  27487. return true;
  27488. }
  27489. }
  27490. return false;
  27491. };
  27492. const backspaceDeleteCaret$1 = (editor, isForward) => {
  27493. return backspaceDeleteFromListToListCaret(editor, isForward) || backspaceDeleteIntoListCaret(editor, isForward);
  27494. };
  27495. const hasListSelection = (editor) => {
  27496. const selectionStartElm = editor.selection.getStart();
  27497. const root = getClosestEditingHost(editor, selectionStartElm);
  27498. const startListParent = editor.dom.getParent(selectionStartElm, 'LI,DT,DD', root);
  27499. return isNonNullable(startListParent) || getSelectedListItems(editor).length > 0;
  27500. };
  27501. const backspaceDeleteRange$1 = (editor) => {
  27502. if (hasListSelection(editor)) {
  27503. editor.undoManager.transact(() => {
  27504. // Some delete actions may prevent the input event from being fired. If we do not detect it, we fire it ourselves.
  27505. let shouldFireInput = true;
  27506. const inputHandler = () => shouldFireInput = false;
  27507. editor.on('input', inputHandler);
  27508. editor.execCommand('Delete');
  27509. editor.off('input', inputHandler);
  27510. if (shouldFireInput) {
  27511. editor.dispatch('input');
  27512. }
  27513. normalizeLists(editor.dom, editor.getBody());
  27514. });
  27515. return true;
  27516. }
  27517. return false;
  27518. };
  27519. const backspaceDelete$c = (editor, isForward) => {
  27520. const selection = editor.selection;
  27521. return !isWithinNonEditableList$1(editor, selection.getNode()) && (selection.isCollapsed() ?
  27522. backspaceDeleteCaret$1(editor, isForward) : backspaceDeleteRange$1(editor));
  27523. };
  27524. const blockPosition = (block, position) => ({
  27525. block,
  27526. position
  27527. });
  27528. const blockBoundary = (from, to) => ({
  27529. from,
  27530. to
  27531. });
  27532. const getBlockPosition = (rootNode, pos) => {
  27533. const rootElm = SugarElement.fromDom(rootNode);
  27534. const containerElm = SugarElement.fromDom(pos.container());
  27535. return getParentBlock$2(rootElm, containerElm).map((block) => blockPosition(block, pos));
  27536. };
  27537. const isNotAncestorial = (blockBoundary) => !(contains(blockBoundary.to.block, blockBoundary.from.block) || contains(blockBoundary.from.block, blockBoundary.to.block));
  27538. const isDifferentBlocks = (blockBoundary) => !eq(blockBoundary.from.block, blockBoundary.to.block);
  27539. const getClosestHost = (root, scope) => {
  27540. const isRoot = (node) => eq(node, root);
  27541. const isHost = (node) => isTableCell$2(node) || isContentEditableTrue$3(node.dom);
  27542. return closest$4(scope, isHost, isRoot).filter(isElement$8).getOr(root);
  27543. };
  27544. const hasSameHost = (rootNode, blockBoundary) => {
  27545. const root = SugarElement.fromDom(rootNode);
  27546. return eq(getClosestHost(root, blockBoundary.from.block), getClosestHost(root, blockBoundary.to.block));
  27547. };
  27548. const isEditable$1 = (blockBoundary) => isContentEditableFalse$a(blockBoundary.from.block.dom) === false && isContentEditableFalse$a(blockBoundary.to.block.dom) === false;
  27549. const hasValidBlocks = (blockBoundary) => {
  27550. const isValidBlock = (block) => isTextBlock$3(block) || hasBlockAttr(block.dom) || isListItem$2(block);
  27551. return isValidBlock(blockBoundary.from.block) && isValidBlock(blockBoundary.to.block);
  27552. };
  27553. const skipLastBr = (schema, rootNode, forward, blockPosition) => {
  27554. if (isBr$7(blockPosition.position.getNode()) && !isEmpty$4(schema, blockPosition.block)) {
  27555. return positionIn(false, blockPosition.block.dom).bind((lastPositionInBlock) => {
  27556. if (lastPositionInBlock.isEqual(blockPosition.position)) {
  27557. return fromPosition(forward, rootNode, lastPositionInBlock).bind((to) => getBlockPosition(rootNode, to));
  27558. }
  27559. else {
  27560. return Optional.some(blockPosition);
  27561. }
  27562. }).getOr(blockPosition);
  27563. }
  27564. else {
  27565. return blockPosition;
  27566. }
  27567. };
  27568. const readFromRange = (schema, rootNode, forward, rng) => {
  27569. const fromBlockPos = getBlockPosition(rootNode, CaretPosition.fromRangeStart(rng));
  27570. const toBlockPos = fromBlockPos.bind((blockPos) => fromPosition(forward, rootNode, blockPos.position).bind((to) => getBlockPosition(rootNode, to).map((blockPos) => skipLastBr(schema, rootNode, forward, blockPos))));
  27571. return lift2(fromBlockPos, toBlockPos, blockBoundary).filter((blockBoundary) => isDifferentBlocks(blockBoundary) && hasSameHost(rootNode, blockBoundary) && isEditable$1(blockBoundary) && hasValidBlocks(blockBoundary) && isNotAncestorial(blockBoundary));
  27572. };
  27573. const read$1 = (schema, rootNode, forward, rng) => rng.collapsed ? readFromRange(schema, rootNode, forward, rng) : Optional.none();
  27574. const getChildrenUntilBlockBoundary = (block, schema) => {
  27575. const children = children$1(block);
  27576. return findIndex$2(children, (el) => schema.isBlock(name(el))).fold(constant(children), (index) => children.slice(0, index));
  27577. };
  27578. const extractChildren = (block, schema) => {
  27579. const children = getChildrenUntilBlockBoundary(block, schema);
  27580. each$e(children, remove$8);
  27581. return children;
  27582. };
  27583. const removeEmptyRoot = (schema, rootNode, block) => {
  27584. const parents = parentsAndSelf(block, rootNode);
  27585. return find$2(parents.reverse(), (element) => isEmpty$4(schema, element)).each(remove$8);
  27586. };
  27587. const isEmptyBefore = (schema, el) => filter$5(prevSiblings(el), (el) => !isEmpty$4(schema, el)).length === 0;
  27588. const nestedBlockMerge = (rootNode, fromBlock, toBlock, schema, insertionPoint) => {
  27589. if (isEmpty$4(schema, toBlock)) {
  27590. fillWithPaddingBr(toBlock);
  27591. return firstPositionIn(toBlock.dom);
  27592. }
  27593. if (isEmptyBefore(schema, insertionPoint) && isEmpty$4(schema, fromBlock)) {
  27594. before$4(insertionPoint, SugarElement.fromTag('br'));
  27595. }
  27596. const position = prevPosition(toBlock.dom, CaretPosition.before(insertionPoint.dom));
  27597. each$e(extractChildren(fromBlock, schema), (child) => {
  27598. before$4(insertionPoint, child);
  27599. });
  27600. removeEmptyRoot(schema, rootNode, fromBlock);
  27601. return position;
  27602. };
  27603. const isInline = (schema, node) => schema.isInline(name(node));
  27604. const sidelongBlockMerge = (rootNode, fromBlock, toBlock, schema) => {
  27605. if (isEmpty$4(schema, toBlock)) {
  27606. if (isEmpty$4(schema, fromBlock)) {
  27607. const getInlineToBlockDescendants = (el) => {
  27608. const helper = (node, elements) => firstChild(node).fold(() => elements, (child) => isInline(schema, child) ? helper(child, elements.concat(shallow(child))) : elements);
  27609. return helper(el, []);
  27610. };
  27611. const newFromBlockDescendants = foldr(getInlineToBlockDescendants(toBlock), (element, descendant) => {
  27612. wrap$2(element, descendant);
  27613. return descendant;
  27614. }, createPaddingBr());
  27615. empty(fromBlock);
  27616. append$1(fromBlock, newFromBlockDescendants);
  27617. }
  27618. remove$8(toBlock);
  27619. return firstPositionIn(fromBlock.dom);
  27620. }
  27621. const position = lastPositionIn(toBlock.dom);
  27622. each$e(extractChildren(fromBlock, schema), (child) => {
  27623. append$1(toBlock, child);
  27624. });
  27625. removeEmptyRoot(schema, rootNode, fromBlock);
  27626. return position;
  27627. };
  27628. const findInsertionPoint = (toBlock, block) => {
  27629. const parentsAndSelf$1 = parentsAndSelf(block, toBlock);
  27630. return Optional.from(parentsAndSelf$1[parentsAndSelf$1.length - 1]);
  27631. };
  27632. const getInsertionPoint = (fromBlock, toBlock) => contains(toBlock, fromBlock) ? findInsertionPoint(toBlock, fromBlock) : Optional.none();
  27633. const trimBr = (first, block) => {
  27634. positionIn(first, block.dom)
  27635. .bind((position) => Optional.from(position.getNode()))
  27636. .map(SugarElement.fromDom)
  27637. .filter(isBr$6)
  27638. .each(remove$8);
  27639. };
  27640. const mergeBlockInto = (rootNode, fromBlock, toBlock, schema) => {
  27641. trimBr(true, fromBlock);
  27642. trimBr(false, toBlock);
  27643. return getInsertionPoint(fromBlock, toBlock).fold(curry(sidelongBlockMerge, rootNode, fromBlock, toBlock, schema), curry(nestedBlockMerge, rootNode, fromBlock, toBlock, schema));
  27644. };
  27645. const mergeBlocks = (rootNode, forward, block1, block2, schema) => forward ? mergeBlockInto(rootNode, block2, block1, schema) : mergeBlockInto(rootNode, block1, block2, schema);
  27646. const backspaceDelete$b = (editor, forward) => {
  27647. const rootNode = SugarElement.fromDom(editor.getBody());
  27648. const position = read$1(editor.schema, rootNode.dom, forward, editor.selection.getRng())
  27649. .map((blockBoundary) => () => {
  27650. mergeBlocks(rootNode, forward, blockBoundary.from.block, blockBoundary.to.block, editor.schema)
  27651. .each((pos) => {
  27652. editor.selection.setRng(pos.toRange());
  27653. });
  27654. });
  27655. return position;
  27656. };
  27657. const deleteRangeMergeBlocks = (rootNode, selection, schema) => {
  27658. const rng = selection.getRng();
  27659. return lift2(getParentBlock$2(rootNode, SugarElement.fromDom(rng.startContainer)), getParentBlock$2(rootNode, SugarElement.fromDom(rng.endContainer)), (block1, block2) => {
  27660. if (!eq(block1, block2)) {
  27661. return Optional.some(() => {
  27662. rng.deleteContents();
  27663. mergeBlocks(rootNode, true, block1, block2, schema).each((pos) => {
  27664. selection.setRng(pos.toRange());
  27665. });
  27666. });
  27667. }
  27668. else {
  27669. return Optional.none();
  27670. }
  27671. }).getOr(Optional.none());
  27672. };
  27673. const isRawNodeInTable = (root, rawNode) => {
  27674. const node = SugarElement.fromDom(rawNode);
  27675. const isRoot = curry(eq, root);
  27676. return ancestor$5(node, isTableCell$2, isRoot).isSome();
  27677. };
  27678. const isSelectionInTable = (root, rng) => isRawNodeInTable(root, rng.startContainer) || isRawNodeInTable(root, rng.endContainer);
  27679. const isEverythingSelected = (root, rng) => {
  27680. const noPrevious = prevPosition(root.dom, CaretPosition.fromRangeStart(rng)).isNone();
  27681. const noNext = nextPosition(root.dom, CaretPosition.fromRangeEnd(rng)).isNone();
  27682. return !isSelectionInTable(root, rng) && noPrevious && noNext;
  27683. };
  27684. const emptyEditor = (editor) => {
  27685. return Optional.some(() => {
  27686. editor.setContent('');
  27687. editor.selection.setCursorLocation();
  27688. });
  27689. };
  27690. const deleteRange$3 = (editor) => {
  27691. const rootNode = SugarElement.fromDom(editor.getBody());
  27692. const rng = editor.selection.getRng();
  27693. return isEverythingSelected(rootNode, rng) ? emptyEditor(editor) : deleteRangeMergeBlocks(rootNode, editor.selection, editor.schema);
  27694. };
  27695. const backspaceDelete$a = (editor, _forward) => editor.selection.isCollapsed() ? Optional.none() : deleteRange$3(editor);
  27696. const showCaret = (direction, editor, node, before, scrollIntoView) =>
  27697. // TODO: Figure out a better way to handle this dependency
  27698. Optional.from(editor._selectionOverrides.showCaret(direction, node, before, scrollIntoView));
  27699. const getNodeRange = (node) => {
  27700. const rng = node.ownerDocument.createRange();
  27701. rng.selectNode(node);
  27702. return rng;
  27703. };
  27704. const selectNode = (editor, node) => {
  27705. const e = editor.dispatch('BeforeObjectSelected', { target: node });
  27706. if (e.isDefaultPrevented()) {
  27707. return Optional.none();
  27708. }
  27709. return Optional.some(getNodeRange(node));
  27710. };
  27711. const renderCaretAtRange = (editor, range, scrollIntoView) => {
  27712. const normalizedRange = normalizeRange$2(1, editor.getBody(), range);
  27713. const caretPosition = CaretPosition.fromRangeStart(normalizedRange);
  27714. const caretPositionNode = caretPosition.getNode();
  27715. if (isInlineFakeCaretTarget(caretPositionNode)) {
  27716. return showCaret(1, editor, caretPositionNode, !caretPosition.isAtEnd(), false);
  27717. }
  27718. const caretPositionBeforeNode = caretPosition.getNode(true);
  27719. if (isInlineFakeCaretTarget(caretPositionBeforeNode)) {
  27720. return showCaret(1, editor, caretPositionBeforeNode, false, false);
  27721. }
  27722. // TODO: Should render caret before/after depending on where you click on the page forces after now
  27723. const ceRoot = getContentEditableRoot$1(editor.dom.getRoot(), caretPosition.getNode());
  27724. if (isInlineFakeCaretTarget(ceRoot)) {
  27725. return showCaret(1, editor, ceRoot, false, scrollIntoView);
  27726. }
  27727. return Optional.none();
  27728. };
  27729. const renderRangeCaret = (editor, range, scrollIntoView) => range.collapsed ? renderCaretAtRange(editor, range, scrollIntoView).getOr(range) : range;
  27730. const isBeforeBoundary = (pos) => isBeforeContentEditableFalse(pos) || isBeforeMedia(pos);
  27731. const isAfterBoundary = (pos) => isAfterContentEditableFalse(pos) || isAfterMedia(pos);
  27732. const trimEmptyTextNode = (dom, node) => {
  27733. if (isText$b(node) && node.data.length === 0) {
  27734. dom.remove(node);
  27735. }
  27736. };
  27737. const deleteContentAndShowCaret = (editor, range, node, direction, forward, peekCaretPosition) => {
  27738. showCaret(direction, editor, peekCaretPosition.getNode(!forward), forward, true).each((caretRange) => {
  27739. // Delete the selected content
  27740. if (range.collapsed) {
  27741. const deleteRange = range.cloneRange();
  27742. if (forward) {
  27743. deleteRange.setEnd(caretRange.startContainer, caretRange.startOffset);
  27744. }
  27745. else {
  27746. deleteRange.setStart(caretRange.endContainer, caretRange.endOffset);
  27747. }
  27748. deleteRange.deleteContents();
  27749. }
  27750. else {
  27751. range.deleteContents();
  27752. }
  27753. editor.selection.setRng(caretRange);
  27754. });
  27755. trimEmptyTextNode(editor.dom, node);
  27756. };
  27757. // If the caret position is next to a fake caret target element (eg cef/media) after a delete operation, then ensure a caret is added
  27758. // eg. <span cE=false>a|b -> <span cE=false>|bc
  27759. // Note: We also need to handle the actual deletion, as some browsers (eg IE) move the selection to the opposite side of the cef element
  27760. const deleteBoundaryText = (editor, forward) => {
  27761. const range = editor.selection.getRng();
  27762. if (!isText$b(range.commonAncestorContainer)) {
  27763. return Optional.none();
  27764. }
  27765. const direction = forward ? 1 /* HDirection.Forwards */ : -1 /* HDirection.Backwards */;
  27766. const caretWalker = CaretWalker(editor.getBody());
  27767. const getNextPosFn = curry(getVisualCaretPosition, forward ? caretWalker.next : caretWalker.prev);
  27768. const isBeforeFn = forward ? isBeforeBoundary : isAfterBoundary;
  27769. // Get the next caret position. ie where it'll be after the delete
  27770. const caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range);
  27771. const nextCaretPosition = getNextPosFn(caretPosition);
  27772. const normalizedNextCaretPosition = nextCaretPosition ? normalizePosition(forward, nextCaretPosition) : nextCaretPosition;
  27773. if (!normalizedNextCaretPosition || !isMoveInsideSameBlock(caretPosition, normalizedNextCaretPosition)) {
  27774. return Optional.none();
  27775. }
  27776. else if (isBeforeFn(normalizedNextCaretPosition)) {
  27777. return Optional.some(() => deleteContentAndShowCaret(editor, range, caretPosition.getNode(), direction, forward, normalizedNextCaretPosition));
  27778. }
  27779. // Peek ahead and see if the next element is a cef/media element
  27780. const peekCaretPosition = getNextPosFn(normalizedNextCaretPosition);
  27781. if (peekCaretPosition && isBeforeFn(peekCaretPosition)) {
  27782. if (isMoveInsideSameBlock(normalizedNextCaretPosition, peekCaretPosition)) {
  27783. return Optional.some(() => deleteContentAndShowCaret(editor, range, caretPosition.getNode(), direction, forward, peekCaretPosition));
  27784. }
  27785. }
  27786. return Optional.none();
  27787. };
  27788. const backspaceDelete$9 = (editor, forward) => deleteBoundaryText(editor, forward);
  27789. const getEdgeCefPosition = (editor, atStart) => {
  27790. const root = editor.getBody();
  27791. return atStart ? firstPositionIn(root).filter(isBeforeContentEditableFalse) :
  27792. lastPositionIn(root).filter(isAfterContentEditableFalse);
  27793. };
  27794. const isCefAtEdgeSelected = (editor) => {
  27795. const rng = editor.selection.getRng();
  27796. return !rng.collapsed
  27797. && (getEdgeCefPosition(editor, true).exists((pos) => pos.isEqual(CaretPosition.fromRangeStart(rng)))
  27798. || getEdgeCefPosition(editor, false).exists((pos) => pos.isEqual(CaretPosition.fromRangeEnd(rng))));
  27799. };
  27800. const isCompoundElement = (node) => isNonNullable(node) && (isTableCell$2(SugarElement.fromDom(node)) || isListItem$2(SugarElement.fromDom(node)));
  27801. const DeleteAction = Adt.generate([
  27802. { remove: ['element'] },
  27803. { moveToElement: ['element'] },
  27804. { moveToPosition: ['position'] }
  27805. ]);
  27806. const isAtContentEditableBlockCaret = (forward, from) => {
  27807. const elm = from.getNode(!forward);
  27808. const caretLocation = forward ? 'after' : 'before';
  27809. return isElement$7(elm) && elm.getAttribute('data-mce-caret') === caretLocation;
  27810. };
  27811. const isDeleteFromCefDifferentBlocks = (root, forward, from, to, schema) => {
  27812. const inSameBlock = (elm) => schema.isInline(elm.nodeName.toLowerCase()) && !isInSameBlock(from, to, root);
  27813. return getRelativeCefElm(!forward, from).fold(() => getRelativeCefElm(forward, to).fold(never, inSameBlock), inSameBlock);
  27814. };
  27815. const deleteEmptyBlockOrMoveToCef = (schema, root, forward, from, to) => {
  27816. // TODO: TINY-8865 - This may not be safe to cast as Node below and alternative solutions need to be looked into
  27817. const toCefElm = to.getNode(!forward);
  27818. return getParentBlock$2(SugarElement.fromDom(root), SugarElement.fromDom(from.getNode())).map((blockElm) => isEmpty$4(schema, blockElm) ? DeleteAction.remove(blockElm.dom) : DeleteAction.moveToElement(toCefElm)).orThunk(() => Optional.some(DeleteAction.moveToElement(toCefElm)));
  27819. };
  27820. const findCefPosition = (root, forward, from, schema) => fromPosition(forward, root, from).bind((to) => {
  27821. if (isCompoundElement(to.getNode())) {
  27822. return Optional.none();
  27823. }
  27824. else if (isDeleteFromCefDifferentBlocks(root, forward, from, to, schema)) {
  27825. return Optional.none();
  27826. }
  27827. else if (forward && isContentEditableFalse$a(to.getNode())) {
  27828. return deleteEmptyBlockOrMoveToCef(schema, root, forward, from, to);
  27829. }
  27830. else if (!forward && isContentEditableFalse$a(to.getNode(true))) {
  27831. return deleteEmptyBlockOrMoveToCef(schema, root, forward, from, to);
  27832. }
  27833. else if (forward && isAfterContentEditableFalse(from)) {
  27834. return Optional.some(DeleteAction.moveToPosition(to));
  27835. }
  27836. else if (!forward && isBeforeContentEditableFalse(from)) {
  27837. return Optional.some(DeleteAction.moveToPosition(to));
  27838. }
  27839. else {
  27840. return Optional.none();
  27841. }
  27842. });
  27843. const getContentEditableBlockAction = (forward, elm) => {
  27844. if (isNullable(elm)) {
  27845. return Optional.none();
  27846. }
  27847. else if (forward && isContentEditableFalse$a(elm.nextSibling)) {
  27848. return Optional.some(DeleteAction.moveToElement(elm.nextSibling));
  27849. }
  27850. else if (!forward && isContentEditableFalse$a(elm.previousSibling)) {
  27851. return Optional.some(DeleteAction.moveToElement(elm.previousSibling));
  27852. }
  27853. else {
  27854. return Optional.none();
  27855. }
  27856. };
  27857. const skipMoveToActionFromInlineCefToContent = (root, from, deleteAction) => deleteAction.fold((elm) => Optional.some(DeleteAction.remove(elm)), (elm) => Optional.some(DeleteAction.moveToElement(elm)), (to) => {
  27858. if (isInSameBlock(from, to, root)) {
  27859. return Optional.none();
  27860. }
  27861. else {
  27862. return Optional.some(DeleteAction.moveToPosition(to));
  27863. }
  27864. });
  27865. const getContentEditableAction = (root, forward, from, schema) => {
  27866. if (isAtContentEditableBlockCaret(forward, from)) {
  27867. return getContentEditableBlockAction(forward, from.getNode(!forward))
  27868. .orThunk(() => findCefPosition(root, forward, from, schema));
  27869. }
  27870. else {
  27871. return findCefPosition(root, forward, from, schema).bind((deleteAction) => skipMoveToActionFromInlineCefToContent(root, from, deleteAction));
  27872. }
  27873. };
  27874. const read = (root, forward, rng, schema) => {
  27875. const normalizedRange = normalizeRange$2(forward ? 1 : -1, root, rng);
  27876. const from = CaretPosition.fromRangeStart(normalizedRange);
  27877. const rootElement = SugarElement.fromDom(root);
  27878. // TODO: TINY-8865 - This may not be safe to cast as Node below and alternative solutions need to be looked into
  27879. if (!forward && isAfterContentEditableFalse(from)) {
  27880. return Optional.some(DeleteAction.remove(from.getNode(true)));
  27881. }
  27882. else if (forward && isBeforeContentEditableFalse(from)) {
  27883. return Optional.some(DeleteAction.remove(from.getNode()));
  27884. }
  27885. else if (!forward && isBeforeContentEditableFalse(from) && isAfterBr(rootElement, from, schema)) {
  27886. return findPreviousBr(rootElement, from, schema).map((br) => DeleteAction.remove(br.getNode()));
  27887. }
  27888. else if (forward && isAfterContentEditableFalse(from) && isBeforeBr$1(rootElement, from, schema)) {
  27889. return findNextBr(rootElement, from, schema).map((br) => DeleteAction.remove(br.getNode()));
  27890. }
  27891. else {
  27892. return getContentEditableAction(root, forward, from, schema);
  27893. }
  27894. };
  27895. const deleteElement$1 = (editor, forward) => (element) => {
  27896. editor._selectionOverrides.hideFakeCaret();
  27897. deleteElement$2(editor, forward, SugarElement.fromDom(element));
  27898. return true;
  27899. };
  27900. const moveToElement = (editor, forward) => (element) => {
  27901. const pos = forward ? CaretPosition.before(element) : CaretPosition.after(element);
  27902. editor.selection.setRng(pos.toRange());
  27903. return true;
  27904. };
  27905. const moveToPosition = (editor) => (pos) => {
  27906. editor.selection.setRng(pos.toRange());
  27907. return true;
  27908. };
  27909. const getAncestorCe = (editor, node) => Optional.from(getContentEditableRoot$1(editor.getBody(), node));
  27910. const backspaceDeleteCaret = (editor, forward) => {
  27911. const selectedNode = editor.selection.getNode(); // is the parent node if cursor before/after cef
  27912. // Cases:
  27913. // 1. CEF selectedNode -> return true
  27914. // 2. CET selectedNode -> try to delete, return true if possible else false
  27915. // 3. CET ancestor -> try to delete, return true if possible else false
  27916. // 4. no CET/CEF ancestor -> try to delete, return true if possible else false
  27917. // 5. CEF ancestor -> return true
  27918. return getAncestorCe(editor, selectedNode).filter(isContentEditableFalse$a).fold(() => read(editor.getBody(), forward, editor.selection.getRng(), editor.schema).map((deleteAction) => () => deleteAction.fold(deleteElement$1(editor, forward), moveToElement(editor, forward), moveToPosition(editor))), () => Optional.some(noop));
  27919. };
  27920. const deleteOffscreenSelection = (rootElement) => {
  27921. each$e(descendants(rootElement, '.mce-offscreen-selection'), remove$8);
  27922. };
  27923. const backspaceDeleteRange = (editor, forward) => {
  27924. const selectedNode = editor.selection.getNode(); // is the cef node if cef is selected
  27925. // Cases:
  27926. // 1. Table cell -> return false, as this is handled by `TableDelete` instead
  27927. // 2. CEF selectedNode
  27928. // a. no ancestor CET/CEF || CET ancestor -> run delete code and return true
  27929. // b. CEF ancestor -> return true
  27930. // 3. non-CEF selectedNode -> return false
  27931. if (isContentEditableFalse$a(selectedNode) && !isTableCell$3(selectedNode)) {
  27932. const hasCefAncestor = getAncestorCe(editor, selectedNode.parentNode).filter(isContentEditableFalse$a);
  27933. return hasCefAncestor.fold(() => Optional.some(() => {
  27934. deleteOffscreenSelection(SugarElement.fromDom(editor.getBody()));
  27935. deleteElement$2(editor, forward, SugarElement.fromDom(editor.selection.getNode()));
  27936. paddEmptyBody(editor);
  27937. }), () => Optional.some(noop));
  27938. }
  27939. if (isCefAtEdgeSelected(editor)) {
  27940. return Optional.some(() => {
  27941. deleteRangeContents(editor, editor.selection.getRng(), SugarElement.fromDom(editor.getBody()));
  27942. });
  27943. }
  27944. return Optional.none();
  27945. };
  27946. const paddEmptyElement = (editor) => {
  27947. const dom = editor.dom, selection = editor.selection;
  27948. const ceRoot = getContentEditableRoot$1(editor.getBody(), selection.getNode());
  27949. if (isContentEditableTrue$3(ceRoot) && dom.isBlock(ceRoot) && dom.isEmpty(ceRoot)) {
  27950. const br = dom.create('br', { 'data-mce-bogus': '1' });
  27951. dom.setHTML(ceRoot, '');
  27952. ceRoot.appendChild(br);
  27953. selection.setRng(CaretPosition.before(br).toRange());
  27954. }
  27955. return true;
  27956. };
  27957. const backspaceDelete$8 = (editor, forward) => {
  27958. if (editor.selection.isCollapsed()) {
  27959. return backspaceDeleteCaret(editor, forward);
  27960. }
  27961. else {
  27962. return backspaceDeleteRange(editor, forward);
  27963. }
  27964. };
  27965. const isTextEndpoint = (endpoint) => endpoint.hasOwnProperty('text');
  27966. const isElementEndpoint = (endpoint) => endpoint.hasOwnProperty('marker');
  27967. const getBookmark = (range, createMarker) => {
  27968. const getEndpoint = (container, offset) => {
  27969. if (isText$b(container)) {
  27970. return { text: container, offset };
  27971. }
  27972. else {
  27973. const marker = createMarker();
  27974. const children = container.childNodes;
  27975. if (offset < children.length) {
  27976. container.insertBefore(marker, children[offset]);
  27977. return { marker, before: true };
  27978. }
  27979. else {
  27980. container.appendChild(marker);
  27981. return { marker, before: false };
  27982. }
  27983. }
  27984. };
  27985. const end = getEndpoint(range.endContainer, range.endOffset);
  27986. const start = getEndpoint(range.startContainer, range.startOffset);
  27987. return { start, end };
  27988. };
  27989. const resolveBookmark = (bm) => {
  27990. var _a, _b;
  27991. const { start, end } = bm;
  27992. const rng = new window.Range();
  27993. if (isTextEndpoint(start)) {
  27994. rng.setStart(start.text, start.offset);
  27995. }
  27996. else {
  27997. if (isElementEndpoint(start)) {
  27998. if (start.before) {
  27999. rng.setStartBefore(start.marker);
  28000. }
  28001. else {
  28002. rng.setStartAfter(start.marker);
  28003. }
  28004. (_a = start.marker.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(start.marker);
  28005. }
  28006. }
  28007. if (isTextEndpoint(end)) {
  28008. rng.setEnd(end.text, end.offset);
  28009. }
  28010. else {
  28011. if (isElementEndpoint(end)) {
  28012. if (end.before) {
  28013. rng.setEndBefore(end.marker);
  28014. }
  28015. else {
  28016. rng.setEndAfter(end.marker);
  28017. }
  28018. (_b = end.marker.parentNode) === null || _b === void 0 ? void 0 : _b.removeChild(end.marker);
  28019. }
  28020. }
  28021. return rng;
  28022. };
  28023. const backspaceDelete$7 = (editor, forward) => {
  28024. var _a;
  28025. const dom = editor.dom;
  28026. const startBlock = dom.getParent(editor.selection.getStart(), dom.isBlock);
  28027. const endBlock = dom.getParent(editor.selection.getEnd(), dom.isBlock);
  28028. const body = editor.getBody();
  28029. const startBlockName = (_a = startBlock === null || startBlock === void 0 ? void 0 : startBlock.nodeName) === null || _a === void 0 ? void 0 : _a.toLowerCase();
  28030. // Only act on single root div that is not empty
  28031. if (startBlockName === 'div' && startBlock && endBlock && startBlock === body.firstChild && endBlock === body.lastChild && !dom.isEmpty(body)) {
  28032. const wrapper = startBlock.cloneNode(false);
  28033. const deleteAction = () => {
  28034. if (forward) {
  28035. execNativeForwardDeleteCommand(editor);
  28036. }
  28037. else {
  28038. execNativeDeleteCommand(editor);
  28039. }
  28040. // Div was deleted by delete operation then lets restore it
  28041. if (body.firstChild !== startBlock) {
  28042. const bookmark = getBookmark(editor.selection.getRng(), () => document.createElement('span'));
  28043. Array.from(body.childNodes).forEach((node) => wrapper.appendChild(node));
  28044. body.appendChild(wrapper);
  28045. editor.selection.setRng(resolveBookmark(bookmark));
  28046. }
  28047. };
  28048. return Optional.some(deleteAction);
  28049. }
  28050. return Optional.none();
  28051. };
  28052. const deleteCaret$2 = (editor, forward) => {
  28053. const fromPos = CaretPosition.fromRangeStart(editor.selection.getRng());
  28054. return fromPosition(forward, editor.getBody(), fromPos)
  28055. .filter((pos) => forward ? isBeforeImageBlock(pos) : isAfterImageBlock(pos))
  28056. .bind((pos) => getChildNodeAtRelativeOffset(forward ? 0 : -1, pos))
  28057. .map((elm) => () => editor.selection.select(elm));
  28058. };
  28059. const backspaceDelete$6 = (editor, forward) => editor.selection.isCollapsed() ? deleteCaret$2(editor, forward) : Optional.none();
  28060. const isText$2 = isText$b;
  28061. const startsWithCaretContainer = (node) => isText$2(node) && node.data[0] === ZWSP$1;
  28062. const endsWithCaretContainer = (node) => isText$2(node) && node.data[node.data.length - 1] === ZWSP$1;
  28063. const createZwsp = (node) => {
  28064. var _a;
  28065. const doc = (_a = node.ownerDocument) !== null && _a !== void 0 ? _a : document;
  28066. return doc.createTextNode(ZWSP$1);
  28067. };
  28068. const insertBefore$1 = (node) => {
  28069. var _a;
  28070. if (isText$2(node.previousSibling)) {
  28071. if (endsWithCaretContainer(node.previousSibling)) {
  28072. return node.previousSibling;
  28073. }
  28074. else {
  28075. node.previousSibling.appendData(ZWSP$1);
  28076. return node.previousSibling;
  28077. }
  28078. }
  28079. else if (isText$2(node)) {
  28080. if (startsWithCaretContainer(node)) {
  28081. return node;
  28082. }
  28083. else {
  28084. node.insertData(0, ZWSP$1);
  28085. return node;
  28086. }
  28087. }
  28088. else {
  28089. const newNode = createZwsp(node);
  28090. (_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(newNode, node);
  28091. return newNode;
  28092. }
  28093. };
  28094. const insertAfter$1 = (node) => {
  28095. var _a, _b;
  28096. if (isText$2(node.nextSibling)) {
  28097. if (startsWithCaretContainer(node.nextSibling)) {
  28098. return node.nextSibling;
  28099. }
  28100. else {
  28101. node.nextSibling.insertData(0, ZWSP$1);
  28102. return node.nextSibling;
  28103. }
  28104. }
  28105. else if (isText$2(node)) {
  28106. if (endsWithCaretContainer(node)) {
  28107. return node;
  28108. }
  28109. else {
  28110. node.appendData(ZWSP$1);
  28111. return node;
  28112. }
  28113. }
  28114. else {
  28115. const newNode = createZwsp(node);
  28116. if (node.nextSibling) {
  28117. (_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(newNode, node.nextSibling);
  28118. }
  28119. else {
  28120. (_b = node.parentNode) === null || _b === void 0 ? void 0 : _b.appendChild(newNode);
  28121. }
  28122. return newNode;
  28123. }
  28124. };
  28125. const insertInline = (before, node) => before ? insertBefore$1(node) : insertAfter$1(node);
  28126. const insertInlineBefore = curry(insertInline, true);
  28127. const insertInlineAfter = curry(insertInline, false);
  28128. const insertInlinePos = (pos, before) => {
  28129. if (isText$b(pos.container())) {
  28130. return insertInline(before, pos.container());
  28131. }
  28132. else {
  28133. // TODO: TINY-8865 - This may not be safe to cast as Node and alternative solutions need to be looked into
  28134. return insertInline(before, pos.getNode());
  28135. }
  28136. };
  28137. const isPosCaretContainer = (pos, caret) => {
  28138. const caretNode = caret.get();
  28139. return caretNode && pos.container() === caretNode && isCaretContainerInline(caretNode);
  28140. };
  28141. const renderCaret = (caret, location) => location.fold((element) => {
  28142. remove$2(caret.get());
  28143. const text = insertInlineBefore(element);
  28144. caret.set(text);
  28145. return Optional.some(CaretPosition(text, text.length - 1));
  28146. }, (element) => // Start
  28147. firstPositionIn(element).map((pos) => {
  28148. if (!isPosCaretContainer(pos, caret)) {
  28149. remove$2(caret.get());
  28150. const text = insertInlinePos(pos, true);
  28151. caret.set(text);
  28152. return CaretPosition(text, 1);
  28153. }
  28154. else {
  28155. const node = caret.get();
  28156. return CaretPosition(node, 1);
  28157. }
  28158. }), (element) => // End
  28159. lastPositionIn(element).map((pos) => {
  28160. if (!isPosCaretContainer(pos, caret)) {
  28161. remove$2(caret.get());
  28162. const text = insertInlinePos(pos, false);
  28163. caret.set(text);
  28164. return CaretPosition(text, text.length - 1);
  28165. }
  28166. else {
  28167. const node = caret.get();
  28168. return CaretPosition(node, node.length - 1);
  28169. }
  28170. }), (element) => {
  28171. remove$2(caret.get());
  28172. const text = insertInlineAfter(element);
  28173. caret.set(text);
  28174. return Optional.some(CaretPosition(text, 1));
  28175. });
  28176. const evaluateUntil = (fns, args) => {
  28177. for (let i = 0; i < fns.length; i++) {
  28178. const result = fns[i].apply(null, args);
  28179. if (result.isSome()) {
  28180. return result;
  28181. }
  28182. }
  28183. return Optional.none();
  28184. };
  28185. const Location = Adt.generate([
  28186. { before: ['element'] },
  28187. { start: ['element'] },
  28188. { end: ['element'] },
  28189. { after: ['element'] }
  28190. ]);
  28191. const rescope$1 = (rootNode, node) => {
  28192. const parentBlock = getParentBlock$3(node, rootNode);
  28193. return parentBlock ? parentBlock : rootNode;
  28194. };
  28195. const before = (isInlineTarget, rootNode, pos) => {
  28196. const nPos = normalizeForwards(pos);
  28197. const scope = rescope$1(rootNode, nPos.container());
  28198. return findRootInline(isInlineTarget, scope, nPos).fold(() => nextPosition(scope, nPos)
  28199. .bind(curry(findRootInline, isInlineTarget, scope))
  28200. .map((inline) => Location.before(inline)), Optional.none);
  28201. };
  28202. const isNotInsideFormatCaretContainer = (rootNode, elm) => getParentCaretContainer(rootNode, elm) === null;
  28203. const findInsideRootInline = (isInlineTarget, rootNode, pos) => findRootInline(isInlineTarget, rootNode, pos).filter(curry(isNotInsideFormatCaretContainer, rootNode));
  28204. const start$1 = (isInlineTarget, rootNode, pos) => {
  28205. const nPos = normalizeBackwards(pos);
  28206. return findInsideRootInline(isInlineTarget, rootNode, nPos).bind((inline) => {
  28207. const prevPos = prevPosition(inline, nPos);
  28208. return prevPos.isNone() ? Optional.some(Location.start(inline)) : Optional.none();
  28209. });
  28210. };
  28211. const end = (isInlineTarget, rootNode, pos) => {
  28212. const nPos = normalizeForwards(pos);
  28213. return findInsideRootInline(isInlineTarget, rootNode, nPos).bind((inline) => {
  28214. const nextPos = nextPosition(inline, nPos);
  28215. return nextPos.isNone() ? Optional.some(Location.end(inline)) : Optional.none();
  28216. });
  28217. };
  28218. const after = (isInlineTarget, rootNode, pos) => {
  28219. const nPos = normalizeBackwards(pos);
  28220. const scope = rescope$1(rootNode, nPos.container());
  28221. return findRootInline(isInlineTarget, scope, nPos).fold(() => prevPosition(scope, nPos)
  28222. .bind(curry(findRootInline, isInlineTarget, scope))
  28223. .map((inline) => Location.after(inline)), Optional.none);
  28224. };
  28225. const isValidLocation = (location) => !isRtl(getElement(location));
  28226. const readLocation = (isInlineTarget, rootNode, pos) => {
  28227. const location = evaluateUntil([
  28228. before,
  28229. start$1,
  28230. end,
  28231. after
  28232. ], [isInlineTarget, rootNode, pos]);
  28233. return location.filter(isValidLocation);
  28234. };
  28235. const getElement = (location) => location.fold(identity, // Before
  28236. identity, // Start
  28237. identity, // End
  28238. identity // After
  28239. );
  28240. const getName = (location) => location.fold(constant('before'), // Before
  28241. constant('start'), // Start
  28242. constant('end'), // End
  28243. constant('after') // After
  28244. );
  28245. const outside = (location) => location.fold(Location.before, // Before
  28246. Location.before, // Start
  28247. Location.after, // End
  28248. Location.after // After
  28249. );
  28250. const inside = (location) => location.fold(Location.start, // Before
  28251. Location.start, // Start
  28252. Location.end, // End
  28253. Location.end // After
  28254. );
  28255. const isEq = (location1, location2) => getName(location1) === getName(location2) && getElement(location1) === getElement(location2);
  28256. const betweenInlines = (forward, isInlineTarget, rootNode, from, to, location) => lift2(findRootInline(isInlineTarget, rootNode, from), findRootInline(isInlineTarget, rootNode, to), (fromInline, toInline) => {
  28257. if (fromInline !== toInline && hasSameParentBlock(rootNode, fromInline, toInline)) {
  28258. // Force after since some browsers normalize and lean left into the closest inline
  28259. return Location.after(forward ? fromInline : toInline);
  28260. }
  28261. else {
  28262. return location;
  28263. }
  28264. }).getOr(location);
  28265. const skipNoMovement = (fromLocation, toLocation) => fromLocation.fold(always, (fromLocation) => !isEq(fromLocation, toLocation));
  28266. const findLocationTraverse = (forward, isInlineTarget, rootNode, fromLocation, pos) => {
  28267. const from = normalizePosition(forward, pos);
  28268. const to = fromPosition(forward, rootNode, from).map(curry(normalizePosition, forward));
  28269. const location = to.fold(() => fromLocation.map(outside), (to) => readLocation(isInlineTarget, rootNode, to)
  28270. .map(curry(betweenInlines, forward, isInlineTarget, rootNode, from, to))
  28271. .filter(curry(skipNoMovement, fromLocation)));
  28272. return location.filter(isValidLocation);
  28273. };
  28274. const findLocationSimple = (forward, location) => {
  28275. if (forward) {
  28276. return location.fold(compose(Optional.some, Location.start), // Before -> Start
  28277. Optional.none, compose(Optional.some, Location.after), // End -> After
  28278. Optional.none);
  28279. }
  28280. else {
  28281. return location.fold(Optional.none, compose(Optional.some, Location.before), // Before <- Start
  28282. Optional.none, compose(Optional.some, Location.end) // End <- After
  28283. );
  28284. }
  28285. };
  28286. const findLocation$1 = (forward, isInlineTarget, rootNode, pos) => {
  28287. const from = normalizePosition(forward, pos);
  28288. const fromLocation = readLocation(isInlineTarget, rootNode, from);
  28289. return readLocation(isInlineTarget, rootNode, from).bind(curry(findLocationSimple, forward))
  28290. .orThunk(() => findLocationTraverse(forward, isInlineTarget, rootNode, fromLocation, pos));
  28291. };
  28292. const hasSelectionModifyApi = (editor) => {
  28293. return isFunction(editor.selection.getSel().modify);
  28294. };
  28295. const moveRel = (forward, selection, pos) => {
  28296. const delta = forward ? 1 : -1;
  28297. selection.setRng(CaretPosition(pos.container(), pos.offset() + delta).toRange());
  28298. selection.getSel().modify('move', forward ? 'forward' : 'backward', 'word');
  28299. return true;
  28300. };
  28301. const moveByWord = (forward, editor) => {
  28302. const rng = editor.selection.getRng();
  28303. const pos = forward ? CaretPosition.fromRangeEnd(rng) : CaretPosition.fromRangeStart(rng);
  28304. if (!hasSelectionModifyApi(editor)) {
  28305. return false;
  28306. }
  28307. else if (forward && isBeforeInline(pos)) {
  28308. return moveRel(true, editor.selection, pos);
  28309. }
  28310. else if (!forward && isAfterInline(pos)) {
  28311. return moveRel(false, editor.selection, pos);
  28312. }
  28313. else {
  28314. return false;
  28315. }
  28316. };
  28317. var BreakType;
  28318. (function (BreakType) {
  28319. BreakType[BreakType["Br"] = 0] = "Br";
  28320. BreakType[BreakType["Block"] = 1] = "Block";
  28321. BreakType[BreakType["Wrap"] = 2] = "Wrap";
  28322. BreakType[BreakType["Eol"] = 3] = "Eol";
  28323. })(BreakType || (BreakType = {}));
  28324. const flip = (direction, positions) => direction === -1 /* HDirection.Backwards */ ? reverse(positions) : positions;
  28325. const walk$1 = (direction, caretWalker, pos) => direction === 1 /* HDirection.Forwards */ ? caretWalker.next(pos) : caretWalker.prev(pos);
  28326. const getBreakType = (scope, direction, currentPos, nextPos) => {
  28327. if (isBr$7(nextPos.getNode(direction === 1 /* HDirection.Forwards */))) {
  28328. return BreakType.Br;
  28329. }
  28330. else if (isInSameBlock(currentPos, nextPos) === false) {
  28331. return BreakType.Block;
  28332. }
  28333. else {
  28334. return BreakType.Wrap;
  28335. }
  28336. };
  28337. const getPositionsUntil = (predicate, direction, scope, start) => {
  28338. const caretWalker = CaretWalker(scope);
  28339. let currentPos = start;
  28340. const positions = [];
  28341. while (currentPos) {
  28342. const nextPos = walk$1(direction, caretWalker, currentPos);
  28343. if (!nextPos) {
  28344. break;
  28345. }
  28346. if (isBr$7(nextPos.getNode(false))) {
  28347. if (direction === 1 /* HDirection.Forwards */) {
  28348. return { positions: flip(direction, positions).concat([nextPos]), breakType: BreakType.Br, breakAt: Optional.some(nextPos) };
  28349. }
  28350. else {
  28351. return { positions: flip(direction, positions), breakType: BreakType.Br, breakAt: Optional.some(nextPos) };
  28352. }
  28353. }
  28354. if (!nextPos.isVisible()) {
  28355. currentPos = nextPos;
  28356. continue;
  28357. }
  28358. if (predicate(currentPos, nextPos)) {
  28359. const breakType = getBreakType(scope, direction, currentPos, nextPos);
  28360. return { positions: flip(direction, positions), breakType, breakAt: Optional.some(nextPos) };
  28361. }
  28362. positions.push(nextPos);
  28363. currentPos = nextPos;
  28364. }
  28365. return { positions: flip(direction, positions), breakType: BreakType.Eol, breakAt: Optional.none() };
  28366. };
  28367. const getAdjacentLinePositions = (direction, getPositionsUntilBreak, scope, start) => getPositionsUntilBreak(scope, start).breakAt.map((pos) => {
  28368. const positions = getPositionsUntilBreak(scope, pos).positions;
  28369. return direction === -1 /* HDirection.Backwards */ ? positions.concat(pos) : [pos].concat(positions);
  28370. }).getOr([]);
  28371. const findClosestHorizontalPositionFromPoint = (positions, x) => foldl(positions, (acc, newPos) => acc.fold(() => Optional.some(newPos), (lastPos) => lift2(head(lastPos.getClientRects()), head(newPos.getClientRects()), (lastRect, newRect) => {
  28372. const lastDist = Math.abs(x - lastRect.left);
  28373. const newDist = Math.abs(x - newRect.left);
  28374. return newDist <= lastDist ? newPos : lastPos;
  28375. }).or(acc)), Optional.none());
  28376. const findClosestHorizontalPosition = (positions, pos) => head(pos.getClientRects()).bind((targetRect) => findClosestHorizontalPositionFromPoint(positions, targetRect.left));
  28377. const getPositionsUntilPreviousLine = curry(getPositionsUntil, CaretPosition.isAbove, -1);
  28378. const getPositionsUntilNextLine = curry(getPositionsUntil, CaretPosition.isBelow, 1);
  28379. const getPositionsAbove = curry(getAdjacentLinePositions, -1, getPositionsUntilPreviousLine);
  28380. const getPositionsBelow = curry(getAdjacentLinePositions, 1, getPositionsUntilNextLine);
  28381. const isAtFirstLine = (scope, pos) => getPositionsUntilPreviousLine(scope, pos).breakAt.isNone();
  28382. const isAtLastLine = (scope, pos) => getPositionsUntilNextLine(scope, pos).breakAt.isNone();
  28383. const getFirstLinePositions = (scope) => firstPositionIn(scope).map((pos) => [pos].concat(getPositionsUntilNextLine(scope, pos).positions)).getOr([]);
  28384. const getLastLinePositions = (scope) => lastPositionIn(scope).map((pos) => getPositionsUntilPreviousLine(scope, pos).positions.concat(pos)).getOr([]);
  28385. const getClosestPositionAbove = (scope, pos) => findClosestHorizontalPosition(getPositionsAbove(scope, pos), pos);
  28386. const getClosestPositionBelow = (scope, pos) => findClosestHorizontalPosition(getPositionsBelow(scope, pos), pos);
  28387. const isContentEditableFalse$4 = isContentEditableFalse$a;
  28388. const distanceToRectLeft$1 = (clientRect, clientX) => Math.abs(clientRect.left - clientX);
  28389. const distanceToRectRight$1 = (clientRect, clientX) => Math.abs(clientRect.right - clientX);
  28390. const isNodeClientRect = (rect) => hasNonNullableKey(rect, 'node');
  28391. const findClosestClientRect = (clientRects, clientX) => reduce(clientRects, (oldClientRect, clientRect) => {
  28392. const oldDistance = Math.min(distanceToRectLeft$1(oldClientRect, clientX), distanceToRectRight$1(oldClientRect, clientX));
  28393. const newDistance = Math.min(distanceToRectLeft$1(clientRect, clientX), distanceToRectRight$1(clientRect, clientX));
  28394. // cE=false has higher priority
  28395. if (newDistance === oldDistance && isNodeClientRect(clientRect) && isContentEditableFalse$4(clientRect.node)) {
  28396. return clientRect;
  28397. }
  28398. if (newDistance < oldDistance) {
  28399. return clientRect;
  28400. }
  28401. return oldClientRect;
  28402. });
  28403. const getNodeClientRects = (node) => {
  28404. const toArrayWithNode = (clientRects) => {
  28405. return map$3(clientRects, (rect) => {
  28406. const clientRect = clone$1(rect);
  28407. clientRect.node = node;
  28408. return clientRect;
  28409. });
  28410. };
  28411. if (isElement$7(node)) {
  28412. return toArrayWithNode(node.getClientRects());
  28413. }
  28414. else if (isText$b(node)) {
  28415. const rng = node.ownerDocument.createRange();
  28416. rng.setStart(node, 0);
  28417. rng.setEnd(node, node.data.length);
  28418. return toArrayWithNode(rng.getClientRects());
  28419. }
  28420. else {
  28421. return [];
  28422. }
  28423. };
  28424. const getClientRects = (nodes) => bind$3(nodes, getNodeClientRects);
  28425. var VDirection;
  28426. (function (VDirection) {
  28427. VDirection[VDirection["Up"] = -1] = "Up";
  28428. VDirection[VDirection["Down"] = 1] = "Down";
  28429. })(VDirection || (VDirection = {}));
  28430. const findUntil = (direction, root, predicateFn, node) => {
  28431. let currentNode = node;
  28432. while ((currentNode = findNode(currentNode, direction, isEditableCaretCandidate$1, root))) {
  28433. if (predicateFn(currentNode)) {
  28434. return;
  28435. }
  28436. }
  28437. };
  28438. const walkUntil = (direction, isAboveFn, isBeflowFn, root, predicateFn, caretPosition) => {
  28439. let line = 0;
  28440. const result = [];
  28441. const add = (node) => {
  28442. let clientRects = getClientRects([node]);
  28443. if (direction === VDirection.Up) {
  28444. clientRects = clientRects.reverse();
  28445. }
  28446. for (let i = 0; i < clientRects.length; i++) {
  28447. const clientRect = clientRects[i];
  28448. if (isBeflowFn(clientRect, targetClientRect)) {
  28449. continue;
  28450. }
  28451. if (result.length > 0 && isAboveFn(clientRect, last(result))) {
  28452. line++;
  28453. }
  28454. clientRect.line = line;
  28455. if (predicateFn(clientRect)) {
  28456. return true;
  28457. }
  28458. result.push(clientRect);
  28459. }
  28460. return false;
  28461. };
  28462. const targetClientRect = last(caretPosition.getClientRects());
  28463. if (!targetClientRect) {
  28464. return result;
  28465. }
  28466. const node = caretPosition.getNode();
  28467. if (node) {
  28468. add(node);
  28469. findUntil(direction, root, add, node);
  28470. }
  28471. return result;
  28472. };
  28473. const aboveLineNumber = (lineNumber, clientRect) => clientRect.line > lineNumber;
  28474. const isLineNumber = (lineNumber, clientRect) => clientRect.line === lineNumber;
  28475. const upUntil = curry(walkUntil, VDirection.Up, isAbove$1, isBelow$1);
  28476. const downUntil = curry(walkUntil, VDirection.Down, isBelow$1, isAbove$1);
  28477. const getLastClientRect = (caretPosition) => {
  28478. // ASSUMPTION: There should always be at least one client rect here
  28479. return last(caretPosition.getClientRects());
  28480. };
  28481. const positionsUntil = (direction, root, predicateFn, node) => {
  28482. const caretWalker = CaretWalker(root);
  28483. let walkFn;
  28484. let isBelowFn;
  28485. let isAboveFn;
  28486. let caretPosition;
  28487. const result = [];
  28488. let line = 0;
  28489. if (direction === VDirection.Down) {
  28490. walkFn = caretWalker.next;
  28491. isBelowFn = isBelow$1;
  28492. isAboveFn = isAbove$1;
  28493. caretPosition = CaretPosition.after(node);
  28494. }
  28495. else {
  28496. walkFn = caretWalker.prev;
  28497. isBelowFn = isAbove$1;
  28498. isAboveFn = isBelow$1;
  28499. caretPosition = CaretPosition.before(node);
  28500. }
  28501. const targetClientRect = getLastClientRect(caretPosition);
  28502. do {
  28503. if (!caretPosition.isVisible()) {
  28504. continue;
  28505. }
  28506. const rect = getLastClientRect(caretPosition);
  28507. if (isAboveFn(rect, targetClientRect)) {
  28508. continue;
  28509. }
  28510. if (result.length > 0 && isBelowFn(rect, last(result))) {
  28511. line++;
  28512. }
  28513. const clientRect = clone$1(rect);
  28514. clientRect.position = caretPosition;
  28515. clientRect.line = line;
  28516. if (predicateFn(clientRect)) {
  28517. return result;
  28518. }
  28519. result.push(clientRect);
  28520. } while ((caretPosition = walkFn(caretPosition)));
  28521. return result;
  28522. };
  28523. const isAboveLine = (lineNumber) => (clientRect) => aboveLineNumber(lineNumber, clientRect);
  28524. const isLine = (lineNumber) => (clientRect) => isLineNumber(lineNumber, clientRect);
  28525. const moveToRange = (editor, rng) => {
  28526. editor.selection.setRng(rng);
  28527. // Don't reuse the original range as TinyMCE will adjust it
  28528. scrollRangeIntoView(editor, editor.selection.getRng());
  28529. };
  28530. const renderRangeCaretOpt = (editor, range, scrollIntoView) => Optional.some(renderRangeCaret(editor, range, scrollIntoView));
  28531. const moveHorizontally = (editor, direction, range, isBefore, isAfter, isElement) => {
  28532. const forwards = direction === 1 /* HDirection.Forwards */;
  28533. const caretWalker = CaretWalker(editor.getBody());
  28534. const getNextPosFn = curry(getVisualCaretPosition, forwards ? caretWalker.next : caretWalker.prev);
  28535. const isBeforeFn = forwards ? isBefore : isAfter;
  28536. if (!range.collapsed) {
  28537. const node = getSelectedNode(range);
  28538. if (isElement(node)) {
  28539. return showCaret(direction, editor, node, direction === -1 /* HDirection.Backwards */, false);
  28540. }
  28541. else if (isCefAtEdgeSelected(editor)) {
  28542. const newRange = range.cloneRange();
  28543. newRange.collapse(direction === -1 /* HDirection.Backwards */);
  28544. return Optional.from(newRange);
  28545. }
  28546. }
  28547. const caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range);
  28548. if (isBeforeFn(caretPosition)) {
  28549. return selectNode(editor, caretPosition.getNode(!forwards));
  28550. }
  28551. let nextCaretPosition = getNextPosFn(caretPosition);
  28552. const rangeIsInContainerBlock = isRangeInCaretContainerBlock(range);
  28553. if (!nextCaretPosition) {
  28554. return rangeIsInContainerBlock ? Optional.some(range) : Optional.none();
  28555. }
  28556. else {
  28557. nextCaretPosition = normalizePosition(forwards, nextCaretPosition);
  28558. }
  28559. if (isBeforeFn(nextCaretPosition)) {
  28560. return showCaret(direction, editor, nextCaretPosition.getNode(!forwards), forwards, false);
  28561. }
  28562. // Peek ahead for handling of ab|c<span cE=false> -> abc|<span cE=false>
  28563. const peekCaretPosition = getNextPosFn(nextCaretPosition);
  28564. if (peekCaretPosition && isBeforeFn(peekCaretPosition)) {
  28565. if (isMoveInsideSameBlock(nextCaretPosition, peekCaretPosition)) {
  28566. return showCaret(direction, editor, peekCaretPosition.getNode(!forwards), forwards, false);
  28567. }
  28568. }
  28569. if (rangeIsInContainerBlock) {
  28570. return renderRangeCaretOpt(editor, nextCaretPosition.toRange(), false);
  28571. }
  28572. return Optional.none();
  28573. };
  28574. const moveVertically = (editor, direction, range, isBefore, isAfter, isElement) => {
  28575. const caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range);
  28576. const caretClientRect = last(caretPosition.getClientRects());
  28577. const forwards = direction === VDirection.Down;
  28578. const root = editor.getBody();
  28579. if (!caretClientRect) {
  28580. return Optional.none();
  28581. }
  28582. if (isCefAtEdgeSelected(editor)) {
  28583. const caretPosition = forwards ? CaretPosition.fromRangeEnd(range) : CaretPosition.fromRangeStart(range);
  28584. const getClosestFn = !forwards ? getClosestPositionAbove : getClosestPositionBelow;
  28585. return getClosestFn(root, caretPosition)
  28586. .orThunk(() => Optional.from(caretPosition))
  28587. .map((pos) => pos.toRange());
  28588. }
  28589. const walkerFn = forwards ? downUntil : upUntil;
  28590. const linePositions = walkerFn(root, isAboveLine(1), caretPosition);
  28591. const nextLinePositions = filter$5(linePositions, isLine(1));
  28592. const clientX = caretClientRect.left;
  28593. const nextLineRect = findClosestClientRect(nextLinePositions, clientX);
  28594. if (nextLineRect && isElement(nextLineRect.node)) {
  28595. const dist1 = Math.abs(clientX - nextLineRect.left);
  28596. const dist2 = Math.abs(clientX - nextLineRect.right);
  28597. return showCaret(direction, editor, nextLineRect.node, dist1 < dist2, false);
  28598. }
  28599. let currentNode;
  28600. if (isBefore(caretPosition)) {
  28601. currentNode = caretPosition.getNode();
  28602. }
  28603. else if (isAfter(caretPosition)) {
  28604. currentNode = caretPosition.getNode(true);
  28605. }
  28606. else {
  28607. currentNode = getSelectedNode(range);
  28608. }
  28609. if (currentNode) {
  28610. const caretPositions = positionsUntil(direction, root, isAboveLine(1), currentNode);
  28611. let closestNextLineRect = findClosestClientRect(filter$5(caretPositions, isLine(1)), clientX);
  28612. if (closestNextLineRect) {
  28613. return renderRangeCaretOpt(editor, closestNextLineRect.position.toRange(), false);
  28614. }
  28615. closestNextLineRect = last(filter$5(caretPositions, isLine(0)));
  28616. if (closestNextLineRect) {
  28617. return renderRangeCaretOpt(editor, closestNextLineRect.position.toRange(), false);
  28618. }
  28619. }
  28620. if (nextLinePositions.length === 0) {
  28621. return getLineEndPoint(editor, forwards).filter(forwards ? isAfter : isBefore)
  28622. .map((pos) => renderRangeCaret(editor, pos.toRange(), false));
  28623. }
  28624. return Optional.none();
  28625. };
  28626. const getLineEndPoint = (editor, forward) => {
  28627. const rng = editor.selection.getRng();
  28628. const from = forward ? CaretPosition.fromRangeEnd(rng) : CaretPosition.fromRangeStart(rng);
  28629. const host = getEditingHost(from.container(), editor.getBody());
  28630. if (forward) {
  28631. const lineInfo = getPositionsUntilNextLine(host, from);
  28632. return last$2(lineInfo.positions);
  28633. }
  28634. else {
  28635. const lineInfo = getPositionsUntilPreviousLine(host, from);
  28636. return head(lineInfo.positions);
  28637. }
  28638. };
  28639. const moveToLineEndPoint$3 = (editor, forward, isElementPosition) => getLineEndPoint(editor, forward).filter(isElementPosition).exists((pos) => {
  28640. editor.selection.setRng(pos.toRange());
  28641. return true;
  28642. });
  28643. const setCaretPosition = (editor, pos) => {
  28644. const rng = editor.dom.createRng();
  28645. rng.setStart(pos.container(), pos.offset());
  28646. rng.setEnd(pos.container(), pos.offset());
  28647. editor.selection.setRng(rng);
  28648. };
  28649. const setSelected = (state, elm) => {
  28650. if (state) {
  28651. elm.setAttribute('data-mce-selected', 'inline-boundary');
  28652. }
  28653. else {
  28654. elm.removeAttribute('data-mce-selected');
  28655. }
  28656. };
  28657. const renderCaretLocation = (editor, caret, location) => renderCaret(caret, location).map((pos) => {
  28658. setCaretPosition(editor, pos);
  28659. return location;
  28660. });
  28661. const getPositionFromRange = (range, root, forward) => {
  28662. const start = CaretPosition.fromRangeStart(range);
  28663. if (range.collapsed) {
  28664. return start;
  28665. }
  28666. else {
  28667. const end = CaretPosition.fromRangeEnd(range);
  28668. return forward ? prevPosition(root, end).getOr(end) : nextPosition(root, start).getOr(start);
  28669. }
  28670. };
  28671. const findLocation = (editor, caret, forward) => {
  28672. const rootNode = editor.getBody();
  28673. const from = getPositionFromRange(editor.selection.getRng(), rootNode, forward);
  28674. const isInlineTarget$1 = curry(isInlineTarget, editor);
  28675. const location = findLocation$1(forward, isInlineTarget$1, rootNode, from);
  28676. return location.bind((location) => renderCaretLocation(editor, caret, location));
  28677. };
  28678. const toggleInlines = (isInlineTarget, dom, elms) => {
  28679. const inlineBoundaries = map$3(descendants(SugarElement.fromDom(dom.getRoot()), '*[data-mce-selected="inline-boundary"]'), (e) => e.dom);
  28680. const selectedInlines = filter$5(inlineBoundaries, isInlineTarget);
  28681. const targetInlines = filter$5(elms, isInlineTarget);
  28682. each$e(difference(selectedInlines, targetInlines), curry(setSelected, false));
  28683. each$e(difference(targetInlines, selectedInlines), curry(setSelected, true));
  28684. };
  28685. const safeRemoveCaretContainer = (editor, caret) => {
  28686. const caretValue = caret.get();
  28687. if (editor.selection.isCollapsed() && !editor.composing && caretValue) {
  28688. const pos = CaretPosition.fromRangeStart(editor.selection.getRng());
  28689. if (CaretPosition.isTextPosition(pos) && !isAtZwsp(pos)) {
  28690. setCaretPosition(editor, removeAndReposition(caretValue, pos));
  28691. caret.set(null);
  28692. }
  28693. }
  28694. };
  28695. const renderInsideInlineCaret = (isInlineTarget, editor, caret, elms) => {
  28696. if (editor.selection.isCollapsed()) {
  28697. const inlines = filter$5(elms, isInlineTarget);
  28698. each$e(inlines, (_inline) => {
  28699. const pos = CaretPosition.fromRangeStart(editor.selection.getRng());
  28700. readLocation(isInlineTarget, editor.getBody(), pos).bind((location) => renderCaretLocation(editor, caret, location));
  28701. });
  28702. }
  28703. };
  28704. const move$3 = (editor, caret, forward) => isInlineBoundariesEnabled(editor) ? findLocation(editor, caret, forward).isSome() : false;
  28705. const moveWord = (forward, editor, _caret) => isInlineBoundariesEnabled(editor) ? moveByWord(forward, editor) : false;
  28706. const setupSelectedState = (editor) => {
  28707. const caret = Cell(null);
  28708. const isInlineTarget$1 = curry(isInlineTarget, editor);
  28709. editor.on('NodeChange', (e) => {
  28710. if (isInlineBoundariesEnabled(editor)) {
  28711. toggleInlines(isInlineTarget$1, editor.dom, e.parents);
  28712. safeRemoveCaretContainer(editor, caret);
  28713. renderInsideInlineCaret(isInlineTarget$1, editor, caret, e.parents);
  28714. }
  28715. });
  28716. return caret;
  28717. };
  28718. const moveNextWord = curry(moveWord, true);
  28719. const movePrevWord = curry(moveWord, false);
  28720. const moveToLineEndPoint$2 = (editor, forward, caret) => {
  28721. if (isInlineBoundariesEnabled(editor)) {
  28722. // Try to find the line endpoint, however if one isn't found then assume we're already at the end point
  28723. const linePoint = getLineEndPoint(editor, forward).getOrThunk(() => {
  28724. const rng = editor.selection.getRng();
  28725. return forward ? CaretPosition.fromRangeEnd(rng) : CaretPosition.fromRangeStart(rng);
  28726. });
  28727. return readLocation(curry(isInlineTarget, editor), editor.getBody(), linePoint).exists((loc) => {
  28728. const outsideLoc = outside(loc);
  28729. return renderCaret(caret, outsideLoc).exists((pos) => {
  28730. setCaretPosition(editor, pos);
  28731. return true;
  28732. });
  28733. });
  28734. }
  28735. else {
  28736. return false;
  28737. }
  28738. };
  28739. const rangeFromPositions = (from, to) => {
  28740. const range = document.createRange();
  28741. range.setStart(from.container(), from.offset());
  28742. range.setEnd(to.container(), to.offset());
  28743. return range;
  28744. };
  28745. // Checks for delete at <code>|a</code> when there is only one item left except the zwsp caret container nodes
  28746. const hasOnlyTwoOrLessPositionsLeft = (elm) => lift2(firstPositionIn(elm), lastPositionIn(elm), (firstPos, lastPos) => {
  28747. const normalizedFirstPos = normalizePosition(true, firstPos);
  28748. const normalizedLastPos = normalizePosition(false, lastPos);
  28749. return nextPosition(elm, normalizedFirstPos).forall((pos) => pos.isEqual(normalizedLastPos));
  28750. }).getOr(true);
  28751. const setCaretLocation = (editor, caret) => (location) => renderCaret(caret, location).map((pos) => () => setCaretPosition(editor, pos));
  28752. const deleteFromTo = (editor, caret, from, to) => {
  28753. const rootNode = editor.getBody();
  28754. const isInlineTarget$1 = curry(isInlineTarget, editor);
  28755. editor.undoManager.ignore(() => {
  28756. editor.selection.setRng(rangeFromPositions(from, to));
  28757. // TODO: TINY-9120 - Investigate if this should be using our custom overrides
  28758. execNativeDeleteCommand(editor);
  28759. readLocation(isInlineTarget$1, rootNode, CaretPosition.fromRangeStart(editor.selection.getRng()))
  28760. .map(inside)
  28761. .bind(setCaretLocation(editor, caret))
  28762. .each(call);
  28763. });
  28764. editor.nodeChanged();
  28765. };
  28766. const rescope = (rootNode, node) => {
  28767. const parentBlock = getParentBlock$3(node, rootNode);
  28768. return parentBlock ? parentBlock : rootNode;
  28769. };
  28770. const backspaceDeleteCollapsed = (editor, caret, forward, from) => {
  28771. const rootNode = rescope(editor.getBody(), from.container());
  28772. const isInlineTarget$1 = curry(isInlineTarget, editor);
  28773. const fromLocation = readLocation(isInlineTarget$1, rootNode, from);
  28774. const location = fromLocation.bind((location) => {
  28775. if (forward) {
  28776. return location.fold(constant(Optional.some(inside(location))), // Before
  28777. Optional.none, // Start
  28778. constant(Optional.some(outside(location))), // End
  28779. Optional.none // After
  28780. );
  28781. }
  28782. else {
  28783. return location.fold(Optional.none, // Before
  28784. constant(Optional.some(outside(location))), // Start
  28785. Optional.none, // End
  28786. constant(Optional.some(inside(location))) // After
  28787. );
  28788. }
  28789. });
  28790. return location.map(setCaretLocation(editor, caret))
  28791. .getOrThunk(() => {
  28792. const toPosition = navigate(forward, rootNode, from);
  28793. const toLocation = toPosition.bind((pos) => readLocation(isInlineTarget$1, rootNode, pos));
  28794. return lift2(fromLocation, toLocation, () => findRootInline(isInlineTarget$1, rootNode, from).bind((elm) => {
  28795. if (hasOnlyTwoOrLessPositionsLeft(elm)) {
  28796. return Optional.some(() => {
  28797. deleteElement$2(editor, forward, SugarElement.fromDom(elm));
  28798. });
  28799. }
  28800. else {
  28801. return Optional.none();
  28802. }
  28803. })).getOrThunk(() => toLocation.bind(() => toPosition.map((to) => {
  28804. return () => {
  28805. if (forward) {
  28806. deleteFromTo(editor, caret, from, to);
  28807. }
  28808. else {
  28809. deleteFromTo(editor, caret, to, from);
  28810. }
  28811. };
  28812. })));
  28813. });
  28814. };
  28815. const backspaceDelete$5 = (editor, caret, forward) => {
  28816. if (editor.selection.isCollapsed() && isInlineBoundariesEnabled(editor)) {
  28817. const from = CaretPosition.fromRangeStart(editor.selection.getRng());
  28818. return backspaceDeleteCollapsed(editor, caret, forward, from);
  28819. }
  28820. return Optional.none();
  28821. };
  28822. const hasMultipleChildren = (elm) => childNodesCount(elm) > 1;
  28823. const getParentsUntil = (editor, pred) => {
  28824. const rootElm = SugarElement.fromDom(editor.getBody());
  28825. const startElm = SugarElement.fromDom(editor.selection.getStart());
  28826. const parents = parentsAndSelf(startElm, rootElm);
  28827. return findIndex$2(parents, pred).fold(constant(parents), (index) => parents.slice(0, index));
  28828. };
  28829. const hasOnlyOneChild = (elm) => childNodesCount(elm) === 1;
  28830. const getParentInlinesUntilMultichildInline = (editor) => getParentsUntil(editor, (elm) => editor.schema.isBlock(name(elm)) || hasMultipleChildren(elm));
  28831. const getParentInlines = (editor) => getParentsUntil(editor, (el) => editor.schema.isBlock(name(el)));
  28832. const getFormatNodes = (editor, parentInlines) => {
  28833. const isFormatElement$1 = curry(isFormatElement, editor);
  28834. return bind$3(parentInlines, (elm) => isFormatElement$1(elm) ? [elm.dom] : []);
  28835. };
  28836. const getFormatNodesAtStart = (editor) => {
  28837. const parentInlines = getParentInlines(editor);
  28838. return getFormatNodes(editor, parentInlines);
  28839. };
  28840. const deleteLastPosition = (forward, editor, target, parentInlines) => {
  28841. const formatNodes = getFormatNodes(editor, parentInlines);
  28842. if (formatNodes.length === 0) {
  28843. deleteElement$2(editor, forward, target);
  28844. }
  28845. else {
  28846. const pos = replaceWithCaretFormat(target.dom, formatNodes);
  28847. editor.selection.setRng(pos.toRange());
  28848. }
  28849. };
  28850. const deleteCaret$1 = (editor, forward) => {
  28851. const parentInlines = filter$5(getParentInlinesUntilMultichildInline(editor), hasOnlyOneChild);
  28852. return last$2(parentInlines).bind((target) => {
  28853. const fromPos = CaretPosition.fromRangeStart(editor.selection.getRng());
  28854. if (willDeleteLastPositionInElement(forward, fromPos, target.dom) && !isEmptyCaretFormatElement(target)) {
  28855. return Optional.some(() => deleteLastPosition(forward, editor, target, parentInlines));
  28856. }
  28857. else {
  28858. return Optional.none();
  28859. }
  28860. });
  28861. };
  28862. const isBrInEmptyElement = (editor, elm) => {
  28863. const parentElm = elm.parentElement;
  28864. return isBr$7(elm) && !isNull(parentElm) && editor.dom.isEmpty(parentElm);
  28865. };
  28866. const isEmptyCaret = (elm) => isEmptyCaretFormatElement(SugarElement.fromDom(elm));
  28867. const createCaretFormatAtStart = (editor, formatNodes) => {
  28868. const startElm = editor.selection.getStart();
  28869. // replace <br> in empty node or existing caret at start if applicable
  28870. // otherwise create new caret format at start
  28871. const pos = isBrInEmptyElement(editor, startElm) || isEmptyCaret(startElm)
  28872. ? replaceWithCaretFormat(startElm, formatNodes)
  28873. : createCaretFormatAtStart$1(editor.selection.getRng(), formatNodes);
  28874. editor.selection.setRng(pos.toRange());
  28875. };
  28876. const updateCaretFormat = (editor, updateFormats) => {
  28877. // Create a caret format at cursor containing missing formats to ensure all formats
  28878. // that are supposed to be retained are retained
  28879. const missingFormats = difference(updateFormats, getFormatNodesAtStart(editor));
  28880. if (missingFormats.length > 0) {
  28881. createCaretFormatAtStart(editor, missingFormats);
  28882. }
  28883. };
  28884. const rangeStartsAtTextContainer = (rng) => isText$b(rng.startContainer);
  28885. const rangeStartsAtStartOfTextContainer = (rng) => rng.startOffset === 0 && rangeStartsAtTextContainer(rng);
  28886. const rangeStartParentIsFormatElement = (editor, rng) => {
  28887. const startParent = rng.startContainer.parentElement;
  28888. return !isNull(startParent) && isFormatElement(editor, SugarElement.fromDom(startParent));
  28889. };
  28890. const rangeStartAndEndHaveSameParent = (rng) => {
  28891. const startParent = rng.startContainer.parentNode;
  28892. const endParent = rng.endContainer.parentNode;
  28893. return !isNull(startParent) && !isNull(endParent) && startParent.isEqualNode(endParent);
  28894. };
  28895. const rangeEndsAtEndOfEndContainer = (rng) => {
  28896. const endContainer = rng.endContainer;
  28897. return rng.endOffset === (isText$b(endContainer) ? endContainer.length : endContainer.childNodes.length);
  28898. };
  28899. const rangeEndsAtEndOfStartContainer = (rng) => rangeStartAndEndHaveSameParent(rng) && rangeEndsAtEndOfEndContainer(rng);
  28900. const rangeEndsAfterEndOfStartContainer = (rng) => !rng.endContainer.isEqualNode(rng.commonAncestorContainer);
  28901. const rangeEndsAtOrAfterEndOfStartContainer = (rng) => rangeEndsAtEndOfStartContainer(rng) || rangeEndsAfterEndOfStartContainer(rng);
  28902. const requiresDeleteRangeOverride = (editor) => {
  28903. const rng = editor.selection.getRng();
  28904. return rangeStartsAtStartOfTextContainer(rng) && rangeStartParentIsFormatElement(editor, rng) && rangeEndsAtOrAfterEndOfStartContainer(rng);
  28905. };
  28906. const deleteRange$2 = (editor) => {
  28907. if (requiresDeleteRangeOverride(editor)) {
  28908. const formatNodes = getFormatNodesAtStart(editor);
  28909. return Optional.some(() => {
  28910. execNativeDeleteCommand(editor);
  28911. updateCaretFormat(editor, formatNodes);
  28912. });
  28913. }
  28914. else {
  28915. return Optional.none();
  28916. }
  28917. };
  28918. const backspaceDelete$4 = (editor, forward) => editor.selection.isCollapsed() ? deleteCaret$1(editor, forward) : deleteRange$2(editor);
  28919. const hasAncestorInlineCaret = (elm, schema) => ancestor$3(elm, (node) => isCaretNode(node.dom), (el) => schema.isBlock(name(el)));
  28920. const hasAncestorInlineCaretAtStart = (editor) => hasAncestorInlineCaret(SugarElement.fromDom(editor.selection.getStart()), editor.schema);
  28921. const requiresRefreshCaretOverride = (editor) => {
  28922. const rng = editor.selection.getRng();
  28923. return rng.collapsed && (rangeStartsAtTextContainer(rng) || editor.dom.isEmpty(rng.startContainer)) && !hasAncestorInlineCaretAtStart(editor);
  28924. };
  28925. const refreshCaret = (editor) => {
  28926. if (requiresRefreshCaretOverride(editor)) {
  28927. createCaretFormatAtStart(editor, []);
  28928. }
  28929. return true;
  28930. };
  28931. const deleteElement = (editor, forward, element) => {
  28932. if (isNonNullable(element)) {
  28933. return Optional.some(() => {
  28934. editor._selectionOverrides.hideFakeCaret();
  28935. deleteElement$2(editor, forward, SugarElement.fromDom(element));
  28936. });
  28937. }
  28938. else {
  28939. return Optional.none();
  28940. }
  28941. };
  28942. const deleteCaret = (editor, forward) => {
  28943. const isNearMedia = forward ? isBeforeMedia : isAfterMedia;
  28944. const direction = forward ? 1 /* HDirection.Forwards */ : -1 /* HDirection.Backwards */;
  28945. const fromPos = getNormalizedRangeEndPoint(direction, editor.getBody(), editor.selection.getRng());
  28946. if (isNearMedia(fromPos)) {
  28947. return deleteElement(editor, forward, fromPos.getNode(!forward));
  28948. }
  28949. else {
  28950. return Optional.from(normalizePosition(forward, fromPos))
  28951. .filter((pos) => isNearMedia(pos) && isMoveInsideSameBlock(fromPos, pos))
  28952. .bind((pos) => deleteElement(editor, forward, pos.getNode(!forward)));
  28953. }
  28954. };
  28955. const deleteRange$1 = (editor, forward) => {
  28956. const selectedNode = editor.selection.getNode();
  28957. return isMedia$2(selectedNode) ? deleteElement(editor, forward, selectedNode) : Optional.none();
  28958. };
  28959. const backspaceDelete$3 = (editor, forward) => editor.selection.isCollapsed() ? deleteCaret(editor, forward) : deleteRange$1(editor, forward);
  28960. const isEditable = (target) => closest$4(target, (elm) => isContentEditableTrue$3(elm.dom) || isContentEditableFalse$a(elm.dom))
  28961. .exists((elm) => isContentEditableTrue$3(elm.dom));
  28962. const parseIndentValue = (value) => toInt(value !== null && value !== void 0 ? value : '').getOr(0);
  28963. const getIndentStyleName = (useMargin, element) => {
  28964. const indentStyleName = useMargin || isTable$1(element) ? 'margin' : 'padding';
  28965. const suffix = get$7(element, 'direction') === 'rtl' ? '-right' : '-left';
  28966. return indentStyleName + suffix;
  28967. };
  28968. const indentElement = (dom, command, useMargin, value, unit, element) => {
  28969. const indentStyleName = getIndentStyleName(useMargin, SugarElement.fromDom(element));
  28970. const parsedValue = parseIndentValue(dom.getStyle(element, indentStyleName));
  28971. if (command === 'outdent') {
  28972. const styleValue = Math.max(0, parsedValue - value);
  28973. dom.setStyle(element, indentStyleName, styleValue ? styleValue + unit : '');
  28974. }
  28975. else {
  28976. const styleValue = (parsedValue + value) + unit;
  28977. dom.setStyle(element, indentStyleName, styleValue);
  28978. }
  28979. };
  28980. const validateBlocks = (editor, blocks) => forall(blocks, (block) => {
  28981. const indentStyleName = getIndentStyleName(shouldIndentUseMargin(editor), block);
  28982. const intentValue = getRaw$1(block, indentStyleName).map(parseIndentValue).getOr(0);
  28983. const contentEditable = editor.dom.getContentEditable(block.dom);
  28984. return contentEditable !== 'false' && intentValue > 0;
  28985. });
  28986. const canOutdent = (editor) => {
  28987. const blocks = getBlocksToIndent(editor);
  28988. return !editor.mode.isReadOnly() && (blocks.length > 1 || validateBlocks(editor, blocks));
  28989. };
  28990. const canIndent = (editor) => !editor.mode.isReadOnly() && canIndent$1(editor);
  28991. const isListComponent = (el) => isList$1(el) || isListItem$2(el);
  28992. const parentIsListComponent = (el) => parent(el).exists(isListComponent);
  28993. const getBlocksToIndent = (editor) => filter$5(fromDom$1(editor.selection.getSelectedBlocks()), (el) => !isListComponent(el) && !parentIsListComponent(el) && isEditable(el));
  28994. const handle = (editor, command) => {
  28995. var _a, _b;
  28996. if (editor.mode.isReadOnly()) {
  28997. return;
  28998. }
  28999. const { dom } = editor;
  29000. const indentation = getIndentation(editor);
  29001. const indentUnit = (_b = (_a = /[a-z%]+$/i.exec(indentation)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 'px';
  29002. const indentValue = parseIndentValue(indentation);
  29003. const useMargin = shouldIndentUseMargin(editor);
  29004. each$e(getBlocksToIndent(editor), (block) => {
  29005. indentElement(dom, command, useMargin, indentValue, indentUnit, block.dom);
  29006. });
  29007. if (command === 'indent') {
  29008. indentListSelection(editor);
  29009. }
  29010. else {
  29011. outdentListSelection(editor);
  29012. }
  29013. };
  29014. const indent = (editor) => handle(editor, 'indent');
  29015. const outdent = (editor) => handle(editor, 'outdent');
  29016. const backspaceDelete$2 = (editor) => {
  29017. if (editor.selection.isCollapsed() && canOutdent(editor)) {
  29018. const dom = editor.dom;
  29019. const rng = editor.selection.getRng();
  29020. const pos = CaretPosition.fromRangeStart(rng);
  29021. const block = dom.getParent(rng.startContainer, dom.isBlock);
  29022. if (block !== null && isAtStartOfBlock(SugarElement.fromDom(block), pos, editor.schema)) {
  29023. return Optional.some(() => outdent(editor));
  29024. }
  29025. }
  29026. return Optional.none();
  29027. };
  29028. const deleteRange = (editor, forward) => {
  29029. const rng = normalize(editor.selection.getRng());
  29030. return isSelectionOverWholeHTMLElement(rng)
  29031. ? Optional.some(() => deleteElement$2(editor, forward, SugarElement.fromDom(editor.selection.getNode())))
  29032. : Optional.none();
  29033. };
  29034. const backspaceDelete$1 = (editor, forward) => editor.selection.isCollapsed() ? Optional.none() : deleteRange(editor, forward);
  29035. const findAction = (editor, caret, forward) => findMap([
  29036. backspaceDelete$2,
  29037. backspaceDelete$8,
  29038. backspaceDelete$9,
  29039. (editor, forward) => backspaceDelete$5(editor, caret, forward),
  29040. backspaceDelete$b,
  29041. backspaceDelete$d,
  29042. backspaceDelete$6,
  29043. backspaceDelete$3,
  29044. backspaceDelete$a,
  29045. backspaceDelete$4,
  29046. backspaceDelete$7,
  29047. backspaceDelete$1,
  29048. ], (item) => item(editor, forward))
  29049. .filter((_) => editor.selection.isEditable());
  29050. const deleteCommand = (editor, caret) => {
  29051. const result = findAction(editor, caret, false);
  29052. result.fold(() => {
  29053. // We can't use an `execEditorDeleteCommand` here, otherwise we'd get
  29054. // possible infinite recursion (as it would trigger `deleteCommand` again)
  29055. if (editor.selection.isEditable()) {
  29056. execNativeDeleteCommand(editor);
  29057. paddEmptyBody(editor);
  29058. }
  29059. }, call);
  29060. if (hasListSelection(editor)) {
  29061. normalizeLists(editor.dom, editor.getBody());
  29062. }
  29063. };
  29064. const forwardDeleteCommand = (editor, caret) => {
  29065. const result = findAction(editor, caret, true);
  29066. result.fold(() => {
  29067. if (editor.selection.isEditable()) {
  29068. execNativeForwardDeleteCommand(editor);
  29069. }
  29070. }, call);
  29071. if (hasListSelection(editor)) {
  29072. normalizeLists(editor.dom, editor.getBody());
  29073. }
  29074. };
  29075. const setup$v = (editor, caret) => {
  29076. editor.addCommand('delete', () => {
  29077. deleteCommand(editor, caret);
  29078. });
  29079. editor.addCommand('forwardDelete', () => {
  29080. forwardDeleteCommand(editor, caret);
  29081. });
  29082. };
  29083. // This is based heavily on Alloy's TapEvent.ts, just modified to use TinyMCE's event system.
  29084. const SIGNIFICANT_MOVE = 5;
  29085. const LONGPRESS_DELAY = 400;
  29086. const getTouch = (event) => {
  29087. if (event.touches === undefined || event.touches.length !== 1) {
  29088. return Optional.none();
  29089. }
  29090. return Optional.some(event.touches[0]);
  29091. };
  29092. const isFarEnough = (touch, data) => {
  29093. const distX = Math.abs(touch.clientX - data.x);
  29094. const distY = Math.abs(touch.clientY - data.y);
  29095. return distX > SIGNIFICANT_MOVE || distY > SIGNIFICANT_MOVE;
  29096. };
  29097. const setup$u = (editor) => {
  29098. const startData = value$1();
  29099. const longpressFired = Cell(false);
  29100. const debounceLongpress = last$1((e) => {
  29101. editor.dispatch('longpress', { ...e, type: 'longpress' });
  29102. longpressFired.set(true);
  29103. }, LONGPRESS_DELAY);
  29104. editor.on('touchstart', (e) => {
  29105. getTouch(e).each((touch) => {
  29106. debounceLongpress.cancel();
  29107. const data = {
  29108. x: touch.clientX,
  29109. y: touch.clientY,
  29110. target: e.target
  29111. };
  29112. debounceLongpress.throttle(e);
  29113. longpressFired.set(false);
  29114. startData.set(data);
  29115. });
  29116. }, true);
  29117. editor.on('touchmove', (e) => {
  29118. debounceLongpress.cancel();
  29119. getTouch(e).each((touch) => {
  29120. startData.on((data) => {
  29121. if (isFarEnough(touch, data)) {
  29122. startData.clear();
  29123. longpressFired.set(false);
  29124. editor.dispatch('longpresscancel');
  29125. }
  29126. });
  29127. });
  29128. }, true);
  29129. editor.on('touchend touchcancel', (e) => {
  29130. debounceLongpress.cancel();
  29131. if (e.type === 'touchcancel') {
  29132. return;
  29133. }
  29134. // Cancel the touchend event if a longpress was fired, otherwise fire the tap event
  29135. startData.get()
  29136. .filter((data) => data.target.isEqualNode(e.target))
  29137. .each(() => {
  29138. if (longpressFired.get()) {
  29139. e.preventDefault();
  29140. }
  29141. else {
  29142. editor.dispatch('tap', { ...e, type: 'tap' });
  29143. }
  29144. });
  29145. }, true);
  29146. };
  29147. /**
  29148. * Makes sure that everything gets wrapped in paragraphs.
  29149. *
  29150. * @private
  29151. * @class tinymce.ForceBlocks
  29152. */
  29153. const isBlockElement = (blockElements, node) => has$2(blockElements, node.nodeName);
  29154. const isValidTarget = (schema, node) => {
  29155. if (isText$b(node)) {
  29156. return true;
  29157. }
  29158. else if (isElement$7(node)) {
  29159. return !isBlockElement(schema.getBlockElements(), node) && !isBookmarkNode$1(node) &&
  29160. !isTransparentBlock(schema, node) && !isNonHtmlElementRoot(node) && !isTemplate(node);
  29161. }
  29162. else {
  29163. return false;
  29164. }
  29165. };
  29166. const hasBlockParent = (blockElements, root, node) => {
  29167. return exists(parents(SugarElement.fromDom(node), SugarElement.fromDom(root)), (elm) => {
  29168. return isBlockElement(blockElements, elm.dom);
  29169. });
  29170. };
  29171. const shouldRemoveTextNode = (blockElements, node) => {
  29172. if (isText$b(node)) {
  29173. if (node.data.length === 0) {
  29174. return true;
  29175. }
  29176. else if (/^\s+$/.test(node.data)) {
  29177. return !node.nextSibling || isBlockElement(blockElements, node.nextSibling) || isNonHtmlElementRoot(node.nextSibling);
  29178. }
  29179. }
  29180. return false;
  29181. };
  29182. const createRootBlock = (editor) => editor.dom.create(getForcedRootBlock(editor), getForcedRootBlockAttrs(editor));
  29183. const addRootBlocks = (editor) => {
  29184. const dom = editor.dom, selection = editor.selection;
  29185. const schema = editor.schema;
  29186. const blockElements = schema.getBlockElements();
  29187. const startNode = selection.getStart();
  29188. const rootNode = editor.getBody();
  29189. let rootBlockNode;
  29190. let tempNode;
  29191. let bm = null;
  29192. const forcedRootBlock = getForcedRootBlock(editor);
  29193. if (!startNode || !isElement$7(startNode)) {
  29194. return;
  29195. }
  29196. const rootNodeName = rootNode.nodeName.toLowerCase();
  29197. if (!schema.isValidChild(rootNodeName, forcedRootBlock.toLowerCase()) || hasBlockParent(blockElements, rootNode, startNode)) {
  29198. return;
  29199. }
  29200. // Firefox will automatically remove the last BR if you insert nodes next to it and add a BR back if you remove those siblings
  29201. // and since the bookmark code inserts temporary nodes an new BR will be constantly removed and added and triggering a selection
  29202. // change causing an infinite recursion. So we treat this special case on it's own.
  29203. if (rootNode.firstChild === rootNode.lastChild && isBr$7(rootNode.firstChild)) {
  29204. rootBlockNode = createRootBlock(editor);
  29205. rootBlockNode.appendChild(createPaddingBr().dom);
  29206. rootNode.replaceChild(rootBlockNode, rootNode.firstChild);
  29207. editor.selection.setCursorLocation(rootBlockNode, 0);
  29208. editor.nodeChanged();
  29209. return;
  29210. }
  29211. // Wrap non block elements and text nodes
  29212. let node = rootNode.firstChild;
  29213. while (node) {
  29214. if (isElement$7(node)) {
  29215. updateElement(schema, node);
  29216. }
  29217. if (isValidTarget(schema, node)) {
  29218. // Remove empty text nodes and nodes containing only whitespace
  29219. if (shouldRemoveTextNode(blockElements, node)) {
  29220. tempNode = node;
  29221. node = node.nextSibling;
  29222. dom.remove(tempNode);
  29223. continue;
  29224. }
  29225. if (!rootBlockNode) {
  29226. if (!bm && editor.hasFocus()) {
  29227. bm = getBookmark(editor.selection.getRng(), () => document.createElement('span'));
  29228. }
  29229. // Firefox will remove the last BR element if you insert nodes next to it using DOM APIs like insertBefore
  29230. // so for that weird edge case we stop processing.
  29231. if (!node.parentNode) {
  29232. node = null;
  29233. break;
  29234. }
  29235. rootBlockNode = createRootBlock(editor);
  29236. rootNode.insertBefore(rootBlockNode, node);
  29237. }
  29238. tempNode = node;
  29239. node = node.nextSibling;
  29240. rootBlockNode.appendChild(tempNode);
  29241. }
  29242. else {
  29243. rootBlockNode = null;
  29244. node = node.nextSibling;
  29245. }
  29246. }
  29247. if (bm) {
  29248. editor.selection.setRng(resolveBookmark(bm));
  29249. editor.nodeChanged();
  29250. }
  29251. };
  29252. const insertEmptyLine = (editor, root, insertBlock) => {
  29253. const block = SugarElement.fromDom(createRootBlock(editor));
  29254. const br = createPaddingBr();
  29255. append$1(block, br);
  29256. insertBlock(root, block);
  29257. const rng = document.createRange();
  29258. rng.setStartBefore(br.dom);
  29259. rng.setEndBefore(br.dom);
  29260. return rng;
  29261. };
  29262. const setup$t = (editor) => {
  29263. editor.on('NodeChange', () => addRootBlocks(editor));
  29264. };
  29265. const hasClass = (checkClassName) => (node) => (' ' + node.attr('class') + ' ').indexOf(checkClassName) !== -1;
  29266. const replaceMatchWithSpan = (editor, content, cls) => {
  29267. return function (match) {
  29268. const args = arguments, index = args[args.length - 2];
  29269. const prevChar = index > 0 ? content.charAt(index - 1) : '';
  29270. // Is value inside an attribute then don't replace
  29271. if (prevChar === '"') {
  29272. return match;
  29273. }
  29274. // Is value inside a contentEditable='false' tag
  29275. if (prevChar === '>') {
  29276. const findStartTagIndex = content.lastIndexOf('<', index);
  29277. if (findStartTagIndex !== -1) {
  29278. const tagHtml = content.substring(findStartTagIndex, index);
  29279. if (tagHtml.indexOf('contenteditable="false"') !== -1) {
  29280. return match;
  29281. }
  29282. }
  29283. }
  29284. return ('<span class="' + cls + '" data-mce-content="' + editor.dom.encode(args[0]) + '">' +
  29285. editor.dom.encode(typeof args[1] === 'string' ? args[1] : args[0]) + '</span>');
  29286. };
  29287. };
  29288. const convertRegExpsToNonEditable = (editor, nonEditableRegExps, e) => {
  29289. let i = nonEditableRegExps.length, content = e.content;
  29290. // Don't replace the variables when raw is used for example on undo/redo
  29291. if (e.format === 'raw') {
  29292. return;
  29293. }
  29294. while (i--) {
  29295. content = content.replace(nonEditableRegExps[i], replaceMatchWithSpan(editor, content, getNonEditableClass(editor)));
  29296. }
  29297. e.content = content;
  29298. };
  29299. const isValidContent = (nonEditableRegExps, content) => {
  29300. return forall(nonEditableRegExps, (re) => {
  29301. const matches = content.match(re);
  29302. return matches !== null && matches[0].length === content.length;
  29303. });
  29304. };
  29305. const setup$s = (editor) => {
  29306. const contentEditableAttrName = 'contenteditable';
  29307. const editClass = ' ' + Tools.trim(getEditableClass(editor)) + ' ';
  29308. const nonEditClass = ' ' + Tools.trim(getNonEditableClass(editor)) + ' ';
  29309. const hasEditClass = hasClass(editClass);
  29310. const hasNonEditClass = hasClass(nonEditClass);
  29311. const nonEditableRegExps = getNonEditableRegExps(editor);
  29312. if (nonEditableRegExps.length > 0) {
  29313. editor.on('BeforeSetContent', (e) => {
  29314. convertRegExpsToNonEditable(editor, nonEditableRegExps, e);
  29315. });
  29316. }
  29317. editor.parser.addAttributeFilter('class', (nodes) => {
  29318. let i = nodes.length;
  29319. while (i--) {
  29320. const node = nodes[i];
  29321. if (hasEditClass(node)) {
  29322. node.attr(contentEditableAttrName, 'true');
  29323. }
  29324. else if (hasNonEditClass(node)) {
  29325. node.attr(contentEditableAttrName, 'false');
  29326. }
  29327. }
  29328. });
  29329. editor.serializer.addAttributeFilter(contentEditableAttrName, (nodes) => {
  29330. let i = nodes.length;
  29331. while (i--) {
  29332. const node = nodes[i];
  29333. if (!hasEditClass(node) && !hasNonEditClass(node)) {
  29334. continue;
  29335. }
  29336. const content = node.attr('data-mce-content');
  29337. if (nonEditableRegExps.length > 0 && content) {
  29338. if (isValidContent(nonEditableRegExps, content)) {
  29339. node.name = '#text';
  29340. node.type = 3;
  29341. node.raw = true;
  29342. node.value = content;
  29343. }
  29344. else {
  29345. node.remove();
  29346. }
  29347. }
  29348. else {
  29349. node.attr(contentEditableAttrName, null);
  29350. }
  29351. }
  29352. });
  29353. };
  29354. /**
  29355. * This module shows the invisible block that the caret is currently in when contents is added to that block.
  29356. */
  29357. const findBlockCaretContainer = (editor) => descendant$1(SugarElement.fromDom(editor.getBody()), '*[data-mce-caret]')
  29358. .map((elm) => elm.dom)
  29359. .getOrNull();
  29360. const showBlockCaretContainer = (editor, blockCaretContainer) => {
  29361. if (blockCaretContainer.hasAttribute('data-mce-caret')) {
  29362. showCaretContainerBlock(blockCaretContainer);
  29363. editor.selection.setRng(editor.selection.getRng()); // Clears the fake caret state
  29364. editor.selection.scrollIntoView(blockCaretContainer);
  29365. }
  29366. };
  29367. const handleBlockContainer = (editor, e) => {
  29368. const blockCaretContainer = findBlockCaretContainer(editor);
  29369. if (!blockCaretContainer) {
  29370. return;
  29371. }
  29372. if (e.type === 'compositionstart') {
  29373. e.preventDefault();
  29374. e.stopPropagation();
  29375. showBlockCaretContainer(editor, blockCaretContainer);
  29376. return;
  29377. }
  29378. if (hasContent(blockCaretContainer)) {
  29379. showBlockCaretContainer(editor, blockCaretContainer);
  29380. editor.undoManager.add();
  29381. }
  29382. };
  29383. const setup$r = (editor) => {
  29384. editor.on('keyup compositionstart', curry(handleBlockContainer, editor));
  29385. };
  29386. const isContentEditableFalse$3 = isContentEditableFalse$a;
  29387. const moveToCeFalseHorizontally = (direction, editor, range) => moveHorizontally(editor, direction, range, isBeforeContentEditableFalse, isAfterContentEditableFalse, isContentEditableFalse$3);
  29388. const moveToCeFalseVertically = (direction, editor, range) => {
  29389. const isBefore = (caretPosition) => isBeforeContentEditableFalse(caretPosition) || isBeforeTable(caretPosition);
  29390. const isAfter = (caretPosition) => isAfterContentEditableFalse(caretPosition) || isAfterTable(caretPosition);
  29391. return moveVertically(editor, direction, range, isBefore, isAfter, isContentEditableFalse$3);
  29392. };
  29393. const createTextBlock = (editor) => {
  29394. const textBlock = editor.dom.create(getForcedRootBlock(editor));
  29395. textBlock.innerHTML = '<br data-mce-bogus="1">';
  29396. return textBlock;
  29397. };
  29398. const exitPreBlock = (editor, direction, range) => {
  29399. const caretWalker = CaretWalker(editor.getBody());
  29400. const getVisualCaretPosition$1 = curry(getVisualCaretPosition, direction === 1 /* HDirection.Forwards */ ? caretWalker.next : caretWalker.prev);
  29401. if (range.collapsed) {
  29402. const pre = editor.dom.getParent(range.startContainer, 'PRE');
  29403. if (!pre) {
  29404. return;
  29405. }
  29406. const caretPos = getVisualCaretPosition$1(CaretPosition.fromRangeStart(range));
  29407. if (!caretPos) {
  29408. const newBlock = SugarElement.fromDom(createTextBlock(editor));
  29409. if (direction === 1 /* HDirection.Forwards */) {
  29410. after$4(SugarElement.fromDom(pre), newBlock);
  29411. }
  29412. else {
  29413. before$4(SugarElement.fromDom(pre), newBlock);
  29414. }
  29415. editor.selection.select(newBlock.dom, true);
  29416. editor.selection.collapse();
  29417. }
  29418. }
  29419. };
  29420. const getHorizontalRange = (editor, forward) => {
  29421. const direction = forward ? 1 /* HDirection.Forwards */ : -1 /* HDirection.Backwards */;
  29422. const range = editor.selection.getRng();
  29423. return moveToCeFalseHorizontally(direction, editor, range).orThunk(() => {
  29424. exitPreBlock(editor, direction, range);
  29425. return Optional.none();
  29426. });
  29427. };
  29428. const getVerticalRange = (editor, down) => {
  29429. const direction = down ? 1 : -1;
  29430. const range = editor.selection.getRng();
  29431. return moveToCeFalseVertically(direction, editor, range).orThunk(() => {
  29432. exitPreBlock(editor, direction, range);
  29433. return Optional.none();
  29434. });
  29435. };
  29436. const flipDirection = (selection, forward) => {
  29437. const elm = forward ? selection.getEnd(true) : selection.getStart(true);
  29438. return isRtl(elm) ? !forward : forward;
  29439. };
  29440. const moveH$2 = (editor, forward) => getHorizontalRange(editor, flipDirection(editor.selection, forward)).exists((newRange) => {
  29441. moveToRange(editor, newRange);
  29442. return true;
  29443. });
  29444. const moveV$4 = (editor, down) => getVerticalRange(editor, down).exists((newRange) => {
  29445. moveToRange(editor, newRange);
  29446. return true;
  29447. });
  29448. const moveToLineEndPoint$1 = (editor, forward) => {
  29449. const isCefPosition = forward ? isAfterContentEditableFalse : isBeforeContentEditableFalse;
  29450. return moveToLineEndPoint$3(editor, forward, isCefPosition);
  29451. };
  29452. const selectToEndPoint = (editor, forward) => getEdgeCefPosition(editor, !forward)
  29453. .map((pos) => {
  29454. const rng = pos.toRange();
  29455. const curRng = editor.selection.getRng();
  29456. if (forward) {
  29457. rng.setStart(curRng.startContainer, curRng.startOffset);
  29458. }
  29459. else {
  29460. rng.setEnd(curRng.endContainer, curRng.endOffset);
  29461. }
  29462. return rng;
  29463. })
  29464. .exists((rng) => {
  29465. moveToRange(editor, rng);
  29466. return true;
  29467. });
  29468. const isTarget = (node) => contains$2(['figcaption'], name(node));
  29469. const getClosestTargetBlock = (pos, root, schema) => {
  29470. const isRoot = curry(eq, root);
  29471. return closest$4(SugarElement.fromDom(pos.container()), (el) => schema.isBlock(name(el)), isRoot).filter(isTarget);
  29472. };
  29473. const isAtFirstOrLastLine = (root, forward, pos) => forward ? isAtLastLine(root.dom, pos) : isAtFirstLine(root.dom, pos);
  29474. const moveCaretToNewEmptyLine = (editor, forward) => {
  29475. const root = SugarElement.fromDom(editor.getBody());
  29476. const pos = CaretPosition.fromRangeStart(editor.selection.getRng());
  29477. return getClosestTargetBlock(pos, root, editor.schema).exists(() => {
  29478. if (isAtFirstOrLastLine(root, forward, pos)) {
  29479. const insertFn = forward ? append$1 : prepend;
  29480. const rng = insertEmptyLine(editor, root, insertFn);
  29481. editor.selection.setRng(rng);
  29482. return true;
  29483. }
  29484. else {
  29485. return false;
  29486. }
  29487. });
  29488. };
  29489. const moveV$3 = (editor, forward) => {
  29490. if (editor.selection.isCollapsed()) {
  29491. return moveCaretToNewEmptyLine(editor, forward);
  29492. }
  29493. else {
  29494. return false;
  29495. }
  29496. };
  29497. const moveUp = (editor, details, summary) => {
  29498. const rng = editor.selection.getRng();
  29499. const pos = CaretPosition.fromRangeStart(rng);
  29500. const root = editor.getBody();
  29501. if (root.firstChild === details && isAtFirstLine(summary, pos)) {
  29502. editor.execCommand('InsertNewBlockBefore');
  29503. return true;
  29504. }
  29505. else {
  29506. return false;
  29507. }
  29508. };
  29509. const moveDown = (editor, details) => {
  29510. const rng = editor.selection.getRng();
  29511. const pos = CaretPosition.fromRangeStart(rng);
  29512. const root = editor.getBody();
  29513. if (root.lastChild === details && isAtLastLine(details, pos)) {
  29514. editor.execCommand('InsertNewBlockAfter');
  29515. return true;
  29516. }
  29517. else {
  29518. return false;
  29519. }
  29520. };
  29521. const move$2 = (editor, forward) => {
  29522. if (forward) {
  29523. return Optional.from(editor.dom.getParent(editor.selection.getNode(), 'details'))
  29524. .map((details) => moveDown(editor, details))
  29525. .getOr(false);
  29526. }
  29527. else {
  29528. return Optional.from(editor.dom.getParent(editor.selection.getNode(), 'summary'))
  29529. .bind((summary) => Optional.from(editor.dom.getParent(summary, 'details'))
  29530. .map((details) => moveUp(editor, details, summary))).getOr(false);
  29531. }
  29532. };
  29533. const moveV$2 = (editor, forward) => move$2(editor, forward);
  29534. const baseKeyPattern = {
  29535. shiftKey: false,
  29536. altKey: false,
  29537. ctrlKey: false,
  29538. metaKey: false,
  29539. keyCode: 0
  29540. };
  29541. const defaultPatterns = (patterns) => map$3(patterns, (pattern) => ({
  29542. ...baseKeyPattern,
  29543. ...pattern
  29544. }));
  29545. const defaultDelayedPatterns = (patterns) => map$3(patterns, (pattern) => ({
  29546. ...baseKeyPattern,
  29547. ...pattern
  29548. }));
  29549. const matchesEvent = (pattern, evt) => (evt.keyCode === pattern.keyCode &&
  29550. evt.shiftKey === pattern.shiftKey &&
  29551. evt.altKey === pattern.altKey &&
  29552. evt.ctrlKey === pattern.ctrlKey &&
  29553. evt.metaKey === pattern.metaKey);
  29554. const match$1 = (patterns, evt) => bind$3(defaultPatterns(patterns), (pattern) => matchesEvent(pattern, evt) ? [pattern] : []);
  29555. const matchDelayed = (patterns, evt) => bind$3(defaultDelayedPatterns(patterns), (pattern) => matchesEvent(pattern, evt) ? [pattern] : []);
  29556. const action = (f, ...x) => () => f.apply(null, x);
  29557. const execute = (patterns, evt) => find$2(match$1(patterns, evt), (pattern) => pattern.action());
  29558. const executeWithDelayedAction = (patterns, evt) => findMap(matchDelayed(patterns, evt), (pattern) => pattern.action());
  29559. const moveH$1 = (editor, forward) => {
  29560. const direction = forward ? 1 /* HDirection.Forwards */ : -1 /* HDirection.Backwards */;
  29561. const range = editor.selection.getRng();
  29562. return moveHorizontally(editor, direction, range, isBeforeMedia, isAfterMedia, isMedia$2).exists((newRange) => {
  29563. moveToRange(editor, newRange);
  29564. return true;
  29565. });
  29566. };
  29567. const moveV$1 = (editor, down) => {
  29568. const direction = down ? 1 : -1;
  29569. const range = editor.selection.getRng();
  29570. return moveVertically(editor, direction, range, isBeforeMedia, isAfterMedia, isMedia$2).exists((newRange) => {
  29571. moveToRange(editor, newRange);
  29572. return true;
  29573. });
  29574. };
  29575. const moveToLineEndPoint = (editor, forward) => {
  29576. const isNearMedia = forward ? isAfterMedia : isBeforeMedia;
  29577. return moveToLineEndPoint$3(editor, forward, isNearMedia);
  29578. };
  29579. const firstLayer = (scope, selector) => {
  29580. return filterFirstLayer(scope, selector, always);
  29581. };
  29582. const filterFirstLayer = (scope, selector, predicate) => {
  29583. return bind$3(children$1(scope), (x) => {
  29584. if (is$2(x, selector)) {
  29585. return predicate(x) ? [x] : [];
  29586. }
  29587. else {
  29588. return filterFirstLayer(x, selector, predicate);
  29589. }
  29590. });
  29591. };
  29592. // lookup inside this table
  29593. const lookup$1 = (tags, element, isRoot = never) => {
  29594. // If the element we're inspecting is the root, we definitely don't want it.
  29595. if (isRoot(element)) {
  29596. return Optional.none();
  29597. }
  29598. // This looks a lot like SelectorFind.closest, with one big exception - the isRoot check.
  29599. // The code here will look for parents if passed a table, SelectorFind.closest with that specific isRoot check won't.
  29600. if (contains$2(tags, name(element))) {
  29601. return Optional.some(element);
  29602. }
  29603. const isRootOrUpperTable = (elm) => is$2(elm, 'table') || isRoot(elm);
  29604. return ancestor$4(element, tags.join(','), isRootOrUpperTable);
  29605. };
  29606. /*
  29607. * Identify the optional cell that element represents.
  29608. */
  29609. const cell = (element, isRoot) => lookup$1(['td', 'th'], element, isRoot);
  29610. const cells = (ancestor) => firstLayer(ancestor, 'th,td');
  29611. const table = (element, isRoot) => closest$3(element, 'table', isRoot);
  29612. const adt = Adt.generate([
  29613. { none: ['current'] },
  29614. { first: ['current'] },
  29615. { middle: ['current', 'target'] },
  29616. { last: ['current'] }
  29617. ]);
  29618. const none = (current) => adt.none(current);
  29619. const CellLocation = {
  29620. ...adt,
  29621. none
  29622. };
  29623. /*
  29624. * Walk until the next eligible cell location is found, or the start/end of the table is found.
  29625. */
  29626. const walk = (all, current, index, direction, isEligible = always) => {
  29627. const forwards = direction === 1 /* Direction.Forwards */;
  29628. if (!forwards && index <= 0) {
  29629. return CellLocation.first(all[0]);
  29630. }
  29631. else if (forwards && index >= all.length - 1) {
  29632. return CellLocation.last(all[all.length - 1]);
  29633. }
  29634. else {
  29635. const newIndex = index + direction;
  29636. const elem = all[newIndex];
  29637. return isEligible(elem) ? CellLocation.middle(current, elem) : walk(all, current, newIndex, direction, isEligible);
  29638. }
  29639. };
  29640. /*
  29641. * Identify the index of the current cell within all the cells, and
  29642. * a list of the cells within its table.
  29643. */
  29644. const detect = (current, isRoot) => {
  29645. return table(current, isRoot).bind((table) => {
  29646. const all = cells(table);
  29647. const index = findIndex$2(all, (x) => eq(current, x));
  29648. return index.map((index) => ({ index, all }));
  29649. });
  29650. };
  29651. /*
  29652. * Identify the CellLocation of the cell when navigating forward from current
  29653. */
  29654. const next = (current, isEligible, isRoot) => {
  29655. const detection = detect(current, isRoot);
  29656. return detection.fold(() => {
  29657. return CellLocation.none(current);
  29658. }, (info) => {
  29659. return walk(info.all, current, info.index, 1 /* Direction.Forwards */, isEligible);
  29660. });
  29661. };
  29662. /*
  29663. * Identify the CellLocation of the cell when navigating back from current
  29664. */
  29665. const prev = (current, isEligible, isRoot) => {
  29666. const detection = detect(current, isRoot);
  29667. return detection.fold(() => {
  29668. return CellLocation.none();
  29669. }, (info) => {
  29670. return walk(info.all, current, info.index, -1 /* Direction.Backwards */, isEligible);
  29671. });
  29672. };
  29673. var TagBoundaries = [
  29674. 'body',
  29675. 'p',
  29676. 'div',
  29677. 'article',
  29678. 'aside',
  29679. 'figcaption',
  29680. 'figure',
  29681. 'footer',
  29682. 'header',
  29683. 'nav',
  29684. 'section',
  29685. 'ol',
  29686. 'ul',
  29687. 'li',
  29688. 'table',
  29689. 'thead',
  29690. 'tbody',
  29691. 'tfoot',
  29692. 'caption',
  29693. 'tr',
  29694. 'td',
  29695. 'th',
  29696. 'h1',
  29697. 'h2',
  29698. 'h3',
  29699. 'h4',
  29700. 'h5',
  29701. 'h6',
  29702. 'blockquote',
  29703. 'pre',
  29704. 'address'
  29705. ];
  29706. var DomUniverse = () => {
  29707. const clone = (element) => {
  29708. return SugarElement.fromDom(element.dom.cloneNode(false));
  29709. };
  29710. const document = (element) => documentOrOwner(element).dom;
  29711. const isBoundary = (element) => {
  29712. if (!isElement$8(element)) {
  29713. return false;
  29714. }
  29715. if (name(element) === 'body') {
  29716. return true;
  29717. }
  29718. return contains$2(TagBoundaries, name(element));
  29719. };
  29720. const isEmptyTag = (element) => {
  29721. if (!isElement$8(element)) {
  29722. return false;
  29723. }
  29724. return contains$2(['br', 'img', 'hr', 'input'], name(element));
  29725. };
  29726. const isNonEditable = (element) => isElement$8(element) && get$9(element, 'contenteditable') === 'false';
  29727. const comparePosition = (element, other) => {
  29728. return element.dom.compareDocumentPosition(other.dom);
  29729. };
  29730. const copyAttributesTo = (source, destination) => {
  29731. const as = clone$4(source);
  29732. setAll$1(destination, as);
  29733. };
  29734. const isSpecial = (element) => {
  29735. const tag = name(element);
  29736. return contains$2([
  29737. 'script', 'noscript', 'iframe', 'noframes', 'noembed', 'title', 'style', 'textarea', 'xmp'
  29738. ], tag);
  29739. };
  29740. const getLanguage = (element) => isElement$8(element) ? getOpt(element, 'lang') : Optional.none();
  29741. return {
  29742. up: constant({
  29743. selector: ancestor$4,
  29744. closest: closest$3,
  29745. predicate: ancestor$5,
  29746. all: parents$1
  29747. }),
  29748. down: constant({
  29749. selector: descendants,
  29750. predicate: descendants$1
  29751. }),
  29752. styles: constant({
  29753. get: get$7,
  29754. getRaw: getRaw$1,
  29755. set: set$2,
  29756. remove: remove$7
  29757. }),
  29758. attrs: constant({
  29759. get: get$9,
  29760. set: set$4,
  29761. remove: remove$9,
  29762. copyTo: copyAttributesTo
  29763. }),
  29764. insert: constant({
  29765. before: before$4,
  29766. after: after$4,
  29767. afterAll: after$3,
  29768. append: append$1,
  29769. appendAll: append,
  29770. prepend: prepend,
  29771. wrap: wrap$2
  29772. }),
  29773. remove: constant({
  29774. unwrap: unwrap,
  29775. remove: remove$8
  29776. }),
  29777. create: constant({
  29778. nu: SugarElement.fromTag,
  29779. clone,
  29780. text: SugarElement.fromText
  29781. }),
  29782. query: constant({
  29783. comparePosition,
  29784. prevSibling: prevSibling,
  29785. nextSibling: nextSibling
  29786. }),
  29787. property: constant({
  29788. children: children$1,
  29789. name: name,
  29790. parent: parent,
  29791. document,
  29792. isText: isText$c,
  29793. isComment: isComment$1,
  29794. isElement: isElement$8,
  29795. isSpecial,
  29796. getLanguage,
  29797. getText: get$4,
  29798. setText: set$1,
  29799. isBoundary,
  29800. isEmptyTag,
  29801. isNonEditable
  29802. }),
  29803. eq: eq,
  29804. is: is$1
  29805. };
  29806. };
  29807. const point$1 = (element, offset) => ({
  29808. element,
  29809. offset
  29810. });
  29811. /**
  29812. * Return the last available cursor position in the node.
  29813. */
  29814. const toLast$1 = (universe, node) => {
  29815. if (universe.property().isText(node)) {
  29816. return point$1(node, universe.property().getText(node).length);
  29817. }
  29818. else {
  29819. const children = universe.property().children(node);
  29820. // keep descending if there are children.
  29821. return children.length > 0 ? toLast$1(universe, children[children.length - 1]) : point$1(node, children.length);
  29822. }
  29823. };
  29824. /**
  29825. * Descend down to a leaf node at the given offset.
  29826. */
  29827. const toLeaf$3 = (universe, element, offset) => {
  29828. const children = universe.property().children(element);
  29829. if (children.length > 0 && offset < children.length) {
  29830. return toLeaf$3(universe, children[offset], 0);
  29831. }
  29832. else if (children.length > 0 && universe.property().isElement(element) && children.length === offset) {
  29833. return toLast$1(universe, children[children.length - 1]);
  29834. }
  29835. else {
  29836. return point$1(element, offset);
  29837. }
  29838. };
  29839. const toLeaf$2 = toLeaf$3;
  29840. const universe = DomUniverse();
  29841. const toLeaf$1 = (element, offset) => {
  29842. return toLeaf$2(universe, element, offset);
  29843. };
  29844. const imageId = generate$1('image');
  29845. const getDragImage = (transfer) => {
  29846. const dt = transfer;
  29847. return Optional.from(dt[imageId]);
  29848. };
  29849. const setDragImage = (transfer, imageData) => {
  29850. const dt = transfer;
  29851. dt[imageId] = imageData;
  29852. };
  29853. const eventId = generate$1('event');
  29854. const getEvent = (transfer) => {
  29855. const dt = transfer;
  29856. return Optional.from(dt[eventId]);
  29857. };
  29858. const mkSetEventFn = (type) => (transfer) => {
  29859. const dt = transfer;
  29860. dt[eventId] = type;
  29861. };
  29862. const setEvent = (transfer, type) => mkSetEventFn(type)(transfer);
  29863. const setDragstartEvent = mkSetEventFn(0 /* Event.Dragstart */);
  29864. const setDropEvent = mkSetEventFn(2 /* Event.Drop */);
  29865. const setDragendEvent = mkSetEventFn(1 /* Event.Dragend */);
  29866. const checkEvent = (expectedType) => (transfer) => {
  29867. const dt = transfer;
  29868. return Optional.from(dt[eventId]).exists((type) => type === expectedType);
  29869. };
  29870. const isInDragStartEvent = checkEvent(0 /* Event.Dragstart */);
  29871. const createEmptyFileList = () => Object.freeze({
  29872. length: 0,
  29873. item: (_) => null
  29874. });
  29875. const modeId = generate$1('mode');
  29876. const getMode = (transfer) => {
  29877. const dt = transfer;
  29878. return Optional.from(dt[modeId]);
  29879. };
  29880. const mkSetModeFn = (mode) => (transfer) => {
  29881. const dt = transfer;
  29882. dt[modeId] = mode;
  29883. };
  29884. const setMode$1 = (transfer, mode) => mkSetModeFn(mode)(transfer);
  29885. const setReadWriteMode = mkSetModeFn(0 /* Mode.ReadWrite */);
  29886. const setReadOnlyMode = mkSetModeFn(2 /* Mode.ReadOnly */);
  29887. const setProtectedMode = mkSetModeFn(1 /* Mode.Protected */);
  29888. const checkMode = (expectedMode) => (transfer) => {
  29889. const dt = transfer;
  29890. return Optional.from(dt[modeId]).exists((mode) => mode === expectedMode);
  29891. };
  29892. const isInReadWriteMode = checkMode(0 /* Mode.ReadWrite */);
  29893. const isInProtectedMode = checkMode(1 /* Mode.Protected */);
  29894. const normalizeItems = (dataTransfer, itemsImpl) => ({
  29895. ...itemsImpl,
  29896. get length() {
  29897. return itemsImpl.length;
  29898. },
  29899. add: (data, type) => {
  29900. if (isInReadWriteMode(dataTransfer)) {
  29901. if (isString(data)) {
  29902. if (!isUndefined(type)) {
  29903. return itemsImpl.add(data, type);
  29904. }
  29905. }
  29906. else {
  29907. return itemsImpl.add(data);
  29908. }
  29909. }
  29910. return null;
  29911. },
  29912. remove: (idx) => {
  29913. if (isInReadWriteMode(dataTransfer)) {
  29914. itemsImpl.remove(idx);
  29915. }
  29916. },
  29917. clear: () => {
  29918. if (isInReadWriteMode(dataTransfer)) {
  29919. itemsImpl.clear();
  29920. }
  29921. }
  29922. });
  29923. const validDropEffects = ['none', 'copy', 'link', 'move'];
  29924. const validEffectAlloweds = ['none', 'copy', 'copyLink', 'copyMove', 'link', 'linkMove', 'move', 'all', 'uninitialized'];
  29925. const createDataTransfer = () => {
  29926. const dataTransferImpl = new window.DataTransfer();
  29927. let dropEffect = 'move';
  29928. let effectAllowed = 'all';
  29929. const dataTransfer = {
  29930. get dropEffect() {
  29931. return dropEffect;
  29932. },
  29933. set dropEffect(effect) {
  29934. if (contains$2(validDropEffects, effect)) {
  29935. dropEffect = effect;
  29936. }
  29937. },
  29938. get effectAllowed() {
  29939. return effectAllowed;
  29940. },
  29941. set effectAllowed(allowed) {
  29942. // TINY-9601: Only allow setting effectAllowed to a valid value in a dragstart event
  29943. // https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/effectAllowed
  29944. if (isInDragStartEvent(dataTransfer) && contains$2(validEffectAlloweds, allowed)) {
  29945. effectAllowed = allowed;
  29946. }
  29947. },
  29948. get items() {
  29949. return normalizeItems(dataTransfer, dataTransferImpl.items);
  29950. },
  29951. get files() {
  29952. if (isInProtectedMode(dataTransfer)) {
  29953. return createEmptyFileList();
  29954. }
  29955. else {
  29956. return dataTransferImpl.files;
  29957. }
  29958. },
  29959. get types() {
  29960. return dataTransferImpl.types;
  29961. },
  29962. setDragImage: (image, x, y) => {
  29963. if (isInReadWriteMode(dataTransfer)) {
  29964. setDragImage(dataTransfer, { image, x, y });
  29965. dataTransferImpl.setDragImage(image, x, y);
  29966. }
  29967. },
  29968. getData: (format) => {
  29969. if (isInProtectedMode(dataTransfer)) {
  29970. return '';
  29971. }
  29972. else {
  29973. return dataTransferImpl.getData(format);
  29974. }
  29975. },
  29976. setData: (format, data) => {
  29977. if (isInReadWriteMode(dataTransfer)) {
  29978. dataTransferImpl.setData(format, data);
  29979. }
  29980. },
  29981. clearData: (format) => {
  29982. if (isInReadWriteMode(dataTransfer)) {
  29983. dataTransferImpl.clearData(format);
  29984. }
  29985. }
  29986. };
  29987. setReadWriteMode(dataTransfer);
  29988. return dataTransfer;
  29989. };
  29990. const cloneDataTransfer = (original) => {
  29991. // Create new DataTransfer object to ensure scope is not shared between original and clone
  29992. const clone = createDataTransfer();
  29993. const originalMode = getMode(original);
  29994. // Set original to read-only to ensure data can be copied
  29995. setReadOnlyMode(original);
  29996. // Set clone event to dragstart to ensure effectAllowed can be set
  29997. setDragstartEvent(clone);
  29998. clone.dropEffect = original.dropEffect;
  29999. clone.effectAllowed = original.effectAllowed;
  30000. getDragImage(original).each((imageData) => clone.setDragImage(imageData.image, imageData.x, imageData.y));
  30001. each$e(original.types, (type) => {
  30002. if (type !== 'Files') {
  30003. clone.setData(type, original.getData(type));
  30004. }
  30005. });
  30006. each$e(original.files, (file) => clone.items.add(file));
  30007. getEvent(original).each((type) => {
  30008. setEvent(clone, type);
  30009. });
  30010. originalMode.each((mode) => {
  30011. // Reset original mode since it was set to read-only earlier
  30012. setMode$1(original, mode);
  30013. setMode$1(clone, mode);
  30014. });
  30015. return clone;
  30016. };
  30017. const getHtmlData = (dataTransfer) => {
  30018. const html = dataTransfer.getData('text/html');
  30019. return html === '' ? Optional.none() : Optional.some(html);
  30020. };
  30021. const setHtmlData = (dataTransfer, html) => dataTransfer.setData('text/html', html);
  30022. const deflate = (rect, delta) => ({
  30023. left: rect.left - delta,
  30024. top: rect.top - delta,
  30025. right: rect.right + delta * 2,
  30026. bottom: rect.bottom + delta * 2,
  30027. width: rect.width + delta,
  30028. height: rect.height + delta
  30029. });
  30030. const getCorners = (getYAxisValue, tds) => bind$3(tds, (td) => {
  30031. const rect = deflate(clone$1(td.getBoundingClientRect()), -1);
  30032. return [
  30033. { x: rect.left, y: getYAxisValue(rect), cell: td },
  30034. { x: rect.right, y: getYAxisValue(rect), cell: td }
  30035. ];
  30036. });
  30037. const findClosestCorner = (corners, x, y) => foldl(corners, (acc, newCorner) => acc.fold(() => Optional.some(newCorner), (oldCorner) => {
  30038. const oldDist = Math.sqrt(Math.abs(oldCorner.x - x) + Math.abs(oldCorner.y - y));
  30039. const newDist = Math.sqrt(Math.abs(newCorner.x - x) + Math.abs(newCorner.y - y));
  30040. return Optional.some(newDist < oldDist ? newCorner : oldCorner);
  30041. }), Optional.none());
  30042. const getClosestCell = (getYAxisValue, isTargetCorner, table, x, y) => {
  30043. const cells = descendants(SugarElement.fromDom(table), 'td,th,caption').map((e) => e.dom);
  30044. const corners = filter$5(getCorners(getYAxisValue, cells), (corner) => isTargetCorner(corner, y));
  30045. return findClosestCorner(corners, x, y).map((corner) => corner.cell);
  30046. };
  30047. const getBottomValue = (rect) => rect.bottom;
  30048. const getTopValue = (rect) => rect.top;
  30049. const isAbove = (corner, y) => corner.y < y;
  30050. const isBelow = (corner, y) => corner.y > y;
  30051. const getClosestCellAbove = curry(getClosestCell, getBottomValue, isAbove);
  30052. const getClosestCellBelow = curry(getClosestCell, getTopValue, isBelow);
  30053. const findClosestPositionInAboveCell = (table, pos) => head(pos.getClientRects())
  30054. .bind((rect) => getClosestCellAbove(table, rect.left, rect.top))
  30055. .bind((cell) => findClosestHorizontalPosition(getLastLinePositions(cell), pos));
  30056. const findClosestPositionInBelowCell = (table, pos) => last$2(pos.getClientRects())
  30057. .bind((rect) => getClosestCellBelow(table, rect.left, rect.top))
  30058. .bind((cell) => findClosestHorizontalPosition(getFirstLinePositions(cell), pos));
  30059. const hasNextBreak = (getPositionsUntil, scope, lineInfo) => lineInfo.breakAt.exists((breakPos) => getPositionsUntil(scope, breakPos).breakAt.isSome());
  30060. const startsWithWrapBreak = (lineInfo) => lineInfo.breakType === BreakType.Wrap && lineInfo.positions.length === 0;
  30061. const startsWithBrBreak = (lineInfo) => lineInfo.breakType === BreakType.Br && lineInfo.positions.length === 1;
  30062. const isAtTableCellLine = (getPositionsUntil, scope, pos) => {
  30063. const lineInfo = getPositionsUntil(scope, pos);
  30064. // Since we can't determine if the caret is on the above or below line in a word wrap break we asume it's always
  30065. // on the below/above line based on direction. This will make the caret jump one line if you are at the end of the last
  30066. // line and moving down or at the beginning of the second line moving up.
  30067. if (startsWithWrapBreak(lineInfo) || (!isBr$7(pos.getNode()) && startsWithBrBreak(lineInfo))) {
  30068. return !hasNextBreak(getPositionsUntil, scope, lineInfo);
  30069. }
  30070. else {
  30071. return lineInfo.breakAt.isNone();
  30072. }
  30073. };
  30074. const isAtFirstTableCellLine = curry(isAtTableCellLine, getPositionsUntilPreviousLine);
  30075. const isAtLastTableCellLine = curry(isAtTableCellLine, getPositionsUntilNextLine);
  30076. const isCaretAtStartOrEndOfTable = (forward, rng, table) => {
  30077. const caretPos = CaretPosition.fromRangeStart(rng);
  30078. return positionIn(!forward, table).exists((pos) => pos.isEqual(caretPos));
  30079. };
  30080. const navigateHorizontally = (editor, forward, table, _td) => {
  30081. const rng = editor.selection.getRng();
  30082. const direction = forward ? 1 : -1;
  30083. if (isFakeCaretTableBrowser() && isCaretAtStartOrEndOfTable(forward, rng, table)) {
  30084. showCaret(direction, editor, table, !forward, false).each((newRng) => {
  30085. moveToRange(editor, newRng);
  30086. });
  30087. return true;
  30088. }
  30089. return false;
  30090. };
  30091. const getClosestAbovePosition = (root, table, start) => findClosestPositionInAboveCell(table, start).orThunk(() => head(start.getClientRects()).bind((rect) => findClosestHorizontalPositionFromPoint(getPositionsAbove(root, CaretPosition.before(table)), rect.left))).getOr(CaretPosition.before(table));
  30092. const getClosestBelowPosition = (root, table, start) => findClosestPositionInBelowCell(table, start).orThunk(() => head(start.getClientRects()).bind((rect) => findClosestHorizontalPositionFromPoint(getPositionsBelow(root, CaretPosition.after(table)), rect.left))).getOr(CaretPosition.after(table));
  30093. const getTable = (previous, pos) => {
  30094. const node = pos.getNode(previous);
  30095. return isTable$2(node) ? Optional.some(node) : Optional.none();
  30096. };
  30097. const renderBlock = (down, editor, table) => {
  30098. editor.undoManager.transact(() => {
  30099. const insertFn = down ? after$4 : before$4;
  30100. const rng = insertEmptyLine(editor, SugarElement.fromDom(table), insertFn);
  30101. moveToRange(editor, rng);
  30102. });
  30103. };
  30104. const moveCaret = (editor, down, pos) => {
  30105. const table = down ? getTable(true, pos) : getTable(false, pos);
  30106. const last = down === false;
  30107. table.fold(() => moveToRange(editor, pos.toRange()), (table) => positionIn(last, editor.getBody()).filter((lastPos) => lastPos.isEqual(pos)).fold(() => moveToRange(editor, pos.toRange()), (_) => renderBlock(down, editor, table)));
  30108. };
  30109. const navigateVertically = (editor, down, table, td) => {
  30110. const rng = editor.selection.getRng();
  30111. const pos = CaretPosition.fromRangeStart(rng);
  30112. const root = editor.getBody();
  30113. if (!down && isAtFirstTableCellLine(td, pos)) {
  30114. const newPos = getClosestAbovePosition(root, table, pos);
  30115. moveCaret(editor, down, newPos);
  30116. return true;
  30117. }
  30118. else if (down && isAtLastTableCellLine(td, pos)) {
  30119. const newPos = getClosestBelowPosition(root, table, pos);
  30120. moveCaret(editor, down, newPos);
  30121. return true;
  30122. }
  30123. else {
  30124. return false;
  30125. }
  30126. };
  30127. const move$1 = (editor, forward, mover) => Optional.from(editor.dom.getParent(editor.selection.getNode(), 'td,th'))
  30128. .bind((td) => Optional.from(editor.dom.getParent(td, 'table'))
  30129. .map((table) => mover(editor, forward, table, td))).getOr(false);
  30130. const moveH = (editor, forward) => move$1(editor, forward, navigateHorizontally);
  30131. const moveV = (editor, forward) => move$1(editor, forward, navigateVertically);
  30132. const getCellFirstCursorPosition = (cell) => {
  30133. const selection = SimSelection.exact(cell, 0, cell, 0);
  30134. return toNative(selection);
  30135. };
  30136. const rowHasEditableCell = (cell) => {
  30137. return isCellEditable(cell) || siblings(cell).some((element) => isHTMLElement$1(element) && isCellEditable(element));
  30138. };
  30139. const tabGo = (editor, isRoot, cell) => {
  30140. return cell.fold(Optional.none, Optional.none, (_current, next) => {
  30141. return first(next).map((cell) => {
  30142. return getCellFirstCursorPosition(cell);
  30143. });
  30144. }, (current) => {
  30145. if (editor.mode.isReadOnly() || !isCellInEditableTable(current) || !rowHasEditableCell(current)) {
  30146. return Optional.none();
  30147. }
  30148. editor.execCommand('mceTableInsertRowAfter');
  30149. // Move forward from the last cell so that we move into the first valid position in the new row
  30150. return tabForward(editor, isRoot, current);
  30151. });
  30152. };
  30153. const isCellInEditableTable = (cell) => closest$4(cell, isTag('table')).exists(isEditable$2);
  30154. const tabForward = (editor, isRoot, cell) => tabGo(editor, isRoot, next(cell, isCellEditable));
  30155. const tabBackward = (editor, isRoot, cell) => tabGo(editor, isRoot, prev(cell, isCellEditable));
  30156. const isCellEditable = (cell) => isEditable$2(cell) || descendant(cell, isEditableHTMLElement);
  30157. const isEditableHTMLElement = (node) => isHTMLElement$1(node) && isEditable$2(node);
  30158. const handleTab = (editor, forward) => {
  30159. const rootElements = ['table', 'li', 'dl'];
  30160. const body = SugarElement.fromDom(editor.getBody());
  30161. const isRoot = (element) => {
  30162. const name$1 = name(element);
  30163. return eq(element, body) || contains$2(rootElements, name$1);
  30164. };
  30165. const rng = editor.selection.getRng();
  30166. // If navigating backwards, use the start of the ranged selection
  30167. const container = SugarElement.fromDom(!forward ? rng.startContainer : rng.endContainer);
  30168. return cell(container, isRoot).map((cell) => {
  30169. // Clear fake ranged selection because our new selection will always be collapsed
  30170. table(cell, isRoot).each((table) => {
  30171. editor.model.table.clearSelectedCells(table.dom);
  30172. });
  30173. // Collapse selection to start or end based on shift key
  30174. editor.selection.collapse(!forward);
  30175. const navigation = !forward ? tabBackward : tabForward;
  30176. const rng = navigation(editor, isRoot, cell);
  30177. rng.each((range) => {
  30178. editor.selection.setRng(range);
  30179. });
  30180. return true;
  30181. }).getOr(false);
  30182. };
  30183. const executeKeydownOverride$4 = (editor, caret, evt) => {
  30184. const isMac = Env.os.isMacOS() || Env.os.isiOS();
  30185. execute([
  30186. { keyCode: VK.RIGHT, action: action(moveH$2, editor, true) },
  30187. { keyCode: VK.LEFT, action: action(moveH$2, editor, false) },
  30188. { keyCode: VK.UP, action: action(moveV$4, editor, false) },
  30189. { keyCode: VK.DOWN, action: action(moveV$4, editor, true) },
  30190. ...(isMac ? [
  30191. { keyCode: VK.UP, action: action(selectToEndPoint, editor, false), metaKey: true, shiftKey: true },
  30192. { keyCode: VK.DOWN, action: action(selectToEndPoint, editor, true), metaKey: true, shiftKey: true }
  30193. ] : []),
  30194. { keyCode: VK.RIGHT, action: action(moveH, editor, true) },
  30195. { keyCode: VK.LEFT, action: action(moveH, editor, false) },
  30196. { keyCode: VK.UP, action: action(moveV, editor, false) },
  30197. { keyCode: VK.DOWN, action: action(moveV, editor, true) },
  30198. { keyCode: VK.UP, action: action(moveV, editor, false) },
  30199. { keyCode: VK.UP, action: action(moveV$2, editor, false) },
  30200. { keyCode: VK.DOWN, action: action(moveV$2, editor, true) },
  30201. { keyCode: VK.RIGHT, action: action(moveH$1, editor, true) },
  30202. { keyCode: VK.LEFT, action: action(moveH$1, editor, false) },
  30203. { keyCode: VK.UP, action: action(moveV$1, editor, false) },
  30204. { keyCode: VK.DOWN, action: action(moveV$1, editor, true) },
  30205. { keyCode: VK.RIGHT, action: action(move$3, editor, caret, true) },
  30206. { keyCode: VK.LEFT, action: action(move$3, editor, caret, false) },
  30207. { keyCode: VK.RIGHT, ctrlKey: !isMac, altKey: isMac, action: action(moveNextWord, editor, caret) },
  30208. { keyCode: VK.LEFT, ctrlKey: !isMac, altKey: isMac, action: action(movePrevWord, editor, caret) },
  30209. { keyCode: VK.UP, action: action(moveV$3, editor, false) },
  30210. { keyCode: VK.DOWN, action: action(moveV$3, editor, true) }
  30211. ], evt).each((_) => {
  30212. evt.preventDefault();
  30213. });
  30214. };
  30215. const setup$q = (editor, caret) => {
  30216. editor.on('keydown', (evt) => {
  30217. if (!evt.isDefaultPrevented()) {
  30218. executeKeydownOverride$4(editor, caret, evt);
  30219. }
  30220. });
  30221. };
  30222. const point = (container, offset) => ({
  30223. container,
  30224. offset
  30225. });
  30226. const DOM$7 = DOMUtils.DOM;
  30227. const alwaysNext = (startNode) => (node) => startNode === node ? -1 : 0;
  30228. // This largely is derived from robins isBoundary check, however it also treats contenteditable=false elements as a boundary
  30229. // See robins `Structure.isEmptyTag` for the list of quasi block elements
  30230. const isBoundary = (dom) => (node) => dom.isBlock(node) || contains$2(['BR', 'IMG', 'HR', 'INPUT'], node.nodeName) || dom.getContentEditable(node) === 'false';
  30231. // Finds the text node before the specified node, or just returns the node if it's already on a text node
  30232. const textBefore = (node, offset, rootNode) => {
  30233. if (isText$b(node) && offset >= 0) {
  30234. return Optional.some(point(node, offset));
  30235. }
  30236. else {
  30237. const textSeeker = TextSeeker(DOM$7);
  30238. return Optional.from(textSeeker.backwards(node, offset, alwaysNext(node), rootNode)).map((prev) => point(prev.container, prev.container.data.length));
  30239. }
  30240. };
  30241. const textAfter = (node, offset, rootNode) => {
  30242. if (isText$b(node) && offset >= node.length) {
  30243. return Optional.some(point(node, offset));
  30244. }
  30245. else {
  30246. const textSeeker = TextSeeker(DOM$7);
  30247. return Optional.from(textSeeker.forwards(node, offset, alwaysNext(node), rootNode)).map((prev) => point(prev.container, 0));
  30248. }
  30249. };
  30250. const scanLeft = (node, offset, rootNode) => {
  30251. if (!isText$b(node)) {
  30252. return Optional.none();
  30253. }
  30254. const text = node.data;
  30255. if (offset >= 0 && offset <= text.length) {
  30256. return Optional.some(point(node, offset));
  30257. }
  30258. else {
  30259. const textSeeker = TextSeeker(DOM$7);
  30260. return Optional.from(textSeeker.backwards(node, offset, alwaysNext(node), rootNode)).bind((prev) => {
  30261. const prevText = prev.container.data;
  30262. return scanLeft(prev.container, offset + prevText.length, rootNode);
  30263. });
  30264. }
  30265. };
  30266. const scanRight = (node, offset, rootNode) => {
  30267. if (!isText$b(node)) {
  30268. return Optional.none();
  30269. }
  30270. const text = node.data;
  30271. if (offset <= text.length) {
  30272. return Optional.some(point(node, offset));
  30273. }
  30274. else {
  30275. const textSeeker = TextSeeker(DOM$7);
  30276. return Optional.from(textSeeker.forwards(node, offset, alwaysNext(node), rootNode)).bind((next) => scanRight(next.container, offset - text.length, rootNode));
  30277. }
  30278. };
  30279. const repeatLeft = (dom, node, offset, process, rootNode) => {
  30280. const search = TextSeeker(dom, isBoundary(dom));
  30281. return Optional.from(search.backwards(node, offset, process, rootNode));
  30282. };
  30283. const isValidTextRange = (rng) => rng.collapsed && isText$b(rng.startContainer);
  30284. // Normalize the text by replacing non-breaking spaces with regular spaces and stripping zero-width spaces (fake carets).
  30285. const getText = (rng) => trim$2(rng.toString().replace(/\u00A0/g, ' '));
  30286. const isWhitespace = (chr) => chr !== '' && ' \u00a0\ufeff\f\n\r\t\v'.indexOf(chr) !== -1;
  30287. const stripTrigger = (text, trigger) => text.substring(trigger.length);
  30288. const findTrigger = (text, index, trigger, includeWhitespace = false) => {
  30289. // Identify the `char` in, and start the text from that point forward. If there is ever any whitespace, fail
  30290. let i;
  30291. const firstChar = trigger.charAt(0);
  30292. for (i = index - 1; i >= 0; i--) {
  30293. const char = text.charAt(i);
  30294. if (!includeWhitespace && isWhitespace(char)) {
  30295. return Optional.none();
  30296. }
  30297. if (firstChar === char && contains$1(text, trigger, i, index)) {
  30298. break;
  30299. }
  30300. }
  30301. return Optional.some(i);
  30302. };
  30303. const getContext = (dom, initRange, trigger, includeWhitespace = false) => {
  30304. if (!isValidTextRange(initRange)) {
  30305. return Optional.none();
  30306. }
  30307. const buffer = { text: '', offset: 0 };
  30308. const findTriggerIndex = (element, offset, text) => {
  30309. buffer.text = text + buffer.text;
  30310. buffer.offset += offset;
  30311. // Stop searching by just returning the current offset if whitespace was found (eg Optional.none())
  30312. // and we'll handle the final checks below instead
  30313. return findTrigger(buffer.text, buffer.offset, trigger, includeWhitespace).getOr(offset);
  30314. };
  30315. const root = dom.getParent(initRange.startContainer, dom.isBlock) || dom.getRoot();
  30316. return repeatLeft(dom, initRange.startContainer, initRange.startOffset, findTriggerIndex, root).bind((spot) => {
  30317. const range = initRange.cloneRange();
  30318. range.setStart(spot.container, spot.offset);
  30319. range.setEnd(initRange.endContainer, initRange.endOffset);
  30320. // If the range is collapsed then we didn't find a match so abort
  30321. if (range.collapsed) {
  30322. return Optional.none();
  30323. }
  30324. const text = getText(range);
  30325. const triggerIndex = text.lastIndexOf(trigger);
  30326. // If the match doesn't start with the trigger (eg whitespace found)
  30327. if (triggerIndex !== 0) {
  30328. return Optional.none();
  30329. }
  30330. else {
  30331. return Optional.some({ text: stripTrigger(text, trigger), range, trigger });
  30332. }
  30333. });
  30334. };
  30335. const isText$1 = (node) => node.nodeType === TEXT;
  30336. const isElement = (node) => node.nodeType === ELEMENT;
  30337. const toLast = (node) => {
  30338. if (isText$1(node)) {
  30339. return point(node, node.data.length);
  30340. }
  30341. else {
  30342. const children = node.childNodes;
  30343. // keep descending if there are children.
  30344. return children.length > 0 ? toLast(children[children.length - 1]) : point(node, children.length);
  30345. }
  30346. };
  30347. const toLeaf = (node, offset) => {
  30348. const children = node.childNodes;
  30349. if (children.length > 0 && offset < children.length) {
  30350. return toLeaf(children[offset], 0);
  30351. }
  30352. else if (children.length > 0 && isElement(node) && children.length === offset) {
  30353. return toLast(children[children.length - 1]);
  30354. }
  30355. else {
  30356. return point(node, offset);
  30357. }
  30358. };
  30359. const isPreviousCharContent = (dom, leaf) => {
  30360. var _a;
  30361. // If at the start of the range, then we need to look backwards one more place. Otherwise we just need to look at the current text
  30362. const root = (_a = dom.getParent(leaf.container, dom.isBlock)) !== null && _a !== void 0 ? _a : dom.getRoot();
  30363. return repeatLeft(dom, leaf.container, leaf.offset, (_element, offset) => offset === 0 ? -1 : offset, root).filter((spot) => {
  30364. const char = spot.container.data.charAt(spot.offset - 1);
  30365. return !isWhitespace(char);
  30366. }).isSome();
  30367. };
  30368. const isStartOfWord = (dom) => (rng) => {
  30369. const leaf = toLeaf(rng.startContainer, rng.startOffset);
  30370. return !isPreviousCharContent(dom, leaf);
  30371. };
  30372. const getTriggerContext = (dom, initRange, database) => findMap(database.triggers, (trigger) => getContext(dom, initRange, trigger));
  30373. const lookup = (editor, getDatabase) => {
  30374. const database = getDatabase();
  30375. const rng = editor.selection.getRng();
  30376. return getTriggerContext(editor.dom, rng, database).bind((context) => lookupWithContext(editor, getDatabase, context));
  30377. };
  30378. const lookupWithContext = (editor, getDatabase, context, fetchOptions = {}) => {
  30379. var _a;
  30380. const database = getDatabase();
  30381. const rng = editor.selection.getRng();
  30382. const startText = (_a = rng.startContainer.nodeValue) !== null && _a !== void 0 ? _a : '';
  30383. const autocompleters = filter$5(database.lookupByTrigger(context.trigger), (autocompleter) => context.text.length >= autocompleter.minChars && autocompleter.matches.getOrThunk(() => isStartOfWord(editor.dom))(context.range, startText, context.text));
  30384. if (autocompleters.length === 0) {
  30385. return Optional.none();
  30386. }
  30387. const lookupData = Promise.all(map$3(autocompleters, (ac) => {
  30388. // TODO: Find a sensible way to do maxResults
  30389. const fetchResult = ac.fetch(context.text, ac.maxResults, fetchOptions);
  30390. return fetchResult.then((results) => ({
  30391. matchText: context.text,
  30392. items: results,
  30393. columns: ac.columns,
  30394. onAction: ac.onAction,
  30395. highlightOn: ac.highlightOn
  30396. }));
  30397. }));
  30398. return Optional.some({
  30399. lookupData,
  30400. context
  30401. });
  30402. };
  30403. var SimpleResultType;
  30404. (function (SimpleResultType) {
  30405. SimpleResultType[SimpleResultType["Error"] = 0] = "Error";
  30406. SimpleResultType[SimpleResultType["Value"] = 1] = "Value";
  30407. })(SimpleResultType || (SimpleResultType = {}));
  30408. const fold$1 = (res, onError, onValue) => res.stype === SimpleResultType.Error ? onError(res.serror) : onValue(res.svalue);
  30409. const partition = (results) => {
  30410. const values = [];
  30411. const errors = [];
  30412. each$e(results, (obj) => {
  30413. fold$1(obj, (err) => errors.push(err), (val) => values.push(val));
  30414. });
  30415. return { values, errors };
  30416. };
  30417. const mapError = (res, f) => {
  30418. if (res.stype === SimpleResultType.Error) {
  30419. return { stype: SimpleResultType.Error, serror: f(res.serror) };
  30420. }
  30421. else {
  30422. return res;
  30423. }
  30424. };
  30425. const map = (res, f) => {
  30426. if (res.stype === SimpleResultType.Value) {
  30427. return { stype: SimpleResultType.Value, svalue: f(res.svalue) };
  30428. }
  30429. else {
  30430. return res;
  30431. }
  30432. };
  30433. const bind = (res, f) => {
  30434. if (res.stype === SimpleResultType.Value) {
  30435. return f(res.svalue);
  30436. }
  30437. else {
  30438. return res;
  30439. }
  30440. };
  30441. const bindError = (res, f) => {
  30442. if (res.stype === SimpleResultType.Error) {
  30443. return f(res.serror);
  30444. }
  30445. else {
  30446. return res;
  30447. }
  30448. };
  30449. const svalue = (v) => ({ stype: SimpleResultType.Value, svalue: v });
  30450. const serror = (e) => ({ stype: SimpleResultType.Error, serror: e });
  30451. const toResult = (res) => fold$1(res, Result.error, Result.value);
  30452. const fromResult = (res) => res.fold(serror, svalue);
  30453. const SimpleResult = {
  30454. fromResult,
  30455. toResult,
  30456. svalue,
  30457. partition,
  30458. serror,
  30459. bind,
  30460. bindError,
  30461. map,
  30462. mapError,
  30463. fold: fold$1
  30464. };
  30465. const formatObj = (input) => {
  30466. return isObject(input) && keys(input).length > 100 ? ' removed due to size' : JSON.stringify(input, null, 2);
  30467. };
  30468. const formatErrors = (errors) => {
  30469. const es = errors.length > 10 ? errors.slice(0, 10).concat([
  30470. {
  30471. path: [],
  30472. getErrorInfo: constant('... (only showing first ten failures)')
  30473. }
  30474. ]) : errors;
  30475. // TODO: Work out a better split between PrettyPrinter and SchemaError
  30476. return map$3(es, (e) => {
  30477. return 'Failed path: (' + e.path.join(' > ') + ')\n' + e.getErrorInfo();
  30478. });
  30479. };
  30480. const nu = (path, getErrorInfo) => {
  30481. return SimpleResult.serror([{
  30482. path,
  30483. // This is lazy so that it isn't calculated unnecessarily
  30484. getErrorInfo
  30485. }]);
  30486. };
  30487. const missingRequired = (path, key, obj) => nu(path, () => 'Could not find valid *required* value for "' + key + '" in ' + formatObj(obj));
  30488. const custom = (path, err) => nu(path, constant(err));
  30489. const value = (validator) => {
  30490. const extract = (path, val) => {
  30491. return SimpleResult.bindError(validator(val), (err) => custom(path, err));
  30492. };
  30493. const toString = constant('val');
  30494. return {
  30495. extract,
  30496. toString
  30497. };
  30498. };
  30499. const anyValue$1 = value(SimpleResult.svalue);
  30500. const anyValue = constant(anyValue$1);
  30501. const typedValue = (validator, expectedType) => value((a) => {
  30502. const actualType = typeof a;
  30503. return validator(a) ? SimpleResult.svalue(a) : SimpleResult.serror(`Expected type: ${expectedType} but got: ${actualType}`);
  30504. });
  30505. const number = typedValue(isNumber, 'number');
  30506. const string = typedValue(isString, 'string');
  30507. typedValue(isBoolean, 'boolean');
  30508. const functionProcessor = typedValue(isFunction, 'function');
  30509. // Test if a value can be copied by the structured clone algorithm and hence sendable via postMessage
  30510. // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
  30511. // from https://stackoverflow.com/a/32673910/7377237 with adjustments for typescript
  30512. const isPostMessageable = (val) => {
  30513. if (Object(val) !== val) { // Primitive value
  30514. return true;
  30515. }
  30516. switch ({}.toString.call(val).slice(8, -1)) { // Class
  30517. case 'Boolean':
  30518. case 'Number':
  30519. case 'String':
  30520. case 'Date':
  30521. case 'RegExp':
  30522. case 'Blob':
  30523. case 'FileList':
  30524. case 'ImageData':
  30525. case 'ImageBitmap':
  30526. case 'ArrayBuffer':
  30527. return true;
  30528. case 'Array':
  30529. case 'Object':
  30530. return Object.keys(val).every((prop) => isPostMessageable(val[prop]));
  30531. default:
  30532. return false;
  30533. }
  30534. };
  30535. value((a) => {
  30536. if (isPostMessageable(a)) {
  30537. return SimpleResult.svalue(a);
  30538. }
  30539. else {
  30540. return SimpleResult.serror('Expected value to be acceptable for sending via postMessage');
  30541. }
  30542. });
  30543. const required$1 = () => ({ tag: "required" /* FieldPresenceTag.Required */, process: {} });
  30544. const defaultedThunk = (fallbackThunk) => ({ tag: "defaultedThunk" /* FieldPresenceTag.DefaultedThunk */, process: fallbackThunk });
  30545. const defaulted$1 = (fallback) => defaultedThunk(constant(fallback));
  30546. const asOption = () => ({ tag: "option" /* FieldPresenceTag.Option */, process: {} });
  30547. const field$1 = (key, newKey, presence, prop) => ({ tag: "field" /* FieldTag.Field */, key, newKey, presence, prop });
  30548. const fold = (value, ifField, ifCustom) => {
  30549. switch (value.tag) {
  30550. case "field" /* FieldTag.Field */:
  30551. return ifField(value.key, value.newKey, value.presence, value.prop);
  30552. case "custom" /* FieldTag.CustomField */:
  30553. return ifCustom(value.newKey, value.instantiator);
  30554. }
  30555. };
  30556. const mergeValues = (values, base) => {
  30557. return SimpleResult.svalue(deepMerge(base, merge$1.apply(undefined, values)));
  30558. };
  30559. const mergeErrors = (errors) => compose(SimpleResult.serror, flatten)(errors);
  30560. const consolidateObj = (objects, base) => {
  30561. const partition = SimpleResult.partition(objects);
  30562. return partition.errors.length > 0 ? mergeErrors(partition.errors) : mergeValues(partition.values, base);
  30563. };
  30564. const consolidateArr = (objects) => {
  30565. const partitions = SimpleResult.partition(objects);
  30566. return partitions.errors.length > 0 ? mergeErrors(partitions.errors) : SimpleResult.svalue(partitions.values);
  30567. };
  30568. const ResultCombine = {
  30569. consolidateObj,
  30570. consolidateArr
  30571. };
  30572. const requiredAccess = (path, obj, key, bundle) =>
  30573. // In required mode, if it is undefined, it is an error.
  30574. get$a(obj, key).fold(() => missingRequired(path, key, obj), bundle);
  30575. const fallbackAccess = (obj, key, fallback, bundle) => {
  30576. const v = get$a(obj, key).getOrThunk(() => fallback(obj));
  30577. return bundle(v);
  30578. };
  30579. const optionAccess = (obj, key, bundle) => bundle(get$a(obj, key));
  30580. const optionDefaultedAccess = (obj, key, fallback, bundle) => {
  30581. const opt = get$a(obj, key).map((val) => val === true ? fallback(obj) : val);
  30582. return bundle(opt);
  30583. };
  30584. const extractField = (field, path, obj, key, prop) => {
  30585. const bundle = (av) => prop.extract(path.concat([key]), av);
  30586. const bundleAsOption = (optValue) => optValue.fold(() => SimpleResult.svalue(Optional.none()), (ov) => {
  30587. const result = prop.extract(path.concat([key]), ov);
  30588. return SimpleResult.map(result, Optional.some);
  30589. });
  30590. switch (field.tag) {
  30591. case "required" /* FieldPresenceTag.Required */:
  30592. return requiredAccess(path, obj, key, bundle);
  30593. case "defaultedThunk" /* FieldPresenceTag.DefaultedThunk */:
  30594. return fallbackAccess(obj, key, field.process, bundle);
  30595. case "option" /* FieldPresenceTag.Option */:
  30596. return optionAccess(obj, key, bundleAsOption);
  30597. case "defaultedOptionThunk" /* FieldPresenceTag.DefaultedOptionThunk */:
  30598. return optionDefaultedAccess(obj, key, field.process, bundleAsOption);
  30599. case "mergeWithThunk" /* FieldPresenceTag.MergeWithThunk */: {
  30600. return fallbackAccess(obj, key, constant({}), (v) => {
  30601. const result = deepMerge(field.process(obj), v);
  30602. return bundle(result);
  30603. });
  30604. }
  30605. }
  30606. };
  30607. const extractFields = (path, obj, fields) => {
  30608. const success = {};
  30609. const errors = [];
  30610. // PERFORMANCE: We use a for loop here instead of Arr.each as this is a hot code path
  30611. for (const field of fields) {
  30612. fold(field, (key, newKey, presence, prop) => {
  30613. const result = extractField(presence, path, obj, key, prop);
  30614. SimpleResult.fold(result, (err) => {
  30615. errors.push(...err);
  30616. }, (res) => {
  30617. success[newKey] = res;
  30618. });
  30619. }, (newKey, instantiator) => {
  30620. success[newKey] = instantiator(obj);
  30621. });
  30622. }
  30623. return errors.length > 0 ? SimpleResult.serror(errors) : SimpleResult.svalue(success);
  30624. };
  30625. const objOf = (values) => {
  30626. const extract = (path, o) => extractFields(path, o, values);
  30627. const toString = () => {
  30628. const fieldStrings = map$3(values, (value) => fold(value, (key, _okey, _presence, prop) => key + ' -> ' + prop.toString(), (newKey, _instantiator) => 'state(' + newKey + ')'));
  30629. return 'obj{\n' + fieldStrings.join('\n') + '}';
  30630. };
  30631. return {
  30632. extract,
  30633. toString
  30634. };
  30635. };
  30636. const arrOf = (prop) => {
  30637. const extract = (path, array) => {
  30638. const results = map$3(array, (a, i) => prop.extract(path.concat(['[' + i + ']']), a));
  30639. return ResultCombine.consolidateArr(results);
  30640. };
  30641. const toString = () => 'array(' + prop.toString() + ')';
  30642. return {
  30643. extract,
  30644. toString
  30645. };
  30646. };
  30647. const extractValue = (label, prop, obj) => {
  30648. const res = prop.extract([label], obj);
  30649. return SimpleResult.mapError(res, (errs) => ({ input: obj, errors: errs }));
  30650. };
  30651. const asRaw = (label, prop, obj) => SimpleResult.toResult(extractValue(label, prop, obj));
  30652. const formatError = (errInfo) => {
  30653. return 'Errors: \n' + formatErrors(errInfo.errors).join('\n') +
  30654. '\n\nInput object: ' + formatObj(errInfo.input);
  30655. };
  30656. const field = field$1;
  30657. const required = (key) => field(key, key, required$1(), anyValue());
  30658. const requiredOf = (key, schema) => field(key, key, required$1(), schema);
  30659. const requiredString = (key) => requiredOf(key, string);
  30660. const requiredFunction = (key) => requiredOf(key, functionProcessor);
  30661. const option = (key) => field(key, key, asOption(), anyValue());
  30662. const optionOf = (key, schema) => field(key, key, asOption(), schema);
  30663. const optionString = (key) => optionOf(key, string);
  30664. const optionFunction = (key) => optionOf(key, functionProcessor);
  30665. const defaulted = (key, fallback) => field(key, key, defaulted$1(fallback), anyValue());
  30666. const defaultedOf = (key, fallback, schema) => field(key, key, defaulted$1(fallback), schema);
  30667. const defaultedNumber = (key, fallback) => defaultedOf(key, fallback, number);
  30668. const defaultedArrayOf = (key, fallback, schema) => defaultedOf(key, fallback, arrOf(schema));
  30669. const type = requiredString('type');
  30670. const fetch$1 = requiredFunction('fetch');
  30671. const onAction = requiredFunction('onAction');
  30672. optionString('name');
  30673. optionString('text');
  30674. optionString('role');
  30675. optionString('icon');
  30676. optionString('url');
  30677. optionString('tooltip');
  30678. optionString('chevronTooltip');
  30679. optionString('label');
  30680. optionString('shortcut');
  30681. const defaultedColumns = (num) => defaulted('columns', num);
  30682. const autocompleterSchema = objOf([
  30683. type,
  30684. requiredString('trigger'),
  30685. defaultedNumber('minChars', 1),
  30686. defaultedColumns(1),
  30687. defaultedNumber('maxResults', 10),
  30688. optionFunction('matches'),
  30689. fetch$1,
  30690. onAction,
  30691. defaultedArrayOf('highlightOn', [], string)
  30692. ]);
  30693. const createAutocompleter = (spec) => asRaw('Autocompleter', autocompleterSchema, spec);
  30694. const create$6 = () => {
  30695. const buttons = {};
  30696. const menuItems = {};
  30697. const popups = {};
  30698. const icons = {};
  30699. const contextMenus = {};
  30700. const contextToolbars = {};
  30701. const contexts = {};
  30702. const sidebars = {};
  30703. const views = {};
  30704. const add = (collection, type) => (name, spec) => {
  30705. collection[name.toLowerCase()] = { ...spec, type };
  30706. };
  30707. const addDefaulted = (collection, type) => (name, spec) => {
  30708. collection[name.toLowerCase()] = { type, ...spec };
  30709. };
  30710. const addIcon = (name, svgData) => icons[name.toLowerCase()] = svgData;
  30711. const addContext = (name, pred) => contexts[name.toLowerCase()] = pred;
  30712. return {
  30713. addButton: add(buttons, 'button'),
  30714. addGroupToolbarButton: add(buttons, 'grouptoolbarbutton'),
  30715. addToggleButton: add(buttons, 'togglebutton'),
  30716. addMenuButton: add(buttons, 'menubutton'),
  30717. addSplitButton: add(buttons, 'splitbutton'),
  30718. addMenuItem: add(menuItems, 'menuitem'),
  30719. addNestedMenuItem: add(menuItems, 'nestedmenuitem'),
  30720. addToggleMenuItem: add(menuItems, 'togglemenuitem'),
  30721. addAutocompleter: add(popups, 'autocompleter'),
  30722. addContextMenu: add(contextMenus, 'contextmenu'),
  30723. addContextToolbar: add(contextToolbars, 'contexttoolbar'),
  30724. addContextForm: addDefaulted(contextToolbars, 'contextform'),
  30725. addSidebar: add(sidebars, 'sidebar'),
  30726. addView: add(views, 'views'),
  30727. addIcon,
  30728. addContext,
  30729. getAll: () => ({
  30730. buttons,
  30731. menuItems,
  30732. icons,
  30733. // TODO: should popups be combined with context menus? We'd need to make a new add function.
  30734. // Right now using `add` shares the key namespace, which prevents registering both
  30735. // a completer and a context menu with the same name
  30736. popups,
  30737. contextMenus,
  30738. contextToolbars,
  30739. sidebars,
  30740. views,
  30741. contexts
  30742. })
  30743. };
  30744. };
  30745. const register$2 = (editor) => {
  30746. const popups = editor.ui.registry.getAll().popups;
  30747. const dataset = map$2(popups, (popup) => createAutocompleter(popup).fold((err) => {
  30748. throw new Error(formatError(err));
  30749. }, identity));
  30750. const triggers = stringArray(mapToArray(dataset, (v) => v.trigger));
  30751. const datasetValues = values(dataset);
  30752. const lookupByTrigger = (trigger) => filter$5(datasetValues, (dv) => dv.trigger === trigger);
  30753. return {
  30754. dataset,
  30755. triggers,
  30756. lookupByTrigger
  30757. };
  30758. };
  30759. const setupEditorInput = (editor, api) => {
  30760. const update = last$1(api.load, 50);
  30761. editor.on('input', (e) => {
  30762. // TINY-10715: Firefox on Android using Korean Gboard will produce stray composition events when you move the caret by tapping inside the composed text
  30763. if (e.inputType === 'insertCompositionText' && !editor.composing) {
  30764. return;
  30765. }
  30766. update.throttle();
  30767. });
  30768. editor.on('keydown', (e) => {
  30769. const keyCode = e.which;
  30770. // Pressing <backspace> updates the autocompleter
  30771. if (keyCode === 8) {
  30772. update.throttle();
  30773. // Pressing <esc> closes the autocompleter
  30774. }
  30775. else if (keyCode === 27) {
  30776. update.cancel(); // We need to cancel here since Esc cancels the IME composition and triggers an input event
  30777. api.cancelIfNecessary();
  30778. }
  30779. else if (keyCode === 38 || keyCode === 40) {
  30780. // Arrow up and down keys needs to cancel the update since while composing arrow up or down will end the compose and issue a input event
  30781. // that causes the list to update and then the focus moves up to the first item in the auto completer list.
  30782. update.cancel();
  30783. }
  30784. }, true); // Need to add this to the top so that it exectued before the silver keyboard event
  30785. editor.on('remove', update.cancel);
  30786. };
  30787. const setup$p = (editor) => {
  30788. const activeAutocompleter = value$1();
  30789. const uiActive = Cell(false);
  30790. const isActive = activeAutocompleter.isSet;
  30791. const cancelIfNecessary = () => {
  30792. if (isActive()) {
  30793. fireAutocompleterEnd(editor);
  30794. uiActive.set(false);
  30795. activeAutocompleter.clear();
  30796. }
  30797. };
  30798. const commenceIfNecessary = (context) => {
  30799. if (!isActive()) {
  30800. // store the element/context
  30801. activeAutocompleter.set({
  30802. trigger: context.trigger,
  30803. matchLength: context.text.length
  30804. });
  30805. }
  30806. };
  30807. // This needs to be calculated once things are ready, but the key events must be bound
  30808. // before `init` or other keydown / keypress listeners will fire first. Therefore,
  30809. // this is a thunk so that its value is calculated just once when it is used for the
  30810. // first time, and after that it's value is stored.
  30811. const getAutocompleters = cached(() => register$2(editor));
  30812. const doLookup = (fetchOptions) => activeAutocompleter.get().map((ac) => getContext(editor.dom, editor.selection.getRng(), ac.trigger, true)
  30813. .bind((newContext) => lookupWithContext(editor, getAutocompleters, newContext, fetchOptions))).getOrThunk(() => lookup(editor, getAutocompleters));
  30814. const load = (fetchOptions) => {
  30815. doLookup(fetchOptions).fold(cancelIfNecessary, (lookupInfo) => {
  30816. commenceIfNecessary(lookupInfo.context);
  30817. // Wait for the results to return and then display the menu
  30818. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  30819. lookupInfo.lookupData.then((lookupData) => {
  30820. // Lookup the active autocompleter to make sure it's still active, if it isn't then do nothing
  30821. activeAutocompleter.get().map((ac) => {
  30822. const context = lookupInfo.context;
  30823. // Ensure the active autocompleter trigger matches, as the old one may have closed
  30824. // and a new one may have opened. If it doesn't match, then do nothing.
  30825. if (ac.trigger !== context.trigger) {
  30826. return;
  30827. }
  30828. activeAutocompleter.set({
  30829. ...ac,
  30830. matchLength: context.text.length
  30831. });
  30832. if (uiActive.get()) {
  30833. fireAutocompleterUpdateActiveRange(editor, { range: context.range });
  30834. fireAutocompleterUpdate(editor, { lookupData });
  30835. }
  30836. else {
  30837. uiActive.set(true);
  30838. fireAutocompleterUpdateActiveRange(editor, { range: context.range });
  30839. fireAutocompleterStart(editor, { lookupData });
  30840. }
  30841. });
  30842. });
  30843. });
  30844. };
  30845. const isRangeInsideOrEqual = (innerRange, outerRange) => {
  30846. const startComparison = innerRange.compareBoundaryPoints(window.Range.START_TO_START, outerRange);
  30847. const endComparison = innerRange.compareBoundaryPoints(window.Range.END_TO_END, outerRange);
  30848. return startComparison >= 0 && endComparison <= 0;
  30849. };
  30850. const readActiveRange = () => {
  30851. return activeAutocompleter.get().bind(({ trigger }) => {
  30852. const selRange = editor.selection.getRng();
  30853. return getContext(editor.dom, selRange, trigger, uiActive.get())
  30854. .filter(({ range }) => isRangeInsideOrEqual(selRange, range))
  30855. .map(({ range }) => range);
  30856. });
  30857. };
  30858. editor.addCommand('mceAutocompleterReload', (_ui, value) => {
  30859. const fetchOptions = isObject(value) ? value.fetchOptions : {};
  30860. load(fetchOptions);
  30861. });
  30862. editor.addCommand('mceAutocompleterClose', cancelIfNecessary);
  30863. editor.addCommand('mceAutocompleterRefreshActiveRange', () => {
  30864. readActiveRange().each((range) => {
  30865. fireAutocompleterUpdateActiveRange(editor, { range });
  30866. });
  30867. });
  30868. editor.editorCommands.addQueryStateHandler('mceAutoCompleterInRange', () => readActiveRange().isSome());
  30869. setupEditorInput(editor, {
  30870. cancelIfNecessary,
  30871. load
  30872. });
  30873. };
  30874. const browser$1 = detect$1().browser;
  30875. const isSafari = browser$1.isSafari();
  30876. const emptyNodeContents = (node) => fillWithPaddingBr(SugarElement.fromDom(node));
  30877. const isEntireNodeSelected = (rng, node) => { var _a; return rng.startOffset === 0 && rng.endOffset === ((_a = node.textContent) === null || _a === void 0 ? void 0 : _a.length); };
  30878. const getParentDetailsElementAtPos = (dom, pos) => Optional.from(dom.getParent(pos.container(), 'details'));
  30879. const isInDetailsElement = (dom, pos) => getParentDetailsElementAtPos(dom, pos).isSome();
  30880. const getDetailsElements = (dom, rng) => {
  30881. const startDetails = Optional.from(dom.getParent(rng.startContainer, 'details'));
  30882. const endDetails = Optional.from(dom.getParent(rng.endContainer, 'details'));
  30883. if (startDetails.isSome() || endDetails.isSome()) {
  30884. const startSummary = startDetails.bind((details) => Optional.from(dom.select('summary', details)[0]));
  30885. return Optional.some({ startSummary, startDetails, endDetails });
  30886. }
  30887. else {
  30888. return Optional.none();
  30889. }
  30890. };
  30891. const isCaretInTheBeginningOf = (caretPos, element) => firstPositionIn(element).exists((pos) => pos.isEqual(caretPos));
  30892. const isCaretInTheEndOf = (caretPos, element) => {
  30893. return lastPositionIn(element).exists((pos) => {
  30894. // Summary may or may not have a trailing BR
  30895. if (isBr$7(pos.getNode())) {
  30896. return prevPosition(element, pos).exists((pos2) => pos2.isEqual(caretPos)) || pos.isEqual(caretPos);
  30897. }
  30898. else {
  30899. return pos.isEqual(caretPos);
  30900. }
  30901. });
  30902. };
  30903. const isCaretAtStartOfSummary = (caretPos, detailsElements) => detailsElements.startSummary.exists((summary) => isCaretInTheBeginningOf(caretPos, summary));
  30904. const isCaretAtEndOfSummary = (caretPos, detailsElements) => detailsElements.startSummary.exists((summary) => isCaretInTheEndOf(caretPos, summary));
  30905. const isCaretInFirstPositionInBody = (caretPos, detailsElements) => detailsElements.startDetails.exists((details) => prevPosition(details, caretPos).forall((pos) => detailsElements.startSummary.exists((summary) => !summary.contains(caretPos.container()) && summary.contains(pos.container()))));
  30906. const isCaretInLastPositionInBody = (root, caretPos, detailsElements) => detailsElements.startDetails.exists((details) => nextPosition(root, caretPos).forall((pos) => !details.contains(pos.container())));
  30907. const setCaretToPosition = (editor, position) => {
  30908. const node = position.getNode();
  30909. if (!isUndefined(node)) {
  30910. editor.selection.setCursorLocation(node, position.offset());
  30911. }
  30912. };
  30913. const moveCaretToDetailsPos = (editor, pos, forward) => {
  30914. const details = editor.dom.getParent(pos.container(), 'details');
  30915. if (details && !details.open) {
  30916. const summary = editor.dom.select('summary', details)[0];
  30917. if (summary) {
  30918. const newPos = forward ? firstPositionIn(summary) : lastPositionIn(summary);
  30919. newPos.each((pos) => setCaretToPosition(editor, pos));
  30920. }
  30921. }
  30922. else {
  30923. setCaretToPosition(editor, pos);
  30924. }
  30925. };
  30926. const isPartialDelete = (rng, detailsElements) => {
  30927. const containsStart = (element) => element.contains(rng.startContainer);
  30928. const containsEnd = (element) => element.contains(rng.endContainer);
  30929. const startInSummary = detailsElements.startSummary.exists(containsStart);
  30930. const endInSummary = detailsElements.startSummary.exists(containsEnd);
  30931. const isPartiallySelectedDetailsElements = detailsElements.startDetails.forall((startDetails) => detailsElements.endDetails.forall((endDetails) => startDetails !== endDetails));
  30932. const isInPartiallySelectedSummary = (startInSummary || endInSummary) && !(startInSummary && endInSummary);
  30933. return isInPartiallySelectedSummary || isPartiallySelectedDetailsElements;
  30934. };
  30935. const shouldPreventDeleteIntoDetails = (editor, forward, granularity) => {
  30936. const { dom, selection } = editor;
  30937. const root = editor.getBody();
  30938. if (granularity === 'character') {
  30939. const caretPos = CaretPosition.fromRangeStart(selection.getRng());
  30940. const parentBlock = dom.getParent(caretPos.container(), dom.isBlock);
  30941. const parentDetailsAtCaret = getParentDetailsElementAtPos(dom, caretPos);
  30942. const inEmptyParentBlock = parentBlock && dom.isEmpty(parentBlock);
  30943. const isFirstBlock = isNull(parentBlock === null || parentBlock === void 0 ? void 0 : parentBlock.previousSibling);
  30944. const isLastBlock = isNull(parentBlock === null || parentBlock === void 0 ? void 0 : parentBlock.nextSibling);
  30945. // Pressing backspace or delete in an first or last empty block before or after details
  30946. if (inEmptyParentBlock) {
  30947. const firstOrLast = forward ? isLastBlock : isFirstBlock;
  30948. if (firstOrLast) {
  30949. const isBeforeAfterDetails = navigate(!forward, root, caretPos).exists((pos) => {
  30950. return isInDetailsElement(dom, pos) && !equals(parentDetailsAtCaret, getParentDetailsElementAtPos(dom, pos));
  30951. });
  30952. if (isBeforeAfterDetails) {
  30953. return true;
  30954. }
  30955. }
  30956. }
  30957. return navigate(forward, root, caretPos).fold(never, (pos) => {
  30958. const parentDetailsAtNewPos = getParentDetailsElementAtPos(dom, pos);
  30959. if (isInDetailsElement(dom, pos) && !equals(parentDetailsAtCaret, parentDetailsAtNewPos)) {
  30960. if (!forward) {
  30961. moveCaretToDetailsPos(editor, pos, false);
  30962. }
  30963. if (parentBlock && inEmptyParentBlock) {
  30964. if (forward && isFirstBlock) {
  30965. return true;
  30966. }
  30967. else if (!forward && isLastBlock) {
  30968. return true;
  30969. }
  30970. moveCaretToDetailsPos(editor, pos, forward);
  30971. editor.dom.remove(parentBlock);
  30972. }
  30973. return true;
  30974. }
  30975. else {
  30976. return false;
  30977. }
  30978. });
  30979. }
  30980. else {
  30981. return false;
  30982. }
  30983. };
  30984. const shouldPreventDeleteSummaryAction = (editor, detailElements, forward, granularity) => {
  30985. const selection = editor.selection;
  30986. const rng = selection.getRng();
  30987. const caretPos = CaretPosition.fromRangeStart(rng);
  30988. const root = editor.getBody();
  30989. if (granularity === 'selection') {
  30990. return isPartialDelete(rng, detailElements);
  30991. }
  30992. else if (forward) {
  30993. return isCaretAtEndOfSummary(caretPos, detailElements) || isCaretInLastPositionInBody(root, caretPos, detailElements);
  30994. }
  30995. else {
  30996. return isCaretAtStartOfSummary(caretPos, detailElements) || isCaretInFirstPositionInBody(caretPos, detailElements);
  30997. }
  30998. };
  30999. const shouldPreventDeleteAction = (editor, forward, granularity) => getDetailsElements(editor.dom, editor.selection.getRng()).fold(() => shouldPreventDeleteIntoDetails(editor, forward, granularity), (detailsElements) => shouldPreventDeleteSummaryAction(editor, detailsElements, forward, granularity) || shouldPreventDeleteIntoDetails(editor, forward, granularity));
  31000. const handleDeleteActionSafari = (editor, forward, granularity) => {
  31001. const selection = editor.selection;
  31002. const node = selection.getNode();
  31003. const rng = selection.getRng();
  31004. const caretPos = CaretPosition.fromRangeStart(rng);
  31005. if (isSummary$1(node)) {
  31006. // TINY-9951: Safari bug, deleting within the summary causes all content to be removed and no caret position to be left
  31007. // https://bugs.webkit.org/show_bug.cgi?id=257745
  31008. if (granularity === 'selection' && isEntireNodeSelected(rng, node) || willDeleteLastPositionInElement(forward, caretPos, node)) {
  31009. emptyNodeContents(node);
  31010. }
  31011. else {
  31012. editor.undoManager.transact(() => {
  31013. // Wrap all summary children in a temporary container to execute Backspace/Delete there, then unwrap
  31014. const sel = selection.getSel();
  31015. let { anchorNode, anchorOffset, focusNode, focusOffset } = sel !== null && sel !== void 0 ? sel : {};
  31016. const applySelection = () => {
  31017. if (isNonNullable(anchorNode) && isNonNullable(anchorOffset) && isNonNullable(focusNode) && isNonNullable(focusOffset)) {
  31018. sel === null || sel === void 0 ? void 0 : sel.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
  31019. }
  31020. };
  31021. const updateSelection = () => {
  31022. anchorNode = sel === null || sel === void 0 ? void 0 : sel.anchorNode;
  31023. anchorOffset = sel === null || sel === void 0 ? void 0 : sel.anchorOffset;
  31024. focusNode = sel === null || sel === void 0 ? void 0 : sel.focusNode;
  31025. focusOffset = sel === null || sel === void 0 ? void 0 : sel.focusOffset;
  31026. };
  31027. const appendAllChildNodes = (from, to) => {
  31028. each$e(from.childNodes, (child) => {
  31029. if (isNode(child)) {
  31030. to.appendChild(child);
  31031. }
  31032. });
  31033. };
  31034. const container = editor.dom.create('span', { 'data-mce-bogus': '1' });
  31035. appendAllChildNodes(node, container);
  31036. node.appendChild(container);
  31037. applySelection();
  31038. // Manually perform deletion with modified granularities
  31039. if (granularity === 'word' || granularity === 'line') {
  31040. sel === null || sel === void 0 ? void 0 : sel.modify('extend', forward ? 'right' : 'left', granularity);
  31041. }
  31042. if (!selection.isCollapsed() && isEntireNodeSelected(selection.getRng(), container)) {
  31043. emptyNodeContents(node);
  31044. }
  31045. else {
  31046. editor.execCommand(forward ? 'ForwardDelete' : 'Delete');
  31047. updateSelection();
  31048. appendAllChildNodes(container, node);
  31049. applySelection();
  31050. }
  31051. editor.dom.remove(container);
  31052. });
  31053. }
  31054. return true;
  31055. }
  31056. else {
  31057. return false;
  31058. }
  31059. };
  31060. const backspaceDelete = (editor, forward, granularity) => shouldPreventDeleteAction(editor, forward, granularity) || isSafari && handleDeleteActionSafari(editor, forward, granularity)
  31061. ? Optional.some(noop) : Optional.none();
  31062. const createAndFireInputEvent = (eventType) => (editor, inputType, specifics = {}) => {
  31063. const target = editor.getBody();
  31064. const overrides = {
  31065. bubbles: true,
  31066. composed: true,
  31067. data: null,
  31068. isComposing: false,
  31069. detail: 0,
  31070. view: null,
  31071. target,
  31072. currentTarget: target,
  31073. eventPhase: Event.AT_TARGET,
  31074. originalTarget: target,
  31075. explicitOriginalTarget: target,
  31076. isTrusted: false,
  31077. srcElement: target,
  31078. cancelable: false,
  31079. preventDefault: noop,
  31080. inputType
  31081. };
  31082. const input = clone$2(new InputEvent(eventType));
  31083. return editor.dispatch(eventType, { ...input, ...overrides, ...specifics });
  31084. };
  31085. const fireInputEvent = createAndFireInputEvent('input');
  31086. const fireBeforeInputEvent = createAndFireInputEvent('beforeinput');
  31087. const platform$2 = detect$1();
  31088. const os = platform$2.os;
  31089. const isMacOSOriOS = os.isMacOS() || os.isiOS();
  31090. const browser = platform$2.browser;
  31091. const isFirefox = browser.isFirefox();
  31092. const executeKeydownOverride$3 = (editor, caret, evt) => {
  31093. const inputType = evt.keyCode === VK.BACKSPACE ? 'deleteContentBackward' : 'deleteContentForward';
  31094. const isCollapsed = editor.selection.isCollapsed();
  31095. const unmodifiedGranularity = isCollapsed ? 'character' : 'selection';
  31096. const getModifiedGranularity = (isWord) => {
  31097. if (isCollapsed) {
  31098. return isWord ? 'word' : 'line';
  31099. }
  31100. else {
  31101. return 'selection';
  31102. }
  31103. };
  31104. executeWithDelayedAction([
  31105. { keyCode: VK.BACKSPACE, action: action(backspaceDelete$2, editor) },
  31106. { keyCode: VK.BACKSPACE, action: action(backspaceDelete$8, editor, false) },
  31107. { keyCode: VK.DELETE, action: action(backspaceDelete$8, editor, true) },
  31108. { keyCode: VK.BACKSPACE, action: action(backspaceDelete$9, editor, false) },
  31109. { keyCode: VK.DELETE, action: action(backspaceDelete$9, editor, true) },
  31110. { keyCode: VK.BACKSPACE, action: action(backspaceDelete$5, editor, caret, false) },
  31111. { keyCode: VK.DELETE, action: action(backspaceDelete$5, editor, caret, true) },
  31112. { keyCode: VK.BACKSPACE, action: action(backspaceDelete$d, editor, false) },
  31113. { keyCode: VK.DELETE, action: action(backspaceDelete$d, editor, true) },
  31114. { keyCode: VK.BACKSPACE, action: action(backspaceDelete, editor, false, unmodifiedGranularity) },
  31115. { keyCode: VK.DELETE, action: action(backspaceDelete, editor, true, unmodifiedGranularity) },
  31116. ...isMacOSOriOS ? [
  31117. { keyCode: VK.BACKSPACE, altKey: true, action: action(backspaceDelete, editor, false, getModifiedGranularity(true)) },
  31118. { keyCode: VK.DELETE, altKey: true, action: action(backspaceDelete, editor, true, getModifiedGranularity(true)) },
  31119. { keyCode: VK.BACKSPACE, metaKey: true, action: action(backspaceDelete, editor, false, getModifiedGranularity(false)) },
  31120. ] : [
  31121. { keyCode: VK.BACKSPACE, ctrlKey: true, action: action(backspaceDelete, editor, false, getModifiedGranularity(true)) },
  31122. { keyCode: VK.DELETE, ctrlKey: true, action: action(backspaceDelete, editor, true, getModifiedGranularity(true)) }
  31123. ],
  31124. { keyCode: VK.BACKSPACE, action: action(backspaceDelete$6, editor, false) },
  31125. { keyCode: VK.DELETE, action: action(backspaceDelete$6, editor, true) },
  31126. { keyCode: VK.BACKSPACE, action: action(backspaceDelete$3, editor, false) },
  31127. { keyCode: VK.DELETE, action: action(backspaceDelete$3, editor, true) },
  31128. { keyCode: VK.BACKSPACE, action: action(backspaceDelete$a, editor, false) },
  31129. { keyCode: VK.DELETE, action: action(backspaceDelete$a, editor, true) },
  31130. { keyCode: VK.BACKSPACE, action: action(backspaceDelete$b, editor, false) },
  31131. { keyCode: VK.DELETE, action: action(backspaceDelete$b, editor, true) },
  31132. { keyCode: VK.BACKSPACE, action: action(backspaceDelete$4, editor, false) },
  31133. { keyCode: VK.DELETE, action: action(backspaceDelete$4, editor, true) },
  31134. { keyCode: VK.BACKSPACE, action: action(backspaceDelete$7, editor, false) },
  31135. { keyCode: VK.DELETE, action: action(backspaceDelete$7, editor, true) },
  31136. { keyCode: VK.BACKSPACE, action: action(backspaceDelete$1, editor, false) },
  31137. { keyCode: VK.DELETE, action: action(backspaceDelete$1, editor, true) },
  31138. ], evt)
  31139. .filter((_) => editor.selection.isEditable())
  31140. .each((applyAction) => {
  31141. evt.preventDefault();
  31142. const beforeInput = fireBeforeInputEvent(editor, inputType);
  31143. if (!beforeInput.isDefaultPrevented()) {
  31144. applyAction();
  31145. fireInputEvent(editor, inputType);
  31146. }
  31147. });
  31148. };
  31149. const executeKeyupOverride = (editor, evt, isBackspaceKeydown) => execute([
  31150. { keyCode: VK.BACKSPACE, action: action(paddEmptyElement, editor) },
  31151. { keyCode: VK.DELETE, action: action(paddEmptyElement, editor) },
  31152. ...isMacOSOriOS ? [
  31153. { keyCode: VK.BACKSPACE, altKey: true, action: action(refreshCaret, editor) },
  31154. { keyCode: VK.DELETE, altKey: true, action: action(refreshCaret, editor) },
  31155. // macOS surpresses keyup events for most keys including Backspace when Meta key is engaged
  31156. // To emulate Meta + Backspace on macOS, add a pattern for the meta key when backspace was
  31157. // detected on keydown
  31158. ...isBackspaceKeydown ? [{
  31159. // Firefox detects macOS Command key code as "Command" not "Meta"
  31160. keyCode: isFirefox ? 224 : 91,
  31161. action: action(refreshCaret, editor)
  31162. }] : []
  31163. ] : [
  31164. { keyCode: VK.BACKSPACE, ctrlKey: true, action: action(refreshCaret, editor) },
  31165. { keyCode: VK.DELETE, ctrlKey: true, action: action(refreshCaret, editor) }
  31166. ]
  31167. ], evt);
  31168. const setup$o = (editor, caret) => {
  31169. // track backspace keydown state for emulating Meta + Backspace keyup detection on macOS
  31170. let isBackspaceKeydown = false;
  31171. editor.on('keydown', (evt) => {
  31172. isBackspaceKeydown = evt.keyCode === VK.BACKSPACE;
  31173. if (!evt.isDefaultPrevented()) {
  31174. executeKeydownOverride$3(editor, caret, evt);
  31175. }
  31176. });
  31177. editor.on('keyup', (evt) => {
  31178. if (!evt.isDefaultPrevented()) {
  31179. executeKeyupOverride(editor, evt, isBackspaceKeydown);
  31180. }
  31181. isBackspaceKeydown = false;
  31182. });
  31183. };
  31184. const reduceFontStyleNesting = (block, node) => {
  31185. const blockSugar = SugarElement.fromDom(block);
  31186. const nodeSugar = SugarElement.fromDom(node);
  31187. const isSpan = isTag('span');
  31188. const isEndBlock = curry(eq, blockSugar);
  31189. const hasFontSize = (element) => isElement$8(element) && getRaw$1(element, 'font-size').isSome();
  31190. const elementsWithFontSize = [
  31191. ...(hasFontSize(nodeSugar) ? [nodeSugar] : []),
  31192. ...ancestors$1(nodeSugar, hasFontSize, isEndBlock)
  31193. ];
  31194. each$e(elementsWithFontSize.slice(1), (element) => {
  31195. remove$7(element, 'font-size');
  31196. remove$9(element, 'data-mce-style');
  31197. if (isSpan(element) && hasNone(element)) {
  31198. unwrap(element);
  31199. }
  31200. });
  31201. };
  31202. const firstNonWhiteSpaceNodeSibling = (node) => {
  31203. while (node) {
  31204. if (isElement$7(node) || (isText$b(node) && node.data && /[\r\n\s]/.test(node.data))) {
  31205. return Optional.from(SugarElement.fromDom(node));
  31206. }
  31207. node = node.nextSibling;
  31208. }
  31209. return Optional.none();
  31210. };
  31211. const moveToCaretPosition = (editor, root) => {
  31212. const dom = editor.dom;
  31213. const moveCaretBeforeOnEnterElementsMap = editor.schema.getMoveCaretBeforeOnEnterElements();
  31214. if (!root) {
  31215. return;
  31216. }
  31217. if (/^(LI|DT|DD)$/.test(root.nodeName)) {
  31218. const isList = (e) => /^(ul|ol|dl)$/.test(name(e));
  31219. const findFirstList = (e) => isList(e) ? Optional.from(e) : descendant$2(e, isList);
  31220. const isEmpty = (e) => dom.isEmpty(e.dom);
  31221. firstNonWhiteSpaceNodeSibling(root.firstChild).each((firstChild) => {
  31222. findFirstList(firstChild).fold(() => {
  31223. if (isEmpty(firstChild)) {
  31224. const element = toLeaf$1(firstChild, 0).element;
  31225. if (!isBr$6(element)) {
  31226. append$1(element, SugarElement.fromText(nbsp));
  31227. }
  31228. }
  31229. }, (firstList) => {
  31230. before$4(firstList, SugarElement.fromText(nbsp));
  31231. });
  31232. });
  31233. }
  31234. const rng = dom.createRng();
  31235. root.normalize();
  31236. if (root.hasChildNodes()) {
  31237. const walker = new DomTreeWalker(root, root);
  31238. let lastNode = root;
  31239. let node;
  31240. while ((node = walker.current())) {
  31241. if (isText$b(node)) {
  31242. rng.setStart(node, 0);
  31243. rng.setEnd(node, 0);
  31244. break;
  31245. }
  31246. if (moveCaretBeforeOnEnterElementsMap[node.nodeName.toLowerCase()]) {
  31247. rng.setStartBefore(node);
  31248. rng.setEndBefore(node);
  31249. break;
  31250. }
  31251. lastNode = node;
  31252. node = walker.next();
  31253. }
  31254. if (!node) {
  31255. rng.setStart(lastNode, 0);
  31256. rng.setEnd(lastNode, 0);
  31257. }
  31258. }
  31259. else {
  31260. if (isBr$7(root)) {
  31261. if (root.nextSibling && dom.isBlock(root.nextSibling)) {
  31262. rng.setStartBefore(root);
  31263. rng.setEndBefore(root);
  31264. }
  31265. else {
  31266. rng.setStartAfter(root);
  31267. rng.setEndAfter(root);
  31268. }
  31269. }
  31270. else {
  31271. rng.setStart(root, 0);
  31272. rng.setEnd(root, 0);
  31273. }
  31274. }
  31275. editor.selection.setRng(rng);
  31276. scrollRangeIntoView(editor, rng);
  31277. };
  31278. const getEditableRoot = (dom, node) => {
  31279. const root = dom.getRoot();
  31280. let editableRoot;
  31281. // Get all parents until we hit a non editable parent or the root
  31282. let parent = node;
  31283. while (parent !== root && parent && dom.getContentEditable(parent) !== 'false') {
  31284. if (dom.getContentEditable(parent) === 'true') {
  31285. editableRoot = parent;
  31286. break;
  31287. }
  31288. parent = parent.parentNode;
  31289. }
  31290. return parent !== root ? editableRoot : root;
  31291. };
  31292. const getParentBlock$1 = (editor) => {
  31293. return Optional.from(editor.dom.getParent(editor.selection.getStart(true), editor.dom.isBlock));
  31294. };
  31295. const getParentBlockName = (editor) => {
  31296. return getParentBlock$1(editor).fold(constant(''), (parentBlock) => {
  31297. return parentBlock.nodeName.toUpperCase();
  31298. });
  31299. };
  31300. const isListItemParentBlock = (editor) => {
  31301. return getParentBlock$1(editor).filter((elm) => {
  31302. return isListItem$2(SugarElement.fromDom(elm));
  31303. }).isSome();
  31304. };
  31305. const emptyBlock = (elm) => {
  31306. elm.innerHTML = '<br data-mce-bogus="1">';
  31307. };
  31308. const applyAttributes = (editor, node, forcedRootBlockAttrs) => {
  31309. const dom = editor.dom;
  31310. // Merge and apply style attribute
  31311. Optional.from(forcedRootBlockAttrs.style)
  31312. .map(dom.parseStyle)
  31313. .each((attrStyles) => {
  31314. const currentStyles = getAllRaw(SugarElement.fromDom(node));
  31315. const newStyles = { ...currentStyles, ...attrStyles };
  31316. dom.setStyles(node, newStyles);
  31317. });
  31318. // Merge and apply class attribute
  31319. const attrClassesOpt = Optional.from(forcedRootBlockAttrs.class).map((attrClasses) => attrClasses.split(/\s+/));
  31320. const currentClassesOpt = Optional.from(node.className).map((currentClasses) => filter$5(currentClasses.split(/\s+/), (clazz) => clazz !== ''));
  31321. lift2(attrClassesOpt, currentClassesOpt, (attrClasses, currentClasses) => {
  31322. const filteredClasses = filter$5(currentClasses, (clazz) => !contains$2(attrClasses, clazz));
  31323. const newClasses = [...attrClasses, ...filteredClasses];
  31324. dom.setAttrib(node, 'class', newClasses.join(' '));
  31325. });
  31326. // Apply any remaining forced root block attributes
  31327. const appliedAttrs = ['style', 'class'];
  31328. const remainingAttrs = filter$4(forcedRootBlockAttrs, (_, attrs) => !contains$2(appliedAttrs, attrs));
  31329. dom.setAttribs(node, remainingAttrs);
  31330. };
  31331. const setForcedBlockAttrs = (editor, node) => {
  31332. const forcedRootBlockName = getForcedRootBlock(editor);
  31333. if (forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) {
  31334. const forcedRootBlockAttrs = getForcedRootBlockAttrs(editor);
  31335. applyAttributes(editor, node, forcedRootBlockAttrs);
  31336. }
  31337. };
  31338. // Creates a new block element by cloning the current one or creating a new one if the name is specified
  31339. // This function will also copy any text formatting from the parent block and add it to the new one
  31340. const createNewBlock = (editor, container, parentBlock, editableRoot, keepStyles = true, name, styles) => {
  31341. const dom = editor.dom;
  31342. const schema = editor.schema;
  31343. const newBlockName = getForcedRootBlock(editor);
  31344. const parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
  31345. let node = container;
  31346. const textInlineElements = schema.getTextInlineElements();
  31347. let block;
  31348. if (name || parentBlockName === 'TABLE' || parentBlockName === 'HR') {
  31349. block = dom.create(name || newBlockName, styles || {});
  31350. }
  31351. else {
  31352. block = parentBlock.cloneNode(false);
  31353. }
  31354. let caretNode = block;
  31355. if (!keepStyles) {
  31356. dom.setAttrib(block, 'style', null); // wipe out any styles that came over with the block
  31357. dom.setAttrib(block, 'class', null);
  31358. }
  31359. else {
  31360. // Clone any parent styles
  31361. do {
  31362. if (textInlineElements[node.nodeName]) {
  31363. // Ignore caret or bookmark nodes when cloning
  31364. if (isCaretNode(node) || isBookmarkNode$1(node)) {
  31365. continue;
  31366. }
  31367. const clonedNode = node.cloneNode(false);
  31368. dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique
  31369. if (block.hasChildNodes()) {
  31370. clonedNode.appendChild(block.firstChild);
  31371. block.appendChild(clonedNode);
  31372. }
  31373. else {
  31374. caretNode = clonedNode;
  31375. block.appendChild(clonedNode);
  31376. }
  31377. }
  31378. } while ((node = node.parentNode) && node !== editableRoot);
  31379. reduceFontStyleNesting(block, caretNode);
  31380. }
  31381. setForcedBlockAttrs(editor, block);
  31382. emptyBlock(caretNode);
  31383. return block;
  31384. };
  31385. const getDetailsRoot = (editor, element) => editor.dom.getParent(element, isDetails);
  31386. const isAtDetailsEdge = (root, element, isTextBlock) => {
  31387. let node = element;
  31388. while (node && node !== root && isNull(node.nextSibling)) {
  31389. const parent = node.parentElement;
  31390. if (!parent || !isTextBlock(parent)) {
  31391. return isDetails(parent);
  31392. }
  31393. node = parent;
  31394. }
  31395. return false;
  31396. };
  31397. const isLastEmptyBlockInDetails = (editor, shiftKey, element) => !shiftKey &&
  31398. element.nodeName.toLowerCase() === getForcedRootBlock(editor) &&
  31399. editor.dom.isEmpty(element) &&
  31400. isAtDetailsEdge(editor.getBody(), element, (el) => has$2(editor.schema.getTextBlockElements(), el.nodeName.toLowerCase()));
  31401. const insertNewLine = (editor, createNewBlock, parentBlock) => {
  31402. var _a, _b, _c;
  31403. const newBlock = createNewBlock(getForcedRootBlock(editor));
  31404. const root = getDetailsRoot(editor, parentBlock);
  31405. if (!root) {
  31406. return;
  31407. }
  31408. editor.dom.insertAfter(newBlock, root);
  31409. moveToCaretPosition(editor, newBlock);
  31410. // TODO: This now only works with our Accordions not details with multiple root level children should we support that
  31411. if (((_c = (_b = (_a = parentBlock.parentElement) === null || _a === void 0 ? void 0 : _a.childNodes) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) > 1) {
  31412. editor.dom.remove(parentBlock);
  31413. }
  31414. };
  31415. const hasFirstChild = (elm, name) => {
  31416. return elm.firstChild && elm.firstChild.nodeName === name;
  31417. };
  31418. const isFirstChild = (elm) => {
  31419. var _a;
  31420. return ((_a = elm.parentNode) === null || _a === void 0 ? void 0 : _a.firstChild) === elm;
  31421. };
  31422. const hasParent = (elm, parentName) => {
  31423. const parentNode = elm === null || elm === void 0 ? void 0 : elm.parentNode;
  31424. return isNonNullable(parentNode) && parentNode.nodeName === parentName;
  31425. };
  31426. const isListBlock = (elm) => {
  31427. return isNonNullable(elm) && /^(OL|UL|LI)$/.test(elm.nodeName);
  31428. };
  31429. const isListItem = (elm) => {
  31430. return isNonNullable(elm) && /^(LI|DT|DD)$/.test(elm.nodeName);
  31431. };
  31432. const isNestedList = (elm) => {
  31433. return isListBlock(elm) && isListBlock(elm.parentNode);
  31434. };
  31435. const getContainerBlock = (containerBlock) => {
  31436. const containerBlockParent = containerBlock.parentNode;
  31437. return isListItem(containerBlockParent) ? containerBlockParent : containerBlock;
  31438. };
  31439. const isFirstOrLastLi = (containerBlock, parentBlock, first) => {
  31440. let node = containerBlock[first ? 'firstChild' : 'lastChild'];
  31441. // Find first/last element since there might be whitespace there
  31442. while (node) {
  31443. if (isElement$7(node)) {
  31444. break;
  31445. }
  31446. node = node[first ? 'nextSibling' : 'previousSibling'];
  31447. }
  31448. return node === parentBlock;
  31449. };
  31450. const getStyles = (elm) => foldl(mapToArray(getAllRaw(SugarElement.fromDom(elm)), (style, styleName) => `${styleName}: ${style};`), (acc, s) => acc + s, '');
  31451. // Inserts a block or br before/after or in the middle of a split list of the LI is empty
  31452. const insert$4 = (editor, createNewBlock, containerBlock, parentBlock, newBlockName) => {
  31453. const dom = editor.dom;
  31454. const rng = editor.selection.getRng();
  31455. const containerParent = containerBlock.parentNode;
  31456. if (containerBlock === editor.getBody() || !containerParent) {
  31457. return;
  31458. }
  31459. if (isNestedList(containerBlock)) {
  31460. newBlockName = 'LI';
  31461. }
  31462. const parentBlockStyles = isListItem(parentBlock) ? getStyles(parentBlock) : undefined;
  31463. let newBlock = isListItem(parentBlock) && parentBlockStyles
  31464. ? createNewBlock(newBlockName, { style: getStyles(parentBlock) })
  31465. : createNewBlock(newBlockName);
  31466. if (isFirstOrLastLi(containerBlock, parentBlock, true) && isFirstOrLastLi(containerBlock, parentBlock, false)) {
  31467. if (hasParent(containerBlock, 'LI')) {
  31468. // Nested list is inside a LI
  31469. const containerBlockParent = getContainerBlock(containerBlock);
  31470. dom.insertAfter(newBlock, containerBlockParent);
  31471. if (isFirstChild(containerBlock)) {
  31472. dom.remove(containerBlockParent);
  31473. }
  31474. else {
  31475. dom.remove(containerBlock);
  31476. }
  31477. }
  31478. else {
  31479. // Is first and last list item then replace the OL/UL with a text block
  31480. dom.replace(newBlock, containerBlock);
  31481. }
  31482. }
  31483. else if (isFirstOrLastLi(containerBlock, parentBlock, true)) {
  31484. if (hasParent(containerBlock, 'LI')) {
  31485. // List nested in an LI then move the list to a new sibling LI
  31486. dom.insertAfter(newBlock, getContainerBlock(containerBlock));
  31487. newBlock.appendChild(dom.doc.createTextNode(' ')); // Needed for IE so the caret can be placed
  31488. newBlock.appendChild(containerBlock);
  31489. }
  31490. else {
  31491. // First LI in list then remove LI and add text block before list
  31492. containerParent.insertBefore(newBlock, containerBlock);
  31493. }
  31494. dom.remove(parentBlock);
  31495. }
  31496. else if (isFirstOrLastLi(containerBlock, parentBlock, false)) {
  31497. // Last LI in list then remove LI and add text block after list
  31498. dom.insertAfter(newBlock, getContainerBlock(containerBlock));
  31499. dom.remove(parentBlock);
  31500. }
  31501. else {
  31502. // Middle LI in list then split the list and insert a text block in the middle
  31503. // Extract after fragment and insert it after the current block
  31504. containerBlock = getContainerBlock(containerBlock);
  31505. const tmpRng = rng.cloneRange();
  31506. tmpRng.setStartAfter(parentBlock);
  31507. tmpRng.setEndAfter(containerBlock);
  31508. const fragment = tmpRng.extractContents();
  31509. if (newBlockName === 'LI' && hasFirstChild(fragment, 'LI')) {
  31510. const previousChildren = filter$5(map$3(newBlock.children, SugarElement.fromDom), not(isTag('br')));
  31511. newBlock = fragment.firstChild;
  31512. dom.insertAfter(fragment, containerBlock);
  31513. each$e(previousChildren, (child) => prepend(SugarElement.fromDom(newBlock), child));
  31514. if (parentBlockStyles) {
  31515. newBlock.setAttribute('style', parentBlockStyles);
  31516. }
  31517. }
  31518. else {
  31519. dom.insertAfter(fragment, containerBlock);
  31520. dom.insertAfter(newBlock, containerBlock);
  31521. }
  31522. dom.remove(parentBlock);
  31523. }
  31524. moveToCaretPosition(editor, newBlock);
  31525. };
  31526. const trimZwsp = (fragment) => {
  31527. each$e(descendants$1(SugarElement.fromDom(fragment), isText$c), (text) => {
  31528. const rawNode = text.dom;
  31529. rawNode.nodeValue = trim$2(rawNode.data);
  31530. });
  31531. };
  31532. const isWithinNonEditableList = (editor, node) => {
  31533. const parentList = editor.dom.getParent(node, 'ol,ul,dl');
  31534. return parentList !== null && editor.dom.getContentEditableParent(parentList) === 'false';
  31535. };
  31536. const isEmptyAnchor = (dom, elm) => {
  31537. return elm && elm.nodeName === 'A' && dom.isEmpty(elm);
  31538. };
  31539. const containerAndPreviousSiblingName = (container, nodeName) => {
  31540. return container.nodeName === nodeName || (container.previousSibling && container.previousSibling.nodeName === nodeName);
  31541. };
  31542. const containerAndNextSiblingName = (container, nodeName) => {
  31543. return container.nodeName === nodeName || (container.nextSibling && container.nextSibling.nodeName === nodeName);
  31544. };
  31545. // Returns true if the block can be split into two blocks or not
  31546. const canSplitBlock = (dom, node) => {
  31547. return isNonNullable(node) &&
  31548. dom.isBlock(node) &&
  31549. !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) &&
  31550. !/^(fixed|absolute)/i.test(node.style.position) &&
  31551. dom.isEditable(node.parentNode) && dom.getContentEditable(node) !== 'false';
  31552. };
  31553. // Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p>
  31554. const trimInlineElementsOnLeftSideOfBlock = (dom, nonEmptyElementsMap, block) => {
  31555. var _a;
  31556. const firstChilds = [];
  31557. if (!block) {
  31558. return;
  31559. }
  31560. // Find inner most first child ex: <p><i><b>*</b></i></p>
  31561. let currentNode = block;
  31562. while ((currentNode = currentNode.firstChild)) {
  31563. if (dom.isBlock(currentNode)) {
  31564. return;
  31565. }
  31566. if (isElement$7(currentNode) && !nonEmptyElementsMap[currentNode.nodeName.toLowerCase()]) {
  31567. firstChilds.push(currentNode);
  31568. }
  31569. }
  31570. let i = firstChilds.length;
  31571. while (i--) {
  31572. currentNode = firstChilds[i];
  31573. if (!currentNode.hasChildNodes() || (currentNode.firstChild === currentNode.lastChild && ((_a = currentNode.firstChild) === null || _a === void 0 ? void 0 : _a.nodeValue) === '')) {
  31574. dom.remove(currentNode);
  31575. }
  31576. else {
  31577. if (isEmptyAnchor(dom, currentNode)) {
  31578. dom.remove(currentNode);
  31579. }
  31580. }
  31581. }
  31582. };
  31583. const normalizeZwspOffset = (start, container, offset) => {
  31584. if (!isText$b(container)) {
  31585. return offset;
  31586. }
  31587. else if (start) {
  31588. return offset === 1 && container.data.charAt(offset - 1) === ZWSP$1 ? 0 : offset;
  31589. }
  31590. else {
  31591. return offset === container.data.length - 1 && container.data.charAt(offset) === ZWSP$1 ? container.data.length : offset;
  31592. }
  31593. };
  31594. const includeZwspInRange = (rng) => {
  31595. const newRng = rng.cloneRange();
  31596. newRng.setStart(rng.startContainer, normalizeZwspOffset(true, rng.startContainer, rng.startOffset));
  31597. newRng.setEnd(rng.endContainer, normalizeZwspOffset(false, rng.endContainer, rng.endOffset));
  31598. return newRng;
  31599. };
  31600. // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element
  31601. const trimLeadingLineBreaks = (node) => {
  31602. let currentNode = node;
  31603. do {
  31604. if (isText$b(currentNode)) {
  31605. currentNode.data = currentNode.data.replace(/^[\r\n]+/, '');
  31606. }
  31607. currentNode = currentNode.firstChild;
  31608. } while (currentNode);
  31609. };
  31610. // Wraps any text nodes or inline elements in the specified forced root block name
  31611. const wrapSelfAndSiblingsInDefaultBlock = (editor, newBlockName, rng, container, offset) => {
  31612. var _a, _b;
  31613. const dom = editor.dom;
  31614. const editableRoot = (_a = getEditableRoot(dom, container)) !== null && _a !== void 0 ? _a : dom.getRoot();
  31615. // Not in a block element or in a table cell or caption
  31616. let parentBlock = dom.getParent(container, dom.isBlock);
  31617. if (!parentBlock || !canSplitBlock(dom, parentBlock)) {
  31618. parentBlock = parentBlock || editableRoot;
  31619. if (!parentBlock.hasChildNodes()) {
  31620. const newBlock = dom.create(newBlockName);
  31621. setForcedBlockAttrs(editor, newBlock);
  31622. parentBlock.appendChild(newBlock);
  31623. rng.setStart(newBlock, 0);
  31624. rng.setEnd(newBlock, 0);
  31625. return newBlock;
  31626. }
  31627. // Find parent that is the first child of parentBlock
  31628. let node = container;
  31629. while (node && node.parentNode !== parentBlock) {
  31630. node = node.parentNode;
  31631. }
  31632. // Loop left to find start node start wrapping at
  31633. let startNode;
  31634. while (node && !dom.isBlock(node)) {
  31635. startNode = node;
  31636. node = node.previousSibling;
  31637. }
  31638. const startNodeName = (_b = startNode === null || startNode === void 0 ? void 0 : startNode.parentElement) === null || _b === void 0 ? void 0 : _b.nodeName;
  31639. if (startNode && startNodeName && editor.schema.isValidChild(startNodeName, newBlockName.toLowerCase())) {
  31640. // This should never be null since we check it above
  31641. const startNodeParent = startNode.parentNode;
  31642. const newBlock = dom.create(newBlockName);
  31643. setForcedBlockAttrs(editor, newBlock);
  31644. startNodeParent.insertBefore(newBlock, startNode);
  31645. // Start wrapping until we hit a block
  31646. node = startNode;
  31647. while (node && !dom.isBlock(node)) {
  31648. const next = node.nextSibling;
  31649. newBlock.appendChild(node);
  31650. node = next;
  31651. }
  31652. // Restore range to it's past location
  31653. rng.setStart(container, offset);
  31654. rng.setEnd(container, offset);
  31655. }
  31656. }
  31657. return container;
  31658. };
  31659. // Adds a BR at the end of blocks that only contains an IMG or INPUT since
  31660. // these might be floated and then they won't expand the block
  31661. const addBrToBlockIfNeeded = (dom, block) => {
  31662. // IE will render the blocks correctly other browsers needs a BR
  31663. block.normalize(); // Remove empty text nodes that got left behind by the extract
  31664. // Check if the block is empty or contains a floated last child
  31665. const lastChild = block.lastChild;
  31666. if (!lastChild || isElement$7(lastChild) && (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) {
  31667. dom.add(block, 'br');
  31668. }
  31669. };
  31670. const shouldEndContainer = (editor, container) => {
  31671. const optionValue = shouldEndContainerOnEmptyBlock(editor);
  31672. if (isNullable(container)) {
  31673. return false;
  31674. }
  31675. else if (isString(optionValue)) {
  31676. return contains$2(Tools.explode(optionValue), container.nodeName.toLowerCase());
  31677. }
  31678. else {
  31679. return optionValue;
  31680. }
  31681. };
  31682. const insert$3 = (editor, evt) => {
  31683. let container;
  31684. let offset;
  31685. let parentBlockName;
  31686. let containerBlock;
  31687. let isAfterLastNodeInContainer = false;
  31688. const dom = editor.dom;
  31689. const schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements();
  31690. const rng = editor.selection.getRng();
  31691. const newBlockName = getForcedRootBlock(editor);
  31692. const start = SugarElement.fromDom(rng.startContainer);
  31693. const child = child$1(start, rng.startOffset);
  31694. const isCef = child.exists((element) => isHTMLElement$1(element) && !isEditable$2(element));
  31695. const collapsedAndCef = rng.collapsed && isCef;
  31696. const createNewBlock$1 = (name, styles) => {
  31697. return createNewBlock(editor, container, parentBlock, editableRoot, shouldKeepStyles(editor), name, styles);
  31698. };
  31699. // Returns true/false if the caret is at the start/end of the parent block element
  31700. const isCaretAtStartOrEndOfBlock = (start) => {
  31701. const normalizedOffset = normalizeZwspOffset(start, container, offset);
  31702. // Caret is in the middle of a text node like "a|b"
  31703. if (isText$b(container) && (start ? normalizedOffset > 0 : normalizedOffset < container.data.length)) {
  31704. return false;
  31705. }
  31706. // If after the last element in block node edge case for #5091
  31707. if ((container.parentNode === parentBlock || container === parentBlock) && isAfterLastNodeInContainer && !start) {
  31708. return true;
  31709. }
  31710. // If the caret if before the first element in parentBlock
  31711. if (start && isElement$7(container) && container === parentBlock.firstChild) {
  31712. return true;
  31713. }
  31714. // Caret can be before/after a table or a hr
  31715. if (containerAndPreviousSiblingName(container, 'TABLE') || containerAndPreviousSiblingName(container, 'HR')) {
  31716. if (containerAndNextSiblingName(container, 'BR')) {
  31717. return !start;
  31718. }
  31719. return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start);
  31720. }
  31721. // Walk the DOM and look for text nodes or non empty elements
  31722. const walker = new DomTreeWalker(container, parentBlock);
  31723. // If caret is in beginning or end of a text block then jump to the next/previous node
  31724. if (isText$b(container)) {
  31725. if (start && normalizedOffset === 0) {
  31726. walker.prev();
  31727. }
  31728. else if (!start && normalizedOffset === container.data.length) {
  31729. walker.next();
  31730. }
  31731. }
  31732. let node;
  31733. while ((node = walker.current())) {
  31734. if (isElement$7(node)) {
  31735. // Ignore bogus elements
  31736. if (!node.getAttribute('data-mce-bogus')) {
  31737. // Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p>
  31738. const name = node.nodeName.toLowerCase();
  31739. if (nonEmptyElementsMap[name] && name !== 'br') {
  31740. return false;
  31741. }
  31742. }
  31743. }
  31744. else if (isText$b(node) && !isWhitespaceText(node.data)) {
  31745. return false;
  31746. }
  31747. if (start) {
  31748. walker.prev();
  31749. }
  31750. else {
  31751. walker.next();
  31752. }
  31753. }
  31754. return true;
  31755. };
  31756. const insertNewBlockAfter = () => {
  31757. let block;
  31758. // If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup
  31759. if (/^(H[1-6]|PRE|FIGURE)$/.test(parentBlockName) && containerBlockName !== 'HGROUP') {
  31760. block = createNewBlock$1(newBlockName);
  31761. }
  31762. else {
  31763. block = createNewBlock$1();
  31764. }
  31765. // Split the current container block element if enter is pressed inside an empty inner block element
  31766. if (shouldEndContainer(editor, containerBlock) && canSplitBlock(dom, containerBlock) && dom.isEmpty(parentBlock, undefined, { includeZwsp: true })) {
  31767. // Split container block for example a BLOCKQUOTE at the current blockParent location for example a P
  31768. block = dom.split(containerBlock, parentBlock);
  31769. }
  31770. else {
  31771. dom.insertAfter(block, parentBlock);
  31772. }
  31773. moveToCaretPosition(editor, block);
  31774. return block;
  31775. };
  31776. // Setup range items and newBlockName
  31777. normalize$2(dom, rng).each((normRng) => {
  31778. rng.setStart(normRng.startContainer, normRng.startOffset);
  31779. rng.setEnd(normRng.endContainer, normRng.endOffset);
  31780. });
  31781. container = rng.startContainer;
  31782. offset = rng.startOffset;
  31783. const shiftKey = !!(evt && evt.shiftKey);
  31784. const ctrlKey = !!(evt && evt.ctrlKey);
  31785. // Resolve node index
  31786. if (isElement$7(container) && container.hasChildNodes() && !collapsedAndCef) {
  31787. isAfterLastNodeInContainer = offset > container.childNodes.length - 1;
  31788. container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
  31789. if (isAfterLastNodeInContainer && isText$b(container)) {
  31790. offset = container.data.length;
  31791. }
  31792. else {
  31793. offset = 0;
  31794. }
  31795. }
  31796. // Get editable root node, normally the body element but sometimes a div or span
  31797. const editableRoot = getEditableRoot(dom, container);
  31798. // If there is no editable root then enter is done inside a contentEditable false element
  31799. if (!editableRoot || isWithinNonEditableList(editor, container)) {
  31800. return;
  31801. }
  31802. // Wrap the current node and it's sibling in a default block if it's needed.
  31803. // for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td>
  31804. // This won't happen if root blocks are disabled or the shiftKey is pressed
  31805. if (!shiftKey) {
  31806. container = wrapSelfAndSiblingsInDefaultBlock(editor, newBlockName, rng, container, offset);
  31807. }
  31808. // Find parent block and setup empty block paddings
  31809. let parentBlock = dom.getParent(container, dom.isBlock) || dom.getRoot();
  31810. containerBlock = isNonNullable(parentBlock === null || parentBlock === void 0 ? void 0 : parentBlock.parentNode) ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null;
  31811. // Setup block names
  31812. parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
  31813. const containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
  31814. // Enter inside block contained within a LI then split or insert before/after LI
  31815. if (containerBlockName === 'LI' && !ctrlKey) {
  31816. const liBlock = containerBlock;
  31817. parentBlock = liBlock;
  31818. containerBlock = liBlock.parentNode;
  31819. parentBlockName = containerBlockName;
  31820. }
  31821. if (isElement$7(containerBlock) && isLastEmptyBlockInDetails(editor, shiftKey, parentBlock)) {
  31822. return insertNewLine(editor, createNewBlock$1, parentBlock);
  31823. }
  31824. // Handle enter in list item
  31825. if (/^(LI|DT|DD)$/.test(parentBlockName) && isElement$7(containerBlock)) {
  31826. // Handle enter inside an empty list item
  31827. if (dom.isEmpty(parentBlock)) {
  31828. insert$4(editor, createNewBlock$1, containerBlock, parentBlock, newBlockName);
  31829. return;
  31830. }
  31831. }
  31832. // Never split the body or blocks that we can't split like noneditable host elements
  31833. if (!collapsedAndCef && (parentBlock === editor.getBody() || !canSplitBlock(dom, parentBlock))) {
  31834. return;
  31835. }
  31836. const parentBlockParent = parentBlock.parentNode;
  31837. // Insert new block before/after the parent block depending on caret location
  31838. let newBlock;
  31839. if (collapsedAndCef) {
  31840. newBlock = createNewBlock$1(newBlockName);
  31841. child.fold(() => {
  31842. append$1(start, SugarElement.fromDom(newBlock));
  31843. }, (child) => {
  31844. before$4(child, SugarElement.fromDom(newBlock));
  31845. });
  31846. editor.selection.setCursorLocation(newBlock, 0);
  31847. }
  31848. else if (isCaretContainerBlock$1(parentBlock)) {
  31849. // TODO: TINY-10384 NOTE: Added logic to make sure pressing enter is consistent between browsers.
  31850. // As an example a fake caret is used before/after tables on Firefox but not on Chrome. So different behaviour could occur
  31851. newBlock = showCaretContainerBlock(parentBlock);
  31852. if (dom.isEmpty(parentBlock)) {
  31853. emptyBlock(parentBlock);
  31854. }
  31855. setForcedBlockAttrs(editor, newBlock);
  31856. moveToCaretPosition(editor, newBlock);
  31857. }
  31858. else if (isCaretAtStartOrEndOfBlock(false)) {
  31859. // Caret is moved to the new block in the insertNewBlockAfter fn
  31860. newBlock = insertNewBlockAfter();
  31861. }
  31862. else if (isCaretAtStartOrEndOfBlock(true) && parentBlockParent) {
  31863. // Check where caret is positioned before it is potentially moved by 'insertBefore' fn
  31864. const caretPos = CaretPosition.fromRangeStart(rng);
  31865. const afterTable = isAfterTable(caretPos);
  31866. const parentBlockSugar = SugarElement.fromDom(parentBlock);
  31867. const afterBr = isAfterBr(parentBlockSugar, caretPos, editor.schema);
  31868. const prevBrOpt = afterBr
  31869. ? findPreviousBr(parentBlockSugar, caretPos, editor.schema).bind((pos) => Optional.from(pos.getNode()))
  31870. : Optional.none();
  31871. newBlock = parentBlockParent.insertBefore(createNewBlock$1(), parentBlock);
  31872. const root = containerAndPreviousSiblingName(parentBlock, 'HR') || afterTable ? newBlock : prevBrOpt.getOr(parentBlock);
  31873. moveToCaretPosition(editor, root);
  31874. }
  31875. else {
  31876. // Extract after fragment and insert it after the current block
  31877. const tmpRng = includeZwspInRange(rng).cloneRange();
  31878. tmpRng.setEndAfter(parentBlock);
  31879. const fragment = tmpRng.extractContents();
  31880. trimZwsp(fragment);
  31881. trimLeadingLineBreaks(fragment);
  31882. newBlock = fragment.firstChild;
  31883. if (parentBlock === newBlock) { // Can't add yourself to yourself. Additionally the newBlock is removed from the DOM earlier, so even if you could, it'd still not work.
  31884. if (isNonNullable(parentBlockParent)) {
  31885. dom.insertAfter(fragment, parentBlockParent);
  31886. }
  31887. }
  31888. else {
  31889. dom.insertAfter(fragment, parentBlock);
  31890. }
  31891. trimInlineElementsOnLeftSideOfBlock(dom, nonEmptyElementsMap, newBlock);
  31892. addBrToBlockIfNeeded(dom, parentBlock);
  31893. if (dom.isEmpty(parentBlock)) {
  31894. emptyBlock(parentBlock);
  31895. }
  31896. newBlock.normalize();
  31897. // New block might become empty if it's <p><b>a |</b></p>
  31898. if (dom.isEmpty(newBlock)) {
  31899. dom.remove(newBlock);
  31900. insertNewBlockAfter();
  31901. }
  31902. else {
  31903. setForcedBlockAttrs(editor, newBlock);
  31904. moveToCaretPosition(editor, newBlock);
  31905. }
  31906. }
  31907. dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique
  31908. // Allow custom handling of new blocks
  31909. editor.dispatch('NewBlock', { newBlock });
  31910. };
  31911. const fakeEventName$1 = 'insertParagraph';
  31912. const blockbreak = {
  31913. insert: insert$3,
  31914. fakeEventName: fakeEventName$1
  31915. };
  31916. // Walks the parent block to the right and look for BR elements
  31917. const hasRightSideContent = (schema, container, parentBlock) => {
  31918. const walker = new DomTreeWalker(container, parentBlock);
  31919. let node;
  31920. const nonEmptyElementsMap = schema.getNonEmptyElements();
  31921. while ((node = walker.next())) {
  31922. if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || isText$b(node) && node.length > 0) {
  31923. return true;
  31924. }
  31925. }
  31926. return false;
  31927. };
  31928. const moveSelectionToBr = (editor, brElm, extraBr) => {
  31929. const rng = editor.dom.createRng();
  31930. if (!extraBr) {
  31931. rng.setStartAfter(brElm);
  31932. rng.setEndAfter(brElm);
  31933. }
  31934. else {
  31935. rng.setStartBefore(brElm);
  31936. rng.setEndBefore(brElm);
  31937. }
  31938. editor.selection.setRng(rng);
  31939. scrollRangeIntoView(editor, rng);
  31940. };
  31941. const insertBrAtCaret = (editor, evt) => {
  31942. // We load the current event in from EnterKey.js when appropriate to heed
  31943. // certain event-specific variations such as ctrl-enter in a list
  31944. const selection = editor.selection;
  31945. const dom = editor.dom;
  31946. const rng = selection.getRng();
  31947. let brElm;
  31948. let extraBr = false;
  31949. normalize$2(dom, rng).each((normRng) => {
  31950. rng.setStart(normRng.startContainer, normRng.startOffset);
  31951. rng.setEnd(normRng.endContainer, normRng.endOffset);
  31952. });
  31953. let offset = rng.startOffset;
  31954. let container = rng.startContainer;
  31955. // Resolve node index
  31956. if (isElement$7(container) && container.hasChildNodes()) {
  31957. const isAfterLastNodeInContainer = offset > container.childNodes.length - 1;
  31958. container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
  31959. if (isAfterLastNodeInContainer && isText$b(container)) {
  31960. offset = container.data.length;
  31961. }
  31962. else {
  31963. offset = 0;
  31964. }
  31965. }
  31966. let parentBlock = dom.getParent(container, dom.isBlock);
  31967. const containerBlock = parentBlock && parentBlock.parentNode ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null;
  31968. const containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
  31969. // Enter inside block contained within a LI then split or insert before/after LI
  31970. const isControlKey = !!(evt && evt.ctrlKey);
  31971. if (containerBlockName === 'LI' && !isControlKey) {
  31972. parentBlock = containerBlock;
  31973. }
  31974. if (isText$b(container) && offset >= container.data.length) {
  31975. // Insert extra BR element at the end block elements
  31976. if (!hasRightSideContent(editor.schema, container, parentBlock || dom.getRoot())) {
  31977. brElm = dom.create('br');
  31978. rng.insertNode(brElm);
  31979. rng.setStartAfter(brElm);
  31980. rng.setEndAfter(brElm);
  31981. extraBr = true;
  31982. }
  31983. }
  31984. brElm = dom.create('br');
  31985. rangeInsertNode(dom, rng, brElm);
  31986. moveSelectionToBr(editor, brElm, extraBr);
  31987. editor.undoManager.add();
  31988. };
  31989. const insertBrBefore = (editor, inline) => {
  31990. const br = SugarElement.fromTag('br');
  31991. before$4(SugarElement.fromDom(inline), br);
  31992. editor.undoManager.add();
  31993. };
  31994. const insertBrAfter = (editor, inline) => {
  31995. if (!hasBrAfter(editor.getBody(), inline)) {
  31996. after$4(SugarElement.fromDom(inline), SugarElement.fromTag('br'));
  31997. }
  31998. const br = SugarElement.fromTag('br');
  31999. after$4(SugarElement.fromDom(inline), br);
  32000. moveSelectionToBr(editor, br.dom, false);
  32001. editor.undoManager.add();
  32002. };
  32003. const isBeforeBr = (pos) => {
  32004. return isBr$7(pos.getNode());
  32005. };
  32006. const hasBrAfter = (rootNode, startNode) => {
  32007. if (isBeforeBr(CaretPosition.after(startNode))) {
  32008. return true;
  32009. }
  32010. else {
  32011. return nextPosition(rootNode, CaretPosition.after(startNode)).map((pos) => {
  32012. return isBr$7(pos.getNode());
  32013. }).getOr(false);
  32014. }
  32015. };
  32016. const isAnchorLink = (elm) => {
  32017. return elm && elm.nodeName === 'A' && 'href' in elm;
  32018. };
  32019. const isInsideAnchor = (location) => {
  32020. return location.fold(never, isAnchorLink, isAnchorLink, never);
  32021. };
  32022. const readInlineAnchorLocation = (editor) => {
  32023. const isInlineTarget$1 = curry(isInlineTarget, editor);
  32024. const position = CaretPosition.fromRangeStart(editor.selection.getRng());
  32025. return readLocation(isInlineTarget$1, editor.getBody(), position).filter(isInsideAnchor);
  32026. };
  32027. const insertBrOutsideAnchor = (editor, location) => {
  32028. location.fold(noop, curry(insertBrBefore, editor), curry(insertBrAfter, editor), noop);
  32029. };
  32030. const insert$2 = (editor, evt) => {
  32031. const anchorLocation = readInlineAnchorLocation(editor);
  32032. if (anchorLocation.isSome()) {
  32033. anchorLocation.each(curry(insertBrOutsideAnchor, editor));
  32034. }
  32035. else {
  32036. insertBrAtCaret(editor, evt);
  32037. }
  32038. };
  32039. const fakeEventName = 'insertLineBreak';
  32040. const linebreak = {
  32041. insert: insert$2,
  32042. fakeEventName
  32043. };
  32044. const matchesSelector = (editor, selector) => {
  32045. return getParentBlock$1(editor).filter((parentBlock) => {
  32046. return selector.length > 0 && is$2(SugarElement.fromDom(parentBlock), selector);
  32047. }).isSome();
  32048. };
  32049. const shouldInsertBr = (editor) => {
  32050. return matchesSelector(editor, getBrNewLineSelector(editor));
  32051. };
  32052. const shouldBlockNewLine$1 = (editor) => {
  32053. return matchesSelector(editor, getNoNewLineSelector(editor));
  32054. };
  32055. const newLineAction = Adt.generate([
  32056. { br: [] },
  32057. { block: [] },
  32058. { none: [] }
  32059. ]);
  32060. const shouldBlockNewLine = (editor, _shiftKey) => {
  32061. return shouldBlockNewLine$1(editor);
  32062. };
  32063. const inListBlock = (requiredState) => {
  32064. return (editor, _shiftKey) => {
  32065. return isListItemParentBlock(editor) === requiredState;
  32066. };
  32067. };
  32068. const inBlock = (blockName, requiredState) => (editor, _shiftKey) => {
  32069. const state = getParentBlockName(editor) === blockName.toUpperCase();
  32070. return state === requiredState;
  32071. };
  32072. const inCefBlock = (editor) => {
  32073. const editableRoot = getEditableRoot(editor.dom, editor.selection.getStart());
  32074. return isNullable(editableRoot);
  32075. };
  32076. const inPreBlock = (requiredState) => inBlock('pre', requiredState);
  32077. const inSummaryBlock = () => inBlock('summary', true);
  32078. const shouldPutBrInPre = (requiredState) => {
  32079. return (editor, _shiftKey) => {
  32080. return shouldPutBrInPre$1(editor) === requiredState;
  32081. };
  32082. };
  32083. const inBrContext = (editor, _shiftKey) => {
  32084. return shouldInsertBr(editor);
  32085. };
  32086. const hasShiftKey = (_editor, shiftKey) => {
  32087. return shiftKey;
  32088. };
  32089. const canInsertIntoEditableRoot = (editor) => {
  32090. const forcedRootBlock = getForcedRootBlock(editor);
  32091. const rootEditable = getEditableRoot(editor.dom, editor.selection.getStart());
  32092. return isNonNullable(rootEditable) && editor.schema.isValidChild(rootEditable.nodeName, forcedRootBlock);
  32093. };
  32094. const isInRootWithEmptyOrCEF = (editor) => {
  32095. const rng = editor.selection.getRng();
  32096. const start = SugarElement.fromDom(rng.startContainer);
  32097. const child = child$1(start, rng.startOffset);
  32098. const isCefOpt = child.map((element) => isHTMLElement$1(element) && !isEditable$2(element));
  32099. return rng.collapsed && isCefOpt.getOr(true);
  32100. };
  32101. const match = (predicates, action) => {
  32102. return (editor, shiftKey) => {
  32103. const isMatch = foldl(predicates, (res, p) => {
  32104. return res && p(editor, shiftKey);
  32105. }, true);
  32106. return isMatch ? Optional.some(action) : Optional.none();
  32107. };
  32108. };
  32109. const getAction = (editor, evt) => {
  32110. return evaluateUntil([
  32111. match([shouldBlockNewLine], newLineAction.none()),
  32112. // If the pre block is cef, do not try to insert a new line (or delete contents)
  32113. match([inPreBlock(true), inCefBlock], newLineAction.none()),
  32114. match([inSummaryBlock()], newLineAction.br()),
  32115. match([inPreBlock(true), shouldPutBrInPre(false), hasShiftKey], newLineAction.br()),
  32116. match([inPreBlock(true), shouldPutBrInPre(false)], newLineAction.block()),
  32117. match([inPreBlock(true), shouldPutBrInPre(true), hasShiftKey], newLineAction.block()),
  32118. match([inPreBlock(true), shouldPutBrInPre(true)], newLineAction.br()),
  32119. // TODO: TINY-9127 investigate if the list handling (and pre) is correct here.
  32120. match([inListBlock(true), hasShiftKey], newLineAction.br()),
  32121. match([inListBlock(true)], newLineAction.block()),
  32122. match([inBrContext], newLineAction.br()),
  32123. match([hasShiftKey], newLineAction.br()),
  32124. match([canInsertIntoEditableRoot], newLineAction.block()),
  32125. match([isInRootWithEmptyOrCEF], newLineAction.block())
  32126. ], [editor, !!(evt && evt.shiftKey)]).getOr(newLineAction.none());
  32127. };
  32128. const insertBreak = (breakType, editor, evt) => {
  32129. if (editor.mode.isReadOnly()) {
  32130. return;
  32131. }
  32132. if (!editor.selection.isCollapsed()) {
  32133. // IMPORTANT: We want to use the editor execCommand here, so that our `delete` execCommand
  32134. // overrides will be considered.
  32135. execEditorDeleteCommand(editor);
  32136. }
  32137. if (isNonNullable(evt)) {
  32138. const event = fireBeforeInputEvent(editor, breakType.fakeEventName);
  32139. if (event.isDefaultPrevented()) {
  32140. return;
  32141. }
  32142. }
  32143. breakType.insert(editor, evt);
  32144. if (isNonNullable(evt)) {
  32145. fireInputEvent(editor, breakType.fakeEventName);
  32146. }
  32147. };
  32148. const insert$1 = (editor, evt) => {
  32149. if (editor.mode.isReadOnly()) {
  32150. return;
  32151. }
  32152. const br = () => insertBreak(linebreak, editor, evt);
  32153. const block = () => insertBreak(blockbreak, editor, evt);
  32154. const logicalAction = getAction(editor, evt);
  32155. switch (getNewlineBehavior(editor)) {
  32156. case 'linebreak':
  32157. logicalAction.fold(br, br, noop);
  32158. break;
  32159. case 'block':
  32160. logicalAction.fold(block, block, noop);
  32161. break;
  32162. case 'invert':
  32163. logicalAction.fold(block, br, noop);
  32164. break;
  32165. // implied by the options processor, unnecessary
  32166. // case 'default':
  32167. default:
  32168. logicalAction.fold(br, block, noop);
  32169. break;
  32170. }
  32171. };
  32172. const platform$1 = detect$1();
  32173. const isIOSSafari = platform$1.os.isiOS() && platform$1.browser.isSafari();
  32174. const handleEnterKeyEvent = (editor, event) => {
  32175. if (event.isDefaultPrevented()) {
  32176. return;
  32177. }
  32178. event.preventDefault();
  32179. endTypingLevelIgnoreLocks(editor.undoManager);
  32180. editor.undoManager.transact(() => {
  32181. insert$1(editor, event);
  32182. });
  32183. };
  32184. const isCaretAfterKoreanCharacter = (rng) => {
  32185. if (!rng.collapsed) {
  32186. return false;
  32187. }
  32188. const startContainer = rng.startContainer;
  32189. if (isText$b(startContainer)) {
  32190. // Hangul: \uAC00-\uD7AF
  32191. // Hangul Jamo: \u1100-\u11FF
  32192. // Hangul Compatibility Jamo: \u3130-\u318F
  32193. // Hangul Jamo Extended-A: \uA960-\uA97F
  32194. // Hangul Jamo Extended-B: \uD7B0-\uD7FF
  32195. const koreanCharRegex = /^[\uAC00-\uD7AF\u1100-\u11FF\u3130-\u318F\uA960-\uA97F\uD7B0-\uD7FF]$/;
  32196. const char = startContainer.data.charAt(rng.startOffset - 1);
  32197. return koreanCharRegex.test(char);
  32198. }
  32199. else {
  32200. return false;
  32201. }
  32202. };
  32203. const setup$n = (editor) => {
  32204. let iOSSafariKeydownBookmark = Optional.none();
  32205. const iOSSafariKeydownOverride = (editor) => {
  32206. iOSSafariKeydownBookmark = Optional.some(editor.selection.getBookmark());
  32207. editor.undoManager.add();
  32208. };
  32209. const iOSSafariKeyupOverride = (editor, event) => {
  32210. editor.undoManager.undo();
  32211. iOSSafariKeydownBookmark.fold(noop, (b) => editor.selection.moveToBookmark(b));
  32212. handleEnterKeyEvent(editor, event);
  32213. iOSSafariKeydownBookmark = Optional.none();
  32214. };
  32215. editor.on('keydown', (event) => {
  32216. if (event.keyCode === VK.ENTER) {
  32217. if (isIOSSafari && isCaretAfterKoreanCharacter(editor.selection.getRng())) {
  32218. // TINY-9746: iOS Safari composes Korean characters by deleting the previous partial character and inserting
  32219. // the composed character. If the native Enter keypress event is not fired, iOS Safari will continue to compose across
  32220. // our custom newline by deleting it and inserting the composed character on the previous line, causing a bug. The workaround
  32221. // is to save a bookmark and an undo level on keydown while not preventing default to allow the native Enter keypress.
  32222. // Then on keyup, the effects of the native Enter keypress is undone and our own Enter key handler is called.
  32223. iOSSafariKeydownOverride(editor);
  32224. }
  32225. else {
  32226. handleEnterKeyEvent(editor, event);
  32227. }
  32228. }
  32229. });
  32230. editor.on('keyup', (event) => {
  32231. if (event.keyCode === VK.ENTER) {
  32232. iOSSafariKeydownBookmark.each(() => iOSSafariKeyupOverride(editor, event));
  32233. }
  32234. });
  32235. };
  32236. const executeKeydownOverride$2 = (editor, caret, evt) => {
  32237. const isMac = Env.os.isMacOS() || Env.os.isiOS();
  32238. execute([
  32239. { keyCode: VK.END, action: action(moveToLineEndPoint$1, editor, true) },
  32240. { keyCode: VK.HOME, action: action(moveToLineEndPoint$1, editor, false) },
  32241. ...(!isMac ? [
  32242. { keyCode: VK.HOME, action: action(selectToEndPoint, editor, false), ctrlKey: true, shiftKey: true },
  32243. { keyCode: VK.END, action: action(selectToEndPoint, editor, true), ctrlKey: true, shiftKey: true }
  32244. ] : []),
  32245. { keyCode: VK.END, action: action(moveToLineEndPoint, editor, true) },
  32246. { keyCode: VK.HOME, action: action(moveToLineEndPoint, editor, false) },
  32247. { keyCode: VK.END, action: action(moveToLineEndPoint$2, editor, true, caret) },
  32248. { keyCode: VK.HOME, action: action(moveToLineEndPoint$2, editor, false, caret) }
  32249. ], evt).each((_) => {
  32250. evt.preventDefault();
  32251. });
  32252. };
  32253. const setup$m = (editor, caret) => {
  32254. editor.on('keydown', (evt) => {
  32255. if (!evt.isDefaultPrevented()) {
  32256. executeKeydownOverride$2(editor, caret, evt);
  32257. }
  32258. });
  32259. };
  32260. const setup$l = (editor) => {
  32261. editor.on('input', (e) => {
  32262. // We only care about non composing inputs since moving the caret or modifying the text node will blow away the IME
  32263. if (!e.isComposing) {
  32264. normalizeNbspsInEditor(editor);
  32265. }
  32266. });
  32267. };
  32268. const platform = detect$1();
  32269. const executeKeyupAction = (editor, caret, evt) => {
  32270. execute([
  32271. { keyCode: VK.PAGE_UP, action: action(moveToLineEndPoint$2, editor, false, caret) },
  32272. { keyCode: VK.PAGE_DOWN, action: action(moveToLineEndPoint$2, editor, true, caret) }
  32273. ], evt);
  32274. };
  32275. const stopImmediatePropagation = (e) => e.stopImmediatePropagation();
  32276. const isPageUpDown = (evt) => evt.keyCode === VK.PAGE_UP || evt.keyCode === VK.PAGE_DOWN;
  32277. const setNodeChangeBlocker = (blocked, editor, block) => {
  32278. // Node change event is only blocked while the user is holding down the page up/down key it would have limited effects on other things
  32279. // Prevents a flickering UI while caret move in and out of the inline boundary element
  32280. if (block && !blocked.get()) {
  32281. editor.on('NodeChange', stopImmediatePropagation, true);
  32282. }
  32283. else if (!block && blocked.get()) {
  32284. editor.off('NodeChange', stopImmediatePropagation);
  32285. }
  32286. blocked.set(block);
  32287. };
  32288. // Determining the correct placement on key up/down is very complicated and would require handling many edge cases,
  32289. // which we don't have the resources to handle currently. As such, we allow the browser to change the selection and then make adjustments later.
  32290. const setup$k = (editor, caret) => {
  32291. // Mac OS doesn't move the selection when pressing page up/down and as such TinyMCE shouldn't be moving it either
  32292. if (platform.os.isMacOS()) {
  32293. return;
  32294. }
  32295. const blocked = Cell(false);
  32296. editor.on('keydown', (evt) => {
  32297. if (isPageUpDown(evt)) {
  32298. setNodeChangeBlocker(blocked, editor, true);
  32299. }
  32300. });
  32301. editor.on('keyup', (evt) => {
  32302. if (!evt.isDefaultPrevented()) {
  32303. executeKeyupAction(editor, caret, evt);
  32304. }
  32305. if (isPageUpDown(evt) && blocked.get()) {
  32306. setNodeChangeBlocker(blocked, editor, false);
  32307. editor.nodeChanged();
  32308. }
  32309. });
  32310. };
  32311. const isValidContainer = (root, container) => root === container || root.contains(container);
  32312. const isInEditableRange = (editor, range) => {
  32313. // If the range is not in the body then it's in a shadow root and we should allow that more details in: TINY-11446
  32314. if (!isValidContainer(editor.getBody(), range.startContainer) || !isValidContainer(editor.getBody(), range.endContainer)) {
  32315. return true;
  32316. }
  32317. return isEditableRange(editor.dom, range);
  32318. };
  32319. const setup$j = (editor) => {
  32320. editor.on('beforeinput', (e) => {
  32321. // Normally input is blocked on non-editable elements that have contenteditable="false" however we are also treating
  32322. // SVG elements as non-editable and deleting inside or into is possible in some browsers so we need to detect that and prevent that.
  32323. if (!editor.selection.isEditable() || exists(e.getTargetRanges(), (rng) => !isInEditableRange(editor, rng))) {
  32324. e.preventDefault();
  32325. }
  32326. });
  32327. };
  32328. const insertTextAtPosition = (text, pos) => {
  32329. const container = pos.container();
  32330. const offset = pos.offset();
  32331. if (isText$b(container)) {
  32332. container.insertData(offset, text);
  32333. return Optional.some(CaretPosition(container, offset + text.length));
  32334. }
  32335. else {
  32336. return getElementFromPosition(pos).map((elm) => {
  32337. const textNode = SugarElement.fromText(text);
  32338. if (pos.isAtEnd()) {
  32339. after$4(elm, textNode);
  32340. }
  32341. else {
  32342. before$4(elm, textNode);
  32343. }
  32344. return CaretPosition(textNode.dom, text.length);
  32345. });
  32346. }
  32347. };
  32348. const insertNbspAtPosition = curry(insertTextAtPosition, nbsp);
  32349. const insertSpaceAtPosition = curry(insertTextAtPosition, ' ');
  32350. const insertSpaceOrNbspAtPosition = (root, pos, schema) => needsToHaveNbsp(root, pos, schema) ? insertNbspAtPosition(pos) : insertSpaceAtPosition(pos);
  32351. const locationToCaretPosition = (root) => (location) => location.fold((element) => prevPosition(root.dom, CaretPosition.before(element)), (element) => firstPositionIn(element), (element) => lastPositionIn(element), (element) => nextPosition(root.dom, CaretPosition.after(element)));
  32352. const insertInlineBoundarySpaceOrNbsp = (root, pos, schema) => (checkPos) => needsToHaveNbsp(root, checkPos, schema) ? insertNbspAtPosition(pos) : insertSpaceAtPosition(pos);
  32353. const setSelection = (editor) => (pos) => {
  32354. editor.selection.setRng(pos.toRange());
  32355. editor.nodeChanged();
  32356. };
  32357. const isInsideSummary = (domUtils, node) => domUtils.isEditable(domUtils.getParent(node, 'summary'));
  32358. const insertSpaceOrNbspAtSelection = (editor) => {
  32359. const pos = CaretPosition.fromRangeStart(editor.selection.getRng());
  32360. const root = SugarElement.fromDom(editor.getBody());
  32361. if (editor.selection.isCollapsed()) {
  32362. const isInlineTarget$1 = curry(isInlineTarget, editor);
  32363. const caretPosition = CaretPosition.fromRangeStart(editor.selection.getRng());
  32364. return readLocation(isInlineTarget$1, editor.getBody(), caretPosition)
  32365. .bind(locationToCaretPosition(root))
  32366. .map((checkPos) => () => insertInlineBoundarySpaceOrNbsp(root, pos, editor.schema)(checkPos).each(setSelection(editor)));
  32367. }
  32368. else {
  32369. return Optional.none();
  32370. }
  32371. };
  32372. // TINY-9964: Firefox has a bug where the space key is toggling the open state instead of inserting a space in a summary element
  32373. const insertSpaceInSummaryAtSelectionOnFirefox = (editor) => {
  32374. const insertSpaceThunk = () => {
  32375. const root = SugarElement.fromDom(editor.getBody());
  32376. if (!editor.selection.isCollapsed()) {
  32377. editor.getDoc().execCommand('Delete');
  32378. }
  32379. const pos = CaretPosition.fromRangeStart(editor.selection.getRng());
  32380. insertSpaceOrNbspAtPosition(root, pos, editor.schema).each(setSelection(editor));
  32381. };
  32382. return someIf(Env.browser.isFirefox() && editor.selection.isEditable() && isInsideSummary(editor.dom, editor.selection.getRng().startContainer), insertSpaceThunk);
  32383. };
  32384. const executeKeydownOverride$1 = (editor, evt) => {
  32385. executeWithDelayedAction([
  32386. { keyCode: VK.SPACEBAR, action: action(insertSpaceOrNbspAtSelection, editor) },
  32387. { keyCode: VK.SPACEBAR, action: action(insertSpaceInSummaryAtSelectionOnFirefox, editor) }
  32388. ], evt).each((applyAction) => {
  32389. evt.preventDefault();
  32390. const event = fireBeforeInputEvent(editor, 'insertText', { data: ' ' });
  32391. if (!event.isDefaultPrevented()) {
  32392. applyAction();
  32393. // Browsers sends space in data even if the dom ends up with a nbsp so we should always be sending a space
  32394. fireInputEvent(editor, 'insertText', { data: ' ' });
  32395. }
  32396. });
  32397. };
  32398. const setup$i = (editor) => {
  32399. editor.on('keydown', (evt) => {
  32400. if (!evt.isDefaultPrevented()) {
  32401. executeKeydownOverride$1(editor, evt);
  32402. }
  32403. });
  32404. };
  32405. const tableTabNavigation = (editor) => {
  32406. if (hasTableTabNavigation(editor)) {
  32407. return [
  32408. { keyCode: VK.TAB, action: action(handleTab, editor, true) },
  32409. { keyCode: VK.TAB, shiftKey: true, action: action(handleTab, editor, false) },
  32410. ];
  32411. }
  32412. else {
  32413. return [];
  32414. }
  32415. };
  32416. const executeKeydownOverride = (editor, evt) => {
  32417. execute([
  32418. ...tableTabNavigation(editor)
  32419. ], evt).each((_) => {
  32420. evt.preventDefault();
  32421. });
  32422. };
  32423. const setup$h = (editor) => {
  32424. editor.on('keydown', (evt) => {
  32425. if (!evt.isDefaultPrevented()) {
  32426. executeKeydownOverride(editor, evt);
  32427. }
  32428. });
  32429. };
  32430. const setup$g = (editor) => {
  32431. editor.addShortcut('Meta+P', '', 'mcePrint');
  32432. setup$p(editor);
  32433. if (isRtc(editor)) {
  32434. return Cell(null);
  32435. }
  32436. else {
  32437. const caret = setupSelectedState(editor);
  32438. setup$j(editor);
  32439. setup$r(editor);
  32440. setup$q(editor, caret);
  32441. setup$o(editor, caret);
  32442. setup$n(editor);
  32443. setup$i(editor);
  32444. setup$l(editor);
  32445. setup$h(editor);
  32446. setup$m(editor, caret);
  32447. setup$k(editor, caret);
  32448. return caret;
  32449. }
  32450. };
  32451. const updateList = (editor, update) => {
  32452. const parentList = getParentList(editor);
  32453. if (parentList === null || isWithinNonEditableList$1(editor, parentList)) {
  32454. return;
  32455. }
  32456. editor.undoManager.transact(() => {
  32457. if (isObject(update.styles)) {
  32458. editor.dom.setStyles(parentList, update.styles);
  32459. }
  32460. if (isObject(update.attrs)) {
  32461. each$d(update.attrs, (v, k) => editor.dom.setAttrib(parentList, k, v));
  32462. }
  32463. });
  32464. };
  32465. const queryListCommandState = (editor, listName) => () => {
  32466. const parentList = getParentList(editor);
  32467. return isNonNullable(parentList) && parentList.nodeName === listName;
  32468. };
  32469. const setup$f = (editor) => {
  32470. editor.addCommand('InsertUnorderedList', (ui, detail) => {
  32471. toggleList(editor, 'UL', detail);
  32472. });
  32473. editor.addCommand('InsertOrderedList', (ui, detail) => {
  32474. toggleList(editor, 'OL', detail);
  32475. });
  32476. editor.addCommand('InsertDefinitionList', (ui, detail) => {
  32477. toggleList(editor, 'DL', detail);
  32478. });
  32479. editor.addCommand('RemoveList', () => {
  32480. flattenListSelection(editor);
  32481. });
  32482. editor.addCommand('mceListUpdate', (ui, detail) => {
  32483. if (isObject(detail)) {
  32484. updateList(editor, detail);
  32485. }
  32486. });
  32487. editor.addCommand('mceListBackspaceDelete', (_ui, forward) => {
  32488. backspaceDelete$c(editor, forward);
  32489. });
  32490. editor.addQueryStateHandler('InsertUnorderedList', queryListCommandState(editor, 'UL'));
  32491. editor.addQueryStateHandler('InsertOrderedList', queryListCommandState(editor, 'OL'));
  32492. editor.addQueryStateHandler('InsertDefinitionList', queryListCommandState(editor, 'DL'));
  32493. };
  32494. const setup$e = (editor) => {
  32495. editor.on('keydown', (e) => {
  32496. if (e.keyCode === VK.BACKSPACE) {
  32497. if (backspaceDelete$c(editor, false)) {
  32498. e.preventDefault();
  32499. }
  32500. }
  32501. else if (e.keyCode === VK.DELETE) {
  32502. if (backspaceDelete$c(editor, true)) {
  32503. e.preventDefault();
  32504. }
  32505. }
  32506. });
  32507. };
  32508. const isTextNode = (node) => node.type === 3;
  32509. const isEmpty = (nodeBuffer) => nodeBuffer.length === 0;
  32510. const wrapInvalidChildren = (list) => {
  32511. const insertListItem = (buffer, refNode) => {
  32512. const li = AstNode.create('li');
  32513. each$e(buffer, (node) => li.append(node));
  32514. if (refNode) {
  32515. list.insert(li, refNode, true);
  32516. }
  32517. else {
  32518. list.append(li);
  32519. }
  32520. };
  32521. const reducer = (buffer, node) => {
  32522. if (isTextNode(node)) {
  32523. return [...buffer, node];
  32524. }
  32525. else if (!isEmpty(buffer) && !isTextNode(node)) {
  32526. insertListItem(buffer, node);
  32527. return [];
  32528. }
  32529. else {
  32530. return buffer;
  32531. }
  32532. };
  32533. const restBuffer = foldl(list.children(), reducer, []);
  32534. if (!isEmpty(restBuffer)) {
  32535. insertListItem(restBuffer);
  32536. }
  32537. };
  32538. const setup$d = (editor) => {
  32539. editor.on('PreInit', () => {
  32540. const { parser } = editor;
  32541. parser.addNodeFilter('ul,ol', (nodes) => each$e(nodes, wrapInvalidChildren));
  32542. });
  32543. };
  32544. const setupTabKey = (editor) => {
  32545. editor.on('keydown', (e) => {
  32546. // Check for tab but not ctrl/cmd+tab since it switches browser tabs
  32547. if (e.keyCode !== VK.TAB || VK.metaKeyPressed(e)) {
  32548. return;
  32549. }
  32550. editor.undoManager.transact(() => {
  32551. if (e.shiftKey ? outdentListSelection(editor) : indentListSelection(editor)) {
  32552. e.preventDefault();
  32553. }
  32554. });
  32555. });
  32556. };
  32557. const setup$c = (editor) => {
  32558. if (shouldIndentOnTab(editor)) {
  32559. setupTabKey(editor);
  32560. }
  32561. };
  32562. const setup$b = (editor) => {
  32563. setup$e(editor);
  32564. setup$f(editor);
  32565. setup$d(editor);
  32566. setup$c(editor);
  32567. };
  32568. /**
  32569. * This class handles the nodechange event dispatching both manual and through selection change events.
  32570. *
  32571. * @class tinymce.NodeChange
  32572. * @private
  32573. */
  32574. class NodeChange {
  32575. constructor(editor) {
  32576. this.lastPath = [];
  32577. this.editor = editor;
  32578. let lastRng;
  32579. const self = this;
  32580. // Gecko doesn't support the "selectionchange" event
  32581. if (!('onselectionchange' in editor.getDoc())) {
  32582. editor.on('NodeChange click mouseup keyup focus', (e) => {
  32583. // Since DOM Ranges mutate on modification
  32584. // of the DOM we need to clone it's contents
  32585. const nativeRng = editor.selection.getRng();
  32586. const fakeRng = {
  32587. startContainer: nativeRng.startContainer,
  32588. startOffset: nativeRng.startOffset,
  32589. endContainer: nativeRng.endContainer,
  32590. endOffset: nativeRng.endOffset
  32591. };
  32592. // Always treat nodechange as a selectionchange since applying
  32593. // formatting to the current range wouldn't update the range but it's parent
  32594. if (e.type === 'nodechange' || !isEq$4(fakeRng, lastRng)) {
  32595. editor.dispatch('SelectionChange');
  32596. }
  32597. lastRng = fakeRng;
  32598. });
  32599. }
  32600. // IE has a bug where it fires a selectionchange on right click that has a range at the start of the body
  32601. // When the contextmenu event fires the selection is located at the right location
  32602. editor.on('contextmenu', () => {
  32603. store(editor);
  32604. editor.dispatch('SelectionChange');
  32605. });
  32606. // Selection change is delayed ~200ms on IE when you click inside the current range
  32607. editor.on('SelectionChange', () => {
  32608. const startElm = editor.selection.getStart(true);
  32609. // When focusout from after cef element to other input element the startelm can be undefined
  32610. if (!startElm) {
  32611. return;
  32612. }
  32613. if (hasAnyRanges(editor) && !self.isSameElementPath(startElm) && editor.dom.isChildOf(startElm, editor.getBody())) {
  32614. editor.nodeChanged({ selectionChange: true });
  32615. }
  32616. });
  32617. // Fire an extra nodeChange on mouseup for compatibility reasons
  32618. editor.on('mouseup', (e) => {
  32619. if (!e.isDefaultPrevented() && hasAnyRanges(editor)) {
  32620. // Delay nodeChanged call for WebKit edge case issue where the range
  32621. // isn't updated until after you click outside a selected image
  32622. if (editor.selection.getNode().nodeName === 'IMG') {
  32623. Delay.setEditorTimeout(editor, () => {
  32624. editor.nodeChanged();
  32625. });
  32626. }
  32627. else {
  32628. editor.nodeChanged();
  32629. }
  32630. }
  32631. });
  32632. }
  32633. /**
  32634. * Dispatches out a onNodeChange event to all observers. This method should be called when you
  32635. * need to update the UI states or element path etc.
  32636. *
  32637. * @method nodeChanged
  32638. * @param {Object} args Optional args to pass to NodeChange event handlers.
  32639. */
  32640. nodeChanged(args = {}) {
  32641. const editor = this.editor;
  32642. const selection = editor.selection;
  32643. let node;
  32644. // Fix for bug #1896577 it seems that this can not be fired while the editor is loading
  32645. if (editor.initialized && selection && !shouldDisableNodeChange(editor) && !isDisabled$1(editor)) {
  32646. // Get start node
  32647. const root = editor.getBody();
  32648. node = selection.getStart(true) || root;
  32649. // Make sure the node is within the editor root or is the editor root
  32650. if (node.ownerDocument !== editor.getDoc() || !editor.dom.isChildOf(node, root)) {
  32651. node = root;
  32652. }
  32653. // Get parents and add them to object
  32654. const parents = [];
  32655. editor.dom.getParent(node, (node) => {
  32656. if (node === root) {
  32657. return true;
  32658. }
  32659. else {
  32660. parents.push(node);
  32661. return false;
  32662. }
  32663. });
  32664. editor.dispatch('NodeChange', {
  32665. ...args,
  32666. element: node,
  32667. parents
  32668. });
  32669. }
  32670. }
  32671. /**
  32672. * Returns true/false if the current element path has been changed or not.
  32673. *
  32674. * @private
  32675. * @return {Boolean} True if the element path is the same false if it's not.
  32676. */
  32677. isSameElementPath(startElm) {
  32678. let i;
  32679. const editor = this.editor;
  32680. const currentPath = reverse(editor.dom.getParents(startElm, always, editor.getBody()));
  32681. if (currentPath.length === this.lastPath.length) {
  32682. for (i = currentPath.length; i >= 0; i--) {
  32683. if (currentPath[i] !== this.lastPath[i]) {
  32684. break;
  32685. }
  32686. }
  32687. if (i === -1) {
  32688. this.lastPath = currentPath;
  32689. return true;
  32690. }
  32691. }
  32692. this.lastPath = currentPath;
  32693. return false;
  32694. }
  32695. }
  32696. const internalMimeType = 'x-tinymce/html';
  32697. const internalHtmlMime = constant(internalMimeType);
  32698. const internalMark = '<!-- ' + internalMimeType + ' -->';
  32699. const mark = (html) => internalMark + html;
  32700. const unmark = (html) => html.replace(internalMark, '');
  32701. const isMarked = (html) => html.indexOf(internalMark) !== -1;
  32702. /*
  32703. * This module contains utilities to convert newlines (\n or \r\n) to BRs or to a combination of the specified block element and BRs
  32704. */
  32705. const isPlainText = (text) => {
  32706. // so basically any tag that is not one of the "p, div, span, br", or is one of them, but is followed
  32707. // by some additional characters qualifies the text as not a plain text (having some HTML tags)
  32708. // <span style="white-space:pre"> and <br /> are added as separate exceptions to the rule
  32709. return !/<(?:\/?(?!(?:div|p|br|span)>)\w+|(?:(?!(?:span style="white-space:\s?pre;?">)|br\s?\/>))\w+\s[^>]+)>/i.test(text);
  32710. };
  32711. const openContainer = (rootTag, rootAttrs) => {
  32712. let tag = '<' + rootTag;
  32713. const attrs = mapToArray(rootAttrs, (value, key) => key + '="' + Entities.encodeAllRaw(value) + '"');
  32714. if (attrs.length) {
  32715. tag += ' ' + attrs.join(' ');
  32716. }
  32717. return tag + '>';
  32718. };
  32719. const toBlockElements = (text, rootTag, rootAttrs) => {
  32720. const blocks = text.split(/\n\n/);
  32721. const tagOpen = openContainer(rootTag, rootAttrs);
  32722. const tagClose = '</' + rootTag + '>';
  32723. const paragraphs = map$3(blocks, (p) => {
  32724. return p.split(/\n/).join('<br />');
  32725. });
  32726. const stitch = (p) => {
  32727. return tagOpen + p + tagClose;
  32728. };
  32729. return paragraphs.length === 1 ? paragraphs[0] : map$3(paragraphs, stitch).join('');
  32730. };
  32731. const pasteBinDefaultContent = '%MCEPASTEBIN%';
  32732. /*
  32733. * Creates a paste bin element as close as possible to the current caret location and places the focus inside that element
  32734. * so that when the real paste event occurs the contents gets inserted into this element
  32735. * instead of the current editor selection element.
  32736. */
  32737. const create$5 = (editor, lastRngCell) => {
  32738. const { dom, selection } = editor;
  32739. const body = editor.getBody();
  32740. lastRngCell.set(selection.getRng());
  32741. // Create a pastebin
  32742. const pasteBinElm = dom.add(editor.getBody(), 'div', {
  32743. 'id': 'mcepastebin',
  32744. 'class': 'mce-pastebin',
  32745. 'contentEditable': true,
  32746. 'data-mce-bogus': 'all',
  32747. 'style': 'position: fixed; top: 50%; width: 10px; height: 10px; overflow: hidden; opacity: 0'
  32748. }, pasteBinDefaultContent);
  32749. // Move paste bin out of sight since the controlSelection rect gets displayed otherwise on Gecko
  32750. if (Env.browser.isFirefox()) {
  32751. dom.setStyle(pasteBinElm, 'left', dom.getStyle(body, 'direction', true) === 'rtl' ? 0xFFFF : -0xFFFF);
  32752. }
  32753. // Prevent focus events from bubbling fixed FocusManager issues
  32754. dom.bind(pasteBinElm, 'beforedeactivate focusin focusout', (e) => {
  32755. e.stopPropagation();
  32756. });
  32757. pasteBinElm.focus();
  32758. selection.select(pasteBinElm, true);
  32759. };
  32760. /*
  32761. * Removes the paste bin if it exists.
  32762. */
  32763. const remove = (editor, lastRngCell) => {
  32764. const dom = editor.dom;
  32765. if (getEl(editor)) {
  32766. let pasteBinClone;
  32767. const lastRng = lastRngCell.get();
  32768. // WebKit/Blink might clone the div so
  32769. // lets make sure we remove all clones
  32770. // TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it!
  32771. while ((pasteBinClone = getEl(editor))) {
  32772. dom.remove(pasteBinClone);
  32773. dom.unbind(pasteBinClone);
  32774. }
  32775. if (lastRng) {
  32776. editor.selection.setRng(lastRng);
  32777. }
  32778. }
  32779. lastRngCell.set(null);
  32780. };
  32781. const getEl = (editor) => editor.dom.get('mcepastebin');
  32782. const isPasteBin = (elm) => isNonNullable(elm) && elm.id === 'mcepastebin';
  32783. /*
  32784. * Returns the contents of the paste bin as a HTML string.
  32785. */
  32786. const getHtml = (editor) => {
  32787. const dom = editor.dom;
  32788. // Since WebKit/Chrome might clone the paste bin when pasting
  32789. // for example: <img style="float: right"> we need to check if any of them contains some useful html.
  32790. // TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it!
  32791. const copyAndRemove = (toElm, fromElm) => {
  32792. toElm.appendChild(fromElm);
  32793. dom.remove(fromElm, true); // remove, but keep children
  32794. };
  32795. // find only top level elements (there might be more nested inside them as well, see TINY-1162)
  32796. const [pasteBinElm, ...pasteBinClones] = filter$5(editor.getBody().childNodes, isPasteBin);
  32797. // if clones were found, move their content into the first bin
  32798. each$e(pasteBinClones, (pasteBinClone) => {
  32799. copyAndRemove(pasteBinElm, pasteBinClone);
  32800. });
  32801. // TINY-1162: when copying plain text (from notepad for example) WebKit clones
  32802. // paste bin (with styles and attributes) and uses it as a default wrapper for
  32803. // the chunks of the content, here we cycle over the whole paste bin and replace
  32804. // those wrappers with a basic div
  32805. const dirtyWrappers = dom.select('div[id=mcepastebin]', pasteBinElm);
  32806. for (let i = dirtyWrappers.length - 1; i >= 0; i--) {
  32807. const cleanWrapper = dom.create('div');
  32808. pasteBinElm.insertBefore(cleanWrapper, dirtyWrappers[i]);
  32809. copyAndRemove(cleanWrapper, dirtyWrappers[i]);
  32810. }
  32811. return pasteBinElm ? pasteBinElm.innerHTML : '';
  32812. };
  32813. const isDefaultPasteBinContent = (content) => content === pasteBinDefaultContent;
  32814. const PasteBin = (editor) => {
  32815. const lastRng = Cell(null);
  32816. return {
  32817. create: () => create$5(editor, lastRng),
  32818. remove: () => remove(editor, lastRng),
  32819. getEl: () => getEl(editor),
  32820. getHtml: () => getHtml(editor),
  32821. getLastRng: lastRng.get
  32822. };
  32823. };
  32824. /*
  32825. * This module contains various utility functions for the paste logic.
  32826. */
  32827. const filter = (content, items) => {
  32828. Tools.each(items, (v) => {
  32829. if (is$5(v, RegExp)) {
  32830. content = content.replace(v, '');
  32831. }
  32832. else {
  32833. content = content.replace(v[0], v[1]);
  32834. }
  32835. });
  32836. return content;
  32837. };
  32838. /*
  32839. * Gets the innerText of the specified element. It will handle edge cases
  32840. * and works better than textContent on Gecko.
  32841. */
  32842. const innerText = (html) => {
  32843. const schema = Schema();
  32844. const domParser = DomParser({}, schema);
  32845. let text = '';
  32846. const voidElements = schema.getVoidElements();
  32847. const ignoreElements = Tools.makeMap('script noscript style textarea video audio iframe object', ' ');
  32848. const blockElements = schema.getBlockElements();
  32849. const walk = (node) => {
  32850. const name = node.name, currentNode = node;
  32851. if (name === 'br') {
  32852. text += '\n';
  32853. return;
  32854. }
  32855. // Ignore wbr, to replicate innerText on Chrome/Firefox
  32856. if (name === 'wbr') {
  32857. return;
  32858. }
  32859. // img/input/hr but ignore wbr as it's just a potential word break
  32860. if (voidElements[name]) {
  32861. text += ' ';
  32862. }
  32863. // Ignore script, video contents
  32864. if (ignoreElements[name]) {
  32865. text += ' ';
  32866. return;
  32867. }
  32868. if (node.type === 3) {
  32869. text += node.value;
  32870. }
  32871. // Walk all children
  32872. if (!(node.name in schema.getVoidElements())) {
  32873. let currentNode = node.firstChild;
  32874. if (currentNode) {
  32875. do {
  32876. walk(currentNode);
  32877. } while ((currentNode = currentNode.next));
  32878. }
  32879. }
  32880. // Add \n or \n\n for blocks or P
  32881. if (blockElements[name] && currentNode.next) {
  32882. text += '\n';
  32883. if (name === 'p') {
  32884. text += '\n';
  32885. }
  32886. }
  32887. };
  32888. html = filter(html, [
  32889. /<!\[[^\]]+\]>/g // Conditional comments
  32890. ]);
  32891. walk(domParser.parse(html));
  32892. return text;
  32893. };
  32894. /*
  32895. * Trims the specified HTML by removing all WebKit fragments, all elements wrapping the body trailing BR elements etc.
  32896. */
  32897. const trimHtml = (html) => {
  32898. const trimSpaces = (all, s1, s2) => {
  32899. // WebKit &nbsp; meant to preserve multiple spaces but instead inserted around all inline tags,
  32900. // including the spans with inline styles created on paste
  32901. if (!s1 && !s2) {
  32902. return ' ';
  32903. }
  32904. return nbsp;
  32905. };
  32906. html = filter(html, [
  32907. /^[\s\S]*<body[^>]*>\s*|\s*<\/body[^>]*>[\s\S]*$/ig, // Remove anything but the contents within the BODY element
  32908. /<!--StartFragment-->|<!--EndFragment-->/g, // Inner fragments (tables from excel on mac)
  32909. [/( ?)<span class="Apple-converted-space">\u00a0<\/span>( ?)/g, trimSpaces],
  32910. /<br class="Apple-interchange-newline">/g,
  32911. /<br>$/i // Trailing BR elements
  32912. ]);
  32913. return html;
  32914. };
  32915. // TODO: Should be in some global class
  32916. const createIdGenerator = (prefix) => {
  32917. let count = 0;
  32918. return () => {
  32919. return prefix + (count++);
  32920. };
  32921. };
  32922. const getImageMimeType = (ext) => {
  32923. const lowerExt = ext.toLowerCase();
  32924. const mimeOverrides = {
  32925. jpg: 'jpeg',
  32926. jpe: 'jpeg',
  32927. jfi: 'jpeg',
  32928. jif: 'jpeg',
  32929. jfif: 'jpeg',
  32930. pjpeg: 'jpeg',
  32931. pjp: 'jpeg',
  32932. svg: 'svg+xml'
  32933. };
  32934. return Tools.hasOwn(mimeOverrides, lowerExt) ? 'image/' + mimeOverrides[lowerExt] : 'image/' + lowerExt;
  32935. };
  32936. const preProcess = (editor, html) => {
  32937. const parser = DomParser({
  32938. sanitize: shouldSanitizeXss(editor),
  32939. sandbox_iframes: shouldSandboxIframes(editor),
  32940. sandbox_iframes_exclusions: getSandboxIframesExclusions(editor),
  32941. convert_unsafe_embeds: shouldConvertUnsafeEmbeds(editor)
  32942. }, editor.schema);
  32943. // Strip meta elements
  32944. parser.addNodeFilter('meta', (nodes) => {
  32945. Tools.each(nodes, (node) => {
  32946. node.remove();
  32947. });
  32948. });
  32949. const fragment = parser.parse(html, { forced_root_block: false, isRootContent: true });
  32950. return HtmlSerializer({ validate: true }, editor.schema).serialize(fragment);
  32951. };
  32952. const processResult = (content, cancelled) => ({ content, cancelled });
  32953. const postProcessFilter = (editor, html, internal) => {
  32954. const tempBody = editor.dom.create('div', { style: 'display:none' }, html);
  32955. const postProcessArgs = firePastePostProcess(editor, tempBody, internal);
  32956. return processResult(postProcessArgs.node.innerHTML, postProcessArgs.isDefaultPrevented());
  32957. };
  32958. const filterContent = (editor, content, internal) => {
  32959. const preProcessArgs = firePastePreProcess(editor, content, internal);
  32960. // Filter the content to remove potentially dangerous content (eg scripts)
  32961. const filteredContent = preProcess(editor, preProcessArgs.content);
  32962. if (editor.hasEventListeners('PastePostProcess') && !preProcessArgs.isDefaultPrevented()) {
  32963. return postProcessFilter(editor, filteredContent, internal);
  32964. }
  32965. else {
  32966. return processResult(filteredContent, preProcessArgs.isDefaultPrevented());
  32967. }
  32968. };
  32969. const process = (editor, html, internal) => {
  32970. return filterContent(editor, html, internal);
  32971. };
  32972. const pasteHtml$1 = (editor, html) => {
  32973. editor.insertContent(html, {
  32974. merge: shouldPasteMergeFormats(editor),
  32975. paste: true
  32976. });
  32977. return true;
  32978. };
  32979. const isAbsoluteUrl = (url) => /^https?:\/\/[\w\-\/+=.,!;:&%@^~(){}?#]+$/i.test(url);
  32980. const isImageUrl = (editor, url) => {
  32981. return isAbsoluteUrl(url) && exists(getAllowedImageFileTypes(editor), (type) => endsWith(url.toLowerCase(), `.${type.toLowerCase()}`));
  32982. };
  32983. const createImage = (editor, url, pasteHtmlFn) => {
  32984. editor.undoManager.extra(() => {
  32985. pasteHtmlFn(editor, url);
  32986. }, () => {
  32987. editor.insertContent('<img src="' + url + '">');
  32988. });
  32989. return true;
  32990. };
  32991. const createLink = (editor, url, pasteHtmlFn) => {
  32992. editor.undoManager.extra(() => {
  32993. pasteHtmlFn(editor, url);
  32994. }, () => {
  32995. editor.execCommand('mceInsertLink', false, url);
  32996. });
  32997. return true;
  32998. };
  32999. const linkSelection = (editor, html, pasteHtmlFn) => !editor.selection.isCollapsed() && isAbsoluteUrl(html) ? createLink(editor, html, pasteHtmlFn) : false;
  33000. const insertImage = (editor, html, pasteHtmlFn) => isImageUrl(editor, html) ? createImage(editor, html, pasteHtmlFn) : false;
  33001. const smartInsertContent = (editor, html) => {
  33002. Tools.each([
  33003. linkSelection,
  33004. insertImage,
  33005. pasteHtml$1
  33006. ], (action) => {
  33007. return !action(editor, html, pasteHtml$1);
  33008. });
  33009. };
  33010. const insertContent = (editor, html, pasteAsText) => {
  33011. if (pasteAsText || !isSmartPasteEnabled(editor)) {
  33012. pasteHtml$1(editor, html);
  33013. }
  33014. else {
  33015. smartInsertContent(editor, html);
  33016. }
  33017. };
  33018. const uniqueId = createIdGenerator('mceclip');
  33019. const createPasteDataTransfer = (html) => {
  33020. const dataTransfer = createDataTransfer();
  33021. setHtmlData(dataTransfer, html);
  33022. // TINY-9829: Set to read-only mode as per https://www.w3.org/TR/input-events-2/
  33023. setReadOnlyMode(dataTransfer);
  33024. return dataTransfer;
  33025. };
  33026. const doPaste = (editor, content, internal, pasteAsText, shouldSimulateInputEvent) => {
  33027. const res = process(editor, content, internal);
  33028. if (!res.cancelled) {
  33029. const content = res.content;
  33030. const doPasteAction = () => insertContent(editor, content, pasteAsText);
  33031. if (shouldSimulateInputEvent) {
  33032. const args = fireBeforeInputEvent(editor, 'insertFromPaste', { dataTransfer: createPasteDataTransfer(content) });
  33033. if (!args.isDefaultPrevented()) {
  33034. doPasteAction();
  33035. fireInputEvent(editor, 'insertFromPaste');
  33036. }
  33037. }
  33038. else {
  33039. doPasteAction();
  33040. }
  33041. }
  33042. };
  33043. /*
  33044. * Pastes the specified HTML. This means that the HTML is filtered and then
  33045. * inserted at the current selection in the editor. It will also fire paste events
  33046. * for custom user filtering.
  33047. */
  33048. const pasteHtml = (editor, html, internalFlag, shouldSimulateInputEvent) => {
  33049. const internal = internalFlag ? internalFlag : isMarked(html);
  33050. doPaste(editor, unmark(html), internal, false, shouldSimulateInputEvent);
  33051. };
  33052. /*
  33053. * Pastes the specified text. This means that the plain text is processed
  33054. * and converted into BR and P elements. It will fire paste events for custom filtering.
  33055. */
  33056. const pasteText = (editor, text, shouldSimulateInputEvent) => {
  33057. const encodedText = editor.dom.encode(text).replace(/\r\n/g, '\n');
  33058. const normalizedText = normalize$4(encodedText, getPasteTabSpaces(editor));
  33059. const html = toBlockElements(normalizedText, getForcedRootBlock(editor), getForcedRootBlockAttrs(editor));
  33060. doPaste(editor, html, false, true, shouldSimulateInputEvent);
  33061. };
  33062. /*
  33063. * Gets various content types out of a datatransfer object.
  33064. */
  33065. const getDataTransferItems = (dataTransfer) => {
  33066. const items = {};
  33067. if (dataTransfer && dataTransfer.types) {
  33068. for (let i = 0; i < dataTransfer.types.length; i++) {
  33069. const contentType = dataTransfer.types[i];
  33070. try { // IE11 throws exception when contentType is Files (type is present but data cannot be retrieved via getData())
  33071. items[contentType] = dataTransfer.getData(contentType);
  33072. }
  33073. catch (_a) {
  33074. items[contentType] = ''; // useless in general, but for consistency across browsers
  33075. }
  33076. }
  33077. }
  33078. return items;
  33079. };
  33080. const hasContentType = (clipboardContent, mimeType) => mimeType in clipboardContent && clipboardContent[mimeType].length > 0;
  33081. const hasHtmlOrText = (content) => hasContentType(content, 'text/html') || hasContentType(content, 'text/plain');
  33082. const extractFilename = (editor, str) => {
  33083. const m = str.match(/([\s\S]+?)(?:\.[a-z0-9.]+)$/i);
  33084. return isNonNullable(m) ? editor.dom.encode(m[1]) : undefined;
  33085. };
  33086. const createBlobInfo = (editor, blobCache, file, base64) => {
  33087. const id = uniqueId();
  33088. const useFileName = shouldReuseFileName(editor) && isNonNullable(file.name);
  33089. const name = useFileName ? extractFilename(editor, file.name) : id;
  33090. const filename = useFileName ? file.name : undefined;
  33091. const blobInfo = blobCache.create(id, file, base64, name, filename);
  33092. blobCache.add(blobInfo);
  33093. return blobInfo;
  33094. };
  33095. const pasteImage = (editor, imageItem) => {
  33096. parseDataUri(imageItem.uri).each(({ data, type, base64Encoded }) => {
  33097. const base64 = base64Encoded ? data : btoa(data);
  33098. const file = imageItem.file;
  33099. // TODO: Move the bulk of the cache logic to EditorUpload
  33100. const blobCache = editor.editorUpload.blobCache;
  33101. const existingBlobInfo = blobCache.getByData(base64, type);
  33102. const blobInfo = existingBlobInfo !== null && existingBlobInfo !== void 0 ? existingBlobInfo : createBlobInfo(editor, blobCache, file, base64);
  33103. pasteHtml(editor, `<img src="${blobInfo.blobUri()}">`, false, true);
  33104. });
  33105. };
  33106. const isClipboardEvent = (event) => event.type === 'paste';
  33107. const readFilesAsDataUris = (items) => Promise.all(map$3(items, (file) => {
  33108. return blobToDataUri(file).then((uri) => ({ file, uri }));
  33109. }));
  33110. const isImage = (editor) => {
  33111. const allowedExtensions = getAllowedImageFileTypes(editor);
  33112. return (file) => startsWith(file.type, 'image/') && exists(allowedExtensions, (extension) => {
  33113. return getImageMimeType(extension) === file.type;
  33114. });
  33115. };
  33116. const getImagesFromDataTransfer = (editor, dataTransfer) => {
  33117. const items = dataTransfer.items ? bind$3(from(dataTransfer.items), (item) => {
  33118. return item.kind === 'file' ? [item.getAsFile()] : [];
  33119. }) : [];
  33120. const files = dataTransfer.files ? from(dataTransfer.files) : [];
  33121. return filter$5(items.length > 0 ? items : files, isImage(editor));
  33122. };
  33123. /*
  33124. * Checks if the clipboard contains image data if it does it will take that data
  33125. * and convert it into a data url image and paste that image at the caret location.
  33126. */
  33127. const pasteImageData = (editor, e, rng) => {
  33128. const dataTransfer = isClipboardEvent(e) ? e.clipboardData : e.dataTransfer;
  33129. if (shouldPasteDataImages(editor) && dataTransfer) {
  33130. const images = getImagesFromDataTransfer(editor, dataTransfer);
  33131. if (images.length > 0) {
  33132. e.preventDefault();
  33133. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  33134. readFilesAsDataUris(images).then((fileResults) => {
  33135. if (rng) {
  33136. editor.selection.setRng(rng);
  33137. }
  33138. each$e(fileResults, (result) => {
  33139. pasteImage(editor, result);
  33140. });
  33141. });
  33142. return true;
  33143. }
  33144. }
  33145. return false;
  33146. };
  33147. // Chrome on Android doesn't support proper clipboard access so we have no choice but to allow the browser default behavior.
  33148. const isBrokenAndroidClipboardEvent = (e) => { var _a, _b; return Env.os.isAndroid() && ((_b = (_a = e.clipboardData) === null || _a === void 0 ? void 0 : _a.items) === null || _b === void 0 ? void 0 : _b.length) === 0; };
  33149. // Ctrl+V or Shift+Insert
  33150. const isKeyboardPasteEvent = (e) => (VK.metaKeyPressed(e) && e.keyCode === 86) || (e.shiftKey && e.keyCode === 45);
  33151. const insertClipboardContent = (editor, clipboardContent, html, plainTextMode, shouldSimulateInputEvent) => {
  33152. let content = trimHtml(html);
  33153. const isInternal = hasContentType(clipboardContent, internalHtmlMime()) || isMarked(html);
  33154. const isPlainTextHtml = !isInternal && isPlainText(content);
  33155. const isAbsoluteUrl$1 = isAbsoluteUrl(content);
  33156. // If the paste bin is empty try using plain text mode since that is better than nothing right?
  33157. // Also if we got nothing from clipboard API/pastebin or the content is a plain text (with only
  33158. // some BRs, Ps or DIVs as newlines) then we fallback to plain/text
  33159. if (isDefaultPasteBinContent(content) || !content.length || (isPlainTextHtml && !isAbsoluteUrl$1)) {
  33160. plainTextMode = true;
  33161. }
  33162. // Grab plain text from Clipboard API or convert existing HTML to plain text
  33163. if (plainTextMode || isAbsoluteUrl$1) {
  33164. // Use plain text contents from Clipboard API unless the HTML contains paragraphs then
  33165. // we should convert the HTML to plain text since works better when pasting HTML/Word contents as plain text
  33166. if (hasContentType(clipboardContent, 'text/plain') && isPlainTextHtml) {
  33167. content = clipboardContent['text/plain'];
  33168. }
  33169. else {
  33170. content = innerText(content);
  33171. }
  33172. }
  33173. // If the content is the paste bin default HTML then it was impossible to get the clipboard data out.
  33174. if (isDefaultPasteBinContent(content)) {
  33175. return;
  33176. }
  33177. if (plainTextMode) {
  33178. pasteText(editor, content, shouldSimulateInputEvent);
  33179. }
  33180. else {
  33181. pasteHtml(editor, content, isInternal, shouldSimulateInputEvent);
  33182. }
  33183. };
  33184. const registerEventHandlers = (editor, pasteBin, pasteFormat) => {
  33185. let keyboardPastePlainTextState;
  33186. const getLastRng = () => pasteBin.getLastRng() || editor.selection.getRng();
  33187. editor.on('keydown', (e) => {
  33188. if (isKeyboardPasteEvent(e) && !e.isDefaultPrevented()) {
  33189. keyboardPastePlainTextState = e.shiftKey && e.keyCode === 86;
  33190. }
  33191. });
  33192. editor.on('paste', (e) => {
  33193. if (e.isDefaultPrevented() || isBrokenAndroidClipboardEvent(e)) {
  33194. return;
  33195. }
  33196. const plainTextMode = pasteFormat.get() === 'text' || keyboardPastePlainTextState;
  33197. keyboardPastePlainTextState = false;
  33198. const clipboardContent = getDataTransferItems(e.clipboardData);
  33199. if (!hasHtmlOrText(clipboardContent) && pasteImageData(editor, e, getLastRng())) {
  33200. return;
  33201. }
  33202. // If the clipboard API has HTML then use that directly
  33203. if (hasContentType(clipboardContent, 'text/html')) {
  33204. e.preventDefault();
  33205. insertClipboardContent(editor, clipboardContent, clipboardContent['text/html'], plainTextMode, true);
  33206. }
  33207. else if (hasContentType(clipboardContent, 'text/plain') && hasContentType(clipboardContent, 'text/uri-list')) {
  33208. /*
  33209. Safari adds the uri-list attribute to links copied within it.
  33210. When pasting something with the url-list within safari using the default functionality it will convert it from www.example.com to <a href="www.example.com">www.example.com</a> when pasting into the pasteBin-div.
  33211. This causes issues. To solve this we bypass the default paste functionality for this situation.
  33212. */
  33213. e.preventDefault();
  33214. insertClipboardContent(editor, clipboardContent, clipboardContent['text/plain'], plainTextMode, true);
  33215. }
  33216. else {
  33217. // We can't extract the HTML content from the clipboard so we need to allow the paste
  33218. // to run via the pastebin and then extract from there
  33219. pasteBin.create();
  33220. Delay.setEditorTimeout(editor, () => {
  33221. // Get the pastebin content and then remove it so the selection is restored
  33222. const html = pasteBin.getHtml();
  33223. pasteBin.remove();
  33224. insertClipboardContent(editor, clipboardContent, html, plainTextMode, false);
  33225. }, 0);
  33226. }
  33227. });
  33228. };
  33229. const registerDataImageFilter = (editor) => {
  33230. const isWebKitFakeUrl = (src) => startsWith(src, 'webkit-fake-url');
  33231. const isDataUri = (src) => startsWith(src, 'data:');
  33232. const isPasteInsert = (args) => { var _a; return ((_a = args.data) === null || _a === void 0 ? void 0 : _a.paste) === true; };
  33233. // Remove all data images from paste for example from Gecko
  33234. // except internal images like video elements
  33235. editor.parser.addNodeFilter('img', (nodes, name, args) => {
  33236. if (!shouldPasteDataImages(editor) && isPasteInsert(args)) {
  33237. for (const node of nodes) {
  33238. const src = node.attr('src');
  33239. if (isString(src) && !node.attr('data-mce-object') && src !== Env.transparentSrc) {
  33240. // Safari on Mac produces webkit-fake-url see: https://bugs.webkit.org/show_bug.cgi?id=49141
  33241. if (isWebKitFakeUrl(src)) {
  33242. node.remove();
  33243. }
  33244. else if (!shouldAllowHtmlDataUrls(editor) && isDataUri(src)) {
  33245. node.remove();
  33246. }
  33247. }
  33248. }
  33249. }
  33250. });
  33251. };
  33252. /*
  33253. * This class contains logic for getting HTML contents out of the clipboard.
  33254. *
  33255. * This by default will attempt to use the W3C clipboard API to get HTML content.
  33256. * If that can't be used then fallback to letting the browser paste natively with
  33257. * some logic to clean up what the browser generated, as it can mutate the content.
  33258. *
  33259. * Current implementation steps:
  33260. * 1. On keydown determine if we should paste as plain text.
  33261. * 2. Wait for the browser to fire a "paste" event and get the contents out of clipboard.
  33262. * 3. If no content is available, then attach the paste bin and change the selection to be inside the bin.
  33263. * 4. Extract the contents from the bin in the next event loop.
  33264. * 5. If no HTML is found or we're using plain text paste mode then convert the HTML or lookup the clipboard to get the plain text.
  33265. * 6. Process the content from the clipboard or pastebin and insert it into the editor.
  33266. */
  33267. const registerEventsAndFilters = (editor, pasteBin, pasteFormat) => {
  33268. registerEventHandlers(editor, pasteBin, pasteFormat);
  33269. registerDataImageFilter(editor);
  33270. };
  33271. const togglePlainTextPaste = (editor, pasteFormat) => {
  33272. if (pasteFormat.get() === 'text') {
  33273. pasteFormat.set('html');
  33274. firePastePlainTextToggle(editor, false);
  33275. }
  33276. else {
  33277. pasteFormat.set('text');
  33278. firePastePlainTextToggle(editor, true);
  33279. }
  33280. editor.focus();
  33281. };
  33282. const register$1 = (editor, pasteFormat) => {
  33283. editor.addCommand('mceTogglePlainTextPaste', () => {
  33284. togglePlainTextPaste(editor, pasteFormat);
  33285. });
  33286. editor.addCommand('mceInsertClipboardContent', (ui, value) => {
  33287. if (value.html) {
  33288. // TINY-9997: Input events are not simulated when using paste commands, similar to how the 'mceInsertContent'
  33289. // and 'Delete' commands work.
  33290. pasteHtml(editor, value.html, value.internal, false);
  33291. }
  33292. if (value.text) {
  33293. pasteText(editor, value.text, false);
  33294. }
  33295. });
  33296. };
  33297. const setHtml5Clipboard = (clipboardData, html, text) => {
  33298. if (clipboardData) {
  33299. try {
  33300. clipboardData.clearData();
  33301. clipboardData.setData('text/html', html);
  33302. clipboardData.setData('text/plain', text);
  33303. clipboardData.setData(internalHtmlMime(), html);
  33304. return true;
  33305. }
  33306. catch (_a) {
  33307. return false;
  33308. }
  33309. }
  33310. else {
  33311. return false;
  33312. }
  33313. };
  33314. const setClipboardData = (evt, data, fallback, done) => {
  33315. if (setHtml5Clipboard(evt.clipboardData, data.html, data.text)) {
  33316. evt.preventDefault();
  33317. done();
  33318. }
  33319. else {
  33320. fallback(data.html, done);
  33321. }
  33322. };
  33323. const fallback = (editor) => (html, done) => {
  33324. const { dom, selection } = editor;
  33325. const outer = dom.create('div', { 'contenteditable': 'false', 'data-mce-bogus': 'all' });
  33326. const inner = dom.create('div', { contenteditable: 'true' }, html);
  33327. dom.setStyles(outer, {
  33328. position: 'fixed',
  33329. top: '0',
  33330. left: '-3000px',
  33331. width: '1000px',
  33332. overflow: 'hidden'
  33333. });
  33334. outer.appendChild(inner);
  33335. dom.add(editor.getBody(), outer);
  33336. const range = selection.getRng();
  33337. inner.focus();
  33338. const offscreenRange = dom.createRng();
  33339. offscreenRange.selectNodeContents(inner);
  33340. selection.setRng(offscreenRange);
  33341. Delay.setEditorTimeout(editor, () => {
  33342. selection.setRng(range);
  33343. dom.remove(outer);
  33344. done();
  33345. }, 0);
  33346. };
  33347. const getData = (editor) => ({
  33348. html: mark(editor.selection.getContent({ contextual: true })),
  33349. text: editor.selection.getContent({ format: 'text' })
  33350. });
  33351. const isTableSelection = (editor) => !!editor.dom.getParent(editor.selection.getStart(), 'td[data-mce-selected],th[data-mce-selected]', editor.getBody());
  33352. const hasSelectedContent = (editor) => !editor.selection.isCollapsed() || isTableSelection(editor);
  33353. const cut = (editor, caret) => (evt) => {
  33354. if (!evt.isDefaultPrevented() && hasSelectedContent(editor) && editor.selection.isEditable()) {
  33355. setClipboardData(evt, getData(editor), fallback(editor), () => {
  33356. if (Env.browser.isChromium() || Env.browser.isFirefox()) {
  33357. const rng = editor.selection.getRng();
  33358. // Chrome fails to execCommand from another execCommand with this message:
  33359. // "We don't execute document.execCommand() this time, because it is called recursively.""
  33360. // Firefox 82 now also won't run recursive commands, but it doesn't log an error
  33361. Delay.setEditorTimeout(editor, () => {
  33362. // Restore the range before deleting, as Chrome on Android will
  33363. // collapse the selection after a cut event has fired.
  33364. editor.selection.setRng(rng);
  33365. // Delete command is called directly without using editor.execCommand to avoid running editor.focus() which side effect was selection normalization and additional undo level
  33366. deleteCommand(editor, caret);
  33367. }, 0);
  33368. }
  33369. else {
  33370. // Delete command is called directly without using editor.execCommand to avoid running editor.focus() which side effect was selection normalization and additional undo level
  33371. deleteCommand(editor, caret);
  33372. }
  33373. });
  33374. }
  33375. };
  33376. const copy = (editor) => (evt) => {
  33377. if (!evt.isDefaultPrevented() && hasSelectedContent(editor)) {
  33378. setClipboardData(evt, getData(editor), fallback(editor), noop);
  33379. }
  33380. };
  33381. const register = (editor, caret) => {
  33382. editor.on('cut', cut(editor, caret));
  33383. editor.on('copy', copy(editor));
  33384. };
  33385. const getCaretRangeFromEvent = (editor, e) => { var _a, _b;
  33386. // TODO: TINY-7075 Remove the "?? 0" here when agar passes valid client coords
  33387. return RangeUtils.getCaretRangeFromPoint((_a = e.clientX) !== null && _a !== void 0 ? _a : 0, (_b = e.clientY) !== null && _b !== void 0 ? _b : 0, editor.getDoc()); };
  33388. const isPlainTextFileUrl = (content) => {
  33389. const plainTextContent = content['text/plain'];
  33390. return plainTextContent ? plainTextContent.indexOf('file://') === 0 : false;
  33391. };
  33392. const setFocusedRange = (editor, rng) => {
  33393. editor.focus();
  33394. if (rng) {
  33395. editor.selection.setRng(rng);
  33396. }
  33397. };
  33398. const hasImage = (dataTransfer) => exists(dataTransfer.files, (file) => /^image\//.test(file.type));
  33399. const needsCustomInternalDrop = (dom, schema, target, dropContent) => {
  33400. const parentTransparent = dom.getParent(target, (node) => isTransparentBlock(schema, node));
  33401. const inSummary = !isNull(dom.getParent(target, 'summary'));
  33402. if (inSummary) {
  33403. return true;
  33404. }
  33405. else if (parentTransparent && has$2(dropContent, 'text/html')) {
  33406. const fragment = new DOMParser().parseFromString(dropContent['text/html'], 'text/html').body;
  33407. return !isNull(fragment.querySelector(parentTransparent.nodeName.toLowerCase()));
  33408. }
  33409. else {
  33410. return false;
  33411. }
  33412. };
  33413. const setupSummaryDeleteByDragFix = (editor) => {
  33414. editor.on('input', (e) => {
  33415. const hasNoSummary = (el) => isNull(el.querySelector('summary'));
  33416. if (e.inputType === 'deleteByDrag') {
  33417. const brokenDetailElements = filter$5(editor.dom.select('details'), hasNoSummary);
  33418. each$e(brokenDetailElements, (details) => {
  33419. // Firefox leaves a BR
  33420. if (isBr$7(details.firstChild)) {
  33421. details.firstChild.remove();
  33422. }
  33423. const summary = editor.dom.create('summary');
  33424. summary.appendChild(createPaddingBr().dom);
  33425. details.prepend(summary);
  33426. });
  33427. }
  33428. });
  33429. };
  33430. const setup$a = (editor, draggingInternallyState) => {
  33431. // Block all drag/drop events
  33432. if (shouldPasteBlockDrop(editor)) {
  33433. editor.on('dragend dragover draggesture dragdrop drop drag', (e) => {
  33434. e.preventDefault();
  33435. e.stopPropagation();
  33436. });
  33437. }
  33438. // Prevent users from dropping data images on Gecko
  33439. if (!shouldPasteDataImages(editor)) {
  33440. editor.on('drop', (e) => {
  33441. const dataTransfer = e.dataTransfer;
  33442. if (dataTransfer && hasImage(dataTransfer)) {
  33443. e.preventDefault();
  33444. }
  33445. });
  33446. }
  33447. editor.on('drop', (e) => {
  33448. if (e.isDefaultPrevented()) {
  33449. return;
  33450. }
  33451. const rng = getCaretRangeFromEvent(editor, e);
  33452. if (isNullable(rng)) {
  33453. return;
  33454. }
  33455. const dropContent = getDataTransferItems(e.dataTransfer);
  33456. const internal = hasContentType(dropContent, internalHtmlMime());
  33457. if ((!hasHtmlOrText(dropContent) || isPlainTextFileUrl(dropContent)) && pasteImageData(editor, e, rng)) {
  33458. return;
  33459. }
  33460. const internalContent = dropContent[internalHtmlMime()];
  33461. const content = internalContent || dropContent['text/html'] || dropContent['text/plain'];
  33462. const needsInternalDrop = needsCustomInternalDrop(editor.dom, editor.schema, rng.startContainer, dropContent);
  33463. const isInternalDrop = draggingInternallyState.get();
  33464. if (isInternalDrop && !needsInternalDrop) {
  33465. return;
  33466. }
  33467. if (content) {
  33468. e.preventDefault();
  33469. // FF 45 doesn't paint a caret when dragging in text in due to focus call by execCommand
  33470. Delay.setEditorTimeout(editor, () => {
  33471. editor.undoManager.transact(() => {
  33472. if (internalContent || (isInternalDrop && needsInternalDrop)) {
  33473. editor.execCommand('Delete');
  33474. }
  33475. setFocusedRange(editor, rng);
  33476. const trimmedContent = trimHtml(content);
  33477. if (dropContent['text/html']) {
  33478. pasteHtml(editor, trimmedContent, internal, true);
  33479. }
  33480. else {
  33481. pasteText(editor, trimmedContent, true);
  33482. }
  33483. });
  33484. });
  33485. }
  33486. });
  33487. editor.on('dragstart', (_e) => {
  33488. draggingInternallyState.set(true);
  33489. });
  33490. editor.on('dragover dragend', (e) => {
  33491. if (shouldPasteDataImages(editor) && !draggingInternallyState.get()) {
  33492. e.preventDefault();
  33493. setFocusedRange(editor, getCaretRangeFromEvent(editor, e));
  33494. }
  33495. if (e.type === 'dragend') {
  33496. draggingInternallyState.set(false);
  33497. }
  33498. });
  33499. setupSummaryDeleteByDragFix(editor);
  33500. };
  33501. const setup$9 = (editor) => {
  33502. const processEvent = (f) => (e) => {
  33503. f(editor, e);
  33504. };
  33505. const preProcess = getPastePreProcess(editor);
  33506. if (isFunction(preProcess)) {
  33507. editor.on('PastePreProcess', processEvent(preProcess));
  33508. }
  33509. const postProcess = getPastePostProcess(editor);
  33510. if (isFunction(postProcess)) {
  33511. editor.on('PastePostProcess', processEvent(postProcess));
  33512. }
  33513. };
  33514. /*
  33515. * This module contains various fixes for browsers. These issues can not be feature
  33516. * detected since we have no direct control over the clipboard. However we might be able
  33517. * to remove some of these fixes once the browsers gets updated/fixed.
  33518. */
  33519. const addPreProcessFilter = (editor, filterFunc) => {
  33520. editor.on('PastePreProcess', (e) => {
  33521. e.content = filterFunc(editor, e.content, e.internal);
  33522. });
  33523. };
  33524. const rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi;
  33525. const rgbToHex = (value) => Tools.trim(value).replace(rgbRegExp, rgbaToHexString).toLowerCase();
  33526. /*
  33527. * WebKit has a nasty quirk where the all computed styles gets added to style attributes when copy/pasting contents.
  33528. * This fix solves that by simply removing the whole style attribute.
  33529. *
  33530. * The paste_webkit_styles option can be set to specify what to keep:
  33531. * paste_webkit_styles: "none" // Keep no styles
  33532. * paste_webkit_styles: "all", // Keep all of them
  33533. * paste_webkit_styles: "font-weight color" // Keep specific ones
  33534. */
  33535. const removeWebKitStyles = (editor, content, internal) => {
  33536. const webKitStylesOption = getPasteWebkitStyles(editor);
  33537. // If the content is internal or if we're keeping all styles then we don't need any processing
  33538. if (internal || webKitStylesOption === 'all' || !shouldPasteRemoveWebKitStyles(editor)) {
  33539. return content;
  33540. }
  33541. const webKitStyles = webKitStylesOption ? webKitStylesOption.split(/[, ]/) : [];
  33542. // Keep specific styles that don't match the current node computed style
  33543. if (webKitStyles && webKitStylesOption !== 'none') {
  33544. const dom = editor.dom, node = editor.selection.getNode();
  33545. content = content.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi, (all, before, value, after) => {
  33546. const inputStyles = dom.parseStyle(dom.decode(value));
  33547. const outputStyles = {};
  33548. for (let i = 0; i < webKitStyles.length; i++) {
  33549. const inputValue = inputStyles[webKitStyles[i]];
  33550. let compareInput = inputValue;
  33551. let currentValue = dom.getStyle(node, webKitStyles[i], true);
  33552. if (/color/.test(webKitStyles[i])) {
  33553. compareInput = rgbToHex(compareInput);
  33554. currentValue = rgbToHex(currentValue);
  33555. }
  33556. if (currentValue !== compareInput) {
  33557. outputStyles[webKitStyles[i]] = inputValue;
  33558. }
  33559. }
  33560. const outputStyle = dom.serializeStyle(outputStyles, 'span');
  33561. if (outputStyle) {
  33562. return before + ' style="' + outputStyle + '"' + after;
  33563. }
  33564. return before + after;
  33565. });
  33566. }
  33567. else {
  33568. // Remove all external styles
  33569. content = content.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi, '$1$3');
  33570. }
  33571. // Keep internal styles
  33572. content = content.replace(/(<[^>]+) data-mce-style="([^"]+)"([^>]*>)/gi, (all, before, value, after) => {
  33573. return before + ' style="' + value + '"' + after;
  33574. });
  33575. return content;
  33576. };
  33577. const setup$8 = (editor) => {
  33578. if (Env.browser.isChromium() || Env.browser.isSafari()) {
  33579. addPreProcessFilter(editor, removeWebKitStyles);
  33580. }
  33581. };
  33582. const setup$7 = (editor, caret) => {
  33583. const draggingInternallyState = Cell(false);
  33584. const pasteFormat = Cell(isPasteAsTextEnabled(editor) ? 'text' : 'html');
  33585. const pasteBin = PasteBin(editor);
  33586. setup$8(editor);
  33587. register$1(editor, pasteFormat);
  33588. setup$9(editor);
  33589. editor.addQueryStateHandler('mceTogglePlainTextPaste', () => pasteFormat.get() === 'text');
  33590. // IMPORTANT: The following event hooks need to be setup later so that other things
  33591. // can hook in and prevent the event so core paste doesn't handle them.
  33592. editor.on('PreInit', () => {
  33593. register(editor, caret);
  33594. setup$a(editor, draggingInternallyState);
  33595. registerEventsAndFilters(editor, pasteBin, pasteFormat);
  33596. });
  33597. };
  33598. const preventSummaryToggle = (editor) => {
  33599. editor.on('click', (e) => {
  33600. if (editor.dom.getParent(e.target, 'details')) {
  33601. e.preventDefault();
  33602. }
  33603. });
  33604. };
  33605. const filterDetails = (editor) => {
  33606. editor.parser.addNodeFilter('details', (elms) => {
  33607. const initialStateOption = getDetailsInitialState(editor);
  33608. each$e(elms, (details) => {
  33609. if (initialStateOption === 'expanded') {
  33610. details.attr('open', 'open');
  33611. }
  33612. else if (initialStateOption === 'collapsed') {
  33613. details.attr('open', null);
  33614. }
  33615. });
  33616. });
  33617. editor.serializer.addNodeFilter('details', (elms) => {
  33618. const serializedStateOption = getDetailsSerializedState(editor);
  33619. each$e(elms, (details) => {
  33620. if (serializedStateOption === 'expanded') {
  33621. details.attr('open', 'open');
  33622. }
  33623. else if (serializedStateOption === 'collapsed') {
  33624. details.attr('open', null);
  33625. }
  33626. });
  33627. });
  33628. };
  33629. const setup$6 = (editor) => {
  33630. preventSummaryToggle(editor);
  33631. filterDetails(editor);
  33632. };
  33633. const isBr = isBr$7;
  33634. const isText = isText$b;
  33635. const isContentEditableFalse$2 = (elm) => isContentEditableFalse$a(elm.dom);
  33636. const isContentEditableTrue = (elm) => isContentEditableTrue$3(elm.dom);
  33637. const isRoot = (rootNode) => (elm) => eq(SugarElement.fromDom(rootNode), elm);
  33638. const getClosestScope = (node, rootNode, schema) => closest$4(SugarElement.fromDom(node), (elm) => isContentEditableTrue(elm) || schema.isBlock(name(elm)), isRoot(rootNode))
  33639. .getOr(SugarElement.fromDom(rootNode)).dom;
  33640. const getClosestCef = (node, rootNode) => closest$4(SugarElement.fromDom(node), isContentEditableFalse$2, isRoot(rootNode));
  33641. const findEdgeCaretCandidate = (startNode, scope, forward) => {
  33642. const walker = new DomTreeWalker(startNode, scope);
  33643. const next = forward ? walker.next.bind(walker) : walker.prev.bind(walker);
  33644. let result = startNode;
  33645. for (let current = forward ? startNode : next(); current && !isBr(current); current = next()) {
  33646. if (isCaretCandidate$3(current)) {
  33647. result = current;
  33648. }
  33649. }
  33650. return result;
  33651. };
  33652. const findClosestBlockRange = (startRng, rootNode, schema) => {
  33653. const startPos = CaretPosition.fromRangeStart(startRng);
  33654. // TODO: TINY-8865 - This may not be safe to cast as Node and alternative solutions need to be looked into
  33655. const clickNode = startPos.getNode();
  33656. const scope = getClosestScope(clickNode, rootNode, schema);
  33657. const startNode = findEdgeCaretCandidate(clickNode, scope, false);
  33658. const endNode = findEdgeCaretCandidate(clickNode, scope, true);
  33659. const rng = document.createRange();
  33660. getClosestCef(startNode, scope).fold(() => {
  33661. if (isText(startNode)) {
  33662. rng.setStart(startNode, 0);
  33663. }
  33664. else {
  33665. rng.setStartBefore(startNode);
  33666. }
  33667. }, (cef) => rng.setStartBefore(cef.dom));
  33668. getClosestCef(endNode, scope).fold(() => {
  33669. if (isText(endNode)) {
  33670. rng.setEnd(endNode, endNode.data.length);
  33671. }
  33672. else {
  33673. rng.setEndAfter(endNode);
  33674. }
  33675. }, (cef) => rng.setEndAfter(cef.dom));
  33676. return rng;
  33677. };
  33678. const onTripleClickSelect = (editor) => {
  33679. const rng = findClosestBlockRange(editor.selection.getRng(), editor.getBody(), editor.schema);
  33680. editor.selection.setRng(normalize(rng));
  33681. };
  33682. const setup$5 = (editor) => {
  33683. editor.on('mousedown', (e) => {
  33684. if (e.detail >= 3) {
  33685. e.preventDefault();
  33686. onTripleClickSelect(editor);
  33687. }
  33688. });
  33689. };
  33690. var FakeCaretPosition;
  33691. (function (FakeCaretPosition) {
  33692. FakeCaretPosition["Before"] = "before";
  33693. FakeCaretPosition["After"] = "after";
  33694. })(FakeCaretPosition || (FakeCaretPosition = {}));
  33695. const distanceToRectLeft = (clientRect, clientX) => Math.abs(clientRect.left - clientX);
  33696. const distanceToRectRight = (clientRect, clientX) => Math.abs(clientRect.right - clientX);
  33697. const isInsideY = (clientY, clientRect) => clientY >= clientRect.top && clientY <= clientRect.bottom;
  33698. const collidesY = (r1, r2) => r1.top < r2.bottom && r1.bottom > r2.top;
  33699. const isOverlapping = (r1, r2) => {
  33700. // Rectangles might overlap a bit so this checks if the overlap is more than 50% then we count that as on the same line
  33701. const overlap = overlapY(r1, r2) / Math.min(r1.height, r2.height);
  33702. return collidesY(r1, r2) && overlap > 0.5;
  33703. };
  33704. const splitRectsPerAxis = (rects, y) => {
  33705. const intersectingRects = filter$5(rects, (rect) => isInsideY(y, rect));
  33706. return boundingClientRectFromRects(intersectingRects).fold(() => ([[], rects]), (boundingRect) => {
  33707. const { pass: horizontal, fail: vertical } = partition$2(rects, (rect) => isOverlapping(rect, boundingRect));
  33708. return [horizontal, vertical];
  33709. });
  33710. };
  33711. const clientInfo = (rect, clientX) => {
  33712. return {
  33713. node: rect.node,
  33714. position: distanceToRectLeft(rect, clientX) < distanceToRectRight(rect, clientX) ? FakeCaretPosition.Before : FakeCaretPosition.After
  33715. };
  33716. };
  33717. // Measure the distance between the x and the closest edge of the rect.
  33718. // If the x is inside the rect then always return 0.
  33719. const horizontalDistance = (rect, x, _y) => x > rect.left && x < rect.right ? 0 : Math.min(Math.abs(rect.left - x), Math.abs(rect.right - x));
  33720. const closestChildCaretCandidateNodeRect = (children, clientX, clientY, findCloserTextNode) => {
  33721. const caretCandidateRect = (rect) => {
  33722. if (isCaretCandidate$3(rect.node)) {
  33723. return Optional.some(rect);
  33724. }
  33725. else if (isElement$7(rect.node)) {
  33726. return closestChildCaretCandidateNodeRect(from(rect.node.childNodes), clientX, clientY, false);
  33727. }
  33728. else {
  33729. return Optional.none();
  33730. }
  33731. };
  33732. // If an element and a text node has nearly equal distance then favor the text node over the element to make it easier to select text
  33733. // since setting the selection range will cancel any text select operation.
  33734. const tryFindSecondBestTextNode = (closest, sndClosest, distance) => {
  33735. return caretCandidateRect(sndClosest).filter((rect) => {
  33736. const deltaDistance = Math.abs(distance(closest, clientX, clientY) - distance(rect, clientX, clientY));
  33737. return deltaDistance < 2 && isText$b(rect.node);
  33738. });
  33739. };
  33740. const findClosestCaretCandidateNodeRect = (rects, distance) => {
  33741. const sortedRects = sort(rects, (r1, r2) => distance(r1, clientX, clientY) - distance(r2, clientX, clientY));
  33742. return findMap(sortedRects, caretCandidateRect).map((closest) => {
  33743. // If the closest rect is not a text node then lets try to see if the second rect has a text node that is close enough
  33744. if (findCloserTextNode && !isText$b(closest.node) && sortedRects.length > 1) {
  33745. return tryFindSecondBestTextNode(closest, sortedRects[1], distance).getOr(closest);
  33746. }
  33747. else {
  33748. return closest;
  33749. }
  33750. });
  33751. };
  33752. const [horizontalRects, verticalRects] = splitRectsPerAxis(getClientRects(children), clientY);
  33753. const { pass: above, fail: below } = partition$2(verticalRects, (rect) => rect.top < clientY);
  33754. return findClosestCaretCandidateNodeRect(horizontalRects, horizontalDistance)
  33755. .orThunk(() => findClosestCaretCandidateNodeRect(below, distanceToRectEdgeFromXY))
  33756. .orThunk(() => findClosestCaretCandidateNodeRect(above, distanceToRectEdgeFromXY));
  33757. };
  33758. const traverseUp = (rootElm, scope, clientX, clientY) => {
  33759. const helper = (scope, prevScope) => {
  33760. const isDragGhostContainer = (node) => isElement$7(node) && node.classList.contains('mce-drag-container');
  33761. const childNodesWithoutGhost = filter$5(scope.dom.childNodes, not(isDragGhostContainer));
  33762. return prevScope.fold(() => closestChildCaretCandidateNodeRect(childNodesWithoutGhost, clientX, clientY, true), (prevScope) => {
  33763. const uncheckedChildren = filter$5(childNodesWithoutGhost, (node) => node !== prevScope.dom);
  33764. return closestChildCaretCandidateNodeRect(uncheckedChildren, clientX, clientY, true);
  33765. }).orThunk(() => {
  33766. const parent = eq(scope, rootElm) ? Optional.none() : parentElement(scope);
  33767. return parent.bind((newScope) => helper(newScope, Optional.some(scope)));
  33768. });
  33769. };
  33770. return helper(scope, Optional.none());
  33771. };
  33772. // Rough description of how this algorithm works:
  33773. // 1. It starts by finding the element at the specified X, Y coordinate.
  33774. // 2. Then it checks its children for the closest one and traverses down into those repeating step 2, 3 until it finds a caret candidate.
  33775. // 3. If no caret candidate is found in the closest child node then it checks the second closest and so on until all decendants have been checked.
  33776. // 4. If no caret candidate is found, it traverses up skips the element it already checked and checks its siblings using steps 2, 3.
  33777. // 5. If no caret candidate is found, it continues step 4 until it finds the root. Then we have checked all the nodes in the document.
  33778. //
  33779. // This is less accurate but more performant, since for the common case you are likely to find a caret candidate close to where you are clicking.
  33780. // The more accurate algorithm would be to read all caret candidates rects in the whole document in and in one big step to find the closest one, but that is just too slow for bigger documents.
  33781. const closestCaretCandidateNodeRect = (root, clientX, clientY) => {
  33782. const rootElm = SugarElement.fromDom(root);
  33783. const ownerDoc = documentOrOwner(rootElm);
  33784. const elementAtPoint = SugarElement.fromPoint(ownerDoc, clientX, clientY).filter((elm) => contains(rootElm, elm));
  33785. const element = elementAtPoint.getOr(rootElm);
  33786. return traverseUp(rootElm, element, clientX, clientY);
  33787. };
  33788. const closestFakeCaretCandidate = (root, clientX, clientY) => closestCaretCandidateNodeRect(root, clientX, clientY)
  33789. .filter((rect) => isFakeCaretTarget(rect.node))
  33790. .map((rect) => clientInfo(rect, clientX));
  33791. const getAbsolutePosition = (elm) => {
  33792. var _a, _b;
  33793. const clientRect = elm.getBoundingClientRect();
  33794. const doc = elm.ownerDocument;
  33795. const docElem = doc.documentElement;
  33796. const win = doc.defaultView;
  33797. return {
  33798. top: clientRect.top + ((_a = win === null || win === void 0 ? void 0 : win.scrollY) !== null && _a !== void 0 ? _a : 0) - docElem.clientTop,
  33799. left: clientRect.left + ((_b = win === null || win === void 0 ? void 0 : win.scrollX) !== null && _b !== void 0 ? _b : 0) - docElem.clientLeft
  33800. };
  33801. };
  33802. const getBodyPosition = (editor) => editor.inline ? getAbsolutePosition(editor.getBody()) : { left: 0, top: 0 };
  33803. const getScrollPosition = (editor) => {
  33804. const body = editor.getBody();
  33805. return editor.inline ? { left: body.scrollLeft, top: body.scrollTop } : { left: 0, top: 0 };
  33806. };
  33807. const getBodyScroll = (editor) => {
  33808. const body = editor.getBody(), docElm = editor.getDoc().documentElement;
  33809. const inlineScroll = { left: body.scrollLeft, top: body.scrollTop };
  33810. const iframeScroll = { left: body.scrollLeft || docElm.scrollLeft, top: body.scrollTop || docElm.scrollTop };
  33811. return editor.inline ? inlineScroll : iframeScroll;
  33812. };
  33813. const getMousePosition = (editor, event) => {
  33814. if (event.target.ownerDocument !== editor.getDoc()) {
  33815. const iframePosition = getAbsolutePosition(editor.getContentAreaContainer());
  33816. const scrollPosition = getBodyScroll(editor);
  33817. return {
  33818. left: event.pageX - iframePosition.left + scrollPosition.left,
  33819. top: event.pageY - iframePosition.top + scrollPosition.top
  33820. };
  33821. }
  33822. return {
  33823. left: event.pageX,
  33824. top: event.pageY
  33825. };
  33826. };
  33827. const calculatePosition = (bodyPosition, scrollPosition, mousePosition) => ({
  33828. pageX: (mousePosition.left - bodyPosition.left) + scrollPosition.left,
  33829. pageY: (mousePosition.top - bodyPosition.top) + scrollPosition.top
  33830. });
  33831. const calc = (editor, event) => calculatePosition(getBodyPosition(editor), getScrollPosition(editor), getMousePosition(editor, event));
  33832. const getTargetProps = (target) => ({ target, srcElement: target });
  33833. const makeDndEventFromMouseEvent = (type, mouseEvent, target, dataTransfer) => ({
  33834. ...mouseEvent,
  33835. dataTransfer,
  33836. type,
  33837. ...getTargetProps(target)
  33838. });
  33839. const makeDndEvent = (type, target, dataTransfer) => {
  33840. const fail = die('Function not supported on simulated event.');
  33841. const event = {
  33842. // Event
  33843. bubbles: true,
  33844. cancelBubble: false,
  33845. cancelable: true,
  33846. composed: false,
  33847. currentTarget: null,
  33848. defaultPrevented: false,
  33849. eventPhase: 0,
  33850. isTrusted: true,
  33851. returnValue: false,
  33852. timeStamp: 0,
  33853. type,
  33854. composedPath: fail,
  33855. initEvent: fail,
  33856. preventDefault: noop,
  33857. stopImmediatePropagation: noop,
  33858. stopPropagation: noop,
  33859. AT_TARGET: window.Event.AT_TARGET,
  33860. BUBBLING_PHASE: window.Event.BUBBLING_PHASE,
  33861. CAPTURING_PHASE: window.Event.CAPTURING_PHASE,
  33862. NONE: window.Event.NONE,
  33863. // UIEvent
  33864. altKey: false,
  33865. button: 0,
  33866. buttons: 0,
  33867. clientX: 0,
  33868. clientY: 0,
  33869. ctrlKey: false,
  33870. layerX: 0,
  33871. layerY: 0,
  33872. metaKey: false,
  33873. movementX: 0,
  33874. movementY: 0,
  33875. offsetX: 0,
  33876. offsetY: 0,
  33877. pageX: 0,
  33878. pageY: 0,
  33879. relatedTarget: null,
  33880. screenX: 0,
  33881. screenY: 0,
  33882. shiftKey: false,
  33883. x: 0,
  33884. y: 0,
  33885. detail: 0,
  33886. view: null,
  33887. which: 0,
  33888. initUIEvent: fail,
  33889. initMouseEvent: fail,
  33890. getModifierState: fail,
  33891. // DragEvent
  33892. dataTransfer,
  33893. ...getTargetProps(target)
  33894. };
  33895. return event;
  33896. };
  33897. const makeDataTransferCopyForDragEvent = (dataTransfer, eventType) => {
  33898. const copy = cloneDataTransfer(dataTransfer);
  33899. // TINY-9601: Set mode as per https://html.spec.whatwg.org/dev/dnd.html#concept-dnd-rw
  33900. if (eventType === 'dragstart') {
  33901. setDragstartEvent(copy);
  33902. setReadWriteMode(copy);
  33903. }
  33904. else if (eventType === 'drop') {
  33905. setDropEvent(copy);
  33906. setReadOnlyMode(copy);
  33907. }
  33908. else {
  33909. setDragendEvent(copy);
  33910. setProtectedMode(copy);
  33911. }
  33912. return copy;
  33913. };
  33914. const makeDragEvent = (type, target, dataTransfer, mouseEvent) => {
  33915. // TINY-9601: Get copy for each new event to prevent undesired mutations on dispatched DataTransfer objects
  33916. const dataTransferForDispatch = makeDataTransferCopyForDragEvent(dataTransfer, type);
  33917. return isUndefined(mouseEvent) ? makeDndEvent(type, target, dataTransferForDispatch) : makeDndEventFromMouseEvent(type, mouseEvent, target, dataTransferForDispatch);
  33918. };
  33919. /**
  33920. * This module contains logic overriding the drag/drop logic of the editor.
  33921. *
  33922. * @private
  33923. * @class tinymce.DragDropOverrides
  33924. */
  33925. // Arbitrary values needed when scrolling CEF elements
  33926. const scrollPixelsPerInterval = 32;
  33927. const scrollIntervalValue = 100;
  33928. const mouseRangeToTriggerScrollInsideEditor = 8;
  33929. const mouseRangeToTriggerScrollOutsideEditor = 16;
  33930. const isContentEditableFalse$1 = isContentEditableFalse$a;
  33931. const isContentEditable = or(isContentEditableFalse$1, isContentEditableTrue$3);
  33932. const isDraggable = (dom, rootElm, elm) => isContentEditableFalse$1(elm) && elm !== rootElm && dom.isEditable(elm.parentElement);
  33933. const isValidDropTarget = (editor, targetElement, dragElement) => {
  33934. if (isNullable(targetElement)) {
  33935. return false;
  33936. }
  33937. else if (targetElement === dragElement || editor.dom.isChildOf(targetElement, dragElement)) {
  33938. return false;
  33939. }
  33940. else {
  33941. return editor.dom.isEditable(targetElement);
  33942. }
  33943. };
  33944. const createGhost = (editor, elm, width, height) => {
  33945. const dom = editor.dom;
  33946. const clonedElm = elm.cloneNode(true);
  33947. dom.setStyles(clonedElm, { width, height });
  33948. dom.setAttrib(clonedElm, 'data-mce-selected', null);
  33949. const ghostElm = dom.create('div', {
  33950. 'class': 'mce-drag-container',
  33951. 'data-mce-bogus': 'all',
  33952. 'unselectable': 'on',
  33953. 'contenteditable': 'false'
  33954. });
  33955. dom.setStyles(ghostElm, {
  33956. position: 'absolute',
  33957. opacity: 0.5,
  33958. overflow: 'hidden',
  33959. border: 0,
  33960. padding: 0,
  33961. margin: 0,
  33962. width,
  33963. height
  33964. });
  33965. dom.setStyles(clonedElm, {
  33966. margin: 0,
  33967. boxSizing: 'border-box'
  33968. });
  33969. ghostElm.appendChild(clonedElm);
  33970. return ghostElm;
  33971. };
  33972. const appendGhostToBody = (ghostElm, bodyElm) => {
  33973. if (ghostElm.parentNode !== bodyElm) {
  33974. bodyElm.appendChild(ghostElm);
  33975. }
  33976. };
  33977. // Helper function needed for scrolling the editor inside moveGhost function
  33978. const scrollEditor = (direction, amount) => (win) => () => {
  33979. const current = direction === 'left' ? win.scrollX : win.scrollY;
  33980. win.scroll({
  33981. [direction]: current + amount,
  33982. behavior: 'smooth',
  33983. });
  33984. };
  33985. const scrollLeft = scrollEditor('left', -scrollPixelsPerInterval);
  33986. const scrollRight = scrollEditor('left', scrollPixelsPerInterval);
  33987. const scrollUp = scrollEditor('top', -scrollPixelsPerInterval);
  33988. const scrollDown = scrollEditor('top', scrollPixelsPerInterval);
  33989. const moveGhost = (ghostElm, position, width, height, maxX, maxY, mouseY, mouseX, contentAreaContainer, win, state, mouseEventOriginatedFromWithinTheEditor) => {
  33990. let overflowX = 0, overflowY = 0;
  33991. ghostElm.style.left = position.pageX + 'px';
  33992. ghostElm.style.top = position.pageY + 'px';
  33993. if (position.pageX + width > maxX) {
  33994. overflowX = (position.pageX + width) - maxX;
  33995. }
  33996. if (position.pageY + height > maxY) {
  33997. overflowY = (position.pageY + height) - maxY;
  33998. }
  33999. ghostElm.style.width = (width - overflowX) + 'px';
  34000. ghostElm.style.height = (height - overflowY) + 'px';
  34001. // Code needed for dragging CEF elements (specifically fixing TINY-8874)
  34002. // The idea behind the algorithm is that the user will start dragging the
  34003. // CEF element to the edge of the editor and that would cause scrolling.
  34004. // The way that happens is that the user will trigger a mousedown event,
  34005. // then a mousemove event until they reach the edge of the editor. Then
  34006. // no event triggers. That's when I set an interval to keep scrolling the editor.
  34007. // Once a new event triggers I clear the existing interval and set it back to none.
  34008. const clientHeight = contentAreaContainer.clientHeight;
  34009. const clientWidth = contentAreaContainer.clientWidth;
  34010. const outerMouseY = mouseY + contentAreaContainer.getBoundingClientRect().top;
  34011. const outerMouseX = mouseX + contentAreaContainer.getBoundingClientRect().left;
  34012. state.on((state) => {
  34013. state.intervalId.clear();
  34014. if (state.dragging && mouseEventOriginatedFromWithinTheEditor) {
  34015. // This basically means that the mouse is close to the bottom edge
  34016. // (within MouseRange pixels of the bottom edge)
  34017. if (mouseY + mouseRangeToTriggerScrollInsideEditor >= clientHeight) {
  34018. state.intervalId.set(scrollDown(win));
  34019. // This basically means that the mouse is close to the top edge
  34020. // (within MouseRange pixels)
  34021. }
  34022. else if (mouseY - mouseRangeToTriggerScrollInsideEditor <= 0) {
  34023. state.intervalId.set(scrollUp(win));
  34024. // This basically means that the mouse is close to the right edge
  34025. // (within MouseRange pixels of the right edge)
  34026. }
  34027. else if (mouseX + mouseRangeToTriggerScrollInsideEditor >= clientWidth) {
  34028. state.intervalId.set(scrollRight(win));
  34029. // This basically means that the mouse is close to the left edge
  34030. // (within MouseRange pixels of the left edge)
  34031. }
  34032. else if (mouseX - mouseRangeToTriggerScrollInsideEditor <= 0) {
  34033. state.intervalId.set(scrollLeft(win));
  34034. // This basically means that the mouse is close to the bottom edge
  34035. // of the page (within MouseRange pixels) when the bottom of
  34036. // the editor is offscreen
  34037. }
  34038. else if (outerMouseY + mouseRangeToTriggerScrollOutsideEditor >= window.innerHeight) {
  34039. state.intervalId.set(scrollDown(window));
  34040. // This basically means that the mouse is close to the upper edge
  34041. // of the page (within MouseRange pixels) when the top of
  34042. // the editor is offscreen
  34043. }
  34044. else if (outerMouseY - mouseRangeToTriggerScrollOutsideEditor <= 0) {
  34045. state.intervalId.set(scrollUp(window));
  34046. // This basically means that the mouse is close to the right edge
  34047. // of the page (within MouseRange pixels) when the right edge of
  34048. // the editor is offscreen
  34049. }
  34050. else if (outerMouseX + mouseRangeToTriggerScrollOutsideEditor >= window.innerWidth) {
  34051. state.intervalId.set(scrollRight(window));
  34052. // This basically means that the mouse is close to the left edge
  34053. // of the page (within MouseRange pixels) when the left edge of
  34054. // the editor is offscreen
  34055. }
  34056. else if (outerMouseX - mouseRangeToTriggerScrollOutsideEditor <= 0) {
  34057. state.intervalId.set(scrollLeft(window));
  34058. }
  34059. }
  34060. });
  34061. };
  34062. const removeElement = (elm) => {
  34063. if (elm && elm.parentNode) {
  34064. elm.parentNode.removeChild(elm);
  34065. }
  34066. };
  34067. const removeElementWithPadding = (dom, elm) => {
  34068. const parentBlock = dom.getParent(elm.parentNode, dom.isBlock);
  34069. removeElement(elm);
  34070. if (parentBlock && parentBlock !== dom.getRoot() && dom.isEmpty(parentBlock)) {
  34071. fillWithPaddingBr(SugarElement.fromDom(parentBlock));
  34072. }
  34073. };
  34074. const isLeftMouseButtonPressed = (e) => e.button === 0;
  34075. const applyRelPos = (state, position) => ({
  34076. pageX: position.pageX - state.relX,
  34077. pageY: position.pageY + 5
  34078. });
  34079. const start = (state, editor) => (e) => {
  34080. if (isLeftMouseButtonPressed(e)) {
  34081. const ceElm = find$2(editor.dom.getParents(e.target), isContentEditable).getOr(null);
  34082. if (isNonNullable(ceElm) && isDraggable(editor.dom, editor.getBody(), ceElm)) {
  34083. const elmPos = editor.dom.getPos(ceElm);
  34084. const bodyElm = editor.getBody();
  34085. const docElm = editor.getDoc().documentElement;
  34086. state.set({
  34087. element: ceElm,
  34088. dataTransfer: createDataTransfer(),
  34089. dragging: false,
  34090. screenX: e.screenX,
  34091. screenY: e.screenY,
  34092. maxX: (editor.inline ? bodyElm.scrollWidth : docElm.offsetWidth) - 2,
  34093. maxY: (editor.inline ? bodyElm.scrollHeight : docElm.offsetHeight) - 2,
  34094. relX: e.pageX - elmPos.x,
  34095. relY: e.pageY - elmPos.y,
  34096. width: ceElm.offsetWidth,
  34097. height: ceElm.offsetHeight,
  34098. ghost: createGhost(editor, ceElm, ceElm.offsetWidth, ceElm.offsetHeight),
  34099. intervalId: repeatable(scrollIntervalValue)
  34100. });
  34101. }
  34102. }
  34103. };
  34104. const placeCaretAt = (editor, clientX, clientY) => {
  34105. editor._selectionOverrides.hideFakeCaret();
  34106. closestFakeCaretCandidate(editor.getBody(), clientX, clientY).fold(() => editor.selection.placeCaretAt(clientX, clientY), (caretInfo) => {
  34107. const range = editor._selectionOverrides.showCaret(1, caretInfo.node, caretInfo.position === FakeCaretPosition.Before, false);
  34108. if (range) {
  34109. editor.selection.setRng(range);
  34110. }
  34111. else {
  34112. editor.selection.placeCaretAt(clientX, clientY);
  34113. }
  34114. });
  34115. };
  34116. const dispatchDragEvent = (editor, type, target, dataTransfer, mouseEvent) => {
  34117. if (type === 'dragstart') {
  34118. setHtmlData(dataTransfer, editor.dom.getOuterHTML(target));
  34119. }
  34120. const event = makeDragEvent(type, target, dataTransfer, mouseEvent);
  34121. const args = editor.dispatch(type, event);
  34122. return args;
  34123. };
  34124. const move = (state, editor) => {
  34125. // Reduces laggy drag behavior on Gecko
  34126. const throttledPlaceCaretAt = first$1((clientX, clientY) => placeCaretAt(editor, clientX, clientY), 0);
  34127. editor.on('remove', throttledPlaceCaretAt.cancel);
  34128. const state_ = state;
  34129. return (e) => state.on((state) => {
  34130. const movement = Math.max(Math.abs(e.screenX - state.screenX), Math.abs(e.screenY - state.screenY));
  34131. if (!state.dragging && movement > 10) {
  34132. const args = dispatchDragEvent(editor, 'dragstart', state.element, state.dataTransfer, e);
  34133. // TINY-9601: dataTransfer is writable in dragstart, so keep it up-to-date
  34134. if (isNonNullable(args.dataTransfer)) {
  34135. state.dataTransfer = args.dataTransfer;
  34136. }
  34137. if (args.isDefaultPrevented()) {
  34138. return;
  34139. }
  34140. state.dragging = true;
  34141. editor.focus();
  34142. }
  34143. if (state.dragging) {
  34144. const mouseEventOriginatedFromWithinTheEditor = e.currentTarget === editor.getDoc().documentElement;
  34145. const targetPos = applyRelPos(state, calc(editor, e));
  34146. appendGhostToBody(state.ghost, editor.getBody());
  34147. moveGhost(state.ghost, targetPos, state.width, state.height, state.maxX, state.maxY, e.clientY, e.clientX, editor.getContentAreaContainer(), editor.getWin(), state_, mouseEventOriginatedFromWithinTheEditor);
  34148. throttledPlaceCaretAt.throttle(e.clientX, e.clientY);
  34149. }
  34150. });
  34151. };
  34152. // Returns the raw element instead of the fake cE=false element
  34153. const getRawTarget = (selection) => {
  34154. const sel = selection.getSel();
  34155. if (isNonNullable(sel)) {
  34156. const rng = sel.getRangeAt(0);
  34157. const startContainer = rng.startContainer;
  34158. return isText$b(startContainer) ? startContainer.parentNode : startContainer;
  34159. }
  34160. else {
  34161. return null;
  34162. }
  34163. };
  34164. const drop = (state, editor) => (e) => {
  34165. state.on((state) => {
  34166. var _a;
  34167. state.intervalId.clear();
  34168. if (state.dragging) {
  34169. if (isValidDropTarget(editor, getRawTarget(editor.selection), state.element)) {
  34170. const dropTarget = (_a = editor.getDoc().elementFromPoint(e.clientX, e.clientY)) !== null && _a !== void 0 ? _a : editor.getBody();
  34171. const args = dispatchDragEvent(editor, 'drop', dropTarget, state.dataTransfer, e);
  34172. if (!args.isDefaultPrevented()) {
  34173. editor.undoManager.transact(() => {
  34174. removeElementWithPadding(editor.dom, state.element);
  34175. // TINY-9601: Use dataTransfer property to determine inserted content on drop. This allows users to
  34176. // manipulate drop content by modifying dataTransfer in the dragstart event.
  34177. getHtmlData(state.dataTransfer).each((content) => editor.insertContent(content));
  34178. editor._selectionOverrides.hideFakeCaret();
  34179. });
  34180. }
  34181. }
  34182. // Use body as the target since the element we are dragging no longer exists. Native drag/drop works in a similar way.
  34183. dispatchDragEvent(editor, 'dragend', editor.getBody(), state.dataTransfer, e);
  34184. }
  34185. });
  34186. removeDragState(state);
  34187. };
  34188. const stopDragging = (state, editor, e) => {
  34189. state.on((state) => {
  34190. state.intervalId.clear();
  34191. if (state.dragging) {
  34192. e.fold(() => dispatchDragEvent(editor, 'dragend', state.element, state.dataTransfer), (mouseEvent) => dispatchDragEvent(editor, 'dragend', state.element, state.dataTransfer, mouseEvent));
  34193. }
  34194. });
  34195. removeDragState(state);
  34196. };
  34197. const stop = (state, editor) => (e) => stopDragging(state, editor, Optional.some(e));
  34198. const removeDragState = (state) => {
  34199. state.on((state) => {
  34200. state.intervalId.clear();
  34201. removeElement(state.ghost);
  34202. });
  34203. state.clear();
  34204. };
  34205. const bindFakeDragEvents = (editor) => {
  34206. const state = value$1();
  34207. const pageDom = DOMUtils.DOM;
  34208. const rootDocument = document;
  34209. const dragStartHandler = start(state, editor);
  34210. const dragHandler = move(state, editor);
  34211. const dropHandler = drop(state, editor);
  34212. const dragEndHandler = stop(state, editor);
  34213. editor.on('mousedown', dragStartHandler);
  34214. editor.on('mousemove', dragHandler);
  34215. editor.on('mouseup', dropHandler);
  34216. pageDom.bind(rootDocument, 'mousemove', dragHandler);
  34217. pageDom.bind(rootDocument, 'mouseup', dragEndHandler);
  34218. editor.on('remove', () => {
  34219. pageDom.unbind(rootDocument, 'mousemove', dragHandler);
  34220. pageDom.unbind(rootDocument, 'mouseup', dragEndHandler);
  34221. });
  34222. editor.on('keydown', (e) => {
  34223. // Fire 'dragend' when the escape key is pressed
  34224. if (e.keyCode === VK.ESC) {
  34225. stopDragging(state, editor, Optional.none());
  34226. }
  34227. });
  34228. };
  34229. // Block files being dropped within the editor to prevent accidentally navigating away
  34230. // while editing. Note that we can't use the `editor.on` API here, as we want these
  34231. // to run after the editor event handlers have run. We also bind to the document
  34232. // so that it'll try to ensure it's the last thing that runs, as it bubbles up the dom.
  34233. const blockUnsupportedFileDrop = (editor) => {
  34234. const preventFileDrop = (e) => {
  34235. if (!e.isDefaultPrevented()) {
  34236. // Prevent file drop events within the editor, as they'll cause the browser to navigate away
  34237. const dataTransfer = e.dataTransfer;
  34238. if (dataTransfer && (contains$2(dataTransfer.types, 'Files') || dataTransfer.files.length > 0)) {
  34239. e.preventDefault();
  34240. if (e.type === 'drop') {
  34241. displayError(editor, 'Dropped file type is not supported');
  34242. }
  34243. }
  34244. }
  34245. };
  34246. const preventFileDropIfUIElement = (e) => {
  34247. if (isUIElement(editor, e.target)) {
  34248. preventFileDrop(e);
  34249. }
  34250. };
  34251. const setup = () => {
  34252. const pageDom = DOMUtils.DOM;
  34253. const dom = editor.dom;
  34254. const doc = document;
  34255. const editorRoot = editor.inline ? editor.getBody() : editor.getDoc();
  34256. const eventNames = ['drop', 'dragover'];
  34257. each$e(eventNames, (name) => {
  34258. pageDom.bind(doc, name, preventFileDropIfUIElement);
  34259. dom.bind(editorRoot, name, preventFileDrop);
  34260. });
  34261. editor.on('remove', () => {
  34262. each$e(eventNames, (name) => {
  34263. pageDom.unbind(doc, name, preventFileDropIfUIElement);
  34264. dom.unbind(editorRoot, name, preventFileDrop);
  34265. });
  34266. });
  34267. };
  34268. editor.on('init', () => {
  34269. // Use a timeout to ensure this fires after all other init callbacks
  34270. Delay.setEditorTimeout(editor, setup, 0);
  34271. });
  34272. };
  34273. const init$2 = (editor) => {
  34274. bindFakeDragEvents(editor);
  34275. if (shouldBlockUnsupportedDrop(editor)) {
  34276. blockUnsupportedFileDrop(editor);
  34277. }
  34278. };
  34279. const setup$4 = (editor) => {
  34280. const renderFocusCaret = first$1(() => {
  34281. // AP-24 Added the second condition in this if because of a race condition with setting focus on the PowerPaste
  34282. // remove/keep formatting dialog on paste in IE11. Without this, because we paste twice on IE11, focus ends up set
  34283. // in the editor, not the dialog buttons. Specifically, we focus, blur, focus, blur, focus then enter this throttled
  34284. // code before the next blur has been able to run. With this check, this function doesn't run at all in this case,
  34285. // so focus goes to the dialog's buttons correctly.
  34286. if (!editor.removed && editor.getBody().contains(document.activeElement)) {
  34287. const rng = editor.selection.getRng();
  34288. if (rng.collapsed) { // see TINY-1479
  34289. const caretRange = renderRangeCaret(editor, rng, false);
  34290. editor.selection.setRng(caretRange);
  34291. }
  34292. }
  34293. }, 0);
  34294. editor.on('focus', () => {
  34295. renderFocusCaret.throttle();
  34296. });
  34297. editor.on('blur', () => {
  34298. renderFocusCaret.cancel();
  34299. });
  34300. };
  34301. const setup$3 = (editor) => {
  34302. editor.on('init', () => {
  34303. // Audio elements don't fire mousedown/click events and only fire a focus event so
  34304. // we need to capture that event being fired and use it to update the selection.
  34305. editor.on('focusin', (e) => {
  34306. const target = e.target;
  34307. if (isMedia$2(target)) {
  34308. const ceRoot = getContentEditableRoot$1(editor.getBody(), target);
  34309. const node = isContentEditableFalse$a(ceRoot) ? ceRoot : target;
  34310. if (editor.selection.getNode() !== node) {
  34311. selectNode(editor, node).each((rng) => editor.selection.setRng(rng));
  34312. }
  34313. }
  34314. });
  34315. });
  34316. };
  34317. const isContentEditableFalse = isContentEditableFalse$a;
  34318. const getContentEditableRoot = (editor, node) => getContentEditableRoot$1(editor.getBody(), node);
  34319. const SelectionOverrides = (editor) => {
  34320. const selection = editor.selection, dom = editor.dom;
  34321. const rootNode = editor.getBody();
  34322. const fakeCaret = FakeCaret(editor, rootNode, dom.isBlock, () => hasFocus(editor));
  34323. const realSelectionId = 'sel-' + dom.uniqueId();
  34324. const elementSelectionAttr = 'data-mce-selected';
  34325. let selectedElement;
  34326. const isFakeSelectionElement = (node) => isNonNullable(node) && dom.hasClass(node, 'mce-offscreen-selection');
  34327. // Note: isChildOf will return true if node === rootNode, so we need an additional check for that
  34328. const isFakeSelectionTargetElement = (node) => node !== rootNode && (isContentEditableFalse(node) || isMedia$2(node)) && dom.isChildOf(node, rootNode) && dom.isEditable(node.parentNode);
  34329. const setRange = (range) => {
  34330. if (range) {
  34331. selection.setRng(range);
  34332. }
  34333. };
  34334. const showCaret = (direction, node, before, scrollIntoView = true) => {
  34335. const e = editor.dispatch('ShowCaret', {
  34336. target: node,
  34337. direction,
  34338. before
  34339. });
  34340. if (e.isDefaultPrevented()) {
  34341. return null;
  34342. }
  34343. if (scrollIntoView) {
  34344. selection.scrollIntoView(node, direction === -1);
  34345. }
  34346. return fakeCaret.show(before, node);
  34347. };
  34348. const showBlockCaretContainer = (blockCaretContainer) => {
  34349. if (blockCaretContainer.hasAttribute('data-mce-caret')) {
  34350. showCaretContainerBlock(blockCaretContainer);
  34351. selection.scrollIntoView(blockCaretContainer);
  34352. }
  34353. };
  34354. const registerEvents = () => {
  34355. editor.on('click', (e) => {
  34356. // Prevent clicks on links in a cE=false element
  34357. if (!dom.isEditable(e.target)) {
  34358. e.preventDefault();
  34359. editor.focus();
  34360. }
  34361. });
  34362. editor.on('blur NewBlock', removeElementSelection);
  34363. editor.on('ResizeWindow FullscreenStateChanged', fakeCaret.reposition);
  34364. editor.on('tap', (e) => {
  34365. const targetElm = e.target;
  34366. const contentEditableRoot = getContentEditableRoot(editor, targetElm);
  34367. if (isContentEditableFalse(contentEditableRoot)) {
  34368. e.preventDefault();
  34369. selectNode(editor, contentEditableRoot).each(setElementSelection);
  34370. }
  34371. else if (isFakeSelectionTargetElement(targetElm)) {
  34372. selectNode(editor, targetElm).each(setElementSelection);
  34373. }
  34374. }, true);
  34375. editor.on('mousedown', (e) => {
  34376. const targetElm = e.target;
  34377. if (targetElm !== rootNode && targetElm.nodeName !== 'HTML' && !dom.isChildOf(targetElm, rootNode)) {
  34378. return;
  34379. }
  34380. if (!isXYInContentArea(editor, e.clientX, e.clientY)) {
  34381. return;
  34382. }
  34383. // Remove needs to be called here since the mousedown might alter the selection without calling selection.setRng
  34384. // and therefore not fire the AfterSetSelectionRange event.
  34385. removeElementSelection();
  34386. hideFakeCaret();
  34387. const closestContentEditable = getContentEditableRoot(editor, targetElm);
  34388. if (isContentEditableFalse(closestContentEditable)) {
  34389. e.preventDefault();
  34390. selectNode(editor, closestContentEditable).each(setElementSelection);
  34391. }
  34392. else {
  34393. closestFakeCaretCandidate(rootNode, e.clientX, e.clientY).each((caretInfo) => {
  34394. e.preventDefault();
  34395. const range = showCaret(1, caretInfo.node, caretInfo.position === FakeCaretPosition.Before, false);
  34396. setRange(range);
  34397. // Set the focus after the range has been set to avoid potential issues where the body has no selection
  34398. if (isHTMLElement(closestContentEditable)) {
  34399. closestContentEditable.focus();
  34400. }
  34401. else {
  34402. editor.getBody().focus();
  34403. }
  34404. });
  34405. }
  34406. });
  34407. editor.on('keypress', (e) => {
  34408. if (VK.modifierPressed(e)) {
  34409. return;
  34410. }
  34411. if (isContentEditableFalse(selection.getNode())) {
  34412. e.preventDefault();
  34413. }
  34414. });
  34415. editor.on('GetSelectionRange', (e) => {
  34416. let rng = e.range;
  34417. if (selectedElement) {
  34418. if (!selectedElement.parentNode) {
  34419. selectedElement = null;
  34420. return;
  34421. }
  34422. rng = rng.cloneRange();
  34423. rng.selectNode(selectedElement);
  34424. e.range = rng;
  34425. }
  34426. });
  34427. editor.on('focusin', (e) => {
  34428. // for medias the selection is already managed in `MediaFocus.ts`
  34429. if (editor.selection.isCollapsed() && !isMedia$2(e.target) && editor.getBody().contains(e.target) && e.target !== editor.getBody() && !editor.dom.isEditable(e.target.parentNode)) {
  34430. if (fakeCaret.isShowing()) {
  34431. fakeCaret.hide();
  34432. }
  34433. if (!e.target.contains(editor.selection.getNode())) {
  34434. editor.selection.select(e.target, true);
  34435. editor.selection.collapse(true);
  34436. }
  34437. const rng = setElementSelection(editor.selection.getRng(), true);
  34438. if (rng) {
  34439. editor.selection.setRng(rng);
  34440. }
  34441. }
  34442. });
  34443. editor.on('SetSelectionRange', (e) => {
  34444. // If the range is set inside a short ended element, then move it
  34445. // to the side as IE for example will try to add content inside
  34446. e.range = normalizeVoidElementSelection(e.range);
  34447. const rng = setElementSelection(e.range, e.forward);
  34448. if (rng) {
  34449. e.range = rng;
  34450. }
  34451. });
  34452. const isPasteBin = (node) => isElement$7(node) && node.id === 'mcepastebin';
  34453. editor.on('AfterSetSelectionRange', (e) => {
  34454. const rng = e.range;
  34455. const parent = rng.startContainer.parentElement;
  34456. if (!isRangeInCaretContainer(rng) && !isPasteBin(parent)) {
  34457. hideFakeCaret();
  34458. }
  34459. if (!isFakeSelectionElement(parent)) {
  34460. removeElementSelection();
  34461. }
  34462. });
  34463. init$2(editor);
  34464. setup$4(editor);
  34465. setup$3(editor);
  34466. };
  34467. const isWithinCaretContainer = (node) => (isCaretContainer$2(node) ||
  34468. startsWithCaretContainer$1(node) ||
  34469. endsWithCaretContainer$1(node));
  34470. const isRangeInCaretContainer = (rng) => isWithinCaretContainer(rng.startContainer) || isWithinCaretContainer(rng.endContainer);
  34471. const normalizeVoidElementSelection = (rng) => {
  34472. const voidElements = editor.schema.getVoidElements();
  34473. const newRng = dom.createRng();
  34474. const startContainer = rng.startContainer;
  34475. const startOffset = rng.startOffset;
  34476. const endContainer = rng.endContainer;
  34477. const endOffset = rng.endOffset;
  34478. if (has$2(voidElements, startContainer.nodeName.toLowerCase())) {
  34479. if (startOffset === 0) {
  34480. newRng.setStartBefore(startContainer);
  34481. }
  34482. else {
  34483. newRng.setStartAfter(startContainer);
  34484. }
  34485. }
  34486. else {
  34487. newRng.setStart(startContainer, startOffset);
  34488. }
  34489. if (has$2(voidElements, endContainer.nodeName.toLowerCase())) {
  34490. if (endOffset === 0) {
  34491. newRng.setEndBefore(endContainer);
  34492. }
  34493. else {
  34494. newRng.setEndAfter(endContainer);
  34495. }
  34496. }
  34497. else {
  34498. newRng.setEnd(endContainer, endOffset);
  34499. }
  34500. return newRng;
  34501. };
  34502. const setupOffscreenSelection = (node, targetClone) => {
  34503. const body = SugarElement.fromDom(editor.getBody());
  34504. const doc = editor.getDoc();
  34505. const realSelectionContainer = descendant$1(body, '#' + realSelectionId).getOrThunk(() => {
  34506. const newContainer = SugarElement.fromHtml('<div data-mce-bogus="all" class="mce-offscreen-selection"></div>', doc);
  34507. set$4(newContainer, 'id', realSelectionId);
  34508. append$1(body, newContainer);
  34509. return newContainer;
  34510. });
  34511. const newRange = dom.createRng();
  34512. empty(realSelectionContainer);
  34513. append(realSelectionContainer, [
  34514. SugarElement.fromText(nbsp, doc),
  34515. SugarElement.fromDom(targetClone),
  34516. SugarElement.fromText(nbsp, doc)
  34517. ]);
  34518. newRange.setStart(realSelectionContainer.dom.firstChild, 1);
  34519. newRange.setEnd(realSelectionContainer.dom.lastChild, 0);
  34520. setAll(realSelectionContainer, {
  34521. top: dom.getPos(node, editor.getBody()).y + 'px'
  34522. });
  34523. focus$1(realSelectionContainer);
  34524. const sel = selection.getSel();
  34525. if (sel) {
  34526. sel.removeAllRanges();
  34527. sel.addRange(newRange);
  34528. }
  34529. return newRange;
  34530. };
  34531. const selectElement = (elm) => {
  34532. const targetClone = elm.cloneNode(true);
  34533. const e = editor.dispatch('ObjectSelected', { target: elm, targetClone });
  34534. if (e.isDefaultPrevented()) {
  34535. return null;
  34536. }
  34537. // Setup the offscreen selection
  34538. const range = setupOffscreenSelection(elm, e.targetClone);
  34539. // We used to just remove all data-mce-selected values and set 1 on node.
  34540. // But data-mce-selected can be values other than 1 so keep existing value if
  34541. // node has one, and remove data-mce-selected from everything else
  34542. const nodeElm = SugarElement.fromDom(elm);
  34543. each$e(descendants(SugarElement.fromDom(editor.getBody()), `*[${elementSelectionAttr}]`), (elm) => {
  34544. if (!eq(nodeElm, elm)) {
  34545. remove$9(elm, elementSelectionAttr);
  34546. }
  34547. });
  34548. if (!dom.getAttrib(elm, elementSelectionAttr)) {
  34549. elm.setAttribute(elementSelectionAttr, '1');
  34550. }
  34551. selectedElement = elm;
  34552. hideFakeCaret();
  34553. return range;
  34554. };
  34555. const setElementSelection = (range, forward) => {
  34556. if (!range) {
  34557. return null;
  34558. }
  34559. if (range.collapsed) {
  34560. if (!isRangeInCaretContainer(range)) {
  34561. const dir = forward ? 1 : -1;
  34562. const caretPosition = getNormalizedRangeEndPoint(dir, rootNode, range);
  34563. const beforeNode = caretPosition.getNode(!forward);
  34564. if (isNonNullable(beforeNode)) {
  34565. if (isFakeCaretTarget(beforeNode)) {
  34566. return showCaret(dir, beforeNode, forward ? !caretPosition.isAtEnd() : false, false);
  34567. }
  34568. if (isCaretContainerInline(beforeNode) && isContentEditableFalse$a(beforeNode.nextSibling)) {
  34569. const rng = dom.createRng();
  34570. rng.setStart(beforeNode, 0);
  34571. rng.setEnd(beforeNode, 0);
  34572. return rng;
  34573. }
  34574. }
  34575. const afterNode = caretPosition.getNode(forward);
  34576. if (isNonNullable(afterNode)) {
  34577. if (isFakeCaretTarget(afterNode)) {
  34578. return showCaret(dir, afterNode, forward ? false : !caretPosition.isAtEnd(), false);
  34579. }
  34580. if (isCaretContainerInline(afterNode) && isContentEditableFalse$a(afterNode.previousSibling)) {
  34581. const rng = dom.createRng();
  34582. rng.setStart(afterNode, 1);
  34583. rng.setEnd(afterNode, 1);
  34584. return rng;
  34585. }
  34586. }
  34587. }
  34588. return null;
  34589. }
  34590. let startContainer = range.startContainer;
  34591. let startOffset = range.startOffset;
  34592. const endOffset = range.endOffset;
  34593. // Normalizes <span cE=false>[</span>] to [<span cE=false></span>]
  34594. if (isText$b(startContainer) && startOffset === 0 && isContentEditableFalse(startContainer.parentNode)) {
  34595. startContainer = startContainer.parentNode;
  34596. startOffset = dom.nodeIndex(startContainer);
  34597. startContainer = startContainer.parentNode;
  34598. }
  34599. if (!isElement$7(startContainer)) {
  34600. return null;
  34601. }
  34602. if (endOffset === startOffset + 1 && startContainer === range.endContainer) {
  34603. const node = startContainer.childNodes[startOffset];
  34604. if (isFakeSelectionTargetElement(node)) {
  34605. return selectElement(node);
  34606. }
  34607. }
  34608. return null;
  34609. };
  34610. const removeElementSelection = () => {
  34611. if (selectedElement) {
  34612. selectedElement.removeAttribute(elementSelectionAttr);
  34613. }
  34614. descendant$1(SugarElement.fromDom(editor.getBody()), '#' + realSelectionId).each(remove$8);
  34615. selectedElement = null;
  34616. };
  34617. const destroy = () => {
  34618. fakeCaret.destroy();
  34619. selectedElement = null;
  34620. };
  34621. const hideFakeCaret = () => {
  34622. fakeCaret.hide();
  34623. };
  34624. if (!isRtc(editor)) {
  34625. registerEvents();
  34626. }
  34627. return {
  34628. showCaret,
  34629. showBlockCaretContainer,
  34630. hideFakeCaret,
  34631. destroy
  34632. };
  34633. };
  34634. const getNormalizedTextOffset = (container, offset) => {
  34635. let normalizedOffset = offset;
  34636. for (let node = container.previousSibling; isText$b(node); node = node.previousSibling) {
  34637. normalizedOffset += node.data.length;
  34638. }
  34639. return normalizedOffset;
  34640. };
  34641. const generatePath = (dom, root, node, offset, normalized) => {
  34642. if (isText$b(node) && (offset < 0 || offset > node.data.length)) {
  34643. return [];
  34644. }
  34645. const p = normalized && isText$b(node) ? [getNormalizedTextOffset(node, offset)] : [offset];
  34646. let current = node;
  34647. while (current !== root && current.parentNode) {
  34648. p.push(dom.nodeIndex(current, normalized));
  34649. current = current.parentNode;
  34650. }
  34651. return current === root ? p.reverse() : [];
  34652. };
  34653. const generatePathRange = (dom, root, startNode, startOffset, endNode, endOffset, normalized = false) => {
  34654. const start = generatePath(dom, root, startNode, startOffset, normalized);
  34655. const end = generatePath(dom, root, endNode, endOffset, normalized);
  34656. return { start, end };
  34657. };
  34658. const resolvePath = (root, path) => {
  34659. const nodePath = path.slice();
  34660. const offset = nodePath.pop();
  34661. if (!isNumber(offset)) {
  34662. return Optional.none();
  34663. }
  34664. else {
  34665. const resolvedNode = foldl(nodePath, (optNode, index) => optNode.bind((node) => Optional.from(node.childNodes[index])), Optional.some(root));
  34666. return resolvedNode.bind((node) => {
  34667. if (isText$b(node) && (offset < 0 || offset > node.data.length)) {
  34668. return Optional.none();
  34669. }
  34670. else {
  34671. return Optional.some({ node, offset });
  34672. }
  34673. });
  34674. }
  34675. };
  34676. const resolvePathRange = (root, range) => resolvePath(root, range.start)
  34677. .bind(({ node: startNode, offset: startOffset }) => resolvePath(root, range.end).map(({ node: endNode, offset: endOffset }) => {
  34678. const rng = document.createRange();
  34679. rng.setStart(startNode, startOffset);
  34680. rng.setEnd(endNode, endOffset);
  34681. return rng;
  34682. }));
  34683. const generatePathRangeFromRange = (dom, root, range, normalized = false) => generatePathRange(dom, root, range.startContainer, range.startOffset, range.endContainer, range.endOffset, normalized);
  34684. const cleanEmptyNodes = (dom, node, isRoot) => {
  34685. // Recursively walk up the tree while we have a parent and the node is empty. If the node is empty, then remove it.
  34686. if (node && dom.isEmpty(node) && !isRoot(node)) {
  34687. const parent = node.parentNode;
  34688. dom.remove(node, isText$b(node.firstChild) && isWhitespaceText(node.firstChild.data));
  34689. cleanEmptyNodes(dom, parent, isRoot);
  34690. }
  34691. };
  34692. const deleteRng = (dom, rng, isRoot, clean = true) => {
  34693. const startParent = rng.startContainer.parentNode;
  34694. const endParent = rng.endContainer.parentNode;
  34695. rng.deleteContents();
  34696. // Clean up any empty nodes if required
  34697. if (clean && !isRoot(rng.startContainer)) {
  34698. if (isText$b(rng.startContainer) && rng.startContainer.data.length === 0) {
  34699. dom.remove(rng.startContainer);
  34700. }
  34701. if (isText$b(rng.endContainer) && rng.endContainer.data.length === 0) {
  34702. dom.remove(rng.endContainer);
  34703. }
  34704. cleanEmptyNodes(dom, startParent, isRoot);
  34705. if (startParent !== endParent) {
  34706. cleanEmptyNodes(dom, endParent, isRoot);
  34707. }
  34708. }
  34709. };
  34710. const getParentBlock = (editor, rng) => Optional.from(editor.dom.getParent(rng.startContainer, editor.dom.isBlock));
  34711. const resolveFromDynamicPatterns = (patternSet, block, beforeText) => {
  34712. const dynamicPatterns = patternSet.dynamicPatternsLookup({
  34713. text: beforeText,
  34714. block
  34715. });
  34716. // dynamic patterns take precedence here
  34717. return {
  34718. ...patternSet,
  34719. blockPatterns: getBlockPatterns(dynamicPatterns).concat(patternSet.blockPatterns),
  34720. inlinePatterns: getInlinePatterns(dynamicPatterns).concat(patternSet.inlinePatterns)
  34721. };
  34722. };
  34723. const getBeforeText = (dom, block, node, offset) => {
  34724. const rng = dom.createRng();
  34725. rng.setStart(block, 0);
  34726. rng.setEnd(node, offset);
  34727. return rng.toString();
  34728. };
  34729. const newMarker = (dom, id) => dom.create('span', { 'data-mce-type': 'bookmark', id });
  34730. const rangeFromMarker = (dom, marker) => {
  34731. const rng = dom.createRng();
  34732. rng.setStartAfter(marker.start);
  34733. rng.setEndBefore(marker.end);
  34734. return rng;
  34735. };
  34736. const createMarker = (dom, markerPrefix, pathRange) => {
  34737. // Resolve the path range
  34738. const rng = resolvePathRange(dom.getRoot(), pathRange).getOrDie('Unable to resolve path range');
  34739. const startNode = rng.startContainer;
  34740. const endNode = rng.endContainer;
  34741. // Create the marker
  34742. const textEnd = rng.endOffset === 0 ? endNode : endNode.splitText(rng.endOffset);
  34743. const textStart = rng.startOffset === 0 ? startNode : startNode.splitText(rng.startOffset);
  34744. const startParentNode = textStart.parentNode;
  34745. const endParentNode = textEnd.parentNode;
  34746. return {
  34747. prefix: markerPrefix,
  34748. end: endParentNode.insertBefore(newMarker(dom, markerPrefix + '-end'), textEnd),
  34749. start: startParentNode.insertBefore(newMarker(dom, markerPrefix + '-start'), textStart)
  34750. };
  34751. };
  34752. const removeMarker = (dom, marker, isRoot) => {
  34753. // Note: Use dom.get() here instead of marker.end/start, as applying the format/command can
  34754. // clone the nodes meaning the old reference isn't usable
  34755. cleanEmptyNodes(dom, dom.get(marker.prefix + '-end'), isRoot);
  34756. cleanEmptyNodes(dom, dom.get(marker.prefix + '-start'), isRoot);
  34757. };
  34758. const isReplacementPattern = (pattern) => pattern.start.length === 0;
  34759. const matchesPattern = (patternContent) => (element, offset) => {
  34760. const text = element.data;
  34761. const searchText = text.substring(0, offset);
  34762. const startEndIndex = searchText.lastIndexOf(patternContent.charAt(patternContent.length - 1));
  34763. const startIndex = searchText.lastIndexOf(patternContent);
  34764. if (startIndex !== -1) {
  34765. // Complete string found
  34766. return startIndex + patternContent.length;
  34767. }
  34768. else if (startEndIndex !== -1) {
  34769. // Potential partial string found
  34770. return startEndIndex + 1;
  34771. }
  34772. else {
  34773. // No match in current node, so continue
  34774. return -1;
  34775. }
  34776. };
  34777. const findPatternStartFromSpot = (dom, pattern, block, spot) => {
  34778. const startPattern = pattern.start;
  34779. const startSpot = repeatLeft(dom, spot.container, spot.offset, matchesPattern(startPattern), block);
  34780. return startSpot.bind((spot) => {
  34781. var _a, _b;
  34782. const startPatternIndex = (_b = (_a = block.textContent) === null || _a === void 0 ? void 0 : _a.indexOf(startPattern)) !== null && _b !== void 0 ? _b : -1;
  34783. const isCompleteMatch = startPatternIndex !== -1 && spot.offset >= startPatternIndex + startPattern.length;
  34784. if (isCompleteMatch) {
  34785. // Complete match
  34786. const rng = dom.createRng();
  34787. rng.setStart(spot.container, spot.offset - startPattern.length);
  34788. rng.setEnd(spot.container, spot.offset);
  34789. return Optional.some(rng);
  34790. }
  34791. else {
  34792. // Partial match so lean left to see if the string exists over fragmented text nodes
  34793. const offset = spot.offset - startPattern.length;
  34794. return scanLeft(spot.container, offset, block).map((nextSpot) => {
  34795. // Build up the range between the last char and the first char
  34796. const rng = dom.createRng();
  34797. rng.setStart(nextSpot.container, nextSpot.offset);
  34798. rng.setEnd(spot.container, spot.offset);
  34799. return rng;
  34800. }).filter((rng) =>
  34801. // Ensure the range content matches the start
  34802. rng.toString() === startPattern).orThunk(() =>
  34803. // No match found, so continue searching
  34804. findPatternStartFromSpot(dom, pattern, block, point(spot.container, 0)));
  34805. }
  34806. });
  34807. };
  34808. const findPatternStart = (dom, pattern, node, offset, block, requireGap = false) => {
  34809. if (pattern.start.length === 0 && !requireGap) {
  34810. const rng = dom.createRng();
  34811. rng.setStart(node, offset);
  34812. rng.setEnd(node, offset);
  34813. return Optional.some(rng);
  34814. }
  34815. return textBefore(node, offset, block).bind((spot) => {
  34816. const start = findPatternStartFromSpot(dom, pattern, block, spot);
  34817. return start.bind((startRange) => {
  34818. var _a;
  34819. if (requireGap) {
  34820. if (startRange.endContainer === spot.container && startRange.endOffset === spot.offset) {
  34821. return Optional.none();
  34822. }
  34823. else if (spot.offset === 0 && ((_a = startRange.endContainer.textContent) === null || _a === void 0 ? void 0 : _a.length) === startRange.endOffset) {
  34824. return Optional.none();
  34825. }
  34826. }
  34827. return Optional.some(startRange);
  34828. });
  34829. });
  34830. };
  34831. const findPattern$3 = (editor, block, details, normalizedMatches) => {
  34832. const dom = editor.dom;
  34833. const root = dom.getRoot();
  34834. const pattern = details.pattern;
  34835. const endNode = details.position.container;
  34836. const endOffset = details.position.offset;
  34837. // Lean left to find the start of the end pattern, as it could be across fragmented nodes
  34838. return scanLeft(endNode, endOffset - details.pattern.end.length, block).bind((spot) => {
  34839. const endPathRng = generatePathRange(dom, root, spot.container, spot.offset, endNode, endOffset, normalizedMatches);
  34840. // If we have a replacement pattern, then it can't have nested patterns so just return immediately
  34841. if (isReplacementPattern(pattern)) {
  34842. return Optional.some({
  34843. matches: [{
  34844. pattern,
  34845. startRng: endPathRng,
  34846. endRng: endPathRng
  34847. }],
  34848. position: spot
  34849. });
  34850. }
  34851. else {
  34852. // Find any nested patterns, making sure not to process the current pattern again
  34853. const resultsOpt = findPatternsRec(editor, details.remainingPatterns, spot.container, spot.offset, block, normalizedMatches);
  34854. const results = resultsOpt.getOr({ matches: [], position: spot });
  34855. const pos = results.position;
  34856. // Find the start of the matched pattern
  34857. const start = findPatternStart(dom, pattern, pos.container, pos.offset, block, resultsOpt.isNone());
  34858. return start.map((startRng) => {
  34859. const startPathRng = generatePathRangeFromRange(dom, root, startRng, normalizedMatches);
  34860. return {
  34861. matches: results.matches.concat([{
  34862. pattern,
  34863. startRng: startPathRng,
  34864. endRng: endPathRng
  34865. }]),
  34866. position: point(startRng.startContainer, startRng.startOffset)
  34867. };
  34868. });
  34869. }
  34870. });
  34871. };
  34872. // Assumptions:
  34873. // 0. Patterns are sorted by priority so we should preferentially match earlier entries
  34874. // 1. Patterns may be nested but may only occur once
  34875. // 2. Patterns will not have matching prefixes which contain space or standard punctuation ',', '.', ';', ':', '!', '?'
  34876. // 3. Patterns will not extend outside of the root element
  34877. // 4. All pattern ends must be directly before the cursor (represented by node + offset)
  34878. // 5. Only text nodes matter
  34879. const findPatternsRec = (editor, patterns, node, offset, block, normalizedMatches) => {
  34880. const dom = editor.dom;
  34881. return textBefore(node, offset, dom.getRoot()).bind((endSpot) => {
  34882. const text = getBeforeText(dom, block, node, offset);
  34883. for (let i = 0; i < patterns.length; i++) {
  34884. const pattern = patterns[i];
  34885. // If the text does not end with the same string as the pattern, then we can exit
  34886. // early, because this pattern isn't going to match this text. This saves us doing more
  34887. // expensive matching calls.
  34888. if (!endsWith(text, pattern.end)) {
  34889. continue;
  34890. }
  34891. // Generate a new array without the current pattern
  34892. const patternsWithoutCurrent = patterns.slice();
  34893. patternsWithoutCurrent.splice(i, 1);
  34894. // Try to find the current pattern
  34895. const result = findPattern$3(editor, block, {
  34896. pattern,
  34897. remainingPatterns: patternsWithoutCurrent,
  34898. position: endSpot
  34899. }, normalizedMatches);
  34900. if (result.isNone() && offset > 0) {
  34901. return findPatternsRec(editor, patterns, node, offset - 1, block, normalizedMatches);
  34902. }
  34903. // If a match was found then return that
  34904. if (result.isSome()) {
  34905. return result;
  34906. }
  34907. }
  34908. return Optional.none();
  34909. });
  34910. };
  34911. const applyPattern$2 = (editor, pattern, patternRange) => {
  34912. editor.selection.setRng(patternRange);
  34913. if (pattern.type === 'inline-format') {
  34914. each$e(pattern.format, (format) => {
  34915. editor.formatter.apply(format);
  34916. });
  34917. }
  34918. else {
  34919. editor.execCommand(pattern.cmd, false, pattern.value);
  34920. }
  34921. };
  34922. const applyReplacementPattern = (editor, pattern, marker, isRoot) => {
  34923. // Remove the original text
  34924. const markerRange = rangeFromMarker(editor.dom, marker);
  34925. deleteRng(editor.dom, markerRange, isRoot);
  34926. // Apply the replacement
  34927. applyPattern$2(editor, pattern, markerRange);
  34928. };
  34929. const applyPatternWithContent = (editor, pattern, startMarker, endMarker, isRoot) => {
  34930. const dom = editor.dom;
  34931. // Create the marker ranges for the patterns start/end content
  34932. const markerEndRange = rangeFromMarker(dom, endMarker);
  34933. const markerStartRange = rangeFromMarker(dom, startMarker);
  34934. // Clean up the pattern start/end content
  34935. deleteRng(dom, markerStartRange, isRoot);
  34936. deleteRng(dom, markerEndRange, isRoot);
  34937. // Apply the pattern
  34938. const patternMarker = { prefix: startMarker.prefix, start: startMarker.end, end: endMarker.start };
  34939. const patternRange = rangeFromMarker(dom, patternMarker);
  34940. applyPattern$2(editor, pattern, patternRange);
  34941. };
  34942. const addMarkers = (dom, matches) => {
  34943. const markerPrefix = generate$1('mce_textpattern');
  34944. // Add end markers
  34945. const matchesWithEnds = foldr(matches, (acc, match) => {
  34946. const endMarker = createMarker(dom, markerPrefix + `_end${acc.length}`, match.endRng);
  34947. return acc.concat([{
  34948. ...match,
  34949. endMarker
  34950. }]);
  34951. }, []);
  34952. // Add start markers
  34953. return foldr(matchesWithEnds, (acc, match) => {
  34954. const idx = matchesWithEnds.length - acc.length - 1;
  34955. const startMarker = isReplacementPattern(match.pattern) ? match.endMarker : createMarker(dom, markerPrefix + `_start${idx}`, match.startRng);
  34956. return acc.concat([{
  34957. ...match,
  34958. startMarker
  34959. }]);
  34960. }, []);
  34961. };
  34962. const sortPatterns$1 = (patterns) => sort(patterns, (a, b) => b.end.length - a.end.length);
  34963. const getBestMatches = (matches, matchesWithSortedPatterns) => {
  34964. const hasSameMatches = forall(matches, (match) => exists(matchesWithSortedPatterns, (sortedMatch) => match.pattern.start === sortedMatch.pattern.start && match.pattern.end === sortedMatch.pattern.end));
  34965. if (matches.length === matchesWithSortedPatterns.length) {
  34966. if (hasSameMatches) {
  34967. return matches;
  34968. }
  34969. else {
  34970. return matchesWithSortedPatterns;
  34971. }
  34972. }
  34973. return matches.length > matchesWithSortedPatterns.length ? matches : matchesWithSortedPatterns;
  34974. };
  34975. const findPatterns$2 = (editor, block, node, offset, patternSet, normalizedMatches) => {
  34976. const matches = findPatternsRec(editor, patternSet.inlinePatterns, node, offset, block, normalizedMatches).fold(() => [], (result) => result.matches);
  34977. const matchesWithSortedPatterns = findPatternsRec(editor, sortPatterns$1(patternSet.inlinePatterns), node, offset, block, normalizedMatches).fold(() => [], (result) => result.matches);
  34978. return getBestMatches(matches, matchesWithSortedPatterns);
  34979. };
  34980. const applyMatches$2 = (editor, matches) => {
  34981. if (matches.length === 0) {
  34982. return;
  34983. }
  34984. // Store the current selection
  34985. const dom = editor.dom;
  34986. const bookmark = editor.selection.getBookmark();
  34987. // Add markers for the matched patterns
  34988. const matchesWithMarkers = addMarkers(dom, matches);
  34989. // Do the replacements
  34990. each$e(matchesWithMarkers, (match) => {
  34991. const block = dom.getParent(match.startMarker.start, dom.isBlock);
  34992. const isRoot = (node) => node === block;
  34993. if (isReplacementPattern(match.pattern)) {
  34994. applyReplacementPattern(editor, match.pattern, match.endMarker, isRoot);
  34995. }
  34996. else {
  34997. applyPatternWithContent(editor, match.pattern, match.startMarker, match.endMarker, isRoot);
  34998. }
  34999. // Remove the markers
  35000. removeMarker(dom, match.endMarker, isRoot);
  35001. removeMarker(dom, match.startMarker, isRoot);
  35002. });
  35003. // Restore the selection
  35004. editor.selection.moveToBookmark(bookmark);
  35005. };
  35006. const stripPattern$1 = (dom, block, pattern) => {
  35007. // The pattern could be across fragmented text nodes, so we need to find the end
  35008. // of the pattern and then remove all elements between the start/end range
  35009. return textAfter(block, 0, block).map((spot) => {
  35010. const node = spot.container;
  35011. scanRight(node, pattern.start.length, block).each((end) => {
  35012. const rng = dom.createRng();
  35013. rng.setStart(node, 0);
  35014. rng.setEnd(end.container, end.offset);
  35015. deleteRng(dom, rng, (e) => e === block);
  35016. });
  35017. return node;
  35018. });
  35019. };
  35020. const createApplyPattern = (stripPattern) => (editor, match) => {
  35021. const dom = editor.dom;
  35022. const pattern = match.pattern;
  35023. const rng = resolvePathRange(dom.getRoot(), match.range).getOrDie('Unable to resolve path range');
  35024. const isBlockFormatName = (name, formatter) => {
  35025. const formatSet = formatter.get(name);
  35026. return isArray$1(formatSet) && head(formatSet).exists((format) => has$2(format, 'block'));
  35027. };
  35028. getParentBlock(editor, rng).each((block) => {
  35029. if (pattern.type === 'block-format') {
  35030. if (isBlockFormatName(pattern.format, editor.formatter)) {
  35031. editor.undoManager.transact(() => {
  35032. stripPattern(editor.dom, block, pattern);
  35033. editor.formatter.apply(pattern.format);
  35034. });
  35035. }
  35036. }
  35037. else if (pattern.type === 'block-command') {
  35038. editor.undoManager.transact(() => {
  35039. stripPattern(editor.dom, block, pattern);
  35040. editor.execCommand(pattern.cmd, false, pattern.value);
  35041. });
  35042. }
  35043. });
  35044. return true;
  35045. };
  35046. const sortPatterns = (patterns) => sort(patterns, (a, b) => b.start.length - a.start.length);
  35047. // Finds a matching pattern to the specified text
  35048. const findPattern$2 = (predicate) => (patterns, text) => {
  35049. const sortedPatterns = sortPatterns(patterns);
  35050. const nuText = text.replace(nbsp, ' ');
  35051. return find$2(sortedPatterns, (pattern) => predicate(pattern, text, nuText));
  35052. };
  35053. const createFindPatterns = (findPattern, skipFullMatch) => (editor, block, patternSet, normalizedMatches, text) => {
  35054. var _a;
  35055. if (text === void 0) { text = (_a = block.textContent) !== null && _a !== void 0 ? _a : ''; }
  35056. const dom = editor.dom;
  35057. const forcedRootBlock = getForcedRootBlock(editor);
  35058. if (!dom.is(block, forcedRootBlock)) {
  35059. return [];
  35060. }
  35061. return findPattern(patternSet.blockPatterns, text).map((pattern) => {
  35062. if (skipFullMatch && Tools.trim(text).length === pattern.start.length) {
  35063. return [];
  35064. }
  35065. return [{
  35066. pattern,
  35067. range: generatePathRange(dom, dom.getRoot(), block, 0, block, 0, normalizedMatches)
  35068. }];
  35069. }).getOr([]);
  35070. };
  35071. const startsWithSingleSpace = (s) => /^\s[^\s]/.test(s);
  35072. const stripPattern = (dom, block, pattern) => {
  35073. stripPattern$1(dom, block, pattern).each((node) => {
  35074. /**
  35075. * TINY-9603: If there is a single space between pattern.start and text (e.g. # 1)
  35076. * then it will be left in the text content and then can appear in certain circumstances.
  35077. * This is not an issue with multiple spaces because they are transformed to non-breaking ones.
  35078. *
  35079. * In this specific case we've decided to remove this single space whatsoever
  35080. * as it feels to be the expected behavior.
  35081. */
  35082. const text = SugarElement.fromDom(node);
  35083. const textContent = get$4(text);
  35084. if (startsWithSingleSpace(textContent)) {
  35085. set$1(text, textContent.slice(1));
  35086. }
  35087. });
  35088. };
  35089. const applyPattern$1 = createApplyPattern(stripPattern);
  35090. const findPattern$1 = findPattern$2((pattern, text, nuText) => text.indexOf(pattern.start) === 0 || nuText.indexOf(pattern.start) === 0);
  35091. const findPatterns$1 = createFindPatterns(findPattern$1, true);
  35092. const getMatches$1 = (editor, patternSet) => {
  35093. const rng = editor.selection.getRng();
  35094. return getParentBlock(editor, rng).map((block) => {
  35095. var _a;
  35096. const offset = Math.max(0, rng.startOffset);
  35097. const dynamicPatternSet = resolveFromDynamicPatterns(patternSet, block, (_a = block.textContent) !== null && _a !== void 0 ? _a : '');
  35098. // IMPORTANT: We need to get normalized match results since undoing and redoing the editor state
  35099. // via undoManager.extra() will result in the DOM being normalized.
  35100. const inlineMatches = findPatterns$2(editor, block, rng.startContainer, offset, dynamicPatternSet, true);
  35101. const blockMatches = findPatterns$1(editor, block, dynamicPatternSet, true);
  35102. return { inlineMatches, blockMatches };
  35103. }).filter(({ inlineMatches, blockMatches }) => blockMatches.length > 0 || inlineMatches.length > 0);
  35104. };
  35105. const applyMatches$1 = (editor, matches) => {
  35106. if (matches.length === 0) {
  35107. return;
  35108. }
  35109. // Store the current selection and then apply the matched patterns
  35110. const bookmark = editor.selection.getBookmark();
  35111. each$e(matches, (match) => applyPattern$1(editor, match));
  35112. editor.selection.moveToBookmark(bookmark);
  35113. };
  35114. const applyPattern = createApplyPattern(stripPattern$1);
  35115. const findPattern = findPattern$2((pattern, text, nuText) => text === pattern.start || nuText === pattern.start);
  35116. const findPatterns = createFindPatterns(findPattern, false);
  35117. const getMatches = (editor, patternSet) => {
  35118. const rng = editor.selection.getRng();
  35119. return getParentBlock(editor, rng).map((block) => {
  35120. const offset = Math.max(0, rng.startOffset);
  35121. const beforeText = getBeforeText(editor.dom, block, rng.startContainer, offset);
  35122. const dynamicPatternSet = resolveFromDynamicPatterns(patternSet, block, beforeText);
  35123. return findPatterns(editor, block, dynamicPatternSet, false, beforeText);
  35124. }).filter((matches) => matches.length > 0);
  35125. };
  35126. const applyMatches = (editor, matches) => {
  35127. each$e(matches, (match) => applyPattern(editor, match));
  35128. };
  35129. const handleEnter = (editor, patternSet) => getMatches$1(editor, patternSet).fold(never, ({ inlineMatches, blockMatches }) => {
  35130. editor.undoManager.add();
  35131. editor.undoManager.extra(() => {
  35132. editor.execCommand('mceInsertNewLine');
  35133. }, () => {
  35134. // create a cursor position that we can move to avoid the inline formats
  35135. insert$5(editor);
  35136. applyMatches$2(editor, inlineMatches);
  35137. applyMatches$1(editor, blockMatches);
  35138. // find the spot before the cursor position
  35139. const range = editor.selection.getRng();
  35140. const spot = textBefore(range.startContainer, range.startOffset, editor.dom.getRoot());
  35141. editor.execCommand('mceInsertNewLine');
  35142. // clean up the cursor position we used to preserve the format
  35143. spot.each((s) => {
  35144. const node = s.container;
  35145. if (node.data.charAt(s.offset - 1) === zeroWidth) {
  35146. node.deleteData(s.offset - 1, 1);
  35147. cleanEmptyNodes(editor.dom, node.parentNode, (e) => e === editor.dom.getRoot());
  35148. }
  35149. });
  35150. });
  35151. return true;
  35152. });
  35153. const handleInlineKey = (editor, patternSet) => {
  35154. const rng = editor.selection.getRng();
  35155. getParentBlock(editor, rng).map((block) => {
  35156. const offset = Math.max(0, rng.startOffset - 1);
  35157. const beforeText = getBeforeText(editor.dom, block, rng.startContainer, offset);
  35158. const dynamicPatternSet = resolveFromDynamicPatterns(patternSet, block, beforeText);
  35159. const inlineMatches = findPatterns$2(editor, block, rng.startContainer, offset, dynamicPatternSet, false);
  35160. if (inlineMatches.length > 0) {
  35161. editor.undoManager.transact(() => {
  35162. applyMatches$2(editor, inlineMatches);
  35163. });
  35164. }
  35165. });
  35166. };
  35167. const handleBlockPatternOnSpace = (editor, patternSet) => getMatches(editor, patternSet).fold(never, (matches) => {
  35168. editor.undoManager.transact(() => {
  35169. applyMatches(editor, matches);
  35170. });
  35171. return true;
  35172. });
  35173. const checkKeyEvent = (codes, event, predicate) => {
  35174. for (let i = 0; i < codes.length; i++) {
  35175. if (predicate(codes[i], event)) {
  35176. return true;
  35177. }
  35178. }
  35179. return false;
  35180. };
  35181. const checkKeyCode = (codes, event) => checkKeyEvent(codes, event, (code, event) => {
  35182. return code === event.keyCode && !VK.modifierPressed(event);
  35183. });
  35184. const checkCharCode = (chars, event) => checkKeyEvent(chars, event, (chr, event) => {
  35185. return chr.charCodeAt(0) === event.charCode;
  35186. });
  35187. const setup$2 = (editor) => {
  35188. const charCodes = [',', '.', ';', ':', '!', '?'];
  35189. const keyCodes = [32];
  35190. // This is a thunk so that they reflect changes in the underlying options each time they are requested.
  35191. const getPatternSet = () => createPatternSet(getTextPatterns(editor)
  35192. .filter((pattern) => {
  35193. if (pattern.type === 'inline-command' || pattern.type === 'block-command') {
  35194. return editor.queryCommandSupported(pattern.cmd);
  35195. }
  35196. return true;
  35197. }), getTextPatternsLookup(editor));
  35198. // Only used for skipping text pattern matching altogether if nothing has been defined.
  35199. const hasDynamicPatterns = () => hasTextPatternsLookup(editor);
  35200. editor.on('keydown', (e) => {
  35201. if (e.keyCode === 13 && !VK.modifierPressed(e) && editor.selection.isCollapsed() && editor.selection.isEditable()) {
  35202. const patternSet = filterByTrigger(getPatternSet(), 'enter');
  35203. // Do not process anything if we don't have any inline patterns, block patterns,
  35204. // or dynamic lookup defined
  35205. const hasPatterns = patternSet.inlinePatterns.length > 0 ||
  35206. patternSet.blockPatterns.length > 0 ||
  35207. hasDynamicPatterns();
  35208. if (hasPatterns && handleEnter(editor, patternSet)) {
  35209. e.preventDefault();
  35210. }
  35211. }
  35212. }, true);
  35213. editor.on('keydown', (e) => {
  35214. if (e.keyCode === 32 && editor.selection.isCollapsed() && editor.selection.isEditable()) {
  35215. const patternSet = filterByTrigger(getPatternSet(), 'space');
  35216. const hasPatterns = patternSet.blockPatterns.length > 0 || hasDynamicPatterns();
  35217. if (hasPatterns && handleBlockPatternOnSpace(editor, patternSet)) {
  35218. e.preventDefault();
  35219. }
  35220. }
  35221. }, true);
  35222. const handleInlineTrigger = () => {
  35223. if (editor.selection.isCollapsed() && editor.selection.isEditable()) {
  35224. const patternSet = filterByTrigger(getPatternSet(), 'space');
  35225. // Do not process anything if we don't have any inline patterns or dynamic lookup defined
  35226. const hasPatterns = patternSet.inlinePatterns.length > 0 || hasDynamicPatterns();
  35227. if (hasPatterns) {
  35228. handleInlineKey(editor, patternSet);
  35229. }
  35230. }
  35231. };
  35232. editor.on('keyup', (e) => {
  35233. if (checkKeyCode(keyCodes, e)) {
  35234. handleInlineTrigger();
  35235. }
  35236. });
  35237. editor.on('keypress', (e) => {
  35238. if (checkCharCode(charCodes, e)) {
  35239. Delay.setEditorTimeout(editor, handleInlineTrigger);
  35240. }
  35241. });
  35242. };
  35243. const setup$1 = (editor) => {
  35244. setup$2(editor);
  35245. };
  35246. const Quirks = (editor) => {
  35247. const each = Tools.each;
  35248. const BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection, parser = editor.parser;
  35249. const browser = Env.browser;
  35250. const isGecko = browser.isFirefox();
  35251. const isWebKit = browser.isChromium() || browser.isSafari();
  35252. const isiOS = Env.deviceType.isiPhone() || Env.deviceType.isiPad();
  35253. const isMac = Env.os.isMacOS() || Env.os.isiOS();
  35254. /**
  35255. * Executes a command with a specific state this can be to enable/disable browser editing features.
  35256. */
  35257. const setEditorCommandState = (cmd, state) => {
  35258. try {
  35259. editor.getDoc().execCommand(cmd, false, String(state));
  35260. }
  35261. catch (_a) {
  35262. // Ignore
  35263. }
  35264. };
  35265. /**
  35266. * Returns true/false if the event is prevented or not.
  35267. *
  35268. * @private
  35269. * @param {Event} e Event object.
  35270. * @return {Boolean} true/false if the event is prevented or not.
  35271. */
  35272. const isDefaultPrevented = (e) => {
  35273. return e.isDefaultPrevented();
  35274. };
  35275. /**
  35276. * Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors.
  35277. *
  35278. * For example:
  35279. * <p><b>|</b></p>
  35280. *
  35281. * Or:
  35282. * <h1>|</h1>
  35283. *
  35284. * Or:
  35285. * [<h1></h1>]
  35286. */
  35287. const emptyEditorWhenDeleting = () => {
  35288. const serializeRng = (rng) => {
  35289. const body = dom.create('body');
  35290. const contents = rng.cloneContents();
  35291. body.appendChild(contents);
  35292. return selection.serializer.serialize(body, { format: 'html' });
  35293. };
  35294. const allContentsSelected = (rng) => {
  35295. const selection = serializeRng(rng);
  35296. const allRng = dom.createRng();
  35297. allRng.selectNode(editor.getBody());
  35298. const allSelection = serializeRng(allRng);
  35299. return selection === allSelection;
  35300. };
  35301. editor.on('keydown', (e) => {
  35302. const keyCode = e.keyCode;
  35303. // Empty the editor if it's needed for example backspace at <p><b>|</b></p>
  35304. if (!isDefaultPrevented(e) && (keyCode === DELETE || keyCode === BACKSPACE) && editor.selection.isEditable()) {
  35305. const isCollapsed = editor.selection.isCollapsed();
  35306. const body = editor.getBody();
  35307. // Selection is collapsed but the editor isn't empty
  35308. if (isCollapsed && !isEmptyNode(editor.schema, body)) {
  35309. return;
  35310. }
  35311. // Selection isn't collapsed but not all the contents is selected
  35312. if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) {
  35313. return;
  35314. }
  35315. // Manually empty the editor
  35316. e.preventDefault();
  35317. editor.setContent('');
  35318. if (body.firstChild && dom.isBlock(body.firstChild)) {
  35319. editor.selection.setCursorLocation(body.firstChild, 0);
  35320. }
  35321. else {
  35322. editor.selection.setCursorLocation(body, 0);
  35323. }
  35324. editor.nodeChanged();
  35325. }
  35326. });
  35327. };
  35328. /**
  35329. * WebKit doesn't select all the nodes in the body when you press Ctrl+A.
  35330. * IE selects more than the contents <body>[<p>a</p>]</body> instead of <body><p>[a]</p]</body> see bug #6438
  35331. * This selects the whole body so that backspace/delete logic will delete everything
  35332. */
  35333. const selectAll = () => {
  35334. editor.shortcuts.add('meta+a', null, 'SelectAll');
  35335. };
  35336. /**
  35337. * It seems that Chrome doesn't place the caret if you click on the documentElement in iframe mode
  35338. * something that is very easy to do by accident so this problem is now more generic than the original issue.
  35339. *
  35340. * Original IME specific issue:
  35341. * WebKit has a weird issue where it some times fails to properly convert keypresses to input method keystrokes.
  35342. * The IME on Mac doesn't initialize when it doesn't fire a proper focus event.
  35343. *
  35344. * This seems to happen when the user manages to click the documentElement element then the window doesn't get proper focus until
  35345. * you enter a character into the editor.
  35346. *
  35347. * See: https://bugs.webkit.org/show_bug.cgi?id=83566
  35348. */
  35349. const documentElementEditingFocus = () => {
  35350. if (!editor.inline) {
  35351. // Needs to be both down/up due to weird rendering bug on Chrome Windows
  35352. dom.bind(editor.getDoc(), 'mousedown mouseup', (e) => {
  35353. let rng;
  35354. if (e.target === editor.getDoc().documentElement) {
  35355. rng = selection.getRng();
  35356. editor.getBody().focus();
  35357. if (e.type === 'mousedown') {
  35358. if (isCaretContainer$2(rng.startContainer)) {
  35359. return;
  35360. }
  35361. // Edge case for mousedown, drag select and mousedown again within selection on Chrome Windows to render caret
  35362. selection.placeCaretAt(e.clientX, e.clientY);
  35363. }
  35364. else {
  35365. selection.setRng(rng);
  35366. }
  35367. }
  35368. });
  35369. }
  35370. };
  35371. /**
  35372. * Backspacing in FireFox/IE from a paragraph into a horizontal rule results in a floating text node because the
  35373. * browser just deletes the paragraph - the browser fails to merge the text node with a horizontal rule so it is
  35374. * left there. TinyMCE sees a floating text node and wraps it in a paragraph on the key up event (ForceBlocks.js
  35375. * addRootBlocks), meaning the action does nothing. With this code, FireFox/IE matche the behaviour of other
  35376. * browsers.
  35377. *
  35378. * It also fixes a bug on Firefox where it's impossible to delete HR elements.
  35379. */
  35380. const removeHrOnBackspace = () => {
  35381. editor.on('keydown', (e) => {
  35382. if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {
  35383. // Check if there is any HR elements this is faster since getRng on IE 7 & 8 is slow
  35384. if (!editor.getBody().getElementsByTagName('hr').length) {
  35385. return;
  35386. }
  35387. if (selection.isCollapsed() && selection.getRng().startOffset === 0) {
  35388. const node = selection.getNode();
  35389. const previousSibling = node.previousSibling;
  35390. if (node.nodeName === 'HR') {
  35391. dom.remove(node);
  35392. e.preventDefault();
  35393. return;
  35394. }
  35395. if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === 'hr') {
  35396. dom.remove(previousSibling);
  35397. e.preventDefault();
  35398. }
  35399. }
  35400. }
  35401. });
  35402. };
  35403. /**
  35404. * Firefox 3.x has an issue where the body element won't get proper focus if you click out
  35405. * side it's rectangle.
  35406. */
  35407. const focusBody = () => {
  35408. // Fix for a focus bug in FF 3.x where the body element
  35409. // wouldn't get proper focus if the user clicked on the HTML element
  35410. if (!Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4
  35411. editor.on('mousedown', (e) => {
  35412. if (!isDefaultPrevented(e) && e.target.nodeName === 'HTML') {
  35413. const body = editor.getBody();
  35414. // Blur the body it's focused but not correctly focused
  35415. body.blur();
  35416. // Refocus the body after a little while
  35417. Delay.setEditorTimeout(editor, () => {
  35418. body.focus();
  35419. });
  35420. }
  35421. });
  35422. }
  35423. };
  35424. /**
  35425. * WebKit has a bug where it isn't possible to select image, hr or anchor elements
  35426. * by clicking on them so we need to fake that.
  35427. */
  35428. const selectControlElements = () => {
  35429. const visualAidsAnchorClass = getVisualAidsAnchorClass(editor);
  35430. editor.on('click', (e) => {
  35431. const target = e.target;
  35432. // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250
  35433. // WebKit can't even do simple things like selecting an image
  35434. // Needs to be the setBaseAndExtend or it will fail to select floated images
  35435. if (/^(IMG|HR)$/.test(target.nodeName) && dom.isEditable(target)) {
  35436. e.preventDefault();
  35437. editor.selection.select(target);
  35438. editor.nodeChanged();
  35439. }
  35440. if (target.nodeName === 'A' && dom.hasClass(target, visualAidsAnchorClass) && target.childNodes.length === 0 && dom.isEditable(target.parentNode)) {
  35441. e.preventDefault();
  35442. selection.select(target);
  35443. }
  35444. });
  35445. };
  35446. /**
  35447. * Fixes a Gecko a selection bug where if there is a floating image
  35448. * more details here: https://bugzilla.mozilla.org/show_bug.cgi?id=1959606
  35449. */
  35450. const fixFirefoxImageSelection = () => {
  35451. const isEditableImage = (node) => node.nodeName === 'IMG' && editor.dom.isEditable(node);
  35452. editor.on('mousedown', (e) => {
  35453. lift2(Optional.from(e.clientX), Optional.from(e.clientY), (clientX, clientY) => {
  35454. const caretPos = editor.getDoc().caretPositionFromPoint(clientX, clientY);
  35455. const img = (caretPos === null || caretPos === void 0 ? void 0 : caretPos.offsetNode.childNodes[caretPos.offset - (caretPos.offset > 0 ? 1 : 0)]) || (caretPos === null || caretPos === void 0 ? void 0 : caretPos.offsetNode);
  35456. if (img && isEditableImage(img)) {
  35457. const rect = img.getBoundingClientRect();
  35458. e.preventDefault();
  35459. if (!editor.hasFocus()) {
  35460. editor.focus();
  35461. }
  35462. editor.selection.select(img);
  35463. if (e.clientX < rect.left || e.clientY < rect.top) {
  35464. editor.selection.collapse(true);
  35465. }
  35466. else if (e.clientX > rect.right || e.clientY > rect.bottom) {
  35467. editor.selection.collapse(false);
  35468. }
  35469. }
  35470. });
  35471. });
  35472. };
  35473. /**
  35474. * Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements.
  35475. *
  35476. * Fixes do backspace/delete on this:
  35477. * <p>bla[ck</p><p style="color:red">r]ed</p>
  35478. *
  35479. * Would become:
  35480. * <p>bla|ed</p>
  35481. *
  35482. * Instead of:
  35483. * <p style="color:red">bla|ed</p>
  35484. */
  35485. const removeStylesWhenDeletingAcrossBlockElements = () => {
  35486. const getAttributeApplyFunction = () => {
  35487. const template = dom.getAttribs(selection.getStart().cloneNode(false));
  35488. return () => {
  35489. const target = selection.getStart();
  35490. if (target !== editor.getBody()) {
  35491. dom.setAttrib(target, 'style', null);
  35492. each(template, (attr) => {
  35493. target.setAttributeNode(attr.cloneNode(true));
  35494. });
  35495. }
  35496. };
  35497. };
  35498. const isSelectionAcrossElements = () => {
  35499. return !selection.isCollapsed() &&
  35500. dom.getParent(selection.getStart(), dom.isBlock) !== dom.getParent(selection.getEnd(), dom.isBlock);
  35501. };
  35502. editor.on('keypress', (e) => {
  35503. let applyAttributes;
  35504. if (!isDefaultPrevented(e) && (e.keyCode === 8 || e.keyCode === 46) && isSelectionAcrossElements()) {
  35505. applyAttributes = getAttributeApplyFunction();
  35506. editor.getDoc().execCommand('delete', false);
  35507. applyAttributes();
  35508. e.preventDefault();
  35509. return false;
  35510. }
  35511. else {
  35512. return true;
  35513. }
  35514. });
  35515. dom.bind(editor.getDoc(), 'cut', (e) => {
  35516. if (!isDefaultPrevented(e) && isSelectionAcrossElements()) {
  35517. const applyAttributes = getAttributeApplyFunction();
  35518. Delay.setEditorTimeout(editor, () => {
  35519. applyAttributes();
  35520. });
  35521. }
  35522. });
  35523. };
  35524. /**
  35525. * Backspacing into a table behaves differently depending upon browser type.
  35526. * Therefore, disable Backspace when cursor immediately follows a table.
  35527. */
  35528. const disableBackspaceIntoATable = () => {
  35529. editor.on('keydown', (e) => {
  35530. if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {
  35531. if (selection.isCollapsed() && selection.getRng().startOffset === 0) {
  35532. const previousSibling = selection.getNode().previousSibling;
  35533. if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === 'table') {
  35534. e.preventDefault();
  35535. return false;
  35536. }
  35537. }
  35538. }
  35539. return true;
  35540. });
  35541. };
  35542. /**
  35543. * Removes a blockquote when backspace is pressed at the beginning of it.
  35544. *
  35545. * For example:
  35546. * <blockquote><p>|x</p></blockquote>
  35547. *
  35548. * Becomes:
  35549. * <p>|x</p>
  35550. */
  35551. const removeBlockQuoteOnBackSpace = () => {
  35552. // Add block quote deletion handler
  35553. editor.on('keydown', (e) => {
  35554. if (isDefaultPrevented(e) || e.keyCode !== VK.BACKSPACE) {
  35555. return;
  35556. }
  35557. let rng = selection.getRng();
  35558. const container = rng.startContainer;
  35559. const offset = rng.startOffset;
  35560. const root = dom.getRoot();
  35561. let parent = container;
  35562. if (!rng.collapsed || offset !== 0) {
  35563. return;
  35564. }
  35565. while (parent.parentNode && parent.parentNode.firstChild === parent && parent.parentNode !== root) {
  35566. parent = parent.parentNode;
  35567. }
  35568. // Is the cursor at the beginning of a blockquote?
  35569. if (parent.nodeName === 'BLOCKQUOTE') {
  35570. // Remove the blockquote
  35571. editor.formatter.toggle('blockquote', undefined, parent);
  35572. // Move the caret to the beginning of container
  35573. rng = dom.createRng();
  35574. rng.setStart(container, 0);
  35575. rng.setEnd(container, 0);
  35576. selection.setRng(rng);
  35577. }
  35578. });
  35579. };
  35580. /*
  35581. * Firefox-specific fix for arrow key navigation. In Firefox, users can't move the caret out of a
  35582. * `<figcaption>` element using the left and right arrow keys. This function handles those keystrokes
  35583. * to allow navigation to the previous/next sibling of the figure element.
  35584. */
  35585. const arrowInFigcaption = () => {
  35586. const isFigcaption = isTag('figcaption');
  35587. editor.on('keydown', (e) => {
  35588. if (e.keyCode === VK.LEFT || e.keyCode === VK.RIGHT) {
  35589. const currentNode = SugarElement.fromDom(editor.selection.getNode());
  35590. if (isFigcaption(currentNode) && editor.selection.isCollapsed()) {
  35591. parent(currentNode).bind((parent) => {
  35592. var _a;
  35593. if (editor.selection.getRng().startOffset === 0 && e.keyCode === VK.LEFT) {
  35594. return prevSibling(parent);
  35595. }
  35596. else if (editor.selection.getRng().endOffset === ((_a = currentNode.dom.textContent) === null || _a === void 0 ? void 0 : _a.length) && e.keyCode === VK.RIGHT) {
  35597. return nextSibling(parent);
  35598. }
  35599. else {
  35600. return Optional.none();
  35601. }
  35602. }).each((targetSibling) => {
  35603. editor.selection.setCursorLocation(targetSibling.dom, 0);
  35604. });
  35605. }
  35606. }
  35607. });
  35608. };
  35609. /**
  35610. * Sets various Gecko editing options on mouse down and before a execCommand to disable inline table editing that is broken etc.
  35611. */
  35612. const setGeckoEditingOptions = () => {
  35613. const setOpts = () => {
  35614. setEditorCommandState('StyleWithCSS', false);
  35615. setEditorCommandState('enableInlineTableEditing', false);
  35616. if (!getObjectResizing(editor)) {
  35617. setEditorCommandState('enableObjectResizing', false);
  35618. }
  35619. };
  35620. if (!isReadOnly$1(editor)) {
  35621. editor.on('BeforeExecCommand mousedown', setOpts);
  35622. }
  35623. };
  35624. /**
  35625. * Fixes a gecko link bug, when a link is placed at the end of block elements there is
  35626. * no way to move the caret behind the link. This fix adds a bogus br element after the link.
  35627. *
  35628. * For example this:
  35629. * <p><b><a href="#">x</a></b></p>
  35630. *
  35631. * Becomes this:
  35632. * <p><b><a href="#">x</a></b><br></p>
  35633. */
  35634. const addBrAfterLastLinks = () => {
  35635. const fixLinks = () => {
  35636. each(dom.select('a:not([data-mce-block])'), (node) => {
  35637. var _a;
  35638. let parentNode = node.parentNode;
  35639. const root = dom.getRoot();
  35640. if ((parentNode === null || parentNode === void 0 ? void 0 : parentNode.lastChild) === node) {
  35641. while (parentNode && !dom.isBlock(parentNode)) {
  35642. if (((_a = parentNode.parentNode) === null || _a === void 0 ? void 0 : _a.lastChild) !== parentNode || parentNode === root) {
  35643. return;
  35644. }
  35645. parentNode = parentNode.parentNode;
  35646. }
  35647. dom.add(parentNode, 'br', { 'data-mce-bogus': 1 });
  35648. }
  35649. });
  35650. };
  35651. editor.on('SetContent ExecCommand', (e) => {
  35652. if (e.type === 'setcontent' || e.command === 'mceInsertLink') {
  35653. fixLinks();
  35654. }
  35655. });
  35656. };
  35657. /**
  35658. * WebKit will produce DIV elements here and there by default. But since TinyMCE uses paragraphs by
  35659. * default we want to change that behavior.
  35660. */
  35661. const setDefaultBlockType = () => {
  35662. editor.on('init', () => {
  35663. setEditorCommandState('DefaultParagraphSeparator', getForcedRootBlock(editor));
  35664. });
  35665. };
  35666. const isAllContentSelected = (editor) => {
  35667. const body = editor.getBody();
  35668. const rng = editor.selection.getRng();
  35669. return rng.startContainer === rng.endContainer && rng.startContainer === body && rng.startOffset === 0 && rng.endOffset === body.childNodes.length;
  35670. };
  35671. /**
  35672. * Fixes selection issues where the caret can be placed between two inline elements like <b>a</b>|<b>b</b>
  35673. * this fix will lean the caret right into the closest inline element.
  35674. */
  35675. const normalizeSelection = () => {
  35676. // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i>
  35677. editor.on('keyup focusin mouseup', (e) => {
  35678. // no point to exclude Ctrl+A, since normalization will still run after Ctrl will be unpressed
  35679. // better exclude any key combinations with the modifiers to avoid double normalization
  35680. // (also addresses TINY-1130)
  35681. // The use of isAllContentSelected addresses TINY-4550
  35682. if (!VK.modifierPressed(e) && !isAllContentSelected(editor)) {
  35683. selection.normalize();
  35684. }
  35685. }, true);
  35686. };
  35687. /**
  35688. * Forces Gecko to render a broken image icon if it fails to load an image.
  35689. */
  35690. const showBrokenImageIcon = () => {
  35691. editor.contentStyles.push('img:-moz-broken {' +
  35692. '-moz-force-broken-image-icon:1;' +
  35693. 'min-width:24px;' +
  35694. 'min-height:24px' +
  35695. '}');
  35696. };
  35697. /**
  35698. * iOS has a bug where it's impossible to type if the document has a touchstart event
  35699. * bound and the user touches the document while having the on screen keyboard visible.
  35700. *
  35701. * The touch event moves the focus to the parent document while having the caret inside the iframe
  35702. * this fix moves the focus back into the iframe document.
  35703. */
  35704. const restoreFocusOnKeyDown = () => {
  35705. if (!editor.inline) {
  35706. editor.on('keydown', () => {
  35707. if (document.activeElement === document.body) {
  35708. editor.getWin().focus();
  35709. }
  35710. });
  35711. }
  35712. };
  35713. /**
  35714. * IE 11 has an annoying issue where you can't move focus into the editor
  35715. * by clicking on the white area HTML element. We used to be able to fix this with
  35716. * the fixCaretSelectionOfDocumentElementOnIe fix. But since M$ removed the selection
  35717. * object it's not possible anymore. So we need to hack in a ungly CSS to force the
  35718. * body to be at least 150px. If the user clicks the HTML element out side this 150px region
  35719. * we simply move the focus into the first paragraph. Not ideal since you loose the
  35720. * positioning of the caret but goot enough for most cases.
  35721. */
  35722. const bodyHeight = () => {
  35723. if (!editor.inline) {
  35724. editor.contentStyles.push('body {min-height: 150px}');
  35725. editor.on('click', (e) => {
  35726. let rng;
  35727. if (e.target.nodeName === 'HTML') {
  35728. // Need to store away non collapsed ranges since the focus call will mess that up see #7382
  35729. rng = editor.selection.getRng();
  35730. editor.getBody().focus();
  35731. editor.selection.setRng(rng);
  35732. editor.selection.normalize();
  35733. editor.nodeChanged();
  35734. }
  35735. });
  35736. }
  35737. };
  35738. /**
  35739. * Firefox on Mac OS will move the browser back to the previous page if you press CMD+Left arrow.
  35740. * You might then loose all your work so we need to block that behavior and replace it with our own.
  35741. */
  35742. const blockCmdArrowNavigation = () => {
  35743. if (isMac) {
  35744. editor.on('keydown', (e) => {
  35745. if (VK.metaKeyPressed(e) && !e.shiftKey && (e.keyCode === 37 || e.keyCode === 39)) {
  35746. e.preventDefault();
  35747. // The modify component isn't part of the standard spec, so we need to add the type here
  35748. const selection = editor.selection.getSel();
  35749. selection.modify('move', e.keyCode === 37 ? 'backward' : 'forward', 'lineboundary');
  35750. }
  35751. });
  35752. }
  35753. };
  35754. /**
  35755. * iOS 7.1 introduced two new bugs:
  35756. * 1) It's possible to open links within a contentEditable area by clicking on them.
  35757. * 2) If you hold down the finger it will display the link/image touch callout menu.
  35758. */
  35759. const tapLinksAndImages = () => {
  35760. editor.on('click', (e) => {
  35761. let elm = e.target;
  35762. do {
  35763. if (elm.tagName === 'A') {
  35764. e.preventDefault();
  35765. return;
  35766. }
  35767. } while ((elm = elm.parentNode));
  35768. });
  35769. editor.contentStyles.push('.mce-content-body {-webkit-touch-callout: none}');
  35770. };
  35771. /**
  35772. * iOS Safari and possible other browsers have a bug where it won't fire
  35773. * a click event when a contentEditable is focused. This function fakes click events
  35774. * by using touchstart/touchend and measuring the time and distance travelled.
  35775. */
  35776. /*
  35777. function touchClickEvent() {
  35778. editor.on('touchstart', function(e) {
  35779. var elm, time, startTouch, changedTouches;
  35780. elm = e.target;
  35781. time = new Date().getTime();
  35782. changedTouches = e.changedTouches;
  35783. if (!changedTouches || changedTouches.length > 1) {
  35784. return;
  35785. }
  35786. startTouch = changedTouches[0];
  35787. editor.once('touchend', function(e) {
  35788. var endTouch = e.changedTouches[0], args;
  35789. if (new Date().getTime() - time > 500) {
  35790. return;
  35791. }
  35792. if (Math.abs(startTouch.clientX - endTouch.clientX) > 5) {
  35793. return;
  35794. }
  35795. if (Math.abs(startTouch.clientY - endTouch.clientY) > 5) {
  35796. return;
  35797. }
  35798. args = {
  35799. target: elm
  35800. };
  35801. each('pageX pageY clientX clientY screenX screenY'.split(' '), function(key) {
  35802. args[key] = endTouch[key];
  35803. });
  35804. args = editor.dispatch('click', args);
  35805. if (!args.isDefaultPrevented()) {
  35806. // iOS WebKit can't place the caret properly once
  35807. // you bind touch events so we need to do this manually
  35808. // TODO: Expand to the closest word? Touble tap still works.
  35809. editor.selection.placeCaretAt(endTouch.clientX, endTouch.clientY);
  35810. editor.nodeChanged();
  35811. }
  35812. });
  35813. });
  35814. }
  35815. */
  35816. /**
  35817. * WebKit has a bug where it will allow forms to be submitted if they are inside a contentEditable element.
  35818. * For example this: <form><button></form>
  35819. */
  35820. const blockFormSubmitInsideEditor = () => {
  35821. editor.on('init', () => {
  35822. editor.dom.bind(editor.getBody(), 'submit', (e) => {
  35823. e.preventDefault();
  35824. });
  35825. });
  35826. };
  35827. /**
  35828. * Sometimes WebKit/Blink generates BR elements with the Apple-interchange-newline class.
  35829. *
  35830. * Scenario:
  35831. * 1) Create a table 2x2.
  35832. * 2) Select and copy cells A2-B2.
  35833. * 3) Paste and it will add BR element to table cell.
  35834. */
  35835. const removeAppleInterchangeBrs = () => {
  35836. parser.addNodeFilter('br', (nodes) => {
  35837. let i = nodes.length;
  35838. while (i--) {
  35839. if (nodes[i].attr('class') === 'Apple-interchange-newline') {
  35840. nodes[i].remove();
  35841. }
  35842. }
  35843. });
  35844. };
  35845. // No-op since Mozilla seems to have fixed the caret repaint issues
  35846. const refreshContentEditable = noop;
  35847. const isHidden = () => {
  35848. if (!isGecko || editor.removed) {
  35849. return false;
  35850. }
  35851. // Weird, wheres that cursor selection?
  35852. const sel = editor.selection.getSel();
  35853. return (!sel || !sel.rangeCount || sel.rangeCount === 0);
  35854. };
  35855. const setupRtc = () => {
  35856. if (isWebKit) {
  35857. documentElementEditingFocus();
  35858. selectControlElements();
  35859. blockFormSubmitInsideEditor();
  35860. selectAll();
  35861. if (isiOS) {
  35862. restoreFocusOnKeyDown();
  35863. bodyHeight();
  35864. tapLinksAndImages();
  35865. }
  35866. }
  35867. if (isGecko) {
  35868. focusBody();
  35869. setGeckoEditingOptions();
  35870. showBrokenImageIcon();
  35871. blockCmdArrowNavigation();
  35872. }
  35873. };
  35874. const setup = () => {
  35875. // All browsers
  35876. removeBlockQuoteOnBackSpace();
  35877. emptyEditorWhenDeleting();
  35878. // Windows phone will return a range like [body, 0] on mousedown so
  35879. // it will always normalize to the wrong location
  35880. if (!Env.windowsPhone) {
  35881. normalizeSelection();
  35882. }
  35883. // WebKit
  35884. if (isWebKit) {
  35885. documentElementEditingFocus();
  35886. selectControlElements();
  35887. setDefaultBlockType();
  35888. blockFormSubmitInsideEditor();
  35889. disableBackspaceIntoATable();
  35890. removeAppleInterchangeBrs();
  35891. // touchClickEvent();
  35892. // iOS
  35893. if (isiOS) {
  35894. restoreFocusOnKeyDown();
  35895. bodyHeight();
  35896. tapLinksAndImages();
  35897. }
  35898. else {
  35899. selectAll();
  35900. }
  35901. }
  35902. // Gecko
  35903. if (isGecko) {
  35904. arrowInFigcaption();
  35905. fixFirefoxImageSelection();
  35906. removeHrOnBackspace();
  35907. focusBody();
  35908. removeStylesWhenDeletingAcrossBlockElements();
  35909. setGeckoEditingOptions();
  35910. addBrAfterLastLinks();
  35911. showBrokenImageIcon();
  35912. blockCmdArrowNavigation();
  35913. disableBackspaceIntoATable();
  35914. }
  35915. };
  35916. if (isRtc(editor)) {
  35917. setupRtc();
  35918. }
  35919. else {
  35920. setup();
  35921. }
  35922. return {
  35923. refreshContentEditable,
  35924. isHidden
  35925. };
  35926. };
  35927. const DOM$6 = DOMUtils.DOM;
  35928. const appendStyle = (editor, text) => {
  35929. const body = SugarElement.fromDom(editor.getBody());
  35930. const container = getStyleContainer(getRootNode(body));
  35931. const style = SugarElement.fromTag('style');
  35932. set$4(style, 'type', 'text/css');
  35933. append$1(style, SugarElement.fromText(text));
  35934. append$1(container, style);
  35935. editor.on('remove', () => {
  35936. remove$8(style);
  35937. });
  35938. };
  35939. const getRootName = (editor) => editor.inline ? editor.getElement().nodeName.toLowerCase() : undefined;
  35940. const removeUndefined = (obj) => filter$4(obj, (v) => isUndefined(v) === false);
  35941. const mkParserSettings = (editor) => {
  35942. const getOption = editor.options.get;
  35943. const blobCache = editor.editorUpload.blobCache;
  35944. return removeUndefined({
  35945. allow_conditional_comments: getOption('allow_conditional_comments'),
  35946. allow_html_data_urls: getOption('allow_html_data_urls'),
  35947. allow_svg_data_urls: getOption('allow_svg_data_urls'),
  35948. allow_html_in_named_anchor: getOption('allow_html_in_named_anchor'),
  35949. allow_script_urls: getOption('allow_script_urls'),
  35950. allow_html_in_comments: getOption('allow_html_in_comments'),
  35951. allow_mathml_annotation_encodings: getOption('allow_mathml_annotation_encodings'),
  35952. allow_unsafe_link_target: getOption('allow_unsafe_link_target'),
  35953. convert_unsafe_embeds: getOption('convert_unsafe_embeds'),
  35954. convert_fonts_to_spans: getOption('convert_fonts_to_spans'),
  35955. extended_mathml_attributes: getOption('extended_mathml_attributes'),
  35956. extended_mathml_elements: getOption('extended_mathml_elements'),
  35957. fix_list_elements: getOption('fix_list_elements'),
  35958. font_size_legacy_values: getOption('font_size_legacy_values'),
  35959. forced_root_block: getOption('forced_root_block'),
  35960. forced_root_block_attrs: getOption('forced_root_block_attrs'),
  35961. preserve_cdata: getOption('preserve_cdata'),
  35962. inline_styles: getOption('inline_styles'),
  35963. root_name: getRootName(editor),
  35964. sandbox_iframes: getOption('sandbox_iframes'),
  35965. sandbox_iframes_exclusions: getSandboxIframesExclusions(editor),
  35966. sanitize: getOption('xss_sanitization'),
  35967. validate: true,
  35968. blob_cache: blobCache,
  35969. document: editor.getDoc()
  35970. });
  35971. };
  35972. const mkSchemaSettings = (editor) => {
  35973. const getOption = editor.options.get;
  35974. return removeUndefined({
  35975. custom_elements: getOption('custom_elements'),
  35976. extended_valid_elements: getOption('extended_valid_elements'),
  35977. invalid_elements: getOption('invalid_elements'),
  35978. invalid_styles: getOption('invalid_styles'),
  35979. schema: getOption('schema'),
  35980. valid_children: getOption('valid_children'),
  35981. valid_classes: getOption('valid_classes'),
  35982. valid_elements: getOption('valid_elements'),
  35983. valid_styles: getOption('valid_styles'),
  35984. verify_html: getOption('verify_html'),
  35985. padd_empty_block_inline_children: getOption('format_empty_lines')
  35986. });
  35987. };
  35988. const mkSerializerSettings = (editor) => {
  35989. const getOption = editor.options.get;
  35990. return {
  35991. ...mkParserSettings(editor),
  35992. ...mkSchemaSettings(editor),
  35993. ...removeUndefined({
  35994. // SerializerSettings
  35995. remove_trailing_brs: getOption('remove_trailing_brs'),
  35996. pad_empty_with_br: getOption('pad_empty_with_br'),
  35997. url_converter: getOption('url_converter'),
  35998. url_converter_scope: getOption('url_converter_scope'),
  35999. // Writer settings
  36000. element_format: getOption('element_format'),
  36001. entities: getOption('entities'),
  36002. entity_encoding: getOption('entity_encoding'),
  36003. indent: getOption('indent'),
  36004. indent_after: getOption('indent_after'),
  36005. indent_before: getOption('indent_before')
  36006. })
  36007. };
  36008. };
  36009. const createParser = (editor) => {
  36010. const parser = DomParser(mkParserSettings(editor), editor.schema);
  36011. // Convert src and href into data-mce-src, data-mce-href and data-mce-style
  36012. parser.addAttributeFilter('src,href,style,tabindex', (nodes, name) => {
  36013. const dom = editor.dom;
  36014. const internalName = 'data-mce-' + name;
  36015. let i = nodes.length;
  36016. while (i--) {
  36017. const node = nodes[i];
  36018. let value = node.attr(name);
  36019. // Add internal attribute if we need to we don't on a refresh of the document
  36020. if (value && !node.attr(internalName)) {
  36021. // Don't duplicate these since they won't get modified by any browser
  36022. if (value.indexOf('data:') === 0 || value.indexOf('blob:') === 0) {
  36023. continue;
  36024. }
  36025. if (name === 'style') {
  36026. value = dom.serializeStyle(dom.parseStyle(value), node.name);
  36027. if (!value.length) {
  36028. value = null;
  36029. }
  36030. node.attr(internalName, value);
  36031. node.attr(name, value);
  36032. }
  36033. else if (name === 'tabindex') {
  36034. node.attr(internalName, value);
  36035. node.attr(name, null);
  36036. }
  36037. else {
  36038. node.attr(internalName, editor.convertURL(value, name, node.name));
  36039. }
  36040. }
  36041. }
  36042. });
  36043. // Keep scripts from executing
  36044. parser.addNodeFilter('script', (nodes) => {
  36045. let i = nodes.length;
  36046. while (i--) {
  36047. const node = nodes[i];
  36048. const type = node.attr('type') || 'no/type';
  36049. if (type.indexOf('mce-') !== 0) {
  36050. node.attr('type', 'mce-' + type);
  36051. }
  36052. }
  36053. });
  36054. if (shouldPreserveCData(editor)) {
  36055. parser.addNodeFilter('#cdata', (nodes) => {
  36056. var _a;
  36057. let i = nodes.length;
  36058. while (i--) {
  36059. const node = nodes[i];
  36060. node.type = 8;
  36061. node.name = '#comment';
  36062. node.value = '[CDATA[' + editor.dom.encode((_a = node.value) !== null && _a !== void 0 ? _a : '') + ']]';
  36063. }
  36064. });
  36065. }
  36066. parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', (nodes) => {
  36067. let i = nodes.length;
  36068. const nonEmptyElements = editor.schema.getNonEmptyElements();
  36069. while (i--) {
  36070. const node = nodes[i];
  36071. if (node.isEmpty(nonEmptyElements) && node.getAll('br').length === 0) {
  36072. node.append(new AstNode('br', 1));
  36073. }
  36074. }
  36075. });
  36076. return parser;
  36077. };
  36078. const autoFocus = (editor) => {
  36079. const autoFocus = getAutoFocus(editor);
  36080. if (autoFocus) {
  36081. Delay.setEditorTimeout(editor, () => {
  36082. let focusEditor;
  36083. if (autoFocus === true) {
  36084. focusEditor = editor;
  36085. }
  36086. else {
  36087. focusEditor = editor.editorManager.get(autoFocus);
  36088. }
  36089. if (focusEditor && !focusEditor.destroyed) {
  36090. focusEditor.focus();
  36091. focusEditor.selection.scrollIntoView();
  36092. }
  36093. }, 100);
  36094. }
  36095. };
  36096. const moveSelectionToFirstCaretPosition = (editor) => {
  36097. // If not inline and no useful selection, we want to set selection to the first valid cursor position
  36098. // We don't do this on inline because then it selects the editor container
  36099. // This must run AFTER editor.focus!
  36100. const root = editor.dom.getRoot();
  36101. if (!editor.inline && (!hasAnyRanges(editor) || editor.selection.getStart(true) === root)) {
  36102. firstPositionIn(root).each((pos) => {
  36103. const node = pos.getNode();
  36104. // If a table is the first caret pos, then walk down one more level
  36105. const caretPos = isTable$2(node) ? firstPositionIn(node).getOr(pos) : pos;
  36106. editor.selection.setRng(caretPos.toRange());
  36107. });
  36108. }
  36109. };
  36110. const initEditor = (editor) => {
  36111. editor.bindPendingEventDelegates();
  36112. editor.initialized = true;
  36113. fireInit(editor);
  36114. editor.focus(true);
  36115. moveSelectionToFirstCaretPosition(editor);
  36116. editor.nodeChanged({ initial: true });
  36117. const initInstanceCallback = getInitInstanceCallback(editor);
  36118. if (isFunction(initInstanceCallback)) {
  36119. initInstanceCallback.call(editor, editor);
  36120. }
  36121. autoFocus(editor);
  36122. if (isDisabled(editor)) {
  36123. toggleDisabled(editor, true);
  36124. }
  36125. };
  36126. const getStyleSheetLoader$1 = (editor) => editor.inline ? editor.ui.styleSheetLoader : editor.dom.styleSheetLoader;
  36127. const makeStylesheetLoadingPromises = (editor, css, framedFonts) => {
  36128. const { pass: bundledCss, fail: normalCss } = partition$2(css, (name) => tinymce.Resource.has(toContentSkinResourceName(name)));
  36129. const bundledPromises = bundledCss.map((url) => {
  36130. const css = tinymce.Resource.get(toContentSkinResourceName(url));
  36131. if (isString(css)) {
  36132. return Promise.resolve(getStyleSheetLoader$1(editor).loadRawCss(url, css));
  36133. }
  36134. return Promise.resolve();
  36135. });
  36136. const promises = [...bundledPromises,
  36137. getStyleSheetLoader$1(editor).loadAll(normalCss),
  36138. ];
  36139. if (editor.inline) {
  36140. return promises;
  36141. }
  36142. else {
  36143. return promises.concat([
  36144. editor.ui.styleSheetLoader.loadAll(framedFonts)
  36145. ]);
  36146. }
  36147. };
  36148. const loadContentCss = (editor) => {
  36149. const styleSheetLoader = getStyleSheetLoader$1(editor);
  36150. const fontCss = getFontCss(editor);
  36151. const css = editor.contentCSS;
  36152. const removeCss = () => {
  36153. styleSheetLoader.unloadAll(css);
  36154. if (!editor.inline) {
  36155. editor.ui.styleSheetLoader.unloadAll(fontCss);
  36156. }
  36157. };
  36158. const loaded = () => {
  36159. if (editor.removed) {
  36160. removeCss();
  36161. }
  36162. else {
  36163. editor.on('remove', removeCss);
  36164. }
  36165. };
  36166. // Add editor specific CSS styles
  36167. if (editor.contentStyles.length > 0) {
  36168. let contentCssText = '';
  36169. Tools.each(editor.contentStyles, (style) => {
  36170. contentCssText += style + '\r\n';
  36171. });
  36172. editor.dom.addStyle(contentCssText);
  36173. }
  36174. // Load all stylesheets
  36175. const allStylesheets = Promise.all(makeStylesheetLoadingPromises(editor, css, fontCss)).then(loaded).catch(loaded);
  36176. // Append specified content CSS last
  36177. const contentStyle = getContentStyle(editor);
  36178. if (contentStyle) {
  36179. appendStyle(editor, contentStyle);
  36180. }
  36181. return allStylesheets;
  36182. };
  36183. const preInit = (editor) => {
  36184. const doc = editor.getDoc(), body = editor.getBody();
  36185. firePreInit(editor);
  36186. if (!shouldBrowserSpellcheck(editor)) {
  36187. doc.body.spellcheck = false; // Gecko
  36188. DOM$6.setAttrib(body, 'spellcheck', 'false');
  36189. }
  36190. editor.quirks = Quirks(editor);
  36191. firePostRender(editor);
  36192. const directionality = getDirectionality(editor);
  36193. if (directionality !== undefined) {
  36194. body.dir = directionality;
  36195. }
  36196. const protect = getProtect(editor);
  36197. if (protect) {
  36198. editor.on('BeforeSetContent', (e) => {
  36199. Tools.each(protect, (pattern) => {
  36200. e.content = e.content.replace(pattern, (str) => {
  36201. return '<!--mce:protected ' + escape(str) + '-->';
  36202. });
  36203. });
  36204. });
  36205. }
  36206. editor.on('SetContent', () => {
  36207. editor.addVisual(editor.getBody());
  36208. });
  36209. editor.on('compositionstart compositionend', (e) => {
  36210. editor.composing = e.type === 'compositionstart';
  36211. });
  36212. };
  36213. const loadInitialContent = (editor) => {
  36214. if (!isRtc(editor)) {
  36215. editor.load({ initial: true, format: 'html' });
  36216. }
  36217. editor.startContent = editor.getContent({ format: 'raw' });
  36218. };
  36219. const initEditorWithInitialContent = (editor) => {
  36220. if (editor.removed !== true) {
  36221. loadInitialContent(editor);
  36222. initEditor(editor);
  36223. }
  36224. };
  36225. const startProgress = (editor) => {
  36226. let canceled = false;
  36227. const progressTimeout = setTimeout(() => {
  36228. if (!canceled) {
  36229. editor.setProgressState(true);
  36230. }
  36231. }, 500);
  36232. return () => {
  36233. clearTimeout(progressTimeout);
  36234. canceled = true;
  36235. editor.setProgressState(false);
  36236. };
  36237. };
  36238. const contentBodyLoaded = (editor) => {
  36239. const targetElm = editor.getElement();
  36240. let doc = editor.getDoc();
  36241. if (editor.inline) {
  36242. DOM$6.addClass(targetElm, 'mce-content-body');
  36243. editor.contentDocument = doc = document;
  36244. editor.contentWindow = window;
  36245. editor.bodyElement = targetElm;
  36246. editor.contentAreaContainer = targetElm;
  36247. }
  36248. // It will not steal focus while setting contentEditable
  36249. const body = editor.getBody();
  36250. // disabled isn't valid on all body elements, so need to cast here
  36251. // TODO: See if we actually need to disable/re-enable here
  36252. body.disabled = true;
  36253. editor.readonly = isReadOnly$1(editor);
  36254. editor._editableRoot = hasEditableRoot$1(editor);
  36255. if (!isDisabled$1(editor) && editor.hasEditableRoot()) {
  36256. if (editor.inline && DOM$6.getStyle(body, 'position', true) === 'static') {
  36257. body.style.position = 'relative';
  36258. }
  36259. body.contentEditable = 'true';
  36260. }
  36261. body.disabled = false;
  36262. editor.editorUpload = EditorUpload(editor);
  36263. editor.schema = Schema(mkSchemaSettings(editor));
  36264. editor.dom = DOMUtils(doc, {
  36265. keep_values: true,
  36266. // Note: Don't bind here, as the binding is handled via the `url_converter_scope`
  36267. // eslint-disable-next-line @typescript-eslint/unbound-method
  36268. url_converter: editor.convertURL,
  36269. url_converter_scope: editor,
  36270. update_styles: true,
  36271. root_element: editor.inline ? editor.getBody() : null,
  36272. collect: editor.inline,
  36273. schema: editor.schema,
  36274. contentCssCors: shouldUseContentCssCors(editor),
  36275. referrerPolicy: getReferrerPolicy(editor),
  36276. crossOrigin: getCrossOrigin(editor),
  36277. onSetAttrib: (e) => {
  36278. editor.dispatch('SetAttrib', e);
  36279. },
  36280. });
  36281. editor.parser = createParser(editor);
  36282. editor.serializer = DomSerializer(mkSerializerSettings(editor), editor);
  36283. editor.selection = EditorSelection(editor.dom, editor.getWin(), editor.serializer, editor);
  36284. editor.annotator = Annotator(editor);
  36285. editor.formatter = Formatter(editor);
  36286. editor.undoManager = UndoManager(editor);
  36287. editor._nodeChangeDispatcher = new NodeChange(editor);
  36288. editor._selectionOverrides = SelectionOverrides(editor);
  36289. setup$b(editor);
  36290. setup$u(editor);
  36291. setup$6(editor);
  36292. setup$s(editor);
  36293. if (!isRtc(editor)) {
  36294. setup$5(editor);
  36295. setup$1(editor);
  36296. }
  36297. const caret = setup$g(editor);
  36298. setup$v(editor, caret);
  36299. setup$t(editor);
  36300. setup$w(editor);
  36301. setup$7(editor, caret);
  36302. const setupRtcThunk = setup$z(editor);
  36303. preInit(editor);
  36304. setupRtcThunk.fold(() => {
  36305. const cancelProgress = startProgress(editor);
  36306. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  36307. loadContentCss(editor).then(() => {
  36308. initEditorWithInitialContent(editor);
  36309. cancelProgress();
  36310. });
  36311. }, (setupRtc) => {
  36312. editor.setProgressState(true);
  36313. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  36314. loadContentCss(editor).then(() => {
  36315. setupRtc().then((_rtcMode) => {
  36316. editor.setProgressState(false);
  36317. initEditorWithInitialContent(editor);
  36318. bindEvents(editor);
  36319. }, (err) => {
  36320. editor.notificationManager.open({ type: 'error', text: String(err) });
  36321. initEditorWithInitialContent(editor);
  36322. bindEvents(editor);
  36323. });
  36324. });
  36325. });
  36326. };
  36327. const DOM$5 = DOMUtils.DOM;
  36328. const createIframeElement = (id, title, customAttrs, tabindex) => {
  36329. const iframe = SugarElement.fromTag('iframe');
  36330. // This can also be explicitly set by customAttrs, so do this first
  36331. tabindex.each((t) => set$4(iframe, 'tabindex', t));
  36332. setAll$1(iframe, customAttrs);
  36333. setAll$1(iframe, {
  36334. id: id + '_ifr',
  36335. frameBorder: '0',
  36336. allowTransparency: 'true',
  36337. title
  36338. });
  36339. add$2(iframe, 'tox-edit-area__iframe');
  36340. return iframe;
  36341. };
  36342. const getIframeHtml = (editor) => {
  36343. let iframeHTML = getDocType(editor) + '<html><head>';
  36344. // We only need to override paths if we have to
  36345. // IE has a bug where it remove site absolute urls to relative ones if this is specified
  36346. if (getDocumentBaseUrl(editor) !== editor.editorManager.documentBaseURL) {
  36347. iframeHTML += '<base href="' + editor.documentBaseURI.getURI() + '" />';
  36348. }
  36349. iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />';
  36350. const bodyId = getBodyId(editor);
  36351. const bodyClass = getBodyClass(editor);
  36352. const translatedAriaText = editor.translate(getIframeAriaText(editor));
  36353. if (getContentSecurityPolicy(editor)) {
  36354. iframeHTML += '<meta http-equiv="Content-Security-Policy" content="' + getContentSecurityPolicy(editor) + '" />';
  36355. }
  36356. iframeHTML += '</head>' +
  36357. `<body id="${bodyId}" class="mce-content-body ${bodyClass}" data-id="${editor.id}" aria-label="${translatedAriaText}">` +
  36358. '<br>' +
  36359. '</body></html>';
  36360. return iframeHTML;
  36361. };
  36362. const createIframe = (editor, boxInfo) => {
  36363. const iframeTitle = Env.browser.isFirefox() ? getIframeAriaText(editor) : 'Rich Text Area';
  36364. const translatedTitle = editor.translate(iframeTitle);
  36365. const tabindex = getOpt(SugarElement.fromDom(editor.getElement()), 'tabindex').bind(toInt);
  36366. const ifr = createIframeElement(editor.id, translatedTitle, getIframeAttrs(editor), tabindex).dom;
  36367. ifr.onload = () => {
  36368. ifr.onload = null;
  36369. editor.dispatch('load');
  36370. };
  36371. editor.contentAreaContainer = boxInfo.iframeContainer;
  36372. editor.iframeElement = ifr;
  36373. editor.iframeHTML = getIframeHtml(editor);
  36374. DOM$5.add(boxInfo.iframeContainer, ifr);
  36375. };
  36376. const setupIframeBody = (editor) => {
  36377. // Setup iframe body
  36378. const iframe = editor.iframeElement;
  36379. const ready = () => {
  36380. // Set the content document, now that it is available
  36381. editor.contentDocument = iframe.contentDocument;
  36382. // Continue to init the editor
  36383. contentBodyLoaded(editor);
  36384. };
  36385. // TINY-8916: Firefox has a bug in its srcdoc implementation that prevents cookies being sent so unfortunately we need
  36386. // to fallback to legacy APIs to load the iframe content. See https://bugzilla.mozilla.org/show_bug.cgi?id=1741489
  36387. if (shouldUseDocumentWrite(editor) || Env.browser.isFirefox()) {
  36388. const doc = editor.getDoc();
  36389. doc.open();
  36390. doc.write(editor.iframeHTML);
  36391. doc.close();
  36392. ready();
  36393. }
  36394. else {
  36395. const binder = bind$1(SugarElement.fromDom(iframe), 'load', () => {
  36396. binder.unbind();
  36397. ready();
  36398. });
  36399. iframe.srcdoc = editor.iframeHTML;
  36400. }
  36401. };
  36402. const init$1 = (editor, boxInfo) => {
  36403. createIframe(editor, boxInfo);
  36404. if (boxInfo.editorContainer) {
  36405. boxInfo.editorContainer.style.display = editor.orgDisplay;
  36406. editor.hidden = DOM$5.isHidden(boxInfo.editorContainer);
  36407. }
  36408. editor.getElement().style.display = 'none';
  36409. DOM$5.setAttrib(editor.id, 'aria-hidden', 'true');
  36410. // Restore visibility on target element
  36411. editor.getElement().style.visibility = editor.orgVisibility;
  36412. setupIframeBody(editor);
  36413. };
  36414. const DOM$4 = DOMUtils.DOM;
  36415. const initPlugin = (editor, initializedPlugins, plugin) => {
  36416. const Plugin = PluginManager.get(plugin);
  36417. const pluginUrl = PluginManager.urls[plugin] || editor.editorManager.documentBaseURL.replace(/\/$/, '');
  36418. plugin = Tools.trim(plugin);
  36419. if (Plugin && Tools.inArray(initializedPlugins, plugin) === -1) {
  36420. if (editor.plugins[plugin]) {
  36421. return;
  36422. }
  36423. try {
  36424. const pluginInstance = Plugin(editor, pluginUrl) || {};
  36425. editor.plugins[plugin] = pluginInstance;
  36426. if (isFunction(pluginInstance.init)) {
  36427. pluginInstance.init(editor, pluginUrl);
  36428. initializedPlugins.push(plugin);
  36429. }
  36430. }
  36431. catch (e) {
  36432. pluginInitError(editor, plugin, e);
  36433. }
  36434. }
  36435. };
  36436. const initTooltipClosing = (editor) => {
  36437. const closeTooltipsListener = (event) => {
  36438. if (event.keyCode === VK.ESC && !event.defaultPrevented) {
  36439. if (fireCloseTooltips(editor).isDefaultPrevented()) {
  36440. event.preventDefault();
  36441. }
  36442. }
  36443. };
  36444. document.addEventListener('keyup', closeTooltipsListener);
  36445. if (!editor.inline) {
  36446. editor.on('keyup', closeTooltipsListener);
  36447. }
  36448. editor.on('remove', () => {
  36449. document.removeEventListener('keyup', closeTooltipsListener);
  36450. if (!editor.inline) {
  36451. editor.off('keyup', closeTooltipsListener);
  36452. }
  36453. });
  36454. };
  36455. const trimLegacyPrefix = (name) => {
  36456. // Themes and plugins can be prefixed with - to prevent them from being lazy loaded
  36457. return name.replace(/^\-/, '');
  36458. };
  36459. const initPlugins = (editor) => {
  36460. const initializedPlugins = [];
  36461. each$e(getPlugins(editor), (name) => {
  36462. initPlugin(editor, initializedPlugins, trimLegacyPrefix(name));
  36463. });
  36464. };
  36465. const initIcons = (editor) => {
  36466. const iconPackName = Tools.trim(getIconPackName(editor));
  36467. const currentIcons = editor.ui.registry.getAll().icons;
  36468. const loadIcons = {
  36469. ...IconManager.get('default').icons,
  36470. ...IconManager.get(iconPackName).icons
  36471. };
  36472. each$d(loadIcons, (svgData, icon) => {
  36473. // Don't override an icon registered manually
  36474. if (!has$2(currentIcons, icon)) {
  36475. editor.ui.registry.addIcon(icon, svgData);
  36476. }
  36477. });
  36478. };
  36479. const initTheme = (editor) => {
  36480. const theme = getTheme(editor);
  36481. if (isString(theme)) {
  36482. const Theme = ThemeManager.get(theme);
  36483. editor.theme = Theme(editor, ThemeManager.urls[theme]) || {};
  36484. if (isFunction(editor.theme.init)) {
  36485. editor.theme.init(editor, ThemeManager.urls[theme] || editor.editorManager.documentBaseURL.replace(/\/$/, ''));
  36486. }
  36487. }
  36488. else {
  36489. // Theme set to false or null doesn't produce a theme api
  36490. editor.theme = {};
  36491. }
  36492. };
  36493. const initModel = (editor) => {
  36494. const model = getModel(editor);
  36495. const Model = ModelManager.get(model);
  36496. editor.model = Model(editor, ModelManager.urls[model]);
  36497. };
  36498. const initLicenseKeyManager = (editor) => {
  36499. LicenseKeyManagerLoader.init(editor);
  36500. };
  36501. const renderFromLoadedTheme = (editor) => {
  36502. // Render UI
  36503. const render = editor.theme.renderUI;
  36504. return render ? render() : renderThemeFalse(editor);
  36505. };
  36506. const renderFromThemeFunc = (editor) => {
  36507. const elm = editor.getElement();
  36508. const theme = getTheme(editor);
  36509. const info = theme(editor, elm);
  36510. if (info.editorContainer.nodeType) {
  36511. info.editorContainer.id = info.editorContainer.id || editor.id + '_parent';
  36512. }
  36513. if (info.iframeContainer && info.iframeContainer.nodeType) {
  36514. info.iframeContainer.id = info.iframeContainer.id || editor.id + '_iframecontainer';
  36515. }
  36516. info.height = info.iframeHeight ? info.iframeHeight : elm.offsetHeight;
  36517. return info;
  36518. };
  36519. const createThemeFalseResult = (element, iframe) => {
  36520. return {
  36521. editorContainer: element,
  36522. iframeContainer: iframe,
  36523. api: {}
  36524. };
  36525. };
  36526. const renderThemeFalseIframe = (targetElement) => {
  36527. const iframeContainer = DOM$4.create('div');
  36528. DOM$4.insertAfter(iframeContainer, targetElement);
  36529. return createThemeFalseResult(iframeContainer, iframeContainer);
  36530. };
  36531. const renderThemeFalse = (editor) => {
  36532. const targetElement = editor.getElement();
  36533. return editor.inline ? createThemeFalseResult(null) : renderThemeFalseIframe(targetElement);
  36534. };
  36535. const renderThemeUi = (editor) => {
  36536. const elm = editor.getElement();
  36537. editor.orgDisplay = elm.style.display;
  36538. if (isString(getTheme(editor))) {
  36539. return renderFromLoadedTheme(editor);
  36540. }
  36541. else if (isFunction(getTheme(editor))) {
  36542. return renderFromThemeFunc(editor);
  36543. }
  36544. else {
  36545. return renderThemeFalse(editor);
  36546. }
  36547. };
  36548. const augmentEditorUiApi = (editor, api) => {
  36549. const uiApiFacade = {
  36550. show: Optional.from(api.show).getOr(noop),
  36551. hide: Optional.from(api.hide).getOr(noop),
  36552. isEnabled: Optional.from(api.isEnabled).getOr(always),
  36553. setEnabled: (state) => {
  36554. const shouldSkip = state && (editor.mode.get() === 'readonly' || isDisabled(editor));
  36555. if (!shouldSkip) {
  36556. Optional.from(api.setEnabled).each((f) => f(state));
  36557. }
  36558. }
  36559. };
  36560. editor.ui = { ...editor.ui, ...uiApiFacade };
  36561. };
  36562. const init = async (editor) => {
  36563. editor.dispatch('ScriptsLoaded');
  36564. initIcons(editor);
  36565. initTooltipClosing(editor);
  36566. initTheme(editor);
  36567. initModel(editor);
  36568. initLicenseKeyManager(editor);
  36569. initPlugins(editor);
  36570. const renderInfo = await renderThemeUi(editor);
  36571. augmentEditorUiApi(editor, Optional.from(renderInfo.api).getOr({}));
  36572. editor.editorContainer = renderInfo.editorContainer;
  36573. appendContentCssFromSettings(editor);
  36574. if (editor.inline) {
  36575. contentBodyLoaded(editor);
  36576. }
  36577. else {
  36578. init$1(editor, {
  36579. editorContainer: renderInfo.editorContainer,
  36580. iframeContainer: renderInfo.iframeContainer
  36581. });
  36582. }
  36583. };
  36584. const DOM$3 = DOMUtils.DOM;
  36585. const hasSkipLoadPrefix = (name) => name.charAt(0) === '-';
  36586. const loadLanguage = (scriptLoader, editor) => {
  36587. const languageCode = getLanguageCode(editor);
  36588. const languageUrl = getLanguageUrl(editor);
  36589. if (!I18n.hasCode(languageCode) && languageCode !== 'en') {
  36590. const url = isNotEmpty(languageUrl) ? languageUrl : `${editor.editorManager.baseURL}/langs/${languageCode}.js`;
  36591. scriptLoader.add(url).catch(() => {
  36592. languageLoadError(editor, url, languageCode);
  36593. });
  36594. }
  36595. };
  36596. const loadTheme = (editor, suffix) => {
  36597. const theme = getTheme(editor);
  36598. if (isString(theme) && !hasSkipLoadPrefix(theme) && !has$2(ThemeManager.urls, theme)) {
  36599. const themeUrl = getThemeUrl(editor);
  36600. const url = themeUrl ? editor.documentBaseURI.toAbsolute(themeUrl) : `themes/${theme}/theme${suffix}.js`;
  36601. ThemeManager.load(theme, url).catch(() => {
  36602. themeLoadError(editor, url, theme);
  36603. });
  36604. }
  36605. };
  36606. const loadModel = (editor, suffix) => {
  36607. // Special case the 'wait for model' code if a plugin is responsible for it
  36608. // as the plugin will provide the instance instead
  36609. const model = getModel(editor);
  36610. if (model !== 'plugin' && !has$2(ModelManager.urls, model)) {
  36611. const modelUrl = getModelUrl(editor);
  36612. const url = isString(modelUrl) ? editor.documentBaseURI.toAbsolute(modelUrl) : `models/${model}/model${suffix}.js`;
  36613. ModelManager.load(model, url).catch(() => {
  36614. modelLoadError(editor, url, model);
  36615. });
  36616. }
  36617. };
  36618. const loadLicenseKeyManager = (editor, suffix) => {
  36619. LicenseKeyManagerLoader.load(editor, suffix);
  36620. };
  36621. const getIconsUrlMetaFromUrl = (editor) => Optional.from(getIconsUrl(editor))
  36622. .filter(isNotEmpty)
  36623. .map((url) => ({
  36624. url,
  36625. name: Optional.none()
  36626. }));
  36627. const getIconsUrlMetaFromName = (editor, name, suffix) => Optional.from(name)
  36628. .filter((name) => isNotEmpty(name) && !IconManager.has(name))
  36629. .map((name) => ({
  36630. url: `${editor.editorManager.baseURL}/icons/${name}/icons${suffix}.js`,
  36631. name: Optional.some(name)
  36632. }));
  36633. const loadIcons = (scriptLoader, editor, suffix) => {
  36634. const defaultIconsUrl = getIconsUrlMetaFromName(editor, 'default', suffix);
  36635. const customIconsUrl = getIconsUrlMetaFromUrl(editor).orThunk(() => getIconsUrlMetaFromName(editor, getIconPackName(editor), ''));
  36636. each$e(cat([defaultIconsUrl, customIconsUrl]), (urlMeta) => {
  36637. scriptLoader.add(urlMeta.url).catch(() => {
  36638. iconsLoadError(editor, urlMeta.url, urlMeta.name.getOrUndefined());
  36639. });
  36640. });
  36641. };
  36642. const loadPlugins = (editor, suffix) => {
  36643. const loadPlugin = (name, url) => {
  36644. // If licensekeymanager is included in the plugins list
  36645. // or through external_plugins, skip it
  36646. if (name === 'licensekeymanager') {
  36647. return;
  36648. }
  36649. PluginManager.load(name, url).catch(() => {
  36650. pluginLoadError(editor, url, name);
  36651. });
  36652. };
  36653. each$d(getExternalPlugins$1(editor), (url, name) => {
  36654. loadPlugin(name, url);
  36655. editor.options.set('plugins', getPlugins(editor).concat(name));
  36656. });
  36657. each$e(getPlugins(editor), (plugin) => {
  36658. plugin = Tools.trim(plugin);
  36659. if (plugin && !PluginManager.urls[plugin] && !hasSkipLoadPrefix(plugin)) {
  36660. loadPlugin(plugin, `plugins/${plugin}/plugin${suffix}.js`);
  36661. }
  36662. });
  36663. };
  36664. const isThemeLoaded = (editor) => {
  36665. const theme = getTheme(editor);
  36666. return !isString(theme) || isNonNullable(ThemeManager.get(theme));
  36667. };
  36668. const isModelLoaded = (editor) => {
  36669. const model = getModel(editor);
  36670. return isNonNullable(ModelManager.get(model));
  36671. };
  36672. const loadScripts = (editor, suffix) => {
  36673. const scriptLoader = ScriptLoader.ScriptLoader;
  36674. const initEditor = () => {
  36675. // If the editor has been destroyed or the theme, model haven't loaded then
  36676. // don't continue to load the editor
  36677. if (!editor.removed &&
  36678. isThemeLoaded(editor) &&
  36679. isModelLoaded(editor)) {
  36680. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  36681. init(editor);
  36682. }
  36683. };
  36684. loadTheme(editor, suffix);
  36685. loadModel(editor, suffix);
  36686. loadLicenseKeyManager(editor, suffix);
  36687. loadLanguage(scriptLoader, editor);
  36688. loadIcons(scriptLoader, editor, suffix);
  36689. loadPlugins(editor, suffix);
  36690. scriptLoader.loadQueue().then(initEditor, initEditor);
  36691. };
  36692. const getStyleSheetLoader = (element, editor) => instance.forElement(element, {
  36693. contentCssCors: hasContentCssCors(editor),
  36694. referrerPolicy: getReferrerPolicy(editor)
  36695. });
  36696. const render = (editor) => {
  36697. const id = editor.id;
  36698. // The user might have bundled multiple language packs so we need to switch the active code to the user specified language
  36699. I18n.setCode(getLanguageCode(editor));
  36700. const readyHandler = () => {
  36701. DOM$3.unbind(window, 'ready', readyHandler);
  36702. editor.render();
  36703. };
  36704. // Page is not loaded yet, wait for it
  36705. if (!EventUtils.Event.domLoaded) {
  36706. DOM$3.bind(window, 'ready', readyHandler);
  36707. return;
  36708. }
  36709. // Element not found, then skip initialization
  36710. if (!editor.getElement()) {
  36711. return;
  36712. }
  36713. // snapshot the element we're going to render to
  36714. const element = SugarElement.fromDom(editor.getElement());
  36715. const snapshot = clone$4(element);
  36716. editor.on('remove', () => {
  36717. eachr(element.dom.attributes, (attr) => remove$9(element, attr.name));
  36718. setAll$1(element, snapshot);
  36719. });
  36720. editor.ui.styleSheetLoader = getStyleSheetLoader(element, editor);
  36721. // Hide target element early to prevent content flashing
  36722. if (!isInline$2(editor)) {
  36723. editor.orgVisibility = editor.getElement().style.visibility;
  36724. editor.getElement().style.visibility = 'hidden';
  36725. }
  36726. else {
  36727. editor.inline = true;
  36728. }
  36729. // TODO: Investigate the types here
  36730. const form = editor.getElement().form || DOM$3.getParent(id, 'form');
  36731. if (form) {
  36732. editor.formElement = form;
  36733. // Add hidden input for non input elements inside form elements
  36734. if (hasHiddenInput(editor) && !isTextareaOrInput(editor.getElement())) {
  36735. DOM$3.insertAfter(DOM$3.create('input', { type: 'hidden', name: id }), id);
  36736. editor.hasHiddenInput = true;
  36737. }
  36738. // Pass submit/reset from form to editor instance
  36739. editor.formEventDelegate = (e) => {
  36740. editor.dispatch(e.type, e);
  36741. };
  36742. DOM$3.bind(form, 'submit reset', editor.formEventDelegate);
  36743. // Reset contents in editor when the form is reset
  36744. editor.on('reset', () => {
  36745. editor.resetContent();
  36746. });
  36747. // Check page uses id="submit" or name="submit" for it's submit button
  36748. if (shouldPatchSubmit(editor) && !form.submit.nodeType && !form.submit.length && !form._mceOldSubmit) {
  36749. form._mceOldSubmit = form.submit;
  36750. form.submit = () => {
  36751. editor.editorManager.triggerSave();
  36752. editor.setDirty(false);
  36753. return form._mceOldSubmit(form);
  36754. };
  36755. }
  36756. }
  36757. editor.windowManager = WindowManager(editor);
  36758. editor.notificationManager = NotificationManager(editor);
  36759. if (isEncodingXml(editor)) {
  36760. editor.on('GetContent', (e) => {
  36761. if (e.save) {
  36762. e.content = DOM$3.encode(e.content);
  36763. }
  36764. });
  36765. }
  36766. if (shouldAddFormSubmitTrigger(editor)) {
  36767. editor.on('submit', () => {
  36768. if (editor.initialized) {
  36769. editor.save();
  36770. }
  36771. });
  36772. }
  36773. if (shouldAddUnloadTrigger(editor)) {
  36774. editor._beforeUnload = () => {
  36775. if (editor.initialized && !editor.destroyed && !editor.isHidden()) {
  36776. editor.save({ format: 'raw', no_events: true, set_dirty: false });
  36777. }
  36778. };
  36779. editor.editorManager.on('BeforeUnload', editor._beforeUnload);
  36780. }
  36781. editor.editorManager.add(editor);
  36782. loadScripts(editor, editor.suffix);
  36783. };
  36784. const AvatarColors = [
  36785. '#E41B60', // Pink
  36786. '#AD1457', // Dark Pink
  36787. '#1939EC', // Indigo
  36788. '#001CB5', // Dark Indigo
  36789. '#648000', // Lime
  36790. '#465B00', // Dark Lime
  36791. '#006CE7', // Blue
  36792. '#0054B4', // Dark Blue
  36793. '#00838F', // Cyan
  36794. '#006064', // Dark Cyan
  36795. '#00866F', // Turquoise
  36796. '#004D40', // Dark Turquoise
  36797. '#51742F', // Green
  36798. '#385021', // Dark Green
  36799. '#CF4900', // Orange
  36800. '#A84600', // Dark Orange
  36801. '#CC0000', // Red
  36802. '#6A1B9A', // Dark Red
  36803. '#9C27B0', // Purple
  36804. '#6A00AB', // Dark Purple
  36805. '#3041BA', // Navy Blue
  36806. '#0A1877', // Dark Navy Blue
  36807. '#774433', // Brown
  36808. '#452B24', // Dark Brown
  36809. '#607D8B', // Blue Gray
  36810. '#455A64', // Dark Blue Gray
  36811. ];
  36812. const getFirstChar = (name) => {
  36813. var _a;
  36814. if (Intl.Segmenter) {
  36815. const segmenter = new Intl.Segmenter();
  36816. const iterator = segmenter.segment(name)[Symbol.iterator]();
  36817. return `${(_a = iterator.next().value) === null || _a === void 0 ? void 0 : _a.segment}`;
  36818. }
  36819. else {
  36820. return name.trim()[0];
  36821. }
  36822. };
  36823. const getRandomColor = () => {
  36824. const colorIdx = Math.floor(random() * AvatarColors.length);
  36825. return AvatarColors[colorIdx];
  36826. };
  36827. const generate = (name, color, size = 36) => {
  36828. const halfSize = size / 2;
  36829. return `<svg height="${size}" width="${size}" xmlns="http://www.w3.org/2000/svg">` +
  36830. `<circle cx="${halfSize}" cy="${halfSize}" r="${halfSize}" fill="${color}"/>` +
  36831. `<text x="50%" y="50%" text-anchor="middle" dominant-baseline="central" fill="#FFF" font-family="sans-serif" font-size="${halfSize}">` +
  36832. getFirstChar(name) +
  36833. `</text>` +
  36834. '</svg>';
  36835. };
  36836. const deriveAvatar = (name) => {
  36837. const avatarSvg = generate(name, getRandomColor());
  36838. return 'data:image/svg+xml,' + encodeURIComponent(avatarSvg);
  36839. };
  36840. const userSchema = objOf([
  36841. required('id'),
  36842. optionString('name'),
  36843. optionString('avatar'),
  36844. option('custom')
  36845. ]);
  36846. const objectCat = (obj) => {
  36847. const result = {};
  36848. each$d(obj, (value, key) => {
  36849. value.each((v) => {
  36850. result[key] = v;
  36851. });
  36852. });
  36853. return result;
  36854. };
  36855. const validateResponse = (items) => {
  36856. if (!Array.isArray(items)) {
  36857. throw new Error('fetch_users must return an array');
  36858. }
  36859. const results = map$3(items, (item) => asRaw('Invalid user object', userSchema, item));
  36860. const { errors, values } = partition$1(results);
  36861. if (errors.length > 0) {
  36862. const formattedErrors = map$3(errors, (error, idx) => `User at index ${idx}: ${formatError(error)}`);
  36863. // eslint-disable-next-line no-console
  36864. console.warn('User validation errors:\n' + formattedErrors.join('\n'));
  36865. }
  36866. return map$3(values, (user) => {
  36867. const { id, name, avatar, ...rest } = user;
  36868. return {
  36869. id,
  36870. name: name.getOr(id),
  36871. avatar: avatar.getOr(deriveAvatar(name.getOr(id))),
  36872. ...objectCat(rest),
  36873. };
  36874. });
  36875. };
  36876. const UserLookup = (editor) => {
  36877. const userCache = new Map();
  36878. const pendingResolvers = new Map();
  36879. const lookup = (userId) => Optional.from(userCache.get(userId));
  36880. const store = (user, userId) => {
  36881. userCache.set(userId, user);
  36882. };
  36883. const finallyReject = (userId, error) => Optional
  36884. .from(pendingResolvers.get(userId))
  36885. .each(({ reject }) => {
  36886. reject(error);
  36887. pendingResolvers.delete(userId);
  36888. });
  36889. const finallyResolve = (userId, user) => Optional
  36890. .from(pendingResolvers.get(userId))
  36891. .each(({ resolve }) => {
  36892. resolve(user);
  36893. pendingResolvers.delete(userId);
  36894. });
  36895. const fetchUsers = (userIds) => {
  36896. const fetchUsersFn = getFetchUsers(editor);
  36897. if (!Array.isArray(userIds)) {
  36898. return {};
  36899. }
  36900. else if (!fetchUsersFn) {
  36901. return mapToObject(userIds, (userId) => Promise.resolve({
  36902. id: userId,
  36903. name: userId,
  36904. avatar: deriveAvatar(userId)
  36905. }));
  36906. }
  36907. const uncachedIds = unique$1(filter$5((userIds), (userId) => !lookup(userId).isSome()));
  36908. each$e(uncachedIds, (userId) => {
  36909. const newPromise = new Promise((resolve, reject) => {
  36910. pendingResolvers.set(userId, { resolve, reject });
  36911. });
  36912. store(newPromise, userId);
  36913. });
  36914. if (uncachedIds.length > 0) {
  36915. fetchUsersFn(uncachedIds)
  36916. .then(validateResponse)
  36917. .then((users) => {
  36918. const foundUserIds = new Set(map$3(users, (user) => user.id));
  36919. // Resolve found users
  36920. each$e(users, (user) => finallyResolve(user.id, user));
  36921. // Reject promises for users not found in the response
  36922. each$e(uncachedIds, (userId) => {
  36923. if (!foundUserIds.has(userId)) {
  36924. finallyReject(userId, new Error(`User ${userId} not found`));
  36925. }
  36926. });
  36927. })
  36928. .catch((error) => {
  36929. each$e(uncachedIds, (userId) => finallyReject(userId, error instanceof Error ? error : new Error('Network error')));
  36930. });
  36931. }
  36932. return foldl(userIds, (acc, userId) => {
  36933. acc[userId] = lookup(userId).getOr(Promise.resolve({
  36934. id: userId,
  36935. name: userId,
  36936. avatar: deriveAvatar(userId)
  36937. }));
  36938. return acc;
  36939. }, {});
  36940. };
  36941. const userId = getUserId(editor);
  36942. return Object.freeze({
  36943. userId,
  36944. fetchUsers,
  36945. });
  36946. };
  36947. const createUserLookup = (editor) => UserLookup(editor);
  36948. const setEditableRoot = (editor, state) => {
  36949. if (editor._editableRoot !== state) {
  36950. editor._editableRoot = state;
  36951. if (!isDisabled(editor)) {
  36952. editor.getBody().contentEditable = String(editor.hasEditableRoot());
  36953. editor.nodeChanged();
  36954. }
  36955. fireEditableRootStateChange(editor, state);
  36956. }
  36957. };
  36958. const hasEditableRoot = (editor) => editor._editableRoot;
  36959. const sectionResult = (sections, settings) => ({
  36960. sections: constant(sections),
  36961. options: constant(settings)
  36962. });
  36963. const deviceDetection = detect$1().deviceType;
  36964. const isPhone = deviceDetection.isPhone();
  36965. const isTablet = deviceDetection.isTablet();
  36966. const normalizePlugins = (plugins) => {
  36967. if (isNullable(plugins)) {
  36968. return [];
  36969. }
  36970. else {
  36971. const pluginNames = isArray$1(plugins) ? plugins : plugins.split(/[ ,]/);
  36972. const trimmedPlugins = map$3(pluginNames, trim$4);
  36973. return filter$5(trimmedPlugins, isNotEmpty);
  36974. }
  36975. };
  36976. const extractSections = (keys, options) => {
  36977. const result = bifilter(options, (value, key) => {
  36978. return contains$2(keys, key);
  36979. });
  36980. return sectionResult(result.t, result.f);
  36981. };
  36982. const getSection = (sectionResult, name, defaults = {}) => {
  36983. const sections = sectionResult.sections();
  36984. const sectionOptions = get$a(sections, name).getOr({});
  36985. return Tools.extend({}, defaults, sectionOptions);
  36986. };
  36987. const hasSection = (sectionResult, name) => {
  36988. return has$2(sectionResult.sections(), name);
  36989. };
  36990. const getSectionConfig = (sectionResult, name) => {
  36991. return hasSection(sectionResult, name) ? sectionResult.sections()[name] : {};
  36992. };
  36993. // Get a list of options to override any desktop options
  36994. const getMobileOverrideOptions = (mobileOptions, isPhone) => {
  36995. const defaultMobileOptions = {
  36996. table_grid: false, // Table grid relies on hover, which isn't available for touch devices so use the dialog instead
  36997. object_resizing: false, // No nice way to do object resizing at this stage
  36998. resize: false, // Editor resize doesn't make sense on mobile
  36999. toolbar_mode: get$a(mobileOptions, 'toolbar_mode').getOr('scrolling'), // Use the default side-scrolling toolbar for tablets/phones
  37000. toolbar_sticky: false // Only enable sticky toolbar on desktop by default
  37001. };
  37002. const defaultPhoneOptions = {
  37003. menubar: false // Phones don't have a lot of screen space, so disable the menubar
  37004. };
  37005. return {
  37006. ...defaultMobileOptions,
  37007. ...isPhone ? defaultPhoneOptions : {}
  37008. };
  37009. };
  37010. const getExternalPlugins = (overrideOptions, options) => {
  37011. var _a;
  37012. const userDefinedExternalPlugins = (_a = options.external_plugins) !== null && _a !== void 0 ? _a : {};
  37013. if (overrideOptions && overrideOptions.external_plugins) {
  37014. return Tools.extend({}, overrideOptions.external_plugins, userDefinedExternalPlugins);
  37015. }
  37016. else {
  37017. return userDefinedExternalPlugins;
  37018. }
  37019. };
  37020. const combinePlugins = (forcedPlugins, plugins) => [
  37021. ...normalizePlugins(forcedPlugins),
  37022. ...normalizePlugins(plugins)
  37023. ];
  37024. const getPlatformPlugins = (isMobileDevice, sectionResult, desktopPlugins, mobilePlugins) => {
  37025. // is a mobile device with any mobile options
  37026. if (isMobileDevice && hasSection(sectionResult, 'mobile')) {
  37027. return mobilePlugins;
  37028. // is desktop
  37029. }
  37030. else {
  37031. return desktopPlugins;
  37032. }
  37033. };
  37034. const processPlugins = (isMobileDevice, sectionResult, defaultOverrideOptions, options) => {
  37035. const forcedPlugins = normalizePlugins(defaultOverrideOptions.forced_plugins);
  37036. const desktopPlugins = normalizePlugins(options.plugins);
  37037. const mobileConfig = getSectionConfig(sectionResult, 'mobile');
  37038. const mobilePlugins = mobileConfig.plugins ? normalizePlugins(mobileConfig.plugins) : desktopPlugins;
  37039. const platformPlugins = getPlatformPlugins(isMobileDevice, sectionResult, desktopPlugins, mobilePlugins);
  37040. const combinedPlugins = combinePlugins(forcedPlugins, platformPlugins);
  37041. return Tools.extend(options, {
  37042. forced_plugins: forcedPlugins,
  37043. plugins: combinedPlugins
  37044. });
  37045. };
  37046. const isOnMobile = (isMobileDevice, sectionResult) => {
  37047. return isMobileDevice && hasSection(sectionResult, 'mobile');
  37048. };
  37049. const combineOptions = (isMobileDevice, isPhone, defaultOptions, defaultOverrideOptions, options) => {
  37050. var _a;
  37051. // Use mobile mode by default on phones, so patch in the mobile override options
  37052. const deviceOverrideOptions = isMobileDevice ? { mobile: getMobileOverrideOptions((_a = options.mobile) !== null && _a !== void 0 ? _a : {}, isPhone) } : {};
  37053. const sectionResult = extractSections(['mobile'], deepMerge(deviceOverrideOptions, options));
  37054. const extendedOptions = Tools.extend(
  37055. // Default options
  37056. defaultOptions,
  37057. // tinymce.overrideOptions options
  37058. defaultOverrideOptions,
  37059. // User options
  37060. sectionResult.options(),
  37061. // Sections
  37062. isOnMobile(isMobileDevice, sectionResult) ? getSection(sectionResult, 'mobile') : {},
  37063. // Forced options
  37064. {
  37065. external_plugins: getExternalPlugins(defaultOverrideOptions, sectionResult.options())
  37066. });
  37067. return processPlugins(isMobileDevice, sectionResult, defaultOverrideOptions, extendedOptions);
  37068. };
  37069. const normalizeOptions = (defaultOverrideOptions, options) => {
  37070. const copiedOptions = merge$1(options);
  37071. return combineOptions(isPhone || isTablet, isPhone, copiedOptions, defaultOverrideOptions, copiedOptions);
  37072. };
  37073. const addVisual = (editor, elm) => addVisual$1(editor, elm);
  37074. const registerExecCommands$2 = (editor) => {
  37075. const toggleFormat = (name, value) => {
  37076. editor.formatter.toggle(name, value);
  37077. editor.nodeChanged();
  37078. };
  37079. const toggleAlign = (align) => () => {
  37080. // Remove all other alignments first
  37081. each$e('left,center,right,justify'.split(','), (name) => {
  37082. if (align !== name) {
  37083. editor.formatter.remove('align' + name);
  37084. }
  37085. });
  37086. if (align !== 'none') {
  37087. toggleFormat('align' + align);
  37088. }
  37089. };
  37090. editor.editorCommands.addCommands({
  37091. JustifyLeft: toggleAlign('left'),
  37092. JustifyCenter: toggleAlign('center'),
  37093. JustifyRight: toggleAlign('right'),
  37094. JustifyFull: toggleAlign('justify'),
  37095. JustifyNone: toggleAlign('none')
  37096. });
  37097. };
  37098. const registerQueryStateCommands = (editor) => {
  37099. const alignStates = (name) => () => {
  37100. const selection = editor.selection;
  37101. const nodes = selection.isCollapsed() ? [editor.dom.getParent(selection.getNode(), editor.dom.isBlock)] : selection.getSelectedBlocks();
  37102. return exists(nodes, (node) => isNonNullable(editor.formatter.matchNode(node, name)));
  37103. };
  37104. editor.editorCommands.addCommands({
  37105. JustifyLeft: alignStates('alignleft'),
  37106. JustifyCenter: alignStates('aligncenter'),
  37107. JustifyRight: alignStates('alignright'),
  37108. JustifyFull: alignStates('alignjustify')
  37109. }, 'state');
  37110. };
  37111. const registerCommands$a = (editor) => {
  37112. registerExecCommands$2(editor);
  37113. registerQueryStateCommands(editor);
  37114. };
  37115. const registerCommands$9 = (editor) => {
  37116. editor.editorCommands.addCommands({
  37117. 'Cut,Copy,Paste': (command) => {
  37118. const doc = editor.getDoc();
  37119. let failed;
  37120. // Try executing the native command
  37121. try {
  37122. doc.execCommand(command);
  37123. }
  37124. catch (_a) {
  37125. // Command failed
  37126. failed = true;
  37127. }
  37128. // Chrome reports the paste command as supported however older IE:s will return false for cut/paste
  37129. if (command === 'paste' && !doc.queryCommandEnabled(command)) {
  37130. failed = true;
  37131. }
  37132. // Present alert message about clipboard access not being available
  37133. if (failed || !doc.queryCommandSupported(command)) {
  37134. let msg = editor.translate(`Your browser doesn't support direct access to the clipboard. ` +
  37135. 'Please use the Ctrl+X/C/V keyboard shortcuts instead.');
  37136. if (Env.os.isMacOS() || Env.os.isiOS()) {
  37137. msg = msg.replace(/Ctrl\+/g, '\u2318+');
  37138. }
  37139. editor.notificationManager.open({ text: msg, type: 'error' });
  37140. }
  37141. }
  37142. });
  37143. };
  37144. const trimOrPadLeftRight = (dom, rng, html, schema) => {
  37145. const root = SugarElement.fromDom(dom.getRoot());
  37146. // Adjust the start if it needs to be an nbsp
  37147. if (needsToBeNbspLeft(root, CaretPosition.fromRangeStart(rng), schema)) {
  37148. html = html.replace(/^ /, '&nbsp;');
  37149. }
  37150. else {
  37151. html = html.replace(/^&nbsp;/, ' ');
  37152. }
  37153. // Adjust the end if it needs to be an nbsp
  37154. if (needsToBeNbspRight(root, CaretPosition.fromRangeEnd(rng), schema)) {
  37155. html = html.replace(/(&nbsp;| )(<br( \/)>)?$/, '&nbsp;');
  37156. }
  37157. else {
  37158. html = html.replace(/&nbsp;(<br( \/)?>)?$/, ' ');
  37159. }
  37160. return html;
  37161. };
  37162. const processValue$1 = (value) => {
  37163. if (typeof value !== 'string') {
  37164. const details = Tools.extend({
  37165. paste: value.paste,
  37166. data: {
  37167. paste: value.paste
  37168. }
  37169. }, value);
  37170. return {
  37171. content: value.content,
  37172. details
  37173. };
  37174. }
  37175. return {
  37176. content: value,
  37177. details: {}
  37178. };
  37179. };
  37180. const trimOrPad = (editor, value) => {
  37181. const selection = editor.selection;
  37182. const dom = editor.dom;
  37183. // Check for whitespace before/after value
  37184. if (/^ | $/.test(value)) {
  37185. return trimOrPadLeftRight(dom, selection.getRng(), value, editor.schema);
  37186. }
  37187. else {
  37188. return value;
  37189. }
  37190. };
  37191. const insertAtCaret = (editor, value) => {
  37192. if (editor.selection.isEditable()) {
  37193. const { content, details } = processValue$1(value);
  37194. preProcessSetContent(editor, { ...details, content: trimOrPad(editor, content), format: 'html', set: false, selection: true }).each((args) => {
  37195. const insertedContent = insertContent$1(editor, args.content, details);
  37196. postProcessSetContent(editor, insertedContent, args);
  37197. editor.addVisual();
  37198. });
  37199. }
  37200. };
  37201. const registerCommands$8 = (editor) => {
  37202. editor.editorCommands.addCommands({
  37203. mceCleanup: () => {
  37204. const bm = editor.selection.getBookmark();
  37205. editor.setContent(editor.getContent());
  37206. editor.selection.moveToBookmark(bm);
  37207. },
  37208. insertImage: (_command, _ui, value) => {
  37209. insertAtCaret(editor, editor.dom.createHTML('img', { src: value }));
  37210. },
  37211. insertHorizontalRule: () => {
  37212. editor.execCommand('mceInsertContent', false, '<hr>');
  37213. },
  37214. insertText: (_command, _ui, value) => {
  37215. insertAtCaret(editor, editor.dom.encode(value));
  37216. },
  37217. insertHTML: (_command, _ui, value) => {
  37218. insertAtCaret(editor, value);
  37219. },
  37220. mceInsertContent: (_command, _ui, value) => {
  37221. insertAtCaret(editor, value);
  37222. },
  37223. mceSetContent: (_command, _ui, value) => {
  37224. editor.setContent(value);
  37225. },
  37226. mceReplaceContent: (_command, _ui, value) => {
  37227. editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, editor.selection.getContent({ format: 'text' })));
  37228. },
  37229. mceNewDocument: () => {
  37230. editor.setContent(getNewDocumentContent(editor));
  37231. }
  37232. });
  37233. };
  37234. const legacyPropNames = {
  37235. 'font-size': 'size',
  37236. 'font-family': 'face'
  37237. };
  37238. const isFont = isTag('font');
  37239. const getSpecifiedFontProp = (propName, rootElm, elm) => {
  37240. const getProperty = (elm) => getRaw$1(elm, propName).orThunk(() => {
  37241. if (isFont(elm)) {
  37242. return get$a(legacyPropNames, propName).bind((legacyPropName) => getOpt(elm, legacyPropName));
  37243. }
  37244. else {
  37245. return Optional.none();
  37246. }
  37247. });
  37248. const isRoot = (elm) => eq(SugarElement.fromDom(rootElm), elm);
  37249. return closest$1(SugarElement.fromDom(elm), (elm) => getProperty(elm), isRoot);
  37250. };
  37251. const normalizeFontFamily = (fontFamily) =>
  37252. // 'Font name', Font -> Font name,Font
  37253. fontFamily.replace(/[\'\"\\]/g, '').replace(/,\s+/g, ',');
  37254. const getComputedFontProp = (propName, elm) => Optional.from(DOMUtils.DOM.getStyle(elm, propName, true));
  37255. const getFontProp = (propName) => (rootElm, elm) => Optional.from(elm)
  37256. .map(SugarElement.fromDom)
  37257. .filter(isElement$8)
  37258. .bind((element) => getSpecifiedFontProp(propName, rootElm, element.dom)
  37259. .or(getComputedFontProp(propName, element.dom)))
  37260. .getOr('');
  37261. const getFontSize = getFontProp('font-size');
  37262. const getFontFamily = compose(normalizeFontFamily, getFontProp('font-family'));
  37263. const findFirstCaretElement = (editor) => firstPositionIn(editor.getBody())
  37264. .bind((caret) => {
  37265. const container = caret.container();
  37266. return Optional.from(isText$b(container) ? container.parentNode : container);
  37267. });
  37268. const getCaretElement = (editor) => Optional.from(editor.selection.getRng())
  37269. .bind((rng) => {
  37270. const root = editor.getBody();
  37271. const atStartOfNode = rng.startContainer === root && rng.startOffset === 0;
  37272. return atStartOfNode ? Optional.none() : Optional.from(editor.selection.getStart(true));
  37273. });
  37274. const bindRange = (editor, binder) => getCaretElement(editor)
  37275. .orThunk(curry(findFirstCaretElement, editor))
  37276. .map(SugarElement.fromDom)
  37277. .filter(isElement$8)
  37278. .bind(binder);
  37279. const mapRange = (editor, mapper) => bindRange(editor, compose1(Optional.some, mapper));
  37280. const fromFontSizeNumber = (editor, value) => {
  37281. if (/^[0-9.]+$/.test(value)) {
  37282. const fontSizeNumber = parseInt(value, 10);
  37283. // Convert font size 1-7 to styles
  37284. if (fontSizeNumber >= 1 && fontSizeNumber <= 7) {
  37285. const fontSizes = getFontStyleValues(editor);
  37286. const fontClasses = getFontSizeClasses(editor);
  37287. if (fontClasses.length > 0) {
  37288. return fontClasses[fontSizeNumber - 1] || value;
  37289. }
  37290. else {
  37291. return fontSizes[fontSizeNumber - 1] || value;
  37292. }
  37293. }
  37294. else {
  37295. return value;
  37296. }
  37297. }
  37298. else {
  37299. return value;
  37300. }
  37301. };
  37302. const normalizeFontNames = (font) => {
  37303. const fonts = font.split(/\s*,\s*/);
  37304. return map$3(fonts, (font) => {
  37305. if (font.indexOf(' ') !== -1 && !(startsWith(font, '"') || startsWith(font, `'`))) {
  37306. // TINY-3801: The font has spaces, so need to wrap with quotes as the browser sometimes automatically handles this, but not always
  37307. return `'${font}'`;
  37308. }
  37309. else {
  37310. return font;
  37311. }
  37312. }).join(',');
  37313. };
  37314. const fontNameAction = (editor, value) => {
  37315. const font = fromFontSizeNumber(editor, value);
  37316. editor.formatter.toggle('fontname', { value: normalizeFontNames(font) });
  37317. editor.nodeChanged();
  37318. };
  37319. const fontNameQuery = (editor) => mapRange(editor, (elm) => getFontFamily(editor.getBody(), elm.dom)).getOr('');
  37320. const fontSizeAction = (editor, value) => {
  37321. editor.formatter.toggle('fontsize', { value: fromFontSizeNumber(editor, value) });
  37322. editor.nodeChanged();
  37323. };
  37324. const fontSizeQuery = (editor) => mapRange(editor, (elm) => getFontSize(editor.getBody(), elm.dom)).getOr('');
  37325. const lineHeightQuery = (editor) => mapRange(editor, (elm) => {
  37326. const root = SugarElement.fromDom(editor.getBody());
  37327. const specifiedStyle = closest$1(elm, (elm) => getRaw$1(elm, 'line-height'), curry(eq, root));
  37328. const computedStyle = () => {
  37329. // Css.get returns computed values (in px), and parseFloat will strip any non-number suffix
  37330. const lineHeight = parseFloat(get$7(elm, 'line-height'));
  37331. const fontSize = parseFloat(get$7(elm, 'font-size'));
  37332. return String(lineHeight / fontSize);
  37333. };
  37334. return specifiedStyle.getOrThunk(computedStyle);
  37335. }).getOr('');
  37336. const lineHeightAction = (editor, lineHeight) => {
  37337. editor.formatter.toggle('lineheight', { value: String(lineHeight) });
  37338. editor.nodeChanged();
  37339. };
  37340. const registerExecCommands$1 = (editor) => {
  37341. const toggleFormat = (name, value) => {
  37342. editor.formatter.toggle(name, value);
  37343. editor.nodeChanged();
  37344. };
  37345. editor.editorCommands.addCommands({
  37346. 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': (command) => {
  37347. toggleFormat(command);
  37348. },
  37349. 'ForeColor,HiliteColor': (command, _ui, value) => {
  37350. toggleFormat(command, { value });
  37351. },
  37352. 'BackColor': (_command, _ui, value) => {
  37353. toggleFormat('hilitecolor', { value });
  37354. },
  37355. 'FontName': (_command, _ui, value) => {
  37356. fontNameAction(editor, value);
  37357. },
  37358. 'FontSize': (_command, _ui, value) => {
  37359. fontSizeAction(editor, value);
  37360. },
  37361. 'LineHeight': (_command, _ui, value) => {
  37362. lineHeightAction(editor, value);
  37363. },
  37364. 'Lang': (command, _ui, lang) => {
  37365. var _a;
  37366. toggleFormat(command, { value: lang.code, customValue: (_a = lang.customCode) !== null && _a !== void 0 ? _a : null });
  37367. },
  37368. 'RemoveFormat': (command) => {
  37369. editor.formatter.remove(command);
  37370. },
  37371. 'mceBlockQuote': () => {
  37372. toggleFormat('blockquote');
  37373. },
  37374. 'FormatBlock': (_command, _ui, value) => {
  37375. toggleFormat(isString(value) ? value : 'p');
  37376. },
  37377. 'mceToggleFormat': (_command, _ui, value) => {
  37378. toggleFormat(value);
  37379. }
  37380. });
  37381. };
  37382. const registerQueryValueCommands = (editor) => {
  37383. const isFormatMatch = (name) => editor.formatter.match(name);
  37384. editor.editorCommands.addCommands({
  37385. 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': (command) => isFormatMatch(command),
  37386. 'mceBlockQuote': () => isFormatMatch('blockquote')
  37387. }, 'state');
  37388. editor.editorCommands.addQueryValueHandler('FontName', () => fontNameQuery(editor));
  37389. editor.editorCommands.addQueryValueHandler('FontSize', () => fontSizeQuery(editor));
  37390. editor.editorCommands.addQueryValueHandler('LineHeight', () => lineHeightQuery(editor));
  37391. };
  37392. const registerCommands$7 = (editor) => {
  37393. registerExecCommands$1(editor);
  37394. registerQueryValueCommands(editor);
  37395. };
  37396. const registerCommands$6 = (editor) => {
  37397. editor.editorCommands.addCommands({
  37398. mceAddUndoLevel: () => {
  37399. editor.undoManager.add();
  37400. },
  37401. mceEndUndoLevel: () => {
  37402. editor.undoManager.add();
  37403. },
  37404. Undo: () => {
  37405. editor.undoManager.undo();
  37406. },
  37407. Redo: () => {
  37408. editor.undoManager.redo();
  37409. }
  37410. });
  37411. };
  37412. const registerCommands$5 = (editor) => {
  37413. editor.editorCommands.addCommands({
  37414. Indent: () => {
  37415. indent(editor);
  37416. },
  37417. Outdent: () => {
  37418. outdent(editor);
  37419. },
  37420. });
  37421. editor.editorCommands.addCommands({
  37422. Outdent: () => canOutdent(editor),
  37423. Indent: () => canIndent(editor),
  37424. }, 'state');
  37425. };
  37426. const registerCommands$4 = (editor) => {
  37427. const applyLinkToSelection = (_command, _ui, value) => {
  37428. if (editor.mode.isReadOnly()) {
  37429. return;
  37430. }
  37431. const linkDetails = isString(value) ? { href: value } : value;
  37432. const anchor = editor.dom.getParent(editor.selection.getNode(), 'a');
  37433. if (isObject(linkDetails) && isString(linkDetails.href)) {
  37434. // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here.
  37435. linkDetails.href = linkDetails.href.replace(/ /g, '%20');
  37436. // Remove existing links if there could be child links or that the href isn't specified
  37437. if (!anchor || !linkDetails.href) {
  37438. editor.formatter.remove('link');
  37439. }
  37440. // Apply new link to selection
  37441. if (linkDetails.href) {
  37442. editor.formatter.apply('link', linkDetails, anchor);
  37443. }
  37444. }
  37445. };
  37446. editor.editorCommands.addCommands({
  37447. unlink: () => {
  37448. if (editor.selection.isEditable()) {
  37449. if (editor.selection.isCollapsed()) {
  37450. const elm = editor.dom.getParent(editor.selection.getStart(), 'a');
  37451. if (elm) {
  37452. editor.dom.remove(elm, true);
  37453. }
  37454. return;
  37455. }
  37456. editor.formatter.remove('link');
  37457. }
  37458. },
  37459. mceInsertLink: applyLinkToSelection,
  37460. createLink: applyLinkToSelection
  37461. });
  37462. };
  37463. const getTopParentBlock = (editor, node, root, container) => {
  37464. const dom = editor.dom;
  37465. const selector = (node) => dom.isBlock(node) && node.parentElement === root;
  37466. const topParentBlock = selector(node) ? node : dom.getParent(container, selector, root);
  37467. return Optional.from(topParentBlock).map(SugarElement.fromDom);
  37468. };
  37469. const insert = (editor, before) => {
  37470. if (editor.mode.isReadOnly()) {
  37471. return;
  37472. }
  37473. const dom = editor.dom;
  37474. const rng = editor.selection.getRng();
  37475. const node = before ? editor.selection.getStart() : editor.selection.getEnd();
  37476. const container = before ? rng.startContainer : rng.endContainer;
  37477. const root = getEditableRoot(dom, container);
  37478. if (!root || !root.isContentEditable) {
  37479. return;
  37480. }
  37481. const insertFn = before ? before$4 : after$4;
  37482. const newBlockName = getForcedRootBlock(editor);
  37483. getTopParentBlock(editor, node, root, container).each((parentBlock) => {
  37484. const newBlock = createNewBlock(editor, container, parentBlock.dom, root, false, newBlockName);
  37485. insertFn(parentBlock, SugarElement.fromDom(newBlock));
  37486. editor.selection.setCursorLocation(newBlock, 0);
  37487. editor.dispatch('NewBlock', { newBlock });
  37488. fireInputEvent(editor, 'insertParagraph');
  37489. });
  37490. };
  37491. const insertBefore = (editor) => insert(editor, true);
  37492. const insertAfter = (editor) => insert(editor, false);
  37493. const registerCommands$3 = (editor) => {
  37494. editor.editorCommands.addCommands({
  37495. InsertNewBlockBefore: () => {
  37496. insertBefore(editor);
  37497. },
  37498. InsertNewBlockAfter: () => {
  37499. insertAfter(editor);
  37500. },
  37501. });
  37502. };
  37503. const registerCommands$2 = (editor) => {
  37504. editor.editorCommands.addCommands({
  37505. insertParagraph: () => {
  37506. insertBreak(blockbreak, editor);
  37507. },
  37508. mceInsertNewLine: (_command, _ui, value) => {
  37509. insert$1(editor, value);
  37510. },
  37511. InsertLineBreak: (_command, _ui, _value) => {
  37512. insertBreak(linebreak, editor);
  37513. }
  37514. });
  37515. };
  37516. const registerCommands$1 = (editor) => {
  37517. editor.editorCommands.addCommands({
  37518. mceSelectNodeDepth: (_command, _ui, value) => {
  37519. let counter = 0;
  37520. editor.dom.getParent(editor.selection.getNode(), (node) => {
  37521. if (isElement$7(node) && counter++ === value) {
  37522. editor.selection.select(node);
  37523. return false;
  37524. }
  37525. else {
  37526. return true;
  37527. }
  37528. }, editor.getBody());
  37529. },
  37530. mceSelectNode: (_command, _ui, value) => {
  37531. editor.selection.select(value);
  37532. },
  37533. selectAll: () => {
  37534. const editingHost = editor.dom.getParent(editor.selection.getStart(), isContentEditableTrue$3);
  37535. if (editingHost) {
  37536. const rng = editor.dom.createRng();
  37537. rng.selectNodeContents(editingHost);
  37538. editor.selection.setRng(rng);
  37539. }
  37540. }
  37541. });
  37542. };
  37543. const registerExecCommands = (editor) => {
  37544. editor.editorCommands.addCommands({
  37545. mceRemoveNode: (_command, _ui, value) => {
  37546. const node = value !== null && value !== void 0 ? value : editor.selection.getNode();
  37547. // Make sure that the body node isn't removed
  37548. if (node !== editor.getBody()) {
  37549. const bm = editor.selection.getBookmark();
  37550. editor.dom.remove(node, true);
  37551. editor.selection.moveToBookmark(bm);
  37552. }
  37553. },
  37554. mcePrint: () => {
  37555. editor.getWin().print();
  37556. },
  37557. mceFocus: (_command, _ui, value) => {
  37558. focus(editor, value === true);
  37559. },
  37560. mceToggleVisualAid: () => {
  37561. editor.hasVisual = !editor.hasVisual;
  37562. editor.addVisual();
  37563. }
  37564. });
  37565. };
  37566. const registerCommands = (editor) => {
  37567. registerCommands$a(editor);
  37568. registerCommands$9(editor);
  37569. registerCommands$6(editor);
  37570. registerCommands$1(editor);
  37571. registerCommands$8(editor);
  37572. registerCommands$4(editor);
  37573. registerCommands$5(editor);
  37574. registerCommands$3(editor);
  37575. registerCommands$2(editor);
  37576. registerCommands$7(editor);
  37577. registerExecCommands(editor);
  37578. };
  37579. // List of commands that are considered safe even if the editor has no selection when the iframe is hidden in Firefox. See TINY-9210 for details.
  37580. const selectionSafeCommands = ['toggleview'];
  37581. const isSelectionSafeCommand = (command) => contains$2(selectionSafeCommands, command.toLowerCase());
  37582. class EditorCommands {
  37583. constructor(editor) {
  37584. this.commands = { state: {}, exec: {}, value: {} };
  37585. this.editor = editor;
  37586. }
  37587. /**
  37588. * Executes a registered command on the current instance. A list of available commands can be found in
  37589. * the tinymce command identifiers documentation.
  37590. *
  37591. * @method execCommand
  37592. * @param {String} command Command name to execute, for example mceLink or Bold.
  37593. * @param {Boolean} ui Specifies if a UI (dialog) should be presented or not.
  37594. * @param {Object/Array/String/Number/Boolean} value Optional command value, this can be anything.
  37595. * @param {Object} args Optional arguments object.
  37596. * @return {Boolean} true or false if the command was supported or not.
  37597. */
  37598. execCommand(command, ui = false, value, args) {
  37599. const editor = this.editor;
  37600. const lowerCaseCommand = command.toLowerCase();
  37601. const skipFocus = args === null || args === void 0 ? void 0 : args.skip_focus;
  37602. if (editor.removed) {
  37603. return false;
  37604. }
  37605. if (lowerCaseCommand !== 'mcefocus') {
  37606. if (!/^(mceAddUndoLevel|mceEndUndoLevel)$/i.test(lowerCaseCommand) && !skipFocus) {
  37607. editor.focus();
  37608. }
  37609. else {
  37610. restore(editor);
  37611. }
  37612. }
  37613. const eventArgs = editor.dispatch('BeforeExecCommand', { command, ui, value });
  37614. if (eventArgs.isDefaultPrevented()) {
  37615. return false;
  37616. }
  37617. const func = this.commands.exec[lowerCaseCommand];
  37618. if (isFunction(func)) {
  37619. func(lowerCaseCommand, ui, value, args);
  37620. editor.dispatch('ExecCommand', { command, ui, value, args });
  37621. return true;
  37622. }
  37623. return false;
  37624. }
  37625. /**
  37626. * Queries the current state for a command. For example: If the current selection is "bold".
  37627. *
  37628. * @method queryCommandState
  37629. * @param {String} command Command to check the state of.
  37630. * @return {Boolean} true/false - For example: If the selected contents is bold or not.
  37631. */
  37632. queryCommandState(command) {
  37633. if ((!isSelectionSafeCommand(command) && this.editor.quirks.isHidden()) || this.editor.removed) {
  37634. return false;
  37635. }
  37636. const lowerCaseCommand = command.toLowerCase();
  37637. const func = this.commands.state[lowerCaseCommand];
  37638. if (isFunction(func)) {
  37639. return func(lowerCaseCommand);
  37640. }
  37641. return false;
  37642. }
  37643. /**
  37644. * Returns a command specific value, for example the current font size.
  37645. *
  37646. * @method queryCommandValue
  37647. * @param {String} command Command to query value from.
  37648. * @return {String} Command value, for example the current font size or an empty string (`""`) if the query command is not found.
  37649. */
  37650. queryCommandValue(command) {
  37651. if ((!isSelectionSafeCommand(command) && this.editor.quirks.isHidden()) || this.editor.removed) {
  37652. return '';
  37653. }
  37654. const lowerCaseCommand = command.toLowerCase();
  37655. const func = this.commands.value[lowerCaseCommand];
  37656. if (isFunction(func)) {
  37657. return func(lowerCaseCommand);
  37658. }
  37659. return '';
  37660. }
  37661. addCommands(commandList, type = 'exec') {
  37662. const commands = this.commands;
  37663. each$d(commandList, (callback, command) => {
  37664. each$e(command.toLowerCase().split(','), (command) => {
  37665. commands[type][command] = callback;
  37666. });
  37667. });
  37668. }
  37669. addCommand(command, callback, scope) {
  37670. const lowerCaseCommand = command.toLowerCase();
  37671. this.commands.exec[lowerCaseCommand] = (_command, ui, value, args) => callback.call(scope !== null && scope !== void 0 ? scope : this.editor, ui, value, args);
  37672. }
  37673. /**
  37674. * Returns true/false if the command is supported or not.
  37675. *
  37676. * @method queryCommandSupported
  37677. * @param {String} command Command that we check support for.
  37678. * @return {Boolean} true/false if the command is supported or not.
  37679. */
  37680. queryCommandSupported(command) {
  37681. const lowerCaseCommand = command.toLowerCase();
  37682. if (this.commands.exec[lowerCaseCommand]) {
  37683. return true;
  37684. }
  37685. else {
  37686. return false;
  37687. }
  37688. }
  37689. addQueryStateHandler(command, callback, scope) {
  37690. this.commands.state[command.toLowerCase()] = () => callback.call(scope !== null && scope !== void 0 ? scope : this.editor);
  37691. }
  37692. addQueryValueHandler(command, callback, scope) {
  37693. this.commands.value[command.toLowerCase()] = () => callback.call(scope !== null && scope !== void 0 ? scope : this.editor);
  37694. }
  37695. }
  37696. /**
  37697. * This class lets you add/remove and dispatch events by name on the specified scope. This makes
  37698. * it easy to add event listener logic to any class.
  37699. *
  37700. * @class tinymce.util.EventDispatcher
  37701. * @example
  37702. * const eventDispatcher = new EventDispatcher();
  37703. *
  37704. * eventDispatcher.on('click', () => console.log('data'));
  37705. * eventDispatcher.dispatch('click', { data: 123 });
  37706. */
  37707. const nativeEvents = Tools.makeMap('focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange ' +
  37708. 'mouseout mouseenter mouseleave wheel keydown keypress keyup input beforeinput contextmenu dragstart dragend dragover ' +
  37709. 'draggesture dragdrop drop drag submit ' +
  37710. 'compositionstart compositionend compositionupdate touchstart touchmove touchend touchcancel', ' ');
  37711. class EventDispatcher {
  37712. /**
  37713. * Returns true/false if the specified event name is a native browser event or not.
  37714. *
  37715. * @method isNative
  37716. * @param {String} name Name to check if it's native.
  37717. * @return {Boolean} true/false if the event is native or not.
  37718. * @static
  37719. */
  37720. static isNative(name) {
  37721. return !!nativeEvents[name.toLowerCase()];
  37722. }
  37723. constructor(settings) {
  37724. this.bindings = {};
  37725. this.settings = settings || {};
  37726. this.scope = this.settings.scope || this;
  37727. this.toggleEvent = this.settings.toggleEvent || never;
  37728. }
  37729. /**
  37730. * Fires the specified event by name.
  37731. * <br>
  37732. * <em>Marked for removal in TinyMCE 8.0. Use <code>dispatch</code> instead.</em>
  37733. *
  37734. * @method fire
  37735. * @param {String} name Name of the event to fire.
  37736. * @param {Object?} args Event arguments.
  37737. * @return {Object} Event args instance passed in.
  37738. * @deprecated Use dispatch() instead
  37739. * @example
  37740. * instance.fire('event', {...});
  37741. */
  37742. fire(name, args) {
  37743. return this.dispatch(name, args);
  37744. }
  37745. /**
  37746. * Dispatches the specified event by name.
  37747. *
  37748. * @method dispatch
  37749. * @param {String} name Name of the event to dispatch
  37750. * @param {Object?} args Event arguments.
  37751. * @return {Object} Event args instance passed in.
  37752. * @example
  37753. * instance.dispatch('event', {...});
  37754. */
  37755. dispatch(name, args) {
  37756. const lcName = name.toLowerCase();
  37757. const event = normalize$3(lcName, args !== null && args !== void 0 ? args : {}, this.scope);
  37758. if (this.settings.beforeFire) {
  37759. this.settings.beforeFire(event);
  37760. }
  37761. // Don't clone the array here as this is a hot code path, so instead the handlers
  37762. // array is recreated and the this.bindings[name] reference is updated in the `on`
  37763. // and `off` functions. This is done to avoid the handlers array being mutated while
  37764. // we're iterating over it below.
  37765. const handlers = this.bindings[lcName];
  37766. if (handlers) {
  37767. for (let i = 0, l = handlers.length; i < l; i++) {
  37768. const callback = handlers[i];
  37769. // The handler was removed by an earlier handler in this loop so skip it.
  37770. if (callback.removed) {
  37771. continue;
  37772. }
  37773. // Unbind handlers marked with "once"
  37774. if (callback.once) {
  37775. this.off(lcName, callback.func);
  37776. }
  37777. // Stop immediate propagation if needed
  37778. if (event.isImmediatePropagationStopped()) {
  37779. return event;
  37780. }
  37781. // If callback returns false then prevent default and stop all propagation
  37782. if (callback.func.call(this.scope, event) === false) {
  37783. event.preventDefault();
  37784. return event;
  37785. }
  37786. }
  37787. }
  37788. return event;
  37789. }
  37790. /**
  37791. * Binds an event listener to a specific event by name.
  37792. *
  37793. * @method on
  37794. * @param {String} name Event name or space separated list of events to bind.
  37795. * @param {Function} callback Callback to be executed when the event occurs.
  37796. * @param {Boolean} prepend Optional flag if the event should be prepended. Use this with care.
  37797. * @return {Object} Current class instance.
  37798. * @example
  37799. * instance.on('event', (e) => {
  37800. * // Callback logic
  37801. * });
  37802. */
  37803. on(name, callback, prepend, extra) {
  37804. if (callback === false) {
  37805. callback = never;
  37806. }
  37807. if (callback) {
  37808. const wrappedCallback = {
  37809. func: callback,
  37810. removed: false
  37811. };
  37812. if (extra) {
  37813. Tools.extend(wrappedCallback, extra);
  37814. }
  37815. const names = name.toLowerCase().split(' ');
  37816. let i = names.length;
  37817. while (i--) {
  37818. const currentName = names[i];
  37819. let handlers = this.bindings[currentName];
  37820. if (!handlers) {
  37821. handlers = [];
  37822. this.toggleEvent(currentName, true);
  37823. }
  37824. if (prepend) {
  37825. handlers = [wrappedCallback, ...handlers];
  37826. }
  37827. else {
  37828. handlers = [...handlers, wrappedCallback];
  37829. }
  37830. this.bindings[currentName] = handlers;
  37831. }
  37832. }
  37833. return this;
  37834. }
  37835. /**
  37836. * Unbinds an event listener to a specific event by name.
  37837. *
  37838. * @method off
  37839. * @param {String?} name Name of the event to unbind.
  37840. * @param {Function?} callback Callback to unbind.
  37841. * @return {Object} Current class instance.
  37842. * @example
  37843. * // Unbind specific callback
  37844. * instance.off('event', handler);
  37845. *
  37846. * // Unbind all listeners by name
  37847. * instance.off('event');
  37848. *
  37849. * // Unbind all events
  37850. * instance.off();
  37851. */
  37852. off(name, callback) {
  37853. if (name) {
  37854. const names = name.toLowerCase().split(' ');
  37855. let i = names.length;
  37856. while (i--) {
  37857. const currentName = names[i];
  37858. let handlers = this.bindings[currentName];
  37859. // Unbind all handlers
  37860. if (!currentName) {
  37861. each$d(this.bindings, (_value, bindingName) => {
  37862. this.toggleEvent(bindingName, false);
  37863. delete this.bindings[bindingName];
  37864. });
  37865. return this;
  37866. }
  37867. if (handlers) {
  37868. // Unbind all by name
  37869. if (!callback) {
  37870. handlers.length = 0;
  37871. }
  37872. else {
  37873. // Unbind specific handlers
  37874. const filteredHandlers = partition$2(handlers, (handler) => handler.func === callback);
  37875. handlers = filteredHandlers.fail;
  37876. this.bindings[currentName] = handlers;
  37877. // Mark the removed handlers in case this event is already being processed in `fire`
  37878. each$e(filteredHandlers.pass, (handler) => {
  37879. handler.removed = true;
  37880. });
  37881. }
  37882. if (!handlers.length) {
  37883. this.toggleEvent(name, false);
  37884. delete this.bindings[currentName];
  37885. }
  37886. }
  37887. }
  37888. }
  37889. else {
  37890. each$d(this.bindings, (_value, name) => {
  37891. this.toggleEvent(name, false);
  37892. });
  37893. this.bindings = {};
  37894. }
  37895. return this;
  37896. }
  37897. /**
  37898. * Binds an event listener to a specific event by name
  37899. * and automatically unbind the event once the callback fires.
  37900. *
  37901. * @method once
  37902. * @param {String} name Event name or space separated list of events to bind.
  37903. * @param {Function} callback Callback to be executed when the event occurs.
  37904. * @param {Boolean} prepend Optional flag if the event should be prepended. Use this with care.
  37905. * @return {Object} Current class instance.
  37906. * @example
  37907. * instance.once('event', (e) => {
  37908. * // Callback logic
  37909. * });
  37910. */
  37911. once(name, callback, prepend) {
  37912. return this.on(name, callback, prepend, { once: true });
  37913. }
  37914. /**
  37915. * Returns true/false if the dispatcher has a event of the specified name.
  37916. *
  37917. * @method has
  37918. * @param {String} name Name of the event to check for.
  37919. * @return {Boolean} true/false if the event exists or not.
  37920. */
  37921. has(name) {
  37922. name = name.toLowerCase();
  37923. const binding = this.bindings[name];
  37924. return !(!binding || binding.length === 0);
  37925. }
  37926. }
  37927. /**
  37928. * This mixin adds event binding logic to classes. Adapts the EventDispatcher class.
  37929. *
  37930. * @mixin tinymce.util.Observable
  37931. */
  37932. const getEventDispatcher = (obj) => {
  37933. if (!obj._eventDispatcher) {
  37934. obj._eventDispatcher = new EventDispatcher({
  37935. scope: obj,
  37936. toggleEvent: (name, state) => {
  37937. if (EventDispatcher.isNative(name) && obj.toggleNativeEvent) {
  37938. obj.toggleNativeEvent(name, state);
  37939. }
  37940. }
  37941. });
  37942. }
  37943. return obj._eventDispatcher;
  37944. };
  37945. const Observable = {
  37946. /**
  37947. * Fires the specified event by name. Consult the
  37948. * <a href="https://www.tiny.cloud/docs/tinymce/8/events/">event reference</a> for more details on each event.
  37949. * <br>
  37950. * <em>Deprecated in TinyMCE 6.0 and has been marked for removal in TinyMCE 9.0. Use <code>dispatch</code> instead.</em>
  37951. *
  37952. * @method fire
  37953. * @param {String} name Name of the event to fire.
  37954. * @param {Object?} args Event arguments.
  37955. * @param {Boolean?} bubble True/false if the event is to be bubbled.
  37956. * @return {Object} Event args instance passed in.
  37957. * @deprecated Use dispatch() instead
  37958. * @example
  37959. * instance.fire('event', {...});
  37960. */
  37961. fire(name, args, bubble) {
  37962. logFeatureDeprecationWarning('fire');
  37963. return this.dispatch(name, args, bubble);
  37964. },
  37965. /**
  37966. * Dispatches the specified event by name. Consult the
  37967. * <a href="https://www.tiny.cloud/docs/tinymce/8/events/">event reference</a> for more details on each event.
  37968. *
  37969. * @method dispatch
  37970. * @param {String} name Name of the event to dispatch.
  37971. * @param {Object?} args Event arguments.
  37972. * @param {Boolean?} bubble True/false if the event is to be bubbled.
  37973. * @return {Object} Event args instance passed in.
  37974. * @example
  37975. * instance.dispatch('event', {...});
  37976. */
  37977. dispatch(name, args, bubble) {
  37978. const self = this;
  37979. // Prevent all events except the remove/detach event after the instance has been removed
  37980. if (self.removed && name !== 'remove' && name !== 'detach') {
  37981. return normalize$3(name.toLowerCase(), args !== null && args !== void 0 ? args : {}, self);
  37982. }
  37983. const dispatcherArgs = getEventDispatcher(self).dispatch(name, args);
  37984. // Bubble event up to parents
  37985. if (bubble !== false && self.parent) {
  37986. let parent = self.parent();
  37987. while (parent && !dispatcherArgs.isPropagationStopped()) {
  37988. parent.dispatch(name, dispatcherArgs, false);
  37989. parent = parent.parent ? parent.parent() : undefined;
  37990. }
  37991. }
  37992. return dispatcherArgs;
  37993. },
  37994. /**
  37995. * Binds an event listener to a specific event by name. Consult the
  37996. * <a href="https://www.tiny.cloud/docs/tinymce/8/events/">event reference</a> for more details on each event.
  37997. *
  37998. * @method on
  37999. * @param {String} name Event name or space separated list of events to bind.
  38000. * @param {Function} callback Callback to be executed when the event occurs.
  38001. * @param {Boolean} prepend Optional flag if the event should be prepended. Use this with care.
  38002. * @return {Object} Current class instance.
  38003. * @example
  38004. * instance.on('event', (e) => {
  38005. * // Callback logic
  38006. * });
  38007. */
  38008. on(name, callback, prepend) {
  38009. return getEventDispatcher(this).on(name, callback, prepend);
  38010. },
  38011. /**
  38012. * Unbinds an event listener to a specific event by name. Consult the
  38013. * <a href="https://www.tiny.cloud/docs/tinymce/8/events/">event reference</a> for more details on each event.
  38014. *
  38015. * @method off
  38016. * @param {String?} name Name of the event to unbind.
  38017. * @param {Function?} callback Callback to unbind.
  38018. * @return {Object} Current class instance.
  38019. * @example
  38020. * // Unbind specific callback
  38021. * instance.off('event', handler);
  38022. *
  38023. * // Unbind all listeners by name
  38024. * instance.off('event');
  38025. *
  38026. * // Unbind all events
  38027. * instance.off();
  38028. */
  38029. off(name, callback) {
  38030. return getEventDispatcher(this).off(name, callback);
  38031. },
  38032. /**
  38033. * Bind the event callback and once it fires the callback is removed. Consult the
  38034. * <a href="https://www.tiny.cloud/docs/tinymce/8/events/">event reference</a> for more details on each event.
  38035. *
  38036. * @method once
  38037. * @param {String} name Name of the event to bind.
  38038. * @param {Function} callback Callback to bind only once.
  38039. * @return {Object} Current class instance.
  38040. */
  38041. once(name, callback) {
  38042. return getEventDispatcher(this).once(name, callback);
  38043. },
  38044. /**
  38045. * Returns true/false if the object has a event of the specified name.
  38046. *
  38047. * @method hasEventListeners
  38048. * @param {String} name Name of the event to check for.
  38049. * @return {Boolean} true/false if the event exists or not.
  38050. */
  38051. hasEventListeners(name) {
  38052. return getEventDispatcher(this).has(name);
  38053. }
  38054. };
  38055. /**
  38056. * This mixin contains the event logic for the tinymce.Editor class.
  38057. *
  38058. * @mixin tinymce.EditorObservable
  38059. * @extends tinymce.util.Observable
  38060. * @private
  38061. */
  38062. const DOM$2 = DOMUtils.DOM;
  38063. let customEventRootDelegates;
  38064. /**
  38065. * Returns the event target for the specified event. Some events fire
  38066. * only on document, some fire on documentElement etc. This also handles the
  38067. * custom event root setting where it returns that element instead of the body.
  38068. *
  38069. * @private
  38070. * @param {tinymce.Editor} editor Editor instance to get event target from.
  38071. * @param {String} eventName Name of the event for example "click".
  38072. * @return {Element/Document} HTML Element or document target to bind on.
  38073. */
  38074. const getEventTarget = (editor, eventName) => {
  38075. if (eventName === 'selectionchange') {
  38076. return editor.getDoc();
  38077. }
  38078. // Need to bind mousedown/mouseup etc to document not body in iframe mode
  38079. // Since the user might click on the HTML element not the BODY
  38080. if (!editor.inline && /^(?:mouse|touch|click|contextmenu|drop|dragover|dragend)/.test(eventName)) {
  38081. return editor.getDoc().documentElement;
  38082. }
  38083. // Bind to event root instead of body if it's defined
  38084. const eventRoot = getEventRoot(editor);
  38085. if (eventRoot) {
  38086. if (!editor.eventRoot) {
  38087. editor.eventRoot = DOM$2.select(eventRoot)[0];
  38088. }
  38089. return editor.eventRoot;
  38090. }
  38091. return editor.getBody();
  38092. };
  38093. const isListening = (editor) => !editor.hidden && !isDisabled(editor);
  38094. const fireEvent = (editor, eventName, e) => {
  38095. if (isListening(editor)) {
  38096. editor.dispatch(eventName, e);
  38097. }
  38098. else if (isDisabled(editor)) {
  38099. processDisabledEvents(editor, e);
  38100. }
  38101. };
  38102. /**
  38103. * Binds a event delegate for the specified name this delegate will fire
  38104. * the event to the editor dispatcher.
  38105. *
  38106. * @private
  38107. * @param {tinymce.Editor} editor Editor instance to get event target from.
  38108. * @param {String} eventName Name of the event for example "click".
  38109. */
  38110. const bindEventDelegate = (editor, eventName) => {
  38111. if (!editor.delegates) {
  38112. editor.delegates = {};
  38113. }
  38114. if (editor.delegates[eventName] || editor.removed) {
  38115. return;
  38116. }
  38117. const eventRootElm = getEventTarget(editor, eventName);
  38118. if (getEventRoot(editor)) {
  38119. if (!customEventRootDelegates) {
  38120. customEventRootDelegates = {};
  38121. editor.editorManager.on('removeEditor', () => {
  38122. if (!editor.editorManager.activeEditor) {
  38123. if (customEventRootDelegates) {
  38124. each$d(customEventRootDelegates, (_value, name) => {
  38125. editor.dom.unbind(getEventTarget(editor, name));
  38126. });
  38127. customEventRootDelegates = null;
  38128. }
  38129. }
  38130. });
  38131. }
  38132. if (customEventRootDelegates[eventName]) {
  38133. return;
  38134. }
  38135. const delegate = (e) => {
  38136. const target = e.target;
  38137. const editors = editor.editorManager.get();
  38138. let i = editors.length;
  38139. while (i--) {
  38140. const body = editors[i].getBody();
  38141. if (body === target || DOM$2.isChildOf(target, body)) {
  38142. fireEvent(editors[i], eventName, e);
  38143. }
  38144. }
  38145. };
  38146. customEventRootDelegates[eventName] = delegate;
  38147. DOM$2.bind(eventRootElm, eventName, delegate);
  38148. }
  38149. else {
  38150. const delegate = (e) => {
  38151. fireEvent(editor, eventName, e);
  38152. };
  38153. DOM$2.bind(eventRootElm, eventName, delegate);
  38154. editor.delegates[eventName] = delegate;
  38155. }
  38156. };
  38157. const EditorObservable = {
  38158. ...Observable,
  38159. /**
  38160. * Bind any pending event delegates. This gets executed after the target body/document is created.
  38161. *
  38162. * @private
  38163. */
  38164. bindPendingEventDelegates() {
  38165. const self = this;
  38166. Tools.each(self._pendingNativeEvents, (name) => {
  38167. bindEventDelegate(self, name);
  38168. });
  38169. },
  38170. /**
  38171. * Toggles a native event on/off this is called by the EventDispatcher when
  38172. * the first native event handler is added and when the last native event handler is removed.
  38173. *
  38174. * @private
  38175. */
  38176. toggleNativeEvent(name, state) {
  38177. const self = this;
  38178. // Never bind focus/blur since the FocusManager fakes those
  38179. if (name === 'focus' || name === 'blur') {
  38180. return;
  38181. }
  38182. // If the editor has been removed, `unbindAllNativeEvents` has already deleted all native event delegates
  38183. if (self.removed) {
  38184. return;
  38185. }
  38186. if (state) {
  38187. if (self.initialized) {
  38188. bindEventDelegate(self, name);
  38189. }
  38190. else {
  38191. if (!self._pendingNativeEvents) {
  38192. self._pendingNativeEvents = [name];
  38193. }
  38194. else {
  38195. self._pendingNativeEvents.push(name);
  38196. }
  38197. }
  38198. }
  38199. else if (self.initialized && self.delegates) {
  38200. self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]);
  38201. delete self.delegates[name];
  38202. }
  38203. },
  38204. /**
  38205. * Unbinds all native event handlers that means delegates, custom events bound using the Events API etc.
  38206. *
  38207. * @private
  38208. */
  38209. unbindAllNativeEvents() {
  38210. const self = this;
  38211. const body = self.getBody();
  38212. const dom = self.dom;
  38213. if (self.delegates) {
  38214. each$d(self.delegates, (value, name) => {
  38215. self.dom.unbind(getEventTarget(self, name), name, value);
  38216. });
  38217. delete self.delegates;
  38218. }
  38219. if (!self.inline && body && dom) {
  38220. body.onload = null;
  38221. dom.unbind(self.getWin());
  38222. dom.unbind(self.getDoc());
  38223. }
  38224. if (dom) {
  38225. dom.unbind(body);
  38226. dom.unbind(self.getContainer());
  38227. }
  38228. }
  38229. };
  38230. // A string array allows comma/space separated values as well for ease of use
  38231. const stringListProcessor = (value) => {
  38232. if (isString(value)) {
  38233. return { value: value.split(/[ ,]/), valid: true };
  38234. }
  38235. else if (isArrayOf(value, isString)) {
  38236. return { value, valid: true };
  38237. }
  38238. else {
  38239. return { valid: false, message: `The value must be a string[] or a comma/space separated string.` };
  38240. }
  38241. };
  38242. const getBuiltInProcessor = (type) => {
  38243. const validator = (() => {
  38244. switch (type) {
  38245. case 'array':
  38246. return isArray$1;
  38247. case 'boolean':
  38248. return isBoolean;
  38249. case 'function':
  38250. return isFunction;
  38251. case 'number':
  38252. return isNumber;
  38253. case 'object':
  38254. return isObject;
  38255. case 'string':
  38256. return isString;
  38257. case 'string[]':
  38258. return stringListProcessor;
  38259. case 'object[]':
  38260. return (val) => isArrayOf(val, isObject);
  38261. case 'regexp':
  38262. return (val) => is$5(val, RegExp);
  38263. default:
  38264. return always;
  38265. }
  38266. })();
  38267. return (value) => processValue(value, validator, `The value must be a ${type}.`);
  38268. };
  38269. const isBuiltInSpec = (spec) => isString(spec.processor);
  38270. const getErrorMessage = (message, result) => {
  38271. const additionalText = isEmpty$5(result.message) ? '' : `. ${result.message}`;
  38272. return message + additionalText;
  38273. };
  38274. const isValidResult = (result) => result.valid;
  38275. const processValue = (value, processor, message = '') => {
  38276. const result = processor(value);
  38277. if (isBoolean(result)) {
  38278. // Note: Need to cast here as if a boolean is returned then we're guaranteed to be returning the same value
  38279. return result ? { value: value, valid: true } : { valid: false, message };
  38280. }
  38281. else {
  38282. return result;
  38283. }
  38284. };
  38285. const processDefaultValue = (name, defaultValue, processor) => {
  38286. if (!isUndefined(defaultValue)) {
  38287. const result = processValue(defaultValue, processor);
  38288. if (isValidResult(result)) {
  38289. return result.value;
  38290. }
  38291. else {
  38292. // eslint-disable-next-line no-console
  38293. console.error(getErrorMessage(`Invalid default value passed for the "${name}" option`, result));
  38294. }
  38295. }
  38296. return undefined;
  38297. };
  38298. const create$4 = (editor, initialOptions, rawInitialOptions = initialOptions) => {
  38299. const registry = {};
  38300. const values = {};
  38301. const setValue = (name, value, processor) => {
  38302. const result = processValue(value, processor);
  38303. if (isValidResult(result)) {
  38304. values[name] = result.value;
  38305. return true;
  38306. }
  38307. else {
  38308. // eslint-disable-next-line no-console
  38309. console.warn(getErrorMessage(`Invalid value passed for the ${name} option`, result));
  38310. return false;
  38311. }
  38312. };
  38313. const register = (name, spec) => {
  38314. const processor = isBuiltInSpec(spec) ? getBuiltInProcessor(spec.processor) : spec.processor;
  38315. // Process and validate the default value
  38316. const defaultValue = processDefaultValue(name, spec.default, processor);
  38317. // Register the spec with the validated default and normalized processor
  38318. registry[name] = {
  38319. ...spec,
  38320. default: defaultValue,
  38321. processor
  38322. };
  38323. // Setup the initial values
  38324. const initValue = get$a(values, name).orThunk(() => get$a(initialOptions, name));
  38325. initValue.each((value) => setValue(name, value, processor));
  38326. };
  38327. const isRegistered = (name) => has$2(registry, name);
  38328. const get = (name) => get$a(values, name)
  38329. .orThunk(() => get$a(registry, name).map((spec) => spec.default))
  38330. .getOrUndefined();
  38331. const set = (name, value) => {
  38332. if (!isRegistered(name)) {
  38333. // eslint-disable-next-line no-console
  38334. console.warn(`"${name}" is not a registered option. Ensure the option has been registered before setting a value.`);
  38335. return false;
  38336. }
  38337. else {
  38338. const spec = registry[name];
  38339. if (spec.immutable) {
  38340. // eslint-disable-next-line no-console
  38341. console.error(`"${name}" is an immutable option and cannot be updated`);
  38342. return false;
  38343. }
  38344. else {
  38345. return setValue(name, value, spec.processor);
  38346. }
  38347. }
  38348. };
  38349. const unset = (name) => {
  38350. const registered = isRegistered(name);
  38351. if (registered) {
  38352. delete values[name];
  38353. }
  38354. return registered;
  38355. };
  38356. const isSet = (name) => has$2(values, name);
  38357. const debug = () => {
  38358. try {
  38359. // eslint-disable-next-line no-console
  38360. console.log(JSON.parse(JSON.stringify(rawInitialOptions, (_key, value) => {
  38361. if (isBoolean(value) ||
  38362. isNumber(value) ||
  38363. isString(value) ||
  38364. isNull(value) ||
  38365. isArray$1(value) ||
  38366. isPlainObject(value)) {
  38367. return value;
  38368. }
  38369. return Object.prototype.toString.call(value);
  38370. })));
  38371. }
  38372. catch (error) {
  38373. // eslint-disable-next-line no-console
  38374. console.error(error);
  38375. }
  38376. };
  38377. return {
  38378. register,
  38379. isRegistered,
  38380. get,
  38381. set,
  38382. unset,
  38383. isSet,
  38384. debug,
  38385. };
  38386. };
  38387. const setContentEditable = (elm, state) => {
  38388. elm.dom.contentEditable = state ? 'true' : 'false';
  38389. };
  38390. const toggleReadOnly = (editor, state) => {
  38391. const body = SugarElement.fromDom(editor.getBody());
  38392. if (state) {
  38393. editor.readonly = true;
  38394. if (editor.hasEditableRoot()) {
  38395. setContentEditable(body, true);
  38396. }
  38397. disableEditor(editor);
  38398. }
  38399. else {
  38400. editor.readonly = false;
  38401. enableEditor(editor);
  38402. }
  38403. };
  38404. const isReadOnly = (editor) => editor.readonly;
  38405. const rollbackChange = (editor) => {
  38406. const undoLevel = editor.undoManager.add();
  38407. if (isNonNullable(undoLevel)) {
  38408. editor.undoManager.undo();
  38409. editor.undoManager.reset();
  38410. }
  38411. };
  38412. const hasContentMutations = (mutations) => exists(mutations, (mutation) => mutation.type === 'characterData' || mutation.type === 'childList');
  38413. const registerReadOnlyInputBlockers = (editor) => {
  38414. const handleMutations = (mutations) => {
  38415. if (isReadOnly(editor) && hasContentMutations(mutations)) {
  38416. rollbackChange(editor);
  38417. }
  38418. };
  38419. // Set up mutation observer to detect and revert unintended changes
  38420. const observer = new MutationObserver(handleMutations);
  38421. editor.on('beforeinput paste cut dragend dragover draggesture dragdrop drop drag', (e) => {
  38422. if (isReadOnly(editor)) {
  38423. e.preventDefault();
  38424. }
  38425. });
  38426. editor.on('BeforeExecCommand', (e) => {
  38427. if ((e.command === 'Undo' || e.command === 'Redo') && isReadOnly(editor)) {
  38428. e.preventDefault();
  38429. }
  38430. });
  38431. editor.on('compositionstart', () => {
  38432. if (isReadOnly(editor)) {
  38433. observer.observe(editor.getBody(), {
  38434. characterData: true,
  38435. childList: true,
  38436. subtree: true
  38437. });
  38438. }
  38439. });
  38440. editor.on('compositionend', () => {
  38441. if (isReadOnly(editor)) {
  38442. const records = observer.takeRecords();
  38443. handleMutations(records);
  38444. }
  38445. observer.disconnect();
  38446. });
  38447. };
  38448. const defaultModes = ['design', 'readonly'];
  38449. const switchToMode = (editor, activeMode, availableModes, mode) => {
  38450. const oldMode = availableModes[activeMode.get()];
  38451. const newMode = availableModes[mode];
  38452. // if activate fails, hope nothing bad happened and abort
  38453. try {
  38454. newMode.activate();
  38455. }
  38456. catch (e) {
  38457. // eslint-disable-next-line no-console
  38458. console.error(`problem while activating editor mode ${mode}:`, e);
  38459. return;
  38460. }
  38461. oldMode.deactivate();
  38462. if (oldMode.editorReadOnly !== newMode.editorReadOnly) {
  38463. toggleReadOnly(editor, newMode.editorReadOnly);
  38464. }
  38465. activeMode.set(mode);
  38466. fireSwitchMode(editor, mode);
  38467. };
  38468. const setMode = (editor, availableModes, activeMode, mode) => {
  38469. if (mode === activeMode.get() || (editor.initialized && isDisabled(editor))) {
  38470. return;
  38471. }
  38472. else if (!has$2(availableModes, mode)) {
  38473. throw new Error(`Editor mode '${mode}' is invalid`);
  38474. }
  38475. if (editor.initialized) {
  38476. switchToMode(editor, activeMode, availableModes, mode);
  38477. }
  38478. else {
  38479. editor.on('init', () => switchToMode(editor, activeMode, availableModes, mode));
  38480. }
  38481. };
  38482. const registerMode = (availableModes, mode, api) => {
  38483. if (contains$2(defaultModes, mode)) {
  38484. throw new Error(`Cannot override default mode ${mode}`);
  38485. }
  38486. return {
  38487. ...availableModes,
  38488. [mode]: {
  38489. ...api,
  38490. deactivate: () => {
  38491. // wrap custom deactivate APIs so they can't break the editor
  38492. try {
  38493. api.deactivate();
  38494. }
  38495. catch (e) {
  38496. // eslint-disable-next-line no-console
  38497. console.error(`problem while deactivating editor mode ${mode}:`, e);
  38498. }
  38499. }
  38500. }
  38501. };
  38502. };
  38503. const create$3 = (editor) => {
  38504. const activeMode = Cell('design');
  38505. const availableModes = Cell({
  38506. design: {
  38507. activate: noop,
  38508. deactivate: noop,
  38509. editorReadOnly: false
  38510. },
  38511. readonly: {
  38512. activate: noop,
  38513. deactivate: noop,
  38514. editorReadOnly: true
  38515. }
  38516. });
  38517. registerReadOnlyInputBlockers(editor);
  38518. registerEventsAndFilters$1(editor);
  38519. return {
  38520. isReadOnly: () => isReadOnly(editor),
  38521. set: (mode) => setMode(editor, availableModes.get(), activeMode, mode),
  38522. get: () => activeMode.get(),
  38523. register: (mode, api) => {
  38524. availableModes.set(registerMode(availableModes.get(), mode, api));
  38525. }
  38526. };
  38527. };
  38528. /**
  38529. * Contains logic for handling keyboard shortcuts.
  38530. *
  38531. * @class tinymce.Shortcuts
  38532. * @example
  38533. * editor.shortcuts.add('ctrl+a', 'description of the shortcut', () => {});
  38534. * editor.shortcuts.add('ctrl+alt+a', 'description of the shortcut', () => {});
  38535. * // "meta" maps to Command on Mac and Ctrl on PC
  38536. * editor.shortcuts.add('meta+a', 'description of the shortcut', () => {});
  38537. * // "access" maps to Control+Option on Mac and shift+alt on PC
  38538. * editor.shortcuts.add('access+a', 'description of the shortcut', () => {});
  38539. *
  38540. * editor.shortcuts.add('meta+access+c', 'Opens the code editor dialog.', () => {
  38541. * editor.execCommand('mceCodeEditor');
  38542. * });
  38543. *
  38544. * editor.shortcuts.add('meta+shift+32', 'Inserts "Hello, World!" for meta+shift+space', () => {
  38545. * editor.execCommand('mceInsertContent', false, 'Hello, World!');
  38546. * });
  38547. */
  38548. const each$2 = Tools.each, explode = Tools.explode;
  38549. const keyCodeLookup = {
  38550. f1: 112,
  38551. f2: 113,
  38552. f3: 114,
  38553. f4: 115,
  38554. f5: 116,
  38555. f6: 117,
  38556. f7: 118,
  38557. f8: 119,
  38558. f9: 120,
  38559. f10: 121,
  38560. f11: 122,
  38561. f12: 123
  38562. };
  38563. const modifierNames = Tools.makeMap('alt,ctrl,shift,meta,access');
  38564. const isModifier = (key) => key in modifierNames;
  38565. const parseShortcut = (pattern) => {
  38566. const shortcut = {};
  38567. const isMac = Env.os.isMacOS() || Env.os.isiOS();
  38568. // Parse modifiers and keys ctrl+alt+b for example
  38569. each$2(explode(pattern.toLowerCase(), '+'), (value) => {
  38570. if (isModifier(value)) {
  38571. shortcut[value] = true;
  38572. }
  38573. else {
  38574. // Allow numeric keycodes like ctrl+219 for ctrl+[
  38575. if (/^[0-9]{2,}$/.test(value)) {
  38576. shortcut.keyCode = parseInt(value, 10);
  38577. }
  38578. else {
  38579. shortcut.charCode = value.charCodeAt(0);
  38580. shortcut.keyCode = keyCodeLookup[value] || value.toUpperCase().charCodeAt(0);
  38581. }
  38582. }
  38583. });
  38584. // Generate unique id for modifier combination and set default state for unused modifiers
  38585. const id = [shortcut.keyCode];
  38586. let key;
  38587. for (key in modifierNames) {
  38588. if (shortcut[key]) {
  38589. id.push(key);
  38590. }
  38591. else {
  38592. shortcut[key] = false;
  38593. }
  38594. }
  38595. shortcut.id = id.join(',');
  38596. // Handle special access modifier differently depending on Mac/Win
  38597. if (shortcut.access) {
  38598. shortcut.alt = true;
  38599. if (isMac) {
  38600. shortcut.ctrl = true;
  38601. }
  38602. else {
  38603. shortcut.shift = true;
  38604. }
  38605. }
  38606. // Handle special meta modifier differently depending on Mac/Win
  38607. if (shortcut.meta) {
  38608. if (isMac) {
  38609. shortcut.meta = true;
  38610. }
  38611. else {
  38612. shortcut.ctrl = true;
  38613. shortcut.meta = false;
  38614. }
  38615. }
  38616. return shortcut;
  38617. };
  38618. class Shortcuts {
  38619. constructor(editor) {
  38620. this.shortcuts = {};
  38621. this.pendingPatterns = [];
  38622. this.editor = editor;
  38623. const self = this;
  38624. editor.on('keyup keypress keydown', (e) => {
  38625. if ((self.hasModifier(e) || self.isFunctionKey(e)) && !e.isDefaultPrevented()) {
  38626. each$2(self.shortcuts, (shortcut) => {
  38627. if (self.matchShortcut(e, shortcut)) {
  38628. self.pendingPatterns = shortcut.subpatterns.slice(0);
  38629. if (e.type === 'keydown') {
  38630. self.executeShortcutAction(shortcut);
  38631. }
  38632. }
  38633. });
  38634. if (self.matchShortcut(e, self.pendingPatterns[0])) {
  38635. if (self.pendingPatterns.length === 1) {
  38636. if (e.type === 'keydown') {
  38637. self.executeShortcutAction(self.pendingPatterns[0]);
  38638. }
  38639. }
  38640. self.pendingPatterns.shift();
  38641. }
  38642. }
  38643. });
  38644. }
  38645. /**
  38646. * Adds a keyboard shortcut for some command or function.
  38647. *
  38648. * @method add
  38649. * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
  38650. * @param {String} desc Text description for the command.
  38651. * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed.
  38652. * @param {Object} scope Optional scope to execute the function in.
  38653. * @return {Boolean} true/false state if the shortcut was added or not.
  38654. */
  38655. add(pattern, desc, cmdFunc, scope) {
  38656. const self = this;
  38657. const func = self.normalizeCommandFunc(cmdFunc);
  38658. each$2(explode(Tools.trim(pattern)), (pattern) => {
  38659. const shortcut = self.createShortcut(pattern, desc, func, scope);
  38660. self.shortcuts[shortcut.id] = shortcut;
  38661. });
  38662. return true;
  38663. }
  38664. /**
  38665. * Remove a keyboard shortcut by pattern.
  38666. *
  38667. * @method remove
  38668. * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
  38669. * @return {Boolean} true/false state if the shortcut was removed or not.
  38670. */
  38671. remove(pattern) {
  38672. const shortcut = this.createShortcut(pattern);
  38673. if (this.shortcuts[shortcut.id]) {
  38674. delete this.shortcuts[shortcut.id];
  38675. return true;
  38676. }
  38677. return false;
  38678. }
  38679. normalizeCommandFunc(cmdFunc) {
  38680. const self = this;
  38681. const cmd = cmdFunc;
  38682. if (typeof cmd === 'string') {
  38683. return () => {
  38684. self.editor.execCommand(cmd, false, null);
  38685. };
  38686. }
  38687. else if (Tools.isArray(cmd)) {
  38688. return () => {
  38689. self.editor.execCommand(cmd[0], cmd[1], cmd[2]);
  38690. };
  38691. }
  38692. else {
  38693. return cmd;
  38694. }
  38695. }
  38696. createShortcut(pattern, desc, cmdFunc, scope) {
  38697. const shortcuts = Tools.map(explode(pattern, '>'), parseShortcut);
  38698. shortcuts[shortcuts.length - 1] = Tools.extend(shortcuts[shortcuts.length - 1], {
  38699. func: cmdFunc,
  38700. scope: scope || this.editor
  38701. });
  38702. return Tools.extend(shortcuts[0], {
  38703. desc: this.editor.translate(desc),
  38704. subpatterns: shortcuts.slice(1)
  38705. });
  38706. }
  38707. hasModifier(e) {
  38708. return e.altKey || e.ctrlKey || e.metaKey;
  38709. }
  38710. isFunctionKey(e) {
  38711. return e.type === 'keydown' && e.keyCode >= 112 && e.keyCode <= 123;
  38712. }
  38713. matchShortcut(e, shortcut) {
  38714. if (!shortcut) {
  38715. return false;
  38716. }
  38717. if (shortcut.ctrl !== e.ctrlKey || shortcut.meta !== e.metaKey) {
  38718. return false;
  38719. }
  38720. if (shortcut.alt !== e.altKey || shortcut.shift !== e.shiftKey) {
  38721. return false;
  38722. }
  38723. if (e.keyCode === shortcut.keyCode || (e.charCode && e.charCode === shortcut.charCode)) {
  38724. e.preventDefault();
  38725. return true;
  38726. }
  38727. return false;
  38728. }
  38729. executeShortcutAction(shortcut) {
  38730. return shortcut.func ? shortcut.func.call(shortcut.scope) : null;
  38731. }
  38732. }
  38733. /**
  38734. * TinyMCE UI registration API.
  38735. *
  38736. * @class tinymce.editor.ui.Registry
  38737. */
  38738. const registry = () => {
  38739. const bridge = create$6();
  38740. return {
  38741. /**
  38742. * Registers a new auto completer component. When a configured string pattern
  38743. * is matched in the content while typing, the autocompleter will be triggered.
  38744. * Emoticons and Charmap use an autocompleter.
  38745. * <br>
  38746. * For information on creating an autocompleter, see:
  38747. * <a href="https://www.tiny.cloud/docs/tinymce/8/autocompleter/">
  38748. * UI Components - Autocompleter</a>.
  38749. *
  38750. * @method addAutocompleter
  38751. * @param {String} name Unique name identifying this autocomplete configuration.
  38752. * @param {InlineContent.AutocompleterSpec} obj The autocomplete configuration object.
  38753. */
  38754. addAutocompleter: bridge.addAutocompleter,
  38755. /**
  38756. * Registers a new toolbar button that executes a command when clicked or activated
  38757. * via keyboard navigation controls.
  38758. * <br>
  38759. * For information on creating a basic toolbar button, see:
  38760. * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-basic-toolbar-button/">
  38761. * UI Components - Types of toolbar buttons: Basic button</a>.
  38762. *
  38763. * @method addButton
  38764. * @param {String} name Unique name identifying the button, this button name will be used in the toolbar configuration to reference the button.
  38765. * @param {Toolbar.ToolbarButtonSpec} obj the button configuration object.
  38766. */
  38767. addButton: bridge.addButton,
  38768. /**
  38769. * Registers a new contextual form item.
  38770. * Similar to a context menu item, a contextual form is an item with an input
  38771. * form element appearing when a content predicate is matched. An example
  38772. * of a contextual form is the link plugin when the configuration
  38773. * { link_context_toolbar: true } is used. When the cursor is on a link, a
  38774. * contextual input form appears allowing for quick changes to the url field.
  38775. * <br>
  38776. * For information on creating context forms, see:
  38777. * <a href="https://www.tiny.cloud/docs/tinymce/8/contextform/">
  38778. * UI Components - Context forms</a>.
  38779. *
  38780. * @method addContextForm
  38781. * @param {String} name Unique name identifying the new contextual form item.
  38782. * @param {Toolbar.ContextFormSpec} obj the context form configuration object.
  38783. */
  38784. addContextForm: bridge.addContextForm,
  38785. /**
  38786. * Registers a new context menu section that only appears when a content predicate is matched,
  38787. * for example, the cursor is inside a table.
  38788. * <br>
  38789. * For information on creating context menus, see:
  38790. * <a href="https://www.tiny.cloud/docs/tinymce/8/contextmenu/">
  38791. * UI Components - Context Menu</a>.
  38792. *
  38793. * @method addContextMenu
  38794. * @param {String} name Unique name identifying the new context menu.
  38795. * @param {Menu.ContextMenuSpec} obj The context menu configuration object.
  38796. */
  38797. addContextMenu: bridge.addContextMenu,
  38798. /**
  38799. * Registers a new context toolbar that only appears when a content predicate is matched for example
  38800. * the cursor is on an image element.
  38801. * <br>
  38802. * For information on creating context toolbars, see:
  38803. * <a href="https://www.tiny.cloud/docs/tinymce/8/contexttoolbar/">
  38804. * UI Components - Context Toolbar</a>.
  38805. *
  38806. * @method addContextToolbar
  38807. * @param {String} name Unique name identifying the new context toolbar.
  38808. * @param {Toolbar.ContextToolbarSpec} obj The context menu configuration object.
  38809. */
  38810. addContextToolbar: bridge.addContextToolbar,
  38811. /**
  38812. * Registers a new SVG icon. The icon name reference can be configured by any
  38813. * TinyMCE UI components that can display an icon. The icon is only available
  38814. * to the editor instance it was configured for.
  38815. *
  38816. * @method addIcon
  38817. * @param {String} name Unique name identifying the new icon.
  38818. * @param {String} svgData The SVG data string the browser will use to render the SVG icon.
  38819. * @example
  38820. * //To add a simple triangle icon:
  38821. * editor.ui.registry.addIcon('triangleUp', '<svg height="24" width="24"><path d="M12 0 L24 24 L0 24 Z" /></svg>');
  38822. */
  38823. addIcon: bridge.addIcon,
  38824. /**
  38825. * Registers a new menu button. Adds a toolbar button that opens a menu when
  38826. * clicked. The menu can be populated by items created by addMenuItem,
  38827. * addNestedMenuItem or addToggleMenuItem.
  38828. * <br>
  38829. * For information on creating a toolbar menu button, see:
  38830. * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-menu-toolbar-button/">
  38831. * UI Components - Types of toolbar buttons: Menu button</a>.
  38832. *
  38833. * @method addMenuButton
  38834. * @param {String} name Unique name identifying the new menu button.
  38835. * @param {Toolbar.ToolbarMenuButtonSpec} obj The menu button configuration object.
  38836. */
  38837. addMenuButton: bridge.addMenuButton,
  38838. /**
  38839. * Registers a new menu item that executes a command when clicked or activated
  38840. * via keyboard navigation controls.
  38841. * <br>
  38842. * For information on creating a basic menu item, see:
  38843. * <a href="https://www.tiny.cloud/docs/tinymce/8/creating-custom-menu-items/">
  38844. * UI Components - Custom menu items: Basic menu items</a>.
  38845. *
  38846. * @method addMenuItem
  38847. * @param {String} name Unique name identifying the new menu item.
  38848. * @param {Menu.MenuItemSpec} obj The menu item configuration object.
  38849. */
  38850. addMenuItem: bridge.addMenuItem,
  38851. /**
  38852. * Registers a new menu item that reveals a submenu when clicked or activated
  38853. * by keyboard navigation controls.The submenu can be populated by items
  38854. * created by addMenuItem, addNestedMenuItem or addToggleMenuItem.
  38855. * <br>
  38856. * For information on creating a nested menu item, see:
  38857. * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-nested-menu-items/">
  38858. * UI Components - Custom menu items: Nested menu items</a>.
  38859. *
  38860. * @method addNestedMenuItem
  38861. * @param {String} name Unique name identifying the new nested menu item.
  38862. * @param {Menu.NestedMenuItemSpec} obj The nested menu item configuration object.
  38863. */
  38864. addNestedMenuItem: bridge.addNestedMenuItem,
  38865. /**
  38866. * Registers a new sidebar container.
  38867. * This sidebar container is attached to the right side of the editor and
  38868. * can be toggled open or closed. When registered, a new toolbar toggle
  38869. * button with the same sidebar name is created. Additionally there is a
  38870. * ToggleSidebar command and a 'ToggleSidebar' event that can used to
  38871. * manage the sidebar open/closed state. The tinycomments plugin uses a
  38872. * sidebar for its Ui components.
  38873. * <br>
  38874. * For information on creating a custom sidebar, see:
  38875. * <a href="https://www.tiny.cloud/docs/tinymce/8/customsidebar/">
  38876. * UI Components - Custom sidebar</a>.
  38877. *
  38878. * @method addSidebar
  38879. * @param {String} name Unique name identifying the new sidebar.
  38880. * @param {Sidebar.SidebarSpec} obj The sidebar configuration object.
  38881. */
  38882. addSidebar: bridge.addSidebar,
  38883. /**
  38884. * Registers a new split button for the toolbar. The list styles plugin uses
  38885. * a split button to simplify its functionality.
  38886. * <br>
  38887. * For information on creating a split toolbar button, see:
  38888. * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-split-toolbar-button/">
  38889. * UI Components - Types of toolbar buttons: Split button</a>.
  38890. *
  38891. * @method addSplitButton
  38892. * @param {String} name Unique name identifying the new split button.
  38893. * @param {Toolbar.ToolbarSplitButtonSpec} obj The split button configuration object.
  38894. */
  38895. addSplitButton: bridge.addSplitButton,
  38896. /**
  38897. * Registers a new toggle button for the toolbar. A toggle buttons state can
  38898. * be set in the configuration.
  38899. * <br>
  38900. * For information on creating a toggle toolbar button, see:
  38901. * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-toggle-toolbar-button/">
  38902. * UI Components - Types of toolbar buttons: Toggle button</a>.
  38903. *
  38904. * @method addToggleButton
  38905. * @param {String} name Unique name identifying the new split button.
  38906. * @param {Toolbar.ToolbarToggleButtonSpec} obj The toggle button configuration object.
  38907. */
  38908. addToggleButton: bridge.addToggleButton,
  38909. /**
  38910. * Registers a new group toolbar button for the toolbar. Renders a toolbar button that opens a floating toolbar when
  38911. * clicked.
  38912. * <br>
  38913. * <strong>Note:</strong> Group toolbar buttons can only be used when using the floating toolbar mode.
  38914. * <br>
  38915. * For information on creating a group toolbar button, see:
  38916. * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-group-toolbar-button/">
  38917. * UI Components - Types of toolbar buttons: Group toolbar button</a>.
  38918. *
  38919. * @method addGroupToolbarButton
  38920. * @param {String} name Unique name identifying the new group toolbar button.
  38921. * @param {Toolbar.GroupToolbarButtonSpec} obj The group toolbar button configuration object.
  38922. */
  38923. addGroupToolbarButton: bridge.addGroupToolbarButton,
  38924. /**
  38925. * Registers a new menu item that will act like a toggle button,
  38926. * showing a tick in the menu item to represent state.
  38927. * <br>
  38928. * For information on creating a toggle menu item, see:
  38929. * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-toggle-menu-items/">
  38930. * UI Components - Custom menu items: Toggle menu items</a>.
  38931. *
  38932. * @method addToggleMenuItem
  38933. * @param {String} name Unique name identifying the new menu item.
  38934. * @param {Menu.ToggleMenuItemSpec} obj The menu item configuration object.
  38935. */
  38936. addToggleMenuItem: bridge.addToggleMenuItem,
  38937. /**
  38938. * Registers a new view container.
  38939. * This view container is hidden (off) by default and attached next to the main view.
  38940. * It can be toggled on or off.
  38941. * When it is on, the main editor view is hidden and the specific view is shown.
  38942. * When it is off, the specific view is hidden and the main view is shown.
  38943. * There is also a ToggleView command.
  38944. * The ToggleView command can toggle the view visibility.
  38945. * The ToggleView command can be queried for its current state.
  38946. * <br>
  38947. * For information on creating a custom view, see:
  38948. * <a href="https://www.tiny.cloud/docs/tinymce/8/custom-view/">
  38949. * UI Components - Custom view</a>.
  38950. *
  38951. * @method addView
  38952. * @param {String} name Unique name identifying the new view.
  38953. * @param {View.ViewSpec} obj The view configuration object.
  38954. */
  38955. addView: bridge.addView,
  38956. /**
  38957. * Registers a new context configuration in the registry.
  38958. * The registry stores all context configurations.
  38959. * The buttons in editor configuration object can contain a context property.
  38960. * These button specifications can use the registered contexts to determine
  38961. * whether to enable or disable the buttons based on the current context.
  38962. *
  38963. * @method addContext
  38964. * @param {String} name Unique name identifying the new context configuration.
  38965. * @param {Function} pred A predicate function that determines if the context is active
  38966. */
  38967. addContext: bridge.addContext,
  38968. /* note getAll is an internal method and may not be supported in future revisions */
  38969. getAll: bridge.getAll
  38970. };
  38971. };
  38972. // Shorten these names
  38973. const DOM$1 = DOMUtils.DOM;
  38974. const extend = Tools.extend, each$1 = Tools.each;
  38975. /**
  38976. * Include Editor API docs.
  38977. *
  38978. * @include ../../../../../tools/docs/tinymce.Editor.js
  38979. */
  38980. class Editor {
  38981. /**
  38982. * Constructs a editor instance by id.
  38983. *
  38984. * @constructor
  38985. * @method Editor
  38986. * @param {String} id Unique id for the editor.
  38987. * @param {Object} options Options for the editor.
  38988. * @param {tinymce.EditorManager} editorManager EditorManager instance.
  38989. */
  38990. constructor(id, options, editorManager) {
  38991. /**
  38992. * Name/Value object containing plugin instances.
  38993. *
  38994. * @property plugins
  38995. * @type Object
  38996. * @example
  38997. * // Execute a method inside a plugin directly
  38998. * tinymce.activeEditor.plugins.someplugin.someMethod();
  38999. */
  39000. this.plugins = {};
  39001. /**
  39002. * Array with CSS files to load into the iframe.
  39003. *
  39004. * @property contentCSS
  39005. * @type Array
  39006. */
  39007. this.contentCSS = [];
  39008. /**
  39009. * Array of CSS styles to add to head of document when the editor loads.
  39010. *
  39011. * @property contentStyles
  39012. * @type Array
  39013. */
  39014. this.contentStyles = [];
  39015. this.loadedCSS = {};
  39016. this.isNotDirty = false;
  39017. this.composing = false;
  39018. this.destroyed = false;
  39019. this.hasHiddenInput = false;
  39020. this.iframeElement = null;
  39021. this.initialized = false;
  39022. this.readonly = false;
  39023. this.removed = false;
  39024. this.startContent = '';
  39025. this._pendingNativeEvents = [];
  39026. this._skinLoaded = false;
  39027. this._editableRoot = true;
  39028. this.editorManager = editorManager;
  39029. // Patch in the EditorObservable functions
  39030. extend(this, EditorObservable);
  39031. const self = this;
  39032. this.id = id;
  39033. this.editorUid = uuidV4();
  39034. this.hidden = false;
  39035. const normalizedOptions = normalizeOptions(editorManager.defaultOptions, options);
  39036. this.options = create$4(self, normalizedOptions, options);
  39037. register$7(self);
  39038. this.userLookup = createUserLookup(this);
  39039. const getOption = this.options.get;
  39040. if (getOption('deprecation_warnings')) {
  39041. logWarnings(options, normalizedOptions);
  39042. }
  39043. const suffix = getOption('suffix');
  39044. if (suffix) {
  39045. editorManager.suffix = suffix;
  39046. }
  39047. this.suffix = editorManager.suffix;
  39048. const baseUrl = getOption('base_url');
  39049. if (baseUrl) {
  39050. editorManager._setBaseUrl(baseUrl);
  39051. }
  39052. this.baseUri = editorManager.baseURI;
  39053. const referrerPolicy = getReferrerPolicy(self);
  39054. if (referrerPolicy) {
  39055. ScriptLoader.ScriptLoader._setReferrerPolicy(referrerPolicy);
  39056. DOMUtils.DOM.styleSheetLoader._setReferrerPolicy(referrerPolicy);
  39057. }
  39058. ScriptLoader.ScriptLoader._setCrossOrigin((url) => {
  39059. const crossOrigin = getCrossOrigin(self);
  39060. return crossOrigin(url, 'script');
  39061. });
  39062. DOMUtils.DOM.styleSheetLoader._setCrossOrigin((url) => {
  39063. const crossOrigin = getCrossOrigin(self);
  39064. return crossOrigin(url, 'stylesheet');
  39065. });
  39066. const contentCssCors = hasContentCssCors(self);
  39067. if (isNonNullable(contentCssCors)) {
  39068. DOMUtils.DOM.styleSheetLoader._setContentCssCors(contentCssCors);
  39069. }
  39070. AddOnManager.languageLoad = getOption('language_load');
  39071. AddOnManager.baseURL = editorManager.baseURL;
  39072. this.setDirty(false);
  39073. this.documentBaseURI = new URI(getDocumentBaseUrl(self), {
  39074. base_uri: this.baseUri
  39075. });
  39076. this.baseURI = this.baseUri;
  39077. this.inline = isInline$2(self);
  39078. this.hasVisual = isVisualAidsEnabled(self);
  39079. this.shortcuts = new Shortcuts(this);
  39080. this.editorCommands = new EditorCommands(this);
  39081. registerCommands(this);
  39082. const cacheSuffix = getOption('cache_suffix');
  39083. if (cacheSuffix) {
  39084. Env.cacheSuffix = cacheSuffix.replace(/^[\?\&]+/, '');
  39085. }
  39086. this.ui = {
  39087. registry: registry(),
  39088. styleSheetLoader: undefined,
  39089. show: noop,
  39090. hide: noop,
  39091. setEnabled: noop,
  39092. isEnabled: always
  39093. };
  39094. this.mode = create$3(self);
  39095. // Lock certain properties to reduce misuse
  39096. Object.defineProperty(this, 'editorUid', {
  39097. writable: false,
  39098. configurable: false,
  39099. enumerable: true,
  39100. });
  39101. // Call setup
  39102. editorManager.dispatch('SetupEditor', { editor: this });
  39103. const setupCallback = getSetupCallback(self);
  39104. if (isFunction(setupCallback)) {
  39105. setupCallback.call(self, self);
  39106. }
  39107. }
  39108. /**
  39109. * Renders the editor/adds it to the page.
  39110. *
  39111. * @method render
  39112. */
  39113. render() {
  39114. render(this);
  39115. }
  39116. /**
  39117. * Focuses/activates the editor. This will set this editor as the activeEditor in the tinymce collection
  39118. * it will also place DOM focus inside the editor.
  39119. *
  39120. * @method focus
  39121. * @param {Boolean} skipFocus Skip DOM focus. Just set is as the active editor.
  39122. */
  39123. focus(skipFocus) {
  39124. this.execCommand('mceFocus', false, skipFocus);
  39125. }
  39126. /**
  39127. * Returns true/false if the editor has real keyboard focus.
  39128. *
  39129. * @method hasFocus
  39130. * @return {Boolean} Current focus state of the editor.
  39131. */
  39132. hasFocus() {
  39133. return hasFocus(this);
  39134. }
  39135. /**
  39136. * Translates the specified string by replacing variables with language pack items it will also check if there is
  39137. * a key matching the input.
  39138. *
  39139. * @method translate
  39140. * @param {String} text String to translate by the language pack data.
  39141. * @return {String} Translated string.
  39142. */
  39143. translate(text) {
  39144. return I18n.translate(text);
  39145. }
  39146. getParam(name, defaultVal, type) {
  39147. const options = this.options;
  39148. // To keep the legacy API we need to register the option if it's not already been registered
  39149. if (!options.isRegistered(name)) {
  39150. if (isNonNullable(type)) {
  39151. options.register(name, { processor: type, default: defaultVal });
  39152. }
  39153. else {
  39154. options.register(name, { processor: always, default: defaultVal });
  39155. }
  39156. }
  39157. // Attempt to use the passed default value if nothing has been set already
  39158. return !options.isSet(name) && !isUndefined(defaultVal) ? defaultVal : options.get(name);
  39159. }
  39160. /**
  39161. * Checks that the plugin is in the editor configuration and can optionally check if the plugin has been loaded.
  39162. *
  39163. * @method hasPlugin
  39164. * @param {String} name The name of the plugin, as specified for the TinyMCE `plugins` option.
  39165. * @param {Boolean} loaded If `true`, will also check that the plugin has been loaded.
  39166. * @return {Boolean} If `loaded` is `true`, returns `true` if the plugin is in the configuration and has been loaded. If `loaded` is `false`, returns `true` if the plugin is in the configuration, regardless of plugin load status.
  39167. * @example
  39168. * // Returns `true` if the Comments plugin is in the editor configuration and has loaded successfully:
  39169. * tinymce.activeEditor.hasPlugin('tinycomments', true);
  39170. * // Returns `true` if the Table plugin is in the editor configuration, regardless of whether or not it loads:
  39171. * tinymce.activeEditor.hasPlugin('table');
  39172. */
  39173. hasPlugin(name, loaded) {
  39174. const hasPlugin = contains$2(getPlugins(this), name);
  39175. if (hasPlugin) {
  39176. return loaded ? PluginManager.get(name) !== undefined : true;
  39177. }
  39178. else {
  39179. return false;
  39180. }
  39181. }
  39182. /**
  39183. * Dispatches out a onNodeChange event to all observers. This method should be called when you
  39184. * need to update the UI states or element path etc.
  39185. *
  39186. * @method nodeChanged
  39187. * @param {Object} args Optional args to pass to NodeChange event handlers.
  39188. */
  39189. nodeChanged(args) {
  39190. this._nodeChangeDispatcher.nodeChanged(args);
  39191. }
  39192. addCommand(name, callback, scope) {
  39193. /**
  39194. * Callback function that gets called when a command is executed.
  39195. *
  39196. * @callback addCommandCallback
  39197. * @param {Boolean} ui Display UI state true/false.
  39198. * @param {Object} value Optional value for command.
  39199. * @return {Boolean} True/false state if the command was handled or not.
  39200. */
  39201. this.editorCommands.addCommand(name, callback, scope);
  39202. }
  39203. addQueryStateHandler(name, callback, scope) {
  39204. /**
  39205. * Callback function that gets called when a queryCommandState is executed.
  39206. *
  39207. * @callback addQueryStateHandlerCallback
  39208. * @return {Boolean} True/false state if the command is enabled or not like is it bold.
  39209. */
  39210. this.editorCommands.addQueryStateHandler(name, callback, scope);
  39211. }
  39212. addQueryValueHandler(name, callback, scope) {
  39213. /**
  39214. * Callback function that gets called when a queryCommandValue is executed.
  39215. *
  39216. * @callback addQueryValueHandlerCallback
  39217. * @return {Object} Value of the command or undefined.
  39218. */
  39219. this.editorCommands.addQueryValueHandler(name, callback, scope);
  39220. }
  39221. /**
  39222. * Adds a keyboard shortcut for some command or function.
  39223. *
  39224. * @method addShortcut
  39225. * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
  39226. * @param {String} desc Text description for the command.
  39227. * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed.
  39228. * @param {Object} scope Optional scope to execute the function in.
  39229. * @return {Boolean} true/false state if the shortcut was added or not.
  39230. * @example
  39231. * editor.addShortcut('ctrl+a', 'description of the shortcut', () => {});
  39232. * editor.addShortcut('ctrl+alt+a', 'description of the shortcut', () => {});
  39233. * // "meta" maps to Command on Mac and Ctrl on PC
  39234. * editor.addShortcut('meta+a', 'description of the shortcut', () => {});
  39235. * // "access" maps to Control+Option on Mac and shift+alt on PC
  39236. * editor.addShortcut('access+a', 'description of the shortcut', () => {});
  39237. *
  39238. * editor.addShortcut('meta+access+c', 'Opens the code editor dialog.', () => {
  39239. * editor.execCommand('mceCodeEditor');
  39240. * });
  39241. *
  39242. * editor.addShortcut('meta+shift+32', 'Inserts "Hello, World!" for meta+shift+space', () => {
  39243. * editor.execCommand('mceInsertContent', false, 'Hello, World!');
  39244. * });
  39245. */
  39246. addShortcut(pattern, desc, cmdFunc, scope) {
  39247. this.shortcuts.add(pattern, desc, cmdFunc, scope);
  39248. }
  39249. /**
  39250. * Executes a registered command on the current instance. A list of available commands can be found in
  39251. * the tinymce command identifiers documentation.
  39252. *
  39253. * @method execCommand
  39254. * @param {String} cmd Command name to execute, for example mceLink or Bold.
  39255. * @param {Boolean} ui Specifies if a UI (dialog) should be presented or not.
  39256. * @param {Object/Array/String/Number/Boolean} value Optional command value, this can be anything.
  39257. * @param {Object} args Optional arguments object.
  39258. * @return {Boolean} true or false if the command was supported or not.
  39259. */
  39260. execCommand(cmd, ui, value, args) {
  39261. return this.editorCommands.execCommand(cmd, ui, value, args);
  39262. }
  39263. /**
  39264. * Returns a command specific state, for example if bold is enabled or not.
  39265. *
  39266. * @method queryCommandState
  39267. * @param {String} cmd Command to query state from.
  39268. * @return {Boolean} Command specific state, for example if bold is enabled or not.
  39269. */
  39270. queryCommandState(cmd) {
  39271. return this.editorCommands.queryCommandState(cmd);
  39272. }
  39273. /**
  39274. * Returns a command specific value, for example the current font size.
  39275. *
  39276. * @method queryCommandValue
  39277. * @param {String} cmd Command to query value from.
  39278. * @return {String} Command value, for example the current font size or an empty string (`""`) if the query command is not found.
  39279. */
  39280. queryCommandValue(cmd) {
  39281. return this.editorCommands.queryCommandValue(cmd);
  39282. }
  39283. /**
  39284. * Returns true/false if the command is supported or not.
  39285. *
  39286. * @method queryCommandSupported
  39287. * @param {String} cmd Command that we check support for.
  39288. * @return {Boolean} true/false if the command is supported or not.
  39289. */
  39290. queryCommandSupported(cmd) {
  39291. return this.editorCommands.queryCommandSupported(cmd);
  39292. }
  39293. /**
  39294. * Shows the editor and hides any textarea/div that the editor is supposed to replace.
  39295. *
  39296. * @method show
  39297. */
  39298. show() {
  39299. const self = this;
  39300. if (self.hidden) {
  39301. self.hidden = false;
  39302. if (self.inline) {
  39303. self.getBody().contentEditable = 'true';
  39304. }
  39305. else {
  39306. DOM$1.show(self.getContainer());
  39307. DOM$1.hide(self.id);
  39308. }
  39309. self.load();
  39310. self.dispatch('show');
  39311. }
  39312. }
  39313. /**
  39314. * Hides the editor and shows any textarea/div that the editor is supposed to replace.
  39315. *
  39316. * @method hide
  39317. */
  39318. hide() {
  39319. const self = this;
  39320. if (!self.hidden) {
  39321. // We must save before we hide so Safari doesn't crash
  39322. self.save();
  39323. if (self.inline) {
  39324. self.getBody().contentEditable = 'false';
  39325. // Make sure the editor gets blurred
  39326. if (self === self.editorManager.focusedEditor) {
  39327. self.editorManager.focusedEditor = null;
  39328. }
  39329. }
  39330. else {
  39331. DOM$1.hide(self.getContainer());
  39332. DOM$1.setStyle(self.id, 'display', self.orgDisplay);
  39333. }
  39334. self.hidden = true;
  39335. self.dispatch('hide');
  39336. }
  39337. }
  39338. /**
  39339. * Returns true/false if the editor is hidden or not.
  39340. *
  39341. * @method isHidden
  39342. * @return {Boolean} True/false if the editor is hidden or not.
  39343. */
  39344. isHidden() {
  39345. return this.hidden;
  39346. }
  39347. /**
  39348. * Sets the progress state, this will display a throbber/progess for the editor.
  39349. * This is ideal for asynchronous operations like an AJAX save call.
  39350. *
  39351. * @method setProgressState
  39352. * @param {Boolean} state Boolean state if the progress should be shown or hidden.
  39353. * @param {Number} time Optional time to wait before the progress gets shown.
  39354. * @return {Boolean} Same as the input state.
  39355. * @example
  39356. * // Show progress for the active editor
  39357. * tinymce.activeEditor.setProgressState(true);
  39358. *
  39359. * // Hide progress for the active editor
  39360. * tinymce.activeEditor.setProgressState(false);
  39361. *
  39362. * // Show progress after 3 seconds
  39363. * tinymce.activeEditor.setProgressState(true, 3000);
  39364. */
  39365. setProgressState(state, time) {
  39366. this.dispatch('ProgressState', { state, time });
  39367. }
  39368. /**
  39369. * Loads contents from the textarea, input or other element that got converted into an editor instance.
  39370. * This method will move the contents from that textarea, input or other element into the editor by using setContent
  39371. * so all events etc that method has will get dispatched as well.
  39372. *
  39373. * @method load
  39374. * @param {Object} args Optional content object, this gets passed around through the whole load process.
  39375. */
  39376. load(args = {}) {
  39377. const self = this;
  39378. const elm = self.getElement();
  39379. if (self.removed) {
  39380. return;
  39381. }
  39382. if (elm) {
  39383. const loadArgs = {
  39384. ...args,
  39385. load: true
  39386. };
  39387. const value = isTextareaOrInput(elm) ? elm.value : elm.innerHTML;
  39388. self.setContent(value, loadArgs);
  39389. if (!loadArgs.no_events) {
  39390. self.dispatch('LoadContent', {
  39391. ...loadArgs,
  39392. element: elm
  39393. });
  39394. }
  39395. }
  39396. }
  39397. /**
  39398. * Saves the contents from an editor out to the textarea or div element that got converted into an editor instance.
  39399. * This method will move the HTML contents from the editor into that textarea or div by getContent
  39400. * so all events etc that method has will get dispatched as well.
  39401. *
  39402. * @method save
  39403. * @param {Object} args Optional content object, this gets passed around through the whole save process.
  39404. * @return {String} HTML string that got set into the textarea/div.
  39405. */
  39406. save(args = {}) {
  39407. const self = this;
  39408. let elm = self.getElement();
  39409. if (!elm || !self.initialized || self.removed) {
  39410. return '';
  39411. }
  39412. const getArgs = {
  39413. ...args,
  39414. save: true,
  39415. element: elm
  39416. };
  39417. let html = self.getContent(getArgs);
  39418. const saveArgs = { ...getArgs, content: html };
  39419. if (!saveArgs.no_events) {
  39420. self.dispatch('SaveContent', saveArgs);
  39421. }
  39422. // Always run this internal event
  39423. if (saveArgs.format === 'raw') {
  39424. self.dispatch('RawSaveContent', saveArgs);
  39425. }
  39426. html = saveArgs.content;
  39427. if (!isTextareaOrInput(elm)) {
  39428. if (args.is_removing || !self.inline) {
  39429. elm.innerHTML = html;
  39430. }
  39431. // Update hidden form element
  39432. const form = DOM$1.getParent(self.id, 'form');
  39433. if (form) {
  39434. each$1(form.elements, (elm) => {
  39435. if (elm.name === self.id) {
  39436. elm.value = html;
  39437. return false;
  39438. }
  39439. else {
  39440. return true;
  39441. }
  39442. });
  39443. }
  39444. }
  39445. else {
  39446. elm.value = html;
  39447. }
  39448. saveArgs.element = getArgs.element = elm = null;
  39449. if (saveArgs.set_dirty !== false) {
  39450. self.setDirty(false);
  39451. }
  39452. return html;
  39453. }
  39454. setContent(content, args) {
  39455. setContent(this, content, args);
  39456. }
  39457. getContent(args) {
  39458. return getContent(this, args);
  39459. }
  39460. /**
  39461. * Inserts content at caret position.
  39462. *
  39463. * @method insertContent
  39464. * @param {String} content Content to insert.
  39465. * @param {Object} args Optional args to pass to insert call.
  39466. */
  39467. insertContent(content, args) {
  39468. if (args) {
  39469. content = extend({ content }, args);
  39470. }
  39471. this.execCommand('mceInsertContent', false, content);
  39472. }
  39473. /**
  39474. * Resets the editors content, undo/redo history and dirty state. If `initialContent` isn't specified, then
  39475. * the editor is reset back to the initial start content.
  39476. *
  39477. * @method resetContent
  39478. * @param {String} initialContent An optional string to use as the initial content of the editor.
  39479. */
  39480. resetContent(initialContent) {
  39481. // Set the editor content
  39482. if (initialContent === undefined) {
  39483. // editor.startContent is generated by using the `raw` format, so we should set it the same way
  39484. setContent(this, this.startContent, { initial: true, format: 'raw' });
  39485. }
  39486. else {
  39487. setContent(this, initialContent, { initial: true });
  39488. }
  39489. // Update the editor/undo manager state
  39490. this.undoManager.reset();
  39491. this.setDirty(false);
  39492. // Fire a node change event
  39493. this.nodeChanged();
  39494. }
  39495. /**
  39496. * Returns true/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents.
  39497. *
  39498. * The dirty state is automatically set to `true` when the user modifies editor content after initialization or the
  39499. * last `editor.save()` call. This includes changes made using undo or redo.
  39500. *
  39501. * @method isDirty
  39502. * @return {Boolean} True/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents.
  39503. * @example
  39504. * if (tinymce.activeEditor.isDirty()) {
  39505. * alert("You must save your contents.");
  39506. * }
  39507. */
  39508. isDirty() {
  39509. return !this.isNotDirty;
  39510. }
  39511. /**
  39512. * Explicitly sets the dirty state. This will fire the dirty event if the editor dirty state is changed from false to true
  39513. * by invoking this method.
  39514. *
  39515. * @method setDirty
  39516. * @param {Boolean} state True/false if the editor is considered dirty.
  39517. * @example
  39518. * const ajaxSave = () => {
  39519. * const editor = tinymce.get('elm1');
  39520. *
  39521. * // Save contents using some XHR call
  39522. * alert(editor.getContent());
  39523. *
  39524. * editor.setDirty(false); // Force not dirty state
  39525. * }
  39526. */
  39527. setDirty(state) {
  39528. const oldState = !this.isNotDirty;
  39529. this.isNotDirty = !state;
  39530. if (state && state !== oldState) {
  39531. this.dispatch('dirty');
  39532. }
  39533. }
  39534. /**
  39535. * Returns the container element of the editor. The container element includes
  39536. * all the elements added to the page for the editor. Such as UI, iframe, etc.
  39537. *
  39538. * @method getContainer
  39539. * @return {Element} HTML DOM element for the editor container.
  39540. */
  39541. getContainer() {
  39542. const self = this;
  39543. if (!self.container) {
  39544. self.container = self.editorContainer || DOM$1.get(self.id + '_parent');
  39545. }
  39546. return self.container;
  39547. }
  39548. /**
  39549. * Returns the content area container element of the editor. This element
  39550. * holds the iframe or the editable element.
  39551. *
  39552. * @method getContentAreaContainer
  39553. * @return {Element} HTML DOM element for the editor area container.
  39554. */
  39555. getContentAreaContainer() {
  39556. return this.contentAreaContainer;
  39557. }
  39558. /**
  39559. * Returns the target element/textarea that got replaced with a TinyMCE editor instance.
  39560. *
  39561. * @method getElement
  39562. * @return {Element} HTML DOM element for the replaced element.
  39563. */
  39564. getElement() {
  39565. if (!this.targetElm) {
  39566. this.targetElm = DOM$1.get(this.id);
  39567. }
  39568. return this.targetElm;
  39569. }
  39570. /**
  39571. * Returns the iframes window object.
  39572. *
  39573. * @method getWin
  39574. * @return {Window} Iframe DOM window object.
  39575. */
  39576. getWin() {
  39577. const self = this;
  39578. if (!self.contentWindow) {
  39579. const elm = self.iframeElement;
  39580. if (elm) {
  39581. self.contentWindow = elm.contentWindow;
  39582. }
  39583. }
  39584. return self.contentWindow;
  39585. }
  39586. /**
  39587. * Returns the iframes document object.
  39588. *
  39589. * @method getDoc
  39590. * @return {Document} Iframe DOM document object.
  39591. */
  39592. getDoc() {
  39593. const self = this;
  39594. if (!self.contentDocument) {
  39595. const win = self.getWin();
  39596. if (win) {
  39597. self.contentDocument = win.document;
  39598. }
  39599. }
  39600. return self.contentDocument;
  39601. }
  39602. /**
  39603. * Returns the root element of the editable area.
  39604. * For a non-inline iframe-based editor, returns the iframe's body element.
  39605. *
  39606. * @method getBody
  39607. * @return {Element} The root element of the editable area.
  39608. */
  39609. getBody() {
  39610. var _a, _b;
  39611. const doc = this.getDoc();
  39612. return (_b = (_a = this.bodyElement) !== null && _a !== void 0 ? _a : doc === null || doc === void 0 ? void 0 : doc.body) !== null && _b !== void 0 ? _b : null;
  39613. }
  39614. /**
  39615. * URL converter function this gets executed each time a user adds an img, a or
  39616. * any other element that has a URL in it. This will be called both by the DOM and HTML
  39617. * manipulation functions.
  39618. *
  39619. * @method convertURL
  39620. * @param {String} url URL to convert.
  39621. * @param {String} name Attribute name src, href etc.
  39622. * @param {String/HTMLElement} elm Tag name or HTML DOM element depending on HTML or DOM insert.
  39623. * @return {String} Converted URL string.
  39624. */
  39625. convertURL(url, name, elm) {
  39626. const self = this, getOption = self.options.get;
  39627. // Use callback instead
  39628. const urlConverterCallback = getUrlConverterCallback(self);
  39629. if (isFunction(urlConverterCallback)) {
  39630. return urlConverterCallback.call(self, url, elm, true, name);
  39631. }
  39632. // Don't convert link href since that's the CSS files that gets loaded into the editor also skip local file URLs
  39633. if (!getOption('convert_urls') ||
  39634. elm === 'link' ||
  39635. (isObject(elm) && elm.nodeName === 'LINK') ||
  39636. url.indexOf('file:') === 0 ||
  39637. url.length === 0) {
  39638. return url;
  39639. }
  39640. const urlObject = new URI(url);
  39641. if (urlObject.protocol !== 'http' && urlObject.protocol !== 'https' && urlObject.protocol !== '') {
  39642. return url;
  39643. }
  39644. // Convert to relative
  39645. if (getOption('relative_urls')) {
  39646. return self.documentBaseURI.toRelative(url);
  39647. }
  39648. // Convert to absolute
  39649. url = self.documentBaseURI.toAbsolute(url, getOption('remove_script_host'));
  39650. return url;
  39651. }
  39652. /**
  39653. * Adds visual aids for tables, anchors, etc so they can be more easily edited inside the editor.
  39654. *
  39655. * @method addVisual
  39656. * @param {Element} elm Optional root element to loop though to find tables, etc that needs the visual aid.
  39657. */
  39658. addVisual(elm) {
  39659. addVisual(this, elm);
  39660. }
  39661. /**
  39662. * Changes the editable state of the editor's root element.
  39663. *
  39664. * @method setEditableRoot
  39665. * @param {Boolean} state State to set true for editable and false for non-editable.
  39666. */
  39667. setEditableRoot(state) {
  39668. setEditableRoot(this, state);
  39669. }
  39670. /**
  39671. * Returns the current editable state of the editor's root element.
  39672. *
  39673. * @method hasEditableRoot
  39674. * @return {Boolean} True if the root element is editable, false if it is not editable.
  39675. */
  39676. hasEditableRoot() {
  39677. return hasEditableRoot(this);
  39678. }
  39679. /**
  39680. * Removes the editor from the dom and tinymce collection.
  39681. *
  39682. * @method remove
  39683. */
  39684. remove() {
  39685. remove$1(this);
  39686. }
  39687. /**
  39688. * Destroys the editor instance by removing all events, element references or other resources
  39689. * that could leak memory. This method will be called automatically when the page is unloaded
  39690. * but you can also call it directly if you know what you are doing.
  39691. *
  39692. * @method destroy
  39693. * @param {Boolean} automatic Optional state if the destroy is an automatic destroy or user called one.
  39694. */
  39695. destroy(automatic) {
  39696. destroy(this, automatic);
  39697. }
  39698. /**
  39699. * Uploads all data uri/blob uri images in the editor contents to server.
  39700. *
  39701. * @method uploadImages
  39702. * @return {Promise} Promise instance with images and status for each image.
  39703. */
  39704. uploadImages() {
  39705. return this.editorUpload.uploadImages();
  39706. }
  39707. // Internal functions
  39708. _scanForImages() {
  39709. return this.editorUpload.scanForImages();
  39710. }
  39711. }
  39712. const DOM = DOMUtils.DOM;
  39713. const each = Tools.each;
  39714. let boundGlobalEvents = false;
  39715. let beforeUnloadDelegate;
  39716. let editors = [];
  39717. const globalEventDelegate = (e) => {
  39718. const type = e.type;
  39719. each(EditorManager.get(), (editor) => {
  39720. switch (type) {
  39721. case 'scroll':
  39722. editor.dispatch('ScrollWindow', e);
  39723. break;
  39724. case 'resize':
  39725. editor.dispatch('ResizeWindow', e);
  39726. break;
  39727. }
  39728. });
  39729. };
  39730. const toggleGlobalEvents = (state) => {
  39731. if (state !== boundGlobalEvents) {
  39732. const DOM = DOMUtils.DOM;
  39733. if (state) {
  39734. DOM.bind(window, 'resize', globalEventDelegate);
  39735. DOM.bind(window, 'scroll', globalEventDelegate);
  39736. }
  39737. else {
  39738. DOM.unbind(window, 'resize', globalEventDelegate);
  39739. DOM.unbind(window, 'scroll', globalEventDelegate);
  39740. }
  39741. boundGlobalEvents = state;
  39742. }
  39743. };
  39744. const removeEditorFromList = (targetEditor) => {
  39745. const oldEditors = editors;
  39746. editors = filter$5(editors, (editor) => {
  39747. return targetEditor !== editor;
  39748. });
  39749. // Select another editor since the active one was removed
  39750. if (EditorManager.activeEditor === targetEditor) {
  39751. EditorManager.activeEditor = editors.length > 0 ? editors[0] : null;
  39752. }
  39753. // Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor
  39754. if (EditorManager.focusedEditor === targetEditor) {
  39755. EditorManager.focusedEditor = null;
  39756. }
  39757. return oldEditors.length !== editors.length;
  39758. };
  39759. const purgeDestroyedEditor = (editor) => {
  39760. // User has manually destroyed the editor lets clean up the mess
  39761. if (editor && editor.initialized && !(editor.getContainer() || editor.getBody()).parentNode) {
  39762. removeEditorFromList(editor);
  39763. editor.unbindAllNativeEvents();
  39764. editor.destroy(true);
  39765. editor.removed = true;
  39766. }
  39767. };
  39768. const isQuirksMode = document.compatMode !== 'CSS1Compat';
  39769. const EditorManager = {
  39770. ...Observable,
  39771. baseURI: null,
  39772. baseURL: null,
  39773. /**
  39774. * Object containing the options that will be passed by default to the <code>init</code> method upon each initialization of an editor. These defaults will be shallow merged with other options passed to <code>init</code>.
  39775. *
  39776. * @property defaultOptions
  39777. * @type Object
  39778. */
  39779. defaultOptions: {},
  39780. documentBaseURL: null,
  39781. suffix: null,
  39782. /**
  39783. * A uuid string to anonymously identify the page tinymce is loaded in
  39784. *
  39785. * @property pageUid
  39786. * @type String
  39787. */
  39788. pageUid: uuidV4(),
  39789. /**
  39790. * Major version of TinyMCE build.
  39791. *
  39792. * @property majorVersion
  39793. * @type String
  39794. */
  39795. majorVersion: '8',
  39796. /**
  39797. * Minor version of TinyMCE build.
  39798. *
  39799. * @property minorVersion
  39800. * @type String
  39801. */
  39802. minorVersion: '0.2',
  39803. /**
  39804. * Release date of TinyMCE build.
  39805. *
  39806. * @property releaseDate
  39807. * @type String
  39808. */
  39809. releaseDate: '2025-08-14',
  39810. /**
  39811. * Collection of language pack data.
  39812. *
  39813. * @property i18n
  39814. * @type Object
  39815. */
  39816. i18n: I18n,
  39817. /**
  39818. * Currently active editor instance.
  39819. *
  39820. * @property activeEditor
  39821. * @type tinymce.Editor
  39822. * @example
  39823. * tinyMCE.activeEditor.selection.getContent();
  39824. * tinymce.EditorManager.activeEditor.selection.getContent();
  39825. */
  39826. activeEditor: null,
  39827. focusedEditor: null,
  39828. setup() {
  39829. const self = this;
  39830. let baseURL = '';
  39831. let suffix = '';
  39832. // Get base URL for the current document
  39833. let documentBaseURL = URI.getDocumentBaseUrl(document.location);
  39834. // Check if the URL is a document based format like: http://site/dir/file and file:///
  39835. // leave other formats like applewebdata://... intact
  39836. if (/^[^:]+:\/\/\/?[^\/]+\//.test(documentBaseURL)) {
  39837. documentBaseURL = documentBaseURL.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
  39838. if (!/[\/\\]$/.test(documentBaseURL)) {
  39839. documentBaseURL += '/';
  39840. }
  39841. }
  39842. // If tinymce is defined and has a base use that or use the old tinyMCEPreInit
  39843. const preInit = window.tinymce || window.tinyMCEPreInit;
  39844. if (preInit) {
  39845. baseURL = preInit.base || preInit.baseURL;
  39846. suffix = preInit.suffix;
  39847. }
  39848. else {
  39849. // Get base where the tinymce script is located
  39850. const scripts = document.getElementsByTagName('script');
  39851. for (let i = 0; i < scripts.length; i++) {
  39852. const src = scripts[i].src || '';
  39853. if (src === '') {
  39854. continue;
  39855. }
  39856. // Script types supported:
  39857. // tinymce.js tinymce.min.js tinymce.dev.js
  39858. // tinymce.jquery.js tinymce.jquery.min.js tinymce.jquery.dev.js
  39859. // tinymce.full.js tinymce.full.min.js tinymce.full.dev.js
  39860. const srcScript = src.substring(src.lastIndexOf('/'));
  39861. if (/tinymce(\.full|\.jquery|)(\.min|\.dev|)\.js/.test(src)) {
  39862. if (srcScript.indexOf('.min') !== -1) {
  39863. suffix = '.min';
  39864. }
  39865. baseURL = src.substring(0, src.lastIndexOf('/'));
  39866. break;
  39867. }
  39868. }
  39869. // We didn't find any baseURL by looking at the script elements
  39870. // Try to use the document.currentScript as a fallback
  39871. if (!baseURL && document.currentScript) {
  39872. const src = document.currentScript.src;
  39873. if (src.indexOf('.min') !== -1) {
  39874. suffix = '.min';
  39875. }
  39876. baseURL = src.substring(0, src.lastIndexOf('/'));
  39877. }
  39878. }
  39879. /**
  39880. * Base URL where the root directory if TinyMCE is located.
  39881. *
  39882. * @property baseURL
  39883. * @type String
  39884. */
  39885. self.baseURL = new URI(documentBaseURL).toAbsolute(baseURL);
  39886. /**
  39887. * Document base URL where the current document is located.
  39888. *
  39889. * @property documentBaseURL
  39890. * @type String
  39891. */
  39892. self.documentBaseURL = documentBaseURL;
  39893. /**
  39894. * Absolute baseURI for the installation path of TinyMCE.
  39895. *
  39896. * @property baseURI
  39897. * @type tinymce.util.URI
  39898. */
  39899. self.baseURI = new URI(self.baseURL);
  39900. /**
  39901. * Current suffix to add to each plugin/theme that gets loaded for example ".min".
  39902. *
  39903. * @property suffix
  39904. * @type String
  39905. */
  39906. self.suffix = suffix;
  39907. setup$C(self);
  39908. // Lock certain properties to reduce misuse
  39909. each$e(['majorVersion', 'minorVersion', 'releaseDate', 'pageUid', '_addLicenseKeyManager'], (property) => Object.defineProperty(self, property, {
  39910. writable: false,
  39911. configurable: false,
  39912. enumerable: true,
  39913. }));
  39914. },
  39915. /**
  39916. * Overrides the default options for editor instances. The <code>overrideDefaults</code> method replaces the <code>defaultOptions</code>, including any set by a previous call to the <code>overrideDefaults</code> method.
  39917. * <br /><br />
  39918. * When using the Tiny Cloud, some of these defaults are required for the cloud-based editor to function.
  39919. * <br /><br />
  39920. * Therefore, when using <code>overrideDefaults</code> with the cloud-based editor, any newly integrated options must be combined with the options in <code>tinymce.defaultOptions</code>.
  39921. *
  39922. * @method overrideDefaults
  39923. * @param {Object} defaultOptions Default options object.
  39924. * @example
  39925. * const customOptions = {
  39926. * toolbar_sticky: true
  39927. * };
  39928. *
  39929. * tinymce.overrideDefaults({
  39930. * ...tinymce.defaultOptions,
  39931. * ...customOptions
  39932. * });
  39933. */
  39934. overrideDefaults(defaultOptions) {
  39935. const baseUrl = defaultOptions.base_url;
  39936. if (baseUrl) {
  39937. this._setBaseUrl(baseUrl);
  39938. }
  39939. const suffix = defaultOptions.suffix;
  39940. if (suffix) {
  39941. this.suffix = suffix;
  39942. }
  39943. this.defaultOptions = defaultOptions;
  39944. const pluginBaseUrls = defaultOptions.plugin_base_urls;
  39945. if (pluginBaseUrls !== undefined) {
  39946. each$d(pluginBaseUrls, (pluginBaseUrl, pluginName) => {
  39947. AddOnManager.PluginManager.urls[pluginName] = pluginBaseUrl;
  39948. });
  39949. }
  39950. },
  39951. /**
  39952. * Initializes a set of editors. This method will create editors based on various settings.
  39953. * <br /><br />
  39954. * For information on basic usage of <code>init</code>, see: <a href="https://www.tiny.cloud/docs/tinymce/8/basic-setup/">Basic setup</a>.
  39955. *
  39956. * @method init
  39957. * @param {Object} options Options object to be passed to each editor instance.
  39958. * @return {Promise} Promise that gets resolved with an array of editors when all editor instances are initialized.
  39959. * @example
  39960. * // Initializes a editor using the longer method
  39961. * tinymce.EditorManager.init({
  39962. * some_settings : 'some value'
  39963. * });
  39964. *
  39965. * // Initializes a editor instance using the shorter version and with a promise
  39966. * tinymce.init({
  39967. * some_settings : 'some value'
  39968. * }).then((editors) => {
  39969. * ...
  39970. * });
  39971. */
  39972. init(options) {
  39973. const self = this;
  39974. let result;
  39975. const invalidInlineTargets = Tools.makeMap('area base basefont br col frame hr img input isindex link meta param embed source wbr track ' +
  39976. 'colgroup option table tbody tfoot thead tr th td script noscript style textarea video audio iframe object menu', ' ');
  39977. const isInvalidInlineTarget = (options, elm) => options.inline && elm.tagName.toLowerCase() in invalidInlineTargets;
  39978. const createId = (elm) => {
  39979. let id = elm.id;
  39980. if (!id) {
  39981. id = get$a(elm, 'name').filter((name) => !DOM.get(name)).getOrThunk(DOM.uniqueId);
  39982. elm.setAttribute('id', id);
  39983. }
  39984. return id;
  39985. };
  39986. const execCallback = (name) => {
  39987. const callback = options[name];
  39988. if (!callback) {
  39989. return;
  39990. }
  39991. return callback.apply(self, []);
  39992. };
  39993. const findTargets = (options) => {
  39994. if (Env.browser.isIE() || Env.browser.isEdge()) {
  39995. initError('TinyMCE does not support the browser you are using. For a list of supported' +
  39996. ' browsers please see: https://www.tiny.cloud/docs/tinymce/8/support/#supportedwebbrowsers');
  39997. return [];
  39998. }
  39999. else if (isQuirksMode) {
  40000. initError('Failed to initialize the editor as the document is not in standards mode. ' +
  40001. 'TinyMCE requires standards mode.');
  40002. return [];
  40003. }
  40004. else if (isString(options.selector)) {
  40005. return DOM.select(options.selector);
  40006. }
  40007. else if (isNonNullable(options.target)) {
  40008. return [options.target];
  40009. }
  40010. else {
  40011. return [];
  40012. }
  40013. };
  40014. let provideResults = (editors) => {
  40015. result = editors;
  40016. };
  40017. const initEditors = () => {
  40018. let initCount = 0;
  40019. const editors = [];
  40020. let targets;
  40021. const createEditor = (id, options, targetElm) => {
  40022. const editor = new Editor(id, options, self);
  40023. editors.push(editor);
  40024. editor.on('init', () => {
  40025. if (++initCount === targets.length) {
  40026. provideResults(editors);
  40027. }
  40028. });
  40029. editor.targetElm = editor.targetElm || targetElm;
  40030. editor.render();
  40031. };
  40032. DOM.unbind(window, 'ready', initEditors);
  40033. execCallback('onpageload');
  40034. targets = unique$1(findTargets(options));
  40035. Tools.each(targets, (elm) => {
  40036. purgeDestroyedEditor(self.get(elm.id));
  40037. });
  40038. targets = Tools.grep(targets, (elm) => {
  40039. return !self.get(elm.id);
  40040. });
  40041. if (targets.length === 0) {
  40042. provideResults([]);
  40043. }
  40044. else {
  40045. each(targets, (elm) => {
  40046. if (isInvalidInlineTarget(options, elm)) {
  40047. initError('Could not initialize inline editor on invalid inline target element', elm);
  40048. }
  40049. else {
  40050. createEditor(createId(elm), options, elm);
  40051. }
  40052. });
  40053. }
  40054. };
  40055. DOM.bind(window, 'ready', initEditors);
  40056. return new Promise((resolve) => {
  40057. if (result) {
  40058. resolve(result);
  40059. }
  40060. else {
  40061. provideResults = (editors) => {
  40062. resolve(editors);
  40063. };
  40064. }
  40065. });
  40066. },
  40067. /**
  40068. * Returns an editor instance for a given id.
  40069. *
  40070. * @method get
  40071. * @param {String/Number} id The id or index of the editor instance to return.
  40072. * @return {tinymce.Editor/Array} Editor instance or an array of editor instances.
  40073. * @example
  40074. * // Adds an onclick event to an editor by id
  40075. * tinymce.get('mytextbox').on('click', (e) => {
  40076. * ed.windowManager.alert('Hello world!');
  40077. * });
  40078. *
  40079. * // Adds an onclick event to an editor by index
  40080. * tinymce.get(0).on('click', (e) => {
  40081. * ed.windowManager.alert('Hello world!');
  40082. * });
  40083. *
  40084. * // Adds an onclick event to an editor by id (longer version)
  40085. * tinymce.EditorManager.get('mytextbox').on('click', (e) => {
  40086. * ed.windowManager.alert('Hello world!');
  40087. * });
  40088. */
  40089. get(id) {
  40090. if (arguments.length === 0) {
  40091. return editors.slice(0);
  40092. }
  40093. else if (isString(id)) {
  40094. return find$2(editors, (editor) => {
  40095. return editor.id === id;
  40096. }).getOr(null);
  40097. }
  40098. else if (isNumber(id)) {
  40099. return editors[id] ? editors[id] : null;
  40100. }
  40101. else {
  40102. return null;
  40103. }
  40104. },
  40105. /**
  40106. * Adds an editor instance to the editor collection. This will also set it as the active editor.
  40107. *
  40108. * @method add
  40109. * @param {tinymce.Editor} editor Editor instance to add to the collection.
  40110. * @return {tinymce.Editor} The same instance that got passed in.
  40111. */
  40112. add(editor) {
  40113. const self = this;
  40114. // Prevent existing editors from being added again this could happen
  40115. // if a user calls createEditor then render or add multiple times.
  40116. const existingEditor = self.get(editor.id);
  40117. if (existingEditor === editor) {
  40118. return editor;
  40119. }
  40120. if (existingEditor === null) {
  40121. editors.push(editor);
  40122. }
  40123. toggleGlobalEvents(true);
  40124. // Doesn't call setActive method since we don't want
  40125. // to fire a bunch of activate/deactivate calls while initializing
  40126. self.activeEditor = editor;
  40127. self.dispatch('AddEditor', { editor });
  40128. if (!beforeUnloadDelegate) {
  40129. beforeUnloadDelegate = (e) => {
  40130. const event = self.dispatch('BeforeUnload');
  40131. if (event.returnValue) {
  40132. // browsers are all a little bit special about this: https://developer.mozilla.org/en-US/docs/Web/API/BeforeUnloadEvent
  40133. e.preventDefault();
  40134. e.returnValue = event.returnValue;
  40135. return event.returnValue;
  40136. }
  40137. };
  40138. window.addEventListener('beforeunload', beforeUnloadDelegate);
  40139. }
  40140. return editor;
  40141. },
  40142. /**
  40143. * Creates an editor instance and adds it to the EditorManager collection.
  40144. *
  40145. * @method createEditor
  40146. * @param {String} id Instance id to use for editor.
  40147. * @param {Object} options Editor instance options.
  40148. * @return {tinymce.Editor} Editor instance that got created.
  40149. */
  40150. createEditor(id, options) {
  40151. return this.add(new Editor(id, options, this));
  40152. },
  40153. /**
  40154. * Removes a editor or editors form page.
  40155. *
  40156. * @example
  40157. * // Remove all editors bound to divs
  40158. * tinymce.remove('div');
  40159. *
  40160. * // Remove all editors bound to textareas
  40161. * tinymce.remove('textarea');
  40162. *
  40163. * // Remove all editors
  40164. * tinymce.remove();
  40165. *
  40166. * // Remove specific instance by id
  40167. * tinymce.remove('#id');
  40168. *
  40169. * @method remove
  40170. * @param {tinymce.Editor/String/Object} [selector] CSS selector or editor instance to remove.
  40171. * @return {tinymce.Editor} The editor that got passed in will be return if it was found otherwise null.
  40172. */
  40173. remove(selector) {
  40174. const self = this;
  40175. let editor;
  40176. // Remove all editors
  40177. if (!selector) {
  40178. for (let i = editors.length - 1; i >= 0; i--) {
  40179. self.remove(editors[i]);
  40180. }
  40181. return;
  40182. }
  40183. // Remove editors by selector
  40184. if (isString(selector)) {
  40185. each(DOM.select(selector), (elm) => {
  40186. editor = self.get(elm.id);
  40187. if (editor) {
  40188. self.remove(editor);
  40189. }
  40190. });
  40191. return;
  40192. }
  40193. // Remove specific editor
  40194. editor = selector;
  40195. // Not in the collection
  40196. if (isNull(self.get(editor.id))) {
  40197. return null;
  40198. }
  40199. if (removeEditorFromList(editor)) {
  40200. self.dispatch('RemoveEditor', { editor });
  40201. }
  40202. if (editors.length === 0) {
  40203. window.removeEventListener('beforeunload', beforeUnloadDelegate);
  40204. }
  40205. editor.remove();
  40206. toggleGlobalEvents(editors.length > 0);
  40207. return editor;
  40208. },
  40209. /**
  40210. * Executes a specific command on the currently active editor.
  40211. *
  40212. * @method execCommand
  40213. * @param {String} cmd Command to perform for example Bold.
  40214. * @param {Boolean} ui Optional boolean state if a UI should be presented for the command or not.
  40215. * @param {Object/String/Number/Boolean} value Optional value parameter like for example an URL to a link.
  40216. * @return {Boolean} true/false if the command was executed or not.
  40217. */
  40218. execCommand(cmd, ui, value) {
  40219. var _a;
  40220. const self = this;
  40221. const editorId = isObject(value) ? (_a = value.id) !== null && _a !== void 0 ? _a : value.index : value;
  40222. // Manager commands
  40223. switch (cmd) {
  40224. case 'mceAddEditor': {
  40225. if (!self.get(editorId)) {
  40226. const editorOptions = value.options;
  40227. new Editor(editorId, editorOptions, self).render();
  40228. }
  40229. return true;
  40230. }
  40231. case 'mceRemoveEditor': {
  40232. const editor = self.get(editorId);
  40233. if (editor) {
  40234. editor.remove();
  40235. }
  40236. return true;
  40237. }
  40238. case 'mceToggleEditor': {
  40239. const editor = self.get(editorId);
  40240. if (!editor) {
  40241. self.execCommand('mceAddEditor', false, value);
  40242. return true;
  40243. }
  40244. if (editor.isHidden()) {
  40245. editor.show();
  40246. }
  40247. else {
  40248. editor.hide();
  40249. }
  40250. return true;
  40251. }
  40252. }
  40253. // Run command on active editor
  40254. if (self.activeEditor) {
  40255. return self.activeEditor.execCommand(cmd, ui, value);
  40256. }
  40257. return false;
  40258. },
  40259. /**
  40260. * Calls the save method on all editor instances in the collection. This can be useful when a form is to be submitted.
  40261. *
  40262. * @method triggerSave
  40263. * @example
  40264. * // Saves all contents
  40265. * tinyMCE.triggerSave();
  40266. */
  40267. triggerSave: () => {
  40268. each(editors, (editor) => {
  40269. editor.save();
  40270. });
  40271. },
  40272. /**
  40273. * Adds a language pack, this gets called by the loaded language files like en.js.
  40274. *
  40275. * @method addI18n
  40276. * @param {String} code Optional language code.
  40277. * @param {Object} items Name/value object with translations.
  40278. */
  40279. addI18n: (code, items) => {
  40280. I18n.add(code, items);
  40281. },
  40282. /**
  40283. * Translates the specified string using the language pack items.
  40284. *
  40285. * @method translate
  40286. * @param {String/Array/Object} text String to translate
  40287. * @return {String} Translated string.
  40288. */
  40289. translate: (text) => {
  40290. return I18n.translate(text);
  40291. },
  40292. /**
  40293. * Sets the active editor instance and fires the deactivate/activate events.
  40294. *
  40295. * @method setActive
  40296. * @param {tinymce.Editor} editor Editor instance to set as the active instance.
  40297. */
  40298. setActive(editor) {
  40299. const activeEditor = this.activeEditor;
  40300. if (this.activeEditor !== editor) {
  40301. if (activeEditor) {
  40302. activeEditor.dispatch('deactivate', { relatedTarget: editor });
  40303. }
  40304. editor.dispatch('activate', { relatedTarget: activeEditor });
  40305. }
  40306. this.activeEditor = editor;
  40307. },
  40308. _setBaseUrl(baseUrl) {
  40309. this.baseURL = new URI(this.documentBaseURL).toAbsolute(baseUrl.replace(/\/+$/, ''));
  40310. this.baseURI = new URI(this.baseURL);
  40311. },
  40312. _addLicenseKeyManager: (addOn) => LicenseKeyManagerLoader.add(addOn),
  40313. };
  40314. EditorManager.setup();
  40315. // The FakeClipboard has been designed to match the native Clipboard API as closely as possible
  40316. const setup = () => {
  40317. const dataValue = value$1();
  40318. const FakeClipboardItem = (items) => ({
  40319. items,
  40320. types: keys(items),
  40321. getType: (type) => get$a(items, type).getOrUndefined()
  40322. });
  40323. const write = (data) => {
  40324. dataValue.set(data);
  40325. };
  40326. const read = () => dataValue.get().getOrUndefined();
  40327. const clear = dataValue.clear;
  40328. return {
  40329. FakeClipboardItem,
  40330. write,
  40331. read,
  40332. clear
  40333. };
  40334. };
  40335. const FakeClipboard = setup();
  40336. /**
  40337. * Contains various tools for rect/position calculation.
  40338. *
  40339. * @class tinymce.geom.Rect
  40340. */
  40341. const min = Math.min, max = Math.max, round = Math.round;
  40342. /**
  40343. * Returns the rect positioned based on the relative position name
  40344. * to the target rect.
  40345. *
  40346. * @method relativePosition
  40347. * @param {Rect} rect Source rect to modify into a new rect.
  40348. * @param {Rect} targetRect Rect to move relative to based on the rel option.
  40349. * @param {String} rel Relative position. For example: tr-bl.
  40350. */
  40351. const relativePosition = (rect, targetRect, rel) => {
  40352. let x = targetRect.x;
  40353. let y = targetRect.y;
  40354. const w = rect.w;
  40355. const h = rect.h;
  40356. const targetW = targetRect.w;
  40357. const targetH = targetRect.h;
  40358. const relChars = (rel || '').split('');
  40359. if (relChars[0] === 'b') {
  40360. y += targetH;
  40361. }
  40362. if (relChars[1] === 'r') {
  40363. x += targetW;
  40364. }
  40365. if (relChars[0] === 'c') {
  40366. y += round(targetH / 2);
  40367. }
  40368. if (relChars[1] === 'c') {
  40369. x += round(targetW / 2);
  40370. }
  40371. if (relChars[3] === 'b') {
  40372. y -= h;
  40373. }
  40374. if (relChars[4] === 'r') {
  40375. x -= w;
  40376. }
  40377. if (relChars[3] === 'c') {
  40378. y -= round(h / 2);
  40379. }
  40380. if (relChars[4] === 'c') {
  40381. x -= round(w / 2);
  40382. }
  40383. return create$2(x, y, w, h);
  40384. };
  40385. /**
  40386. * Tests various positions to get the most suitable one.
  40387. *
  40388. * @method findBestRelativePosition
  40389. * @param {Rect} rect Rect to use as source.
  40390. * @param {Rect} targetRect Rect to move relative to.
  40391. * @param {Rect} constrainRect Rect to constrain within.
  40392. * @param {Array} rels Array of relative positions to test against.
  40393. */
  40394. const findBestRelativePosition = (rect, targetRect, constrainRect, rels) => {
  40395. for (let i = 0; i < rels.length; i++) {
  40396. const pos = relativePosition(rect, targetRect, rels[i]);
  40397. if (pos.x >= constrainRect.x && pos.x + pos.w <= constrainRect.w + constrainRect.x &&
  40398. pos.y >= constrainRect.y && pos.y + pos.h <= constrainRect.h + constrainRect.y) {
  40399. return rels[i];
  40400. }
  40401. }
  40402. return null;
  40403. };
  40404. /**
  40405. * Inflates the rect in all directions.
  40406. *
  40407. * @method inflate
  40408. * @param {Rect} rect Rect to expand.
  40409. * @param {Number} w Relative width to expand by.
  40410. * @param {Number} h Relative height to expand by.
  40411. * @return {Rect} New expanded rect.
  40412. */
  40413. const inflate = (rect, w, h) => {
  40414. return create$2(rect.x - w, rect.y - h, rect.w + w * 2, rect.h + h * 2);
  40415. };
  40416. /**
  40417. * Returns the intersection of the specified rectangles.
  40418. *
  40419. * @method intersect
  40420. * @param {Rect} rect The first rectangle to compare.
  40421. * @param {Rect} cropRect The second rectangle to compare.
  40422. * @return {Rect} The intersection of the two rectangles or null if they don't intersect.
  40423. */
  40424. const intersect = (rect, cropRect) => {
  40425. const x1 = max(rect.x, cropRect.x);
  40426. const y1 = max(rect.y, cropRect.y);
  40427. const x2 = min(rect.x + rect.w, cropRect.x + cropRect.w);
  40428. const y2 = min(rect.y + rect.h, cropRect.y + cropRect.h);
  40429. if (x2 - x1 < 0 || y2 - y1 < 0) {
  40430. return null;
  40431. }
  40432. return create$2(x1, y1, x2 - x1, y2 - y1);
  40433. };
  40434. /**
  40435. * Returns a rect clamped within the specified clamp rect. This forces the
  40436. * rect to be inside the clamp rect.
  40437. *
  40438. * @method clamp
  40439. * @param {Rect} rect Rectangle to force within clamp rect.
  40440. * @param {Rect} clampRect Rectable to force within.
  40441. * @param {Boolean} fixedSize True/false if size should be fixed.
  40442. * @return {Rect} Clamped rect.
  40443. */
  40444. const clamp = (rect, clampRect, fixedSize) => {
  40445. let x1 = rect.x;
  40446. let y1 = rect.y;
  40447. let x2 = rect.x + rect.w;
  40448. let y2 = rect.y + rect.h;
  40449. const cx2 = clampRect.x + clampRect.w;
  40450. const cy2 = clampRect.y + clampRect.h;
  40451. const underflowX1 = max(0, clampRect.x - x1);
  40452. const underflowY1 = max(0, clampRect.y - y1);
  40453. const overflowX2 = max(0, x2 - cx2);
  40454. const overflowY2 = max(0, y2 - cy2);
  40455. x1 += underflowX1;
  40456. y1 += underflowY1;
  40457. if (fixedSize) {
  40458. x2 += underflowX1;
  40459. y2 += underflowY1;
  40460. x1 -= overflowX2;
  40461. y1 -= overflowY2;
  40462. }
  40463. x2 -= overflowX2;
  40464. y2 -= overflowY2;
  40465. return create$2(x1, y1, x2 - x1, y2 - y1);
  40466. };
  40467. /**
  40468. * Creates a new rectangle object.
  40469. *
  40470. * @method create
  40471. * @param {Number} x Rectangle x location.
  40472. * @param {Number} y Rectangle y location.
  40473. * @param {Number} w Rectangle width.
  40474. * @param {Number} h Rectangle height.
  40475. * @return {Rect} New rectangle object.
  40476. */
  40477. const create$2 = (x, y, w, h) => {
  40478. return { x, y, w, h };
  40479. };
  40480. /**
  40481. * Creates a new rectangle object form a clientRects object.
  40482. *
  40483. * @method fromClientRect
  40484. * @param {ClientRect} clientRect DOM ClientRect object.
  40485. * @return {Rect} New rectangle object.
  40486. */
  40487. const fromClientRect = (clientRect) => {
  40488. return create$2(clientRect.left, clientRect.top, clientRect.width, clientRect.height);
  40489. };
  40490. const Rect = {
  40491. inflate,
  40492. relativePosition,
  40493. findBestRelativePosition,
  40494. intersect,
  40495. clamp,
  40496. create: create$2,
  40497. fromClientRect
  40498. };
  40499. const awaiter = (resolveCb, rejectCb, timeout = 1000) => {
  40500. let done = false;
  40501. let timer = null;
  40502. const complete = (completer) => (...args) => {
  40503. if (!done) {
  40504. done = true;
  40505. if (timer !== null) {
  40506. window.clearTimeout(timer);
  40507. timer = null;
  40508. }
  40509. completer.apply(null, args);
  40510. }
  40511. };
  40512. const resolve = complete(resolveCb);
  40513. const reject = complete(rejectCb);
  40514. const start = (...args) => {
  40515. if (!done && timer === null) {
  40516. timer = window.setTimeout(() => reject.apply(null, args), timeout);
  40517. }
  40518. };
  40519. return {
  40520. start,
  40521. resolve,
  40522. reject
  40523. };
  40524. };
  40525. const create$1 = () => {
  40526. const tasks = {};
  40527. const resultFns = {};
  40528. const resources = {};
  40529. const load = (id, url) => {
  40530. const loadErrMsg = `Script at URL "${url}" failed to load`;
  40531. const runErrMsg = `Script at URL "${url}" did not call \`tinymce.Resource.add('${id}', data)\` within 1 second`;
  40532. if (tasks[id] !== undefined) {
  40533. return tasks[id];
  40534. }
  40535. else {
  40536. const task = new Promise((resolve, reject) => {
  40537. const waiter = awaiter(resolve, reject);
  40538. resultFns[id] = waiter.resolve;
  40539. ScriptLoader.ScriptLoader.loadScript(url).then(() => waiter.start(runErrMsg), () => waiter.reject(loadErrMsg));
  40540. });
  40541. tasks[id] = task;
  40542. return task;
  40543. }
  40544. };
  40545. const add = (id, data) => {
  40546. if (resultFns[id] !== undefined) {
  40547. resultFns[id](data);
  40548. delete resultFns[id];
  40549. }
  40550. tasks[id] = Promise.resolve(data);
  40551. resources[id] = data;
  40552. };
  40553. const has = (id) => {
  40554. return id in resources;
  40555. };
  40556. const unload = (id) => {
  40557. delete tasks[id];
  40558. delete resources[id];
  40559. };
  40560. const get = (id) => resources[id];
  40561. return {
  40562. load,
  40563. add,
  40564. has,
  40565. get,
  40566. unload
  40567. };
  40568. };
  40569. const Resource = create$1();
  40570. // Simple stub of localstorage with strict security settings #TINY-1782
  40571. const create = () => (() => {
  40572. let data = {};
  40573. let keys = [];
  40574. const storage = {
  40575. getItem: (key) => {
  40576. const item = data[key];
  40577. return item ? item : null;
  40578. },
  40579. setItem: (key, value) => {
  40580. keys.push(key);
  40581. data[key] = String(value);
  40582. },
  40583. key: (index) => {
  40584. return keys[index];
  40585. },
  40586. removeItem: (key) => {
  40587. keys = keys.filter((k) => k === key);
  40588. delete data[key];
  40589. },
  40590. clear: () => {
  40591. keys = [];
  40592. data = {};
  40593. },
  40594. length: 0
  40595. };
  40596. Object.defineProperty(storage, 'length', {
  40597. get: () => keys.length,
  40598. configurable: false,
  40599. enumerable: false
  40600. });
  40601. return storage;
  40602. })();
  40603. /**
  40604. * @class tinymce.util.LocalStorage
  40605. * @static
  40606. * @version 4.0
  40607. * @private
  40608. * @example
  40609. * tinymce.util.LocalStorage.setItem('key', 'value');
  40610. * const value = tinymce.util.LocalStorage.getItem('key');
  40611. */
  40612. let localStorage;
  40613. // Browsers with certain strict security settings will explode when trying to access localStorage
  40614. // so we need to do a try/catch and a simple stub here. #TINY-1782 & #TINY-5935
  40615. try {
  40616. const test = '__storage_test__';
  40617. localStorage = window.localStorage;
  40618. // Make sure we can set a value, as storage may also be full
  40619. localStorage.setItem(test, test);
  40620. localStorage.removeItem(test);
  40621. }
  40622. catch (_a) {
  40623. localStorage = create();
  40624. }
  40625. var LocalStorage = localStorage;
  40626. /**
  40627. * @include ../../../../../tools/docs/tinymce.js
  40628. */
  40629. /**
  40630. * @include ../../../../../tools/docs/tinymce.Event.js
  40631. */
  40632. /**
  40633. * @include ../../../../../tools/docs/tinymce.editor.ui.Ui.js
  40634. */
  40635. const publicApi = {
  40636. geom: {
  40637. Rect
  40638. },
  40639. util: {
  40640. Delay,
  40641. Tools,
  40642. VK,
  40643. URI,
  40644. EventDispatcher,
  40645. Observable,
  40646. I18n,
  40647. LocalStorage,
  40648. ImageUploader
  40649. },
  40650. dom: {
  40651. EventUtils,
  40652. TreeWalker: DomTreeWalker,
  40653. TextSeeker,
  40654. DOMUtils,
  40655. ScriptLoader,
  40656. RangeUtils,
  40657. Serializer: DomSerializer,
  40658. StyleSheetLoader,
  40659. ControlSelection,
  40660. BookmarkManager,
  40661. Selection: EditorSelection,
  40662. Event: EventUtils.Event
  40663. },
  40664. html: {
  40665. Styles,
  40666. Entities,
  40667. Node: AstNode,
  40668. Schema,
  40669. DomParser,
  40670. Writer,
  40671. Serializer: HtmlSerializer
  40672. },
  40673. Env,
  40674. AddOnManager,
  40675. Annotator,
  40676. Formatter,
  40677. UndoManager,
  40678. EditorCommands,
  40679. WindowManager,
  40680. NotificationManager,
  40681. EditorObservable,
  40682. Shortcuts,
  40683. Editor,
  40684. FocusManager,
  40685. EditorManager,
  40686. // Global instances
  40687. DOM: DOMUtils.DOM,
  40688. ScriptLoader: ScriptLoader.ScriptLoader,
  40689. PluginManager,
  40690. ThemeManager,
  40691. ModelManager,
  40692. IconManager,
  40693. Resource,
  40694. FakeClipboard,
  40695. // Global utility functions
  40696. trim: Tools.trim,
  40697. isArray: Tools.isArray,
  40698. is: Tools.is,
  40699. toArray: Tools.toArray,
  40700. makeMap: Tools.makeMap,
  40701. each: Tools.each,
  40702. map: Tools.map,
  40703. grep: Tools.grep,
  40704. inArray: Tools.inArray,
  40705. extend: Tools.extend,
  40706. walk: Tools.walk,
  40707. resolve: Tools.resolve,
  40708. explode: Tools.explode,
  40709. _addCacheSuffix: Tools._addCacheSuffix
  40710. };
  40711. const tinymce$1 = Tools.extend(EditorManager, publicApi);
  40712. const exportToModuleLoaders = (tinymce) => {
  40713. if (typeof module === 'object') {
  40714. try {
  40715. module.exports = tinymce;
  40716. }
  40717. catch (_a) {
  40718. // It will thrown an error when running this module
  40719. // within webpack where the module.exports object is sealed
  40720. }
  40721. }
  40722. };
  40723. const exportToWindowGlobal = (tinymce) => {
  40724. window.tinymce = tinymce;
  40725. window.tinyMCE = tinymce;
  40726. };
  40727. exportToWindowGlobal(tinymce$1);
  40728. exportToModuleLoaders(tinymce$1);
  40729. })();