summernote.js 192 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867
  1. /**
  2. * Super simple wysiwyg editor v0.7.1
  3. * http://summernote.org/
  4. *
  5. * summernote.js
  6. * Copyright 2013-2015 Alan Hong. and other contributors
  7. * summernote may be freely distributed under the MIT license./
  8. *
  9. *
  10. * MODIFIED: Adding support for CodeMirror callbacks.
  11. * If we've got any callbacks for code mirror, register them.
  12. * Line 4670
  13. *
  14. * Date: 2015-12-31T12:09Z
  15. */
  16. (function (factory) {
  17. /* global define */
  18. if (typeof define === 'function' && define.amd) {
  19. // AMD. Register as an anonymous module.
  20. define(['jquery'], factory);
  21. } else if (typeof module === 'object' && module.exports) {
  22. // Node/CommonJS
  23. module.exports = factory(require('jquery'));
  24. } else {
  25. // Browser globals
  26. factory(window.jQuery);
  27. }
  28. }(function ($) {
  29. 'use strict';
  30. /**
  31. * @class core.func
  32. *
  33. * func utils (for high-order func's arg)
  34. *
  35. * @singleton
  36. * @alternateClassName func
  37. */
  38. var func = (function () {
  39. var eq = function (itemA) {
  40. return function (itemB) {
  41. return itemA === itemB;
  42. };
  43. };
  44. var eq2 = function (itemA, itemB) {
  45. return itemA === itemB;
  46. };
  47. var peq2 = function (propName) {
  48. return function (itemA, itemB) {
  49. return itemA[propName] === itemB[propName];
  50. };
  51. };
  52. var ok = function () {
  53. return true;
  54. };
  55. var fail = function () {
  56. return false;
  57. };
  58. var not = function (f) {
  59. return function () {
  60. return !f.apply(f, arguments);
  61. };
  62. };
  63. var and = function (fA, fB) {
  64. return function (item) {
  65. return fA(item) && fB(item);
  66. };
  67. };
  68. var self = function (a) {
  69. return a;
  70. };
  71. var idCounter = 0;
  72. /**
  73. * generate a globally-unique id
  74. *
  75. * @param {String} [prefix]
  76. */
  77. var uniqueId = function (prefix) {
  78. var id = ++idCounter + '';
  79. return prefix ? prefix + id : id;
  80. };
  81. /**
  82. * returns bnd (bounds) from rect
  83. *
  84. * - IE Compatability Issue: http://goo.gl/sRLOAo
  85. * - Scroll Issue: http://goo.gl/sNjUc
  86. *
  87. * @param {Rect} rect
  88. * @return {Object} bounds
  89. * @return {Number} bounds.top
  90. * @return {Number} bounds.left
  91. * @return {Number} bounds.width
  92. * @return {Number} bounds.height
  93. */
  94. var rect2bnd = function (rect) {
  95. var $document = $(document);
  96. return {
  97. top: rect.top + $document.scrollTop(),
  98. left: rect.left + $document.scrollLeft(),
  99. width: rect.right - rect.left,
  100. height: rect.bottom - rect.top
  101. };
  102. };
  103. /**
  104. * returns a copy of the object where the keys have become the values and the values the keys.
  105. * @param {Object} obj
  106. * @return {Object}
  107. */
  108. var invertObject = function (obj) {
  109. var inverted = {};
  110. for (var key in obj) {
  111. if (obj.hasOwnProperty(key)) {
  112. inverted[obj[key]] = key;
  113. }
  114. }
  115. return inverted;
  116. };
  117. /**
  118. * @param {String} namespace
  119. * @param {String} [prefix]
  120. * @return {String}
  121. */
  122. var namespaceToCamel = function (namespace, prefix) {
  123. prefix = prefix || '';
  124. return prefix + namespace.split('.').map(function (name) {
  125. return name.substring(0, 1).toUpperCase() + name.substring(1);
  126. }).join('');
  127. };
  128. return {
  129. eq: eq,
  130. eq2: eq2,
  131. peq2: peq2,
  132. ok: ok,
  133. fail: fail,
  134. self: self,
  135. not: not,
  136. and: and,
  137. uniqueId: uniqueId,
  138. rect2bnd: rect2bnd,
  139. invertObject: invertObject,
  140. namespaceToCamel: namespaceToCamel
  141. };
  142. })();
  143. /**
  144. * @class core.list
  145. *
  146. * list utils
  147. *
  148. * @singleton
  149. * @alternateClassName list
  150. */
  151. var list = (function () {
  152. /**
  153. * returns the first item of an array.
  154. *
  155. * @param {Array} array
  156. */
  157. var head = function (array) {
  158. return array[0];
  159. };
  160. /**
  161. * returns the last item of an array.
  162. *
  163. * @param {Array} array
  164. */
  165. var last = function (array) {
  166. return array[array.length - 1];
  167. };
  168. /**
  169. * returns everything but the last entry of the array.
  170. *
  171. * @param {Array} array
  172. */
  173. var initial = function (array) {
  174. return array.slice(0, array.length - 1);
  175. };
  176. /**
  177. * returns the rest of the items in an array.
  178. *
  179. * @param {Array} array
  180. */
  181. var tail = function (array) {
  182. return array.slice(1);
  183. };
  184. /**
  185. * returns item of array
  186. */
  187. var find = function (array, pred) {
  188. for (var idx = 0, len = array.length; idx < len; idx ++) {
  189. var item = array[idx];
  190. if (pred(item)) {
  191. return item;
  192. }
  193. }
  194. };
  195. /**
  196. * returns true if all of the values in the array pass the predicate truth test.
  197. */
  198. var all = function (array, pred) {
  199. for (var idx = 0, len = array.length; idx < len; idx ++) {
  200. if (!pred(array[idx])) {
  201. return false;
  202. }
  203. }
  204. return true;
  205. };
  206. /**
  207. * returns index of item
  208. */
  209. var indexOf = function (array, item) {
  210. return $.inArray(item, array);
  211. };
  212. /**
  213. * returns true if the value is present in the list.
  214. */
  215. var contains = function (array, item) {
  216. return indexOf(array, item) !== -1;
  217. };
  218. /**
  219. * get sum from a list
  220. *
  221. * @param {Array} array - array
  222. * @param {Function} fn - iterator
  223. */
  224. var sum = function (array, fn) {
  225. fn = fn || func.self;
  226. return array.reduce(function (memo, v) {
  227. return memo + fn(v);
  228. }, 0);
  229. };
  230. /**
  231. * returns a copy of the collection with array type.
  232. * @param {Collection} collection - collection eg) node.childNodes, ...
  233. */
  234. var from = function (collection) {
  235. var result = [], idx = -1, length = collection.length;
  236. while (++idx < length) {
  237. result[idx] = collection[idx];
  238. }
  239. return result;
  240. };
  241. /**
  242. * returns whether list is empty or not
  243. */
  244. var isEmpty = function (array) {
  245. return !array || !array.length;
  246. };
  247. /**
  248. * cluster elements by predicate function.
  249. *
  250. * @param {Array} array - array
  251. * @param {Function} fn - predicate function for cluster rule
  252. * @param {Array[]}
  253. */
  254. var clusterBy = function (array, fn) {
  255. if (!array.length) { return []; }
  256. var aTail = tail(array);
  257. return aTail.reduce(function (memo, v) {
  258. var aLast = last(memo);
  259. if (fn(last(aLast), v)) {
  260. aLast[aLast.length] = v;
  261. } else {
  262. memo[memo.length] = [v];
  263. }
  264. return memo;
  265. }, [[head(array)]]);
  266. };
  267. /**
  268. * returns a copy of the array with all falsy values removed
  269. *
  270. * @param {Array} array - array
  271. * @param {Function} fn - predicate function for cluster rule
  272. */
  273. var compact = function (array) {
  274. var aResult = [];
  275. for (var idx = 0, len = array.length; idx < len; idx ++) {
  276. if (array[idx]) { aResult.push(array[idx]); }
  277. }
  278. return aResult;
  279. };
  280. /**
  281. * produces a duplicate-free version of the array
  282. *
  283. * @param {Array} array
  284. */
  285. var unique = function (array) {
  286. var results = [];
  287. for (var idx = 0, len = array.length; idx < len; idx ++) {
  288. if (!contains(results, array[idx])) {
  289. results.push(array[idx]);
  290. }
  291. }
  292. return results;
  293. };
  294. /**
  295. * returns next item.
  296. * @param {Array} array
  297. */
  298. var next = function (array, item) {
  299. var idx = indexOf(array, item);
  300. if (idx === -1) { return null; }
  301. return array[idx + 1];
  302. };
  303. /**
  304. * returns prev item.
  305. * @param {Array} array
  306. */
  307. var prev = function (array, item) {
  308. var idx = indexOf(array, item);
  309. if (idx === -1) { return null; }
  310. return array[idx - 1];
  311. };
  312. return { head: head, last: last, initial: initial, tail: tail,
  313. prev: prev, next: next, find: find, contains: contains,
  314. all: all, sum: sum, from: from, isEmpty: isEmpty,
  315. clusterBy: clusterBy, compact: compact, unique: unique };
  316. })();
  317. var isSupportAmd = typeof define === 'function' && define.amd;
  318. /**
  319. * returns whether font is installed or not.
  320. *
  321. * @param {String} fontName
  322. * @return {Boolean}
  323. */
  324. var isFontInstalled = function (fontName) {
  325. var testFontName = fontName === 'Comic Sans MS' ? 'Courier New' : 'Comic Sans MS';
  326. var $tester = $('<div>').css({
  327. position: 'absolute',
  328. left: '-9999px',
  329. top: '-9999px',
  330. fontSize: '200px'
  331. }).text('mmmmmmmmmwwwwwww').appendTo(document.body);
  332. var originalWidth = $tester.css('fontFamily', testFontName).width();
  333. var width = $tester.css('fontFamily', fontName + ',' + testFontName).width();
  334. $tester.remove();
  335. return originalWidth !== width;
  336. };
  337. var userAgent = navigator.userAgent;
  338. var isMSIE = /MSIE|Trident/i.test(userAgent);
  339. var browserVersion;
  340. if (isMSIE) {
  341. var matches = /MSIE (\d+[.]\d+)/.exec(userAgent);
  342. if (matches) {
  343. browserVersion = parseFloat(matches[1]);
  344. }
  345. matches = /Trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/.exec(userAgent);
  346. if (matches) {
  347. browserVersion = parseFloat(matches[1]);
  348. }
  349. }
  350. /**
  351. * @class core.agent
  352. *
  353. * Object which check platform and agent
  354. *
  355. * @singleton
  356. * @alternateClassName agent
  357. */
  358. var agent = {
  359. isMac: navigator.appVersion.indexOf('Mac') > -1,
  360. isMSIE: isMSIE,
  361. isFF: /firefox/i.test(userAgent),
  362. isWebkit: /webkit/i.test(userAgent),
  363. isSafari: /safari/i.test(userAgent),
  364. browserVersion: browserVersion,
  365. jqueryVersion: parseFloat($.fn.jquery),
  366. isSupportAmd: isSupportAmd,
  367. hasCodeMirror: isSupportAmd ? require.specified('codemirror') : !!window.CodeMirror,
  368. isFontInstalled: isFontInstalled,
  369. isW3CRangeSupport: !!document.createRange
  370. };
  371. var NBSP_CHAR = String.fromCharCode(160);
  372. var ZERO_WIDTH_NBSP_CHAR = '\ufeff';
  373. /**
  374. * @class core.dom
  375. *
  376. * Dom functions
  377. *
  378. * @singleton
  379. * @alternateClassName dom
  380. */
  381. var dom = (function () {
  382. /**
  383. * @method isEditable
  384. *
  385. * returns whether node is `note-editable` or not.
  386. *
  387. * @param {Node} node
  388. * @return {Boolean}
  389. */
  390. var isEditable = function (node) {
  391. return node && $(node).hasClass('note-editable');
  392. };
  393. /**
  394. * @method isControlSizing
  395. *
  396. * returns whether node is `note-control-sizing` or not.
  397. *
  398. * @param {Node} node
  399. * @return {Boolean}
  400. */
  401. var isControlSizing = function (node) {
  402. return node && $(node).hasClass('note-control-sizing');
  403. };
  404. /**
  405. * @method makePredByNodeName
  406. *
  407. * returns predicate which judge whether nodeName is same
  408. *
  409. * @param {String} nodeName
  410. * @return {Function}
  411. */
  412. var makePredByNodeName = function (nodeName) {
  413. nodeName = nodeName.toUpperCase();
  414. return function (node) {
  415. return node && node.nodeName.toUpperCase() === nodeName;
  416. };
  417. };
  418. /**
  419. * @method isText
  420. *
  421. *
  422. *
  423. * @param {Node} node
  424. * @return {Boolean} true if node's type is text(3)
  425. */
  426. var isText = function (node) {
  427. return node && node.nodeType === 3;
  428. };
  429. /**
  430. * @method isElement
  431. *
  432. *
  433. *
  434. * @param {Node} node
  435. * @return {Boolean} true if node's type is element(1)
  436. */
  437. var isElement = function (node) {
  438. return node && node.nodeType === 1;
  439. };
  440. /**
  441. * ex) br, col, embed, hr, img, input, ...
  442. * @see http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements
  443. */
  444. var isVoid = function (node) {
  445. return node && /^BR|^IMG|^HR|^IFRAME|^BUTTON/.test(node.nodeName.toUpperCase());
  446. };
  447. var isPara = function (node) {
  448. if (isEditable(node)) {
  449. return false;
  450. }
  451. // Chrome(v31.0), FF(v25.0.1) use DIV for paragraph
  452. return node && /^DIV|^P|^LI|^H[1-7]/.test(node.nodeName.toUpperCase());
  453. };
  454. var isHeading = function (node) {
  455. return node && /^H[1-7]/.test(node.nodeName.toUpperCase());
  456. };
  457. var isPre = makePredByNodeName('PRE');
  458. var isLi = makePredByNodeName('LI');
  459. var isPurePara = function (node) {
  460. return isPara(node) && !isLi(node);
  461. };
  462. var isTable = makePredByNodeName('TABLE');
  463. var isInline = function (node) {
  464. return !isBodyContainer(node) &&
  465. !isList(node) &&
  466. !isHr(node) &&
  467. !isPara(node) &&
  468. !isTable(node) &&
  469. !isBlockquote(node);
  470. };
  471. var isList = function (node) {
  472. return node && /^UL|^OL/.test(node.nodeName.toUpperCase());
  473. };
  474. var isHr = makePredByNodeName('HR');
  475. var isCell = function (node) {
  476. return node && /^TD|^TH/.test(node.nodeName.toUpperCase());
  477. };
  478. var isBlockquote = makePredByNodeName('BLOCKQUOTE');
  479. var isBodyContainer = function (node) {
  480. return isCell(node) || isBlockquote(node) || isEditable(node);
  481. };
  482. var isAnchor = makePredByNodeName('A');
  483. var isParaInline = function (node) {
  484. return isInline(node) && !!ancestor(node, isPara);
  485. };
  486. var isBodyInline = function (node) {
  487. return isInline(node) && !ancestor(node, isPara);
  488. };
  489. var isBody = makePredByNodeName('BODY');
  490. /**
  491. * returns whether nodeB is closest sibling of nodeA
  492. *
  493. * @param {Node} nodeA
  494. * @param {Node} nodeB
  495. * @return {Boolean}
  496. */
  497. var isClosestSibling = function (nodeA, nodeB) {
  498. return nodeA.nextSibling === nodeB ||
  499. nodeA.previousSibling === nodeB;
  500. };
  501. /**
  502. * returns array of closest siblings with node
  503. *
  504. * @param {Node} node
  505. * @param {function} [pred] - predicate function
  506. * @return {Node[]}
  507. */
  508. var withClosestSiblings = function (node, pred) {
  509. pred = pred || func.ok;
  510. var siblings = [];
  511. if (node.previousSibling && pred(node.previousSibling)) {
  512. siblings.push(node.previousSibling);
  513. }
  514. siblings.push(node);
  515. if (node.nextSibling && pred(node.nextSibling)) {
  516. siblings.push(node.nextSibling);
  517. }
  518. return siblings;
  519. };
  520. /**
  521. * blank HTML for cursor position
  522. * - [workaround] old IE only works with &nbsp;
  523. * - [workaround] IE11 and other browser works with bogus br
  524. */
  525. var blankHTML = agent.isMSIE && agent.browserVersion < 11 ? '&nbsp;' : '<br>';
  526. /**
  527. * @method nodeLength
  528. *
  529. * returns #text's text size or element's childNodes size
  530. *
  531. * @param {Node} node
  532. */
  533. var nodeLength = function (node) {
  534. if (isText(node)) {
  535. return node.nodeValue.length;
  536. }
  537. return node.childNodes.length;
  538. };
  539. /**
  540. * returns whether node is empty or not.
  541. *
  542. * @param {Node} node
  543. * @return {Boolean}
  544. */
  545. var isEmpty = function (node) {
  546. var len = nodeLength(node);
  547. if (len === 0) {
  548. return true;
  549. } else if (!isText(node) && len === 1 && node.innerHTML === blankHTML) {
  550. // ex) <p><br></p>, <span><br></span>
  551. return true;
  552. } else if (list.all(node.childNodes, isText) && node.innerHTML === '') {
  553. // ex) <p></p>, <span></span>
  554. return true;
  555. }
  556. return false;
  557. };
  558. /**
  559. * padding blankHTML if node is empty (for cursor position)
  560. */
  561. var paddingBlankHTML = function (node) {
  562. if (!isVoid(node) && !nodeLength(node)) {
  563. node.innerHTML = blankHTML;
  564. }
  565. };
  566. /**
  567. * find nearest ancestor predicate hit
  568. *
  569. * @param {Node} node
  570. * @param {Function} pred - predicate function
  571. */
  572. var ancestor = function (node, pred) {
  573. while (node) {
  574. if (pred(node)) { return node; }
  575. if (isEditable(node)) { break; }
  576. node = node.parentNode;
  577. }
  578. return null;
  579. };
  580. /**
  581. * find nearest ancestor only single child blood line and predicate hit
  582. *
  583. * @param {Node} node
  584. * @param {Function} pred - predicate function
  585. */
  586. var singleChildAncestor = function (node, pred) {
  587. node = node.parentNode;
  588. while (node) {
  589. if (nodeLength(node) !== 1) { break; }
  590. if (pred(node)) { return node; }
  591. if (isEditable(node)) { break; }
  592. node = node.parentNode;
  593. }
  594. return null;
  595. };
  596. /**
  597. * returns new array of ancestor nodes (until predicate hit).
  598. *
  599. * @param {Node} node
  600. * @param {Function} [optional] pred - predicate function
  601. */
  602. var listAncestor = function (node, pred) {
  603. pred = pred || func.fail;
  604. var ancestors = [];
  605. ancestor(node, function (el) {
  606. if (!isEditable(el)) {
  607. ancestors.push(el);
  608. }
  609. return pred(el);
  610. });
  611. return ancestors;
  612. };
  613. /**
  614. * find farthest ancestor predicate hit
  615. */
  616. var lastAncestor = function (node, pred) {
  617. var ancestors = listAncestor(node);
  618. return list.last(ancestors.filter(pred));
  619. };
  620. /**
  621. * returns common ancestor node between two nodes.
  622. *
  623. * @param {Node} nodeA
  624. * @param {Node} nodeB
  625. */
  626. var commonAncestor = function (nodeA, nodeB) {
  627. var ancestors = listAncestor(nodeA);
  628. for (var n = nodeB; n; n = n.parentNode) {
  629. if ($.inArray(n, ancestors) > -1) { return n; }
  630. }
  631. return null; // difference document area
  632. };
  633. /**
  634. * listing all previous siblings (until predicate hit).
  635. *
  636. * @param {Node} node
  637. * @param {Function} [optional] pred - predicate function
  638. */
  639. var listPrev = function (node, pred) {
  640. pred = pred || func.fail;
  641. var nodes = [];
  642. while (node) {
  643. if (pred(node)) { break; }
  644. nodes.push(node);
  645. node = node.previousSibling;
  646. }
  647. return nodes;
  648. };
  649. /**
  650. * listing next siblings (until predicate hit).
  651. *
  652. * @param {Node} node
  653. * @param {Function} [pred] - predicate function
  654. */
  655. var listNext = function (node, pred) {
  656. pred = pred || func.fail;
  657. var nodes = [];
  658. while (node) {
  659. if (pred(node)) { break; }
  660. nodes.push(node);
  661. node = node.nextSibling;
  662. }
  663. return nodes;
  664. };
  665. /**
  666. * listing descendant nodes
  667. *
  668. * @param {Node} node
  669. * @param {Function} [pred] - predicate function
  670. */
  671. var listDescendant = function (node, pred) {
  672. var descendents = [];
  673. pred = pred || func.ok;
  674. // start DFS(depth first search) with node
  675. (function fnWalk(current) {
  676. if (node !== current && pred(current)) {
  677. descendents.push(current);
  678. }
  679. for (var idx = 0, len = current.childNodes.length; idx < len; idx++) {
  680. fnWalk(current.childNodes[idx]);
  681. }
  682. })(node);
  683. return descendents;
  684. };
  685. /**
  686. * wrap node with new tag.
  687. *
  688. * @param {Node} node
  689. * @param {Node} tagName of wrapper
  690. * @return {Node} - wrapper
  691. */
  692. var wrap = function (node, wrapperName) {
  693. var parent = node.parentNode;
  694. var wrapper = $('<' + wrapperName + '>')[0];
  695. parent.insertBefore(wrapper, node);
  696. wrapper.appendChild(node);
  697. return wrapper;
  698. };
  699. /**
  700. * insert node after preceding
  701. *
  702. * @param {Node} node
  703. * @param {Node} preceding - predicate function
  704. */
  705. var insertAfter = function (node, preceding) {
  706. var next = preceding.nextSibling, parent = preceding.parentNode;
  707. if (next) {
  708. parent.insertBefore(node, next);
  709. } else {
  710. parent.appendChild(node);
  711. }
  712. return node;
  713. };
  714. /**
  715. * append elements.
  716. *
  717. * @param {Node} node
  718. * @param {Collection} aChild
  719. */
  720. var appendChildNodes = function (node, aChild) {
  721. $.each(aChild, function (idx, child) {
  722. node.appendChild(child);
  723. });
  724. return node;
  725. };
  726. /**
  727. * returns whether boundaryPoint is left edge or not.
  728. *
  729. * @param {BoundaryPoint} point
  730. * @return {Boolean}
  731. */
  732. var isLeftEdgePoint = function (point) {
  733. return point.offset === 0;
  734. };
  735. /**
  736. * returns whether boundaryPoint is right edge or not.
  737. *
  738. * @param {BoundaryPoint} point
  739. * @return {Boolean}
  740. */
  741. var isRightEdgePoint = function (point) {
  742. return point.offset === nodeLength(point.node);
  743. };
  744. /**
  745. * returns whether boundaryPoint is edge or not.
  746. *
  747. * @param {BoundaryPoint} point
  748. * @return {Boolean}
  749. */
  750. var isEdgePoint = function (point) {
  751. return isLeftEdgePoint(point) || isRightEdgePoint(point);
  752. };
  753. /**
  754. * returns wheter node is left edge of ancestor or not.
  755. *
  756. * @param {Node} node
  757. * @param {Node} ancestor
  758. * @return {Boolean}
  759. */
  760. var isLeftEdgeOf = function (node, ancestor) {
  761. while (node && node !== ancestor) {
  762. if (position(node) !== 0) {
  763. return false;
  764. }
  765. node = node.parentNode;
  766. }
  767. return true;
  768. };
  769. /**
  770. * returns whether node is right edge of ancestor or not.
  771. *
  772. * @param {Node} node
  773. * @param {Node} ancestor
  774. * @return {Boolean}
  775. */
  776. var isRightEdgeOf = function (node, ancestor) {
  777. while (node && node !== ancestor) {
  778. if (position(node) !== nodeLength(node.parentNode) - 1) {
  779. return false;
  780. }
  781. node = node.parentNode;
  782. }
  783. return true;
  784. };
  785. /**
  786. * returns whether point is left edge of ancestor or not.
  787. * @param {BoundaryPoint} point
  788. * @param {Node} ancestor
  789. * @return {Boolean}
  790. */
  791. var isLeftEdgePointOf = function (point, ancestor) {
  792. return isLeftEdgePoint(point) && isLeftEdgeOf(point.node, ancestor);
  793. };
  794. /**
  795. * returns whether point is right edge of ancestor or not.
  796. * @param {BoundaryPoint} point
  797. * @param {Node} ancestor
  798. * @return {Boolean}
  799. */
  800. var isRightEdgePointOf = function (point, ancestor) {
  801. return isRightEdgePoint(point) && isRightEdgeOf(point.node, ancestor);
  802. };
  803. /**
  804. * returns offset from parent.
  805. *
  806. * @param {Node} node
  807. */
  808. var position = function (node) {
  809. var offset = 0;
  810. while ((node = node.previousSibling)) {
  811. offset += 1;
  812. }
  813. return offset;
  814. };
  815. var hasChildren = function (node) {
  816. return !!(node && node.childNodes && node.childNodes.length);
  817. };
  818. /**
  819. * returns previous boundaryPoint
  820. *
  821. * @param {BoundaryPoint} point
  822. * @param {Boolean} isSkipInnerOffset
  823. * @return {BoundaryPoint}
  824. */
  825. var prevPoint = function (point, isSkipInnerOffset) {
  826. var node, offset;
  827. if (point.offset === 0) {
  828. if (isEditable(point.node)) {
  829. return null;
  830. }
  831. node = point.node.parentNode;
  832. offset = position(point.node);
  833. } else if (hasChildren(point.node)) {
  834. node = point.node.childNodes[point.offset - 1];
  835. offset = nodeLength(node);
  836. } else {
  837. node = point.node;
  838. offset = isSkipInnerOffset ? 0 : point.offset - 1;
  839. }
  840. return {
  841. node: node,
  842. offset: offset
  843. };
  844. };
  845. /**
  846. * returns next boundaryPoint
  847. *
  848. * @param {BoundaryPoint} point
  849. * @param {Boolean} isSkipInnerOffset
  850. * @return {BoundaryPoint}
  851. */
  852. var nextPoint = function (point, isSkipInnerOffset) {
  853. var node, offset;
  854. if (nodeLength(point.node) === point.offset) {
  855. if (isEditable(point.node)) {
  856. return null;
  857. }
  858. node = point.node.parentNode;
  859. offset = position(point.node) + 1;
  860. } else if (hasChildren(point.node)) {
  861. node = point.node.childNodes[point.offset];
  862. offset = 0;
  863. } else {
  864. node = point.node;
  865. offset = isSkipInnerOffset ? nodeLength(point.node) : point.offset + 1;
  866. }
  867. return {
  868. node: node,
  869. offset: offset
  870. };
  871. };
  872. /**
  873. * returns whether pointA and pointB is same or not.
  874. *
  875. * @param {BoundaryPoint} pointA
  876. * @param {BoundaryPoint} pointB
  877. * @return {Boolean}
  878. */
  879. var isSamePoint = function (pointA, pointB) {
  880. return pointA.node === pointB.node && pointA.offset === pointB.offset;
  881. };
  882. /**
  883. * returns whether point is visible (can set cursor) or not.
  884. *
  885. * @param {BoundaryPoint} point
  886. * @return {Boolean}
  887. */
  888. var isVisiblePoint = function (point) {
  889. if (isText(point.node) || !hasChildren(point.node) || isEmpty(point.node)) {
  890. return true;
  891. }
  892. var leftNode = point.node.childNodes[point.offset - 1];
  893. var rightNode = point.node.childNodes[point.offset];
  894. if ((!leftNode || isVoid(leftNode)) && (!rightNode || isVoid(rightNode))) {
  895. return true;
  896. }
  897. return false;
  898. };
  899. /**
  900. * @method prevPointUtil
  901. *
  902. * @param {BoundaryPoint} point
  903. * @param {Function} pred
  904. * @return {BoundaryPoint}
  905. */
  906. var prevPointUntil = function (point, pred) {
  907. while (point) {
  908. if (pred(point)) {
  909. return point;
  910. }
  911. point = prevPoint(point);
  912. }
  913. return null;
  914. };
  915. /**
  916. * @method nextPointUntil
  917. *
  918. * @param {BoundaryPoint} point
  919. * @param {Function} pred
  920. * @return {BoundaryPoint}
  921. */
  922. var nextPointUntil = function (point, pred) {
  923. while (point) {
  924. if (pred(point)) {
  925. return point;
  926. }
  927. point = nextPoint(point);
  928. }
  929. return null;
  930. };
  931. /**
  932. * returns whether point has character or not.
  933. *
  934. * @param {Point} point
  935. * @return {Boolean}
  936. */
  937. var isCharPoint = function (point) {
  938. if (!isText(point.node)) {
  939. return false;
  940. }
  941. var ch = point.node.nodeValue.charAt(point.offset - 1);
  942. return ch && (ch !== ' ' && ch !== NBSP_CHAR);
  943. };
  944. /**
  945. * @method walkPoint
  946. *
  947. * @param {BoundaryPoint} startPoint
  948. * @param {BoundaryPoint} endPoint
  949. * @param {Function} handler
  950. * @param {Boolean} isSkipInnerOffset
  951. */
  952. var walkPoint = function (startPoint, endPoint, handler, isSkipInnerOffset) {
  953. var point = startPoint;
  954. while (point) {
  955. handler(point);
  956. if (isSamePoint(point, endPoint)) {
  957. break;
  958. }
  959. var isSkipOffset = isSkipInnerOffset &&
  960. startPoint.node !== point.node &&
  961. endPoint.node !== point.node;
  962. point = nextPoint(point, isSkipOffset);
  963. }
  964. };
  965. /**
  966. * @method makeOffsetPath
  967. *
  968. * return offsetPath(array of offset) from ancestor
  969. *
  970. * @param {Node} ancestor - ancestor node
  971. * @param {Node} node
  972. */
  973. var makeOffsetPath = function (ancestor, node) {
  974. var ancestors = listAncestor(node, func.eq(ancestor));
  975. return ancestors.map(position).reverse();
  976. };
  977. /**
  978. * @method fromOffsetPath
  979. *
  980. * return element from offsetPath(array of offset)
  981. *
  982. * @param {Node} ancestor - ancestor node
  983. * @param {array} offsets - offsetPath
  984. */
  985. var fromOffsetPath = function (ancestor, offsets) {
  986. var current = ancestor;
  987. for (var i = 0, len = offsets.length; i < len; i++) {
  988. if (current.childNodes.length <= offsets[i]) {
  989. current = current.childNodes[current.childNodes.length - 1];
  990. } else {
  991. current = current.childNodes[offsets[i]];
  992. }
  993. }
  994. return current;
  995. };
  996. /**
  997. * @method splitNode
  998. *
  999. * split element or #text
  1000. *
  1001. * @param {BoundaryPoint} point
  1002. * @param {Object} [options]
  1003. * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false
  1004. * @param {Boolean} [options.isNotSplitEdgePoint] - default: false
  1005. * @return {Node} right node of boundaryPoint
  1006. */
  1007. var splitNode = function (point, options) {
  1008. var isSkipPaddingBlankHTML = options && options.isSkipPaddingBlankHTML;
  1009. var isNotSplitEdgePoint = options && options.isNotSplitEdgePoint;
  1010. // edge case
  1011. if (isEdgePoint(point) && (isText(point.node) || isNotSplitEdgePoint)) {
  1012. if (isLeftEdgePoint(point)) {
  1013. return point.node;
  1014. } else if (isRightEdgePoint(point)) {
  1015. return point.node.nextSibling;
  1016. }
  1017. }
  1018. // split #text
  1019. if (isText(point.node)) {
  1020. return point.node.splitText(point.offset);
  1021. } else {
  1022. var childNode = point.node.childNodes[point.offset];
  1023. var clone = insertAfter(point.node.cloneNode(false), point.node);
  1024. appendChildNodes(clone, listNext(childNode));
  1025. if (!isSkipPaddingBlankHTML) {
  1026. paddingBlankHTML(point.node);
  1027. paddingBlankHTML(clone);
  1028. }
  1029. return clone;
  1030. }
  1031. };
  1032. /**
  1033. * @method splitTree
  1034. *
  1035. * split tree by point
  1036. *
  1037. * @param {Node} root - split root
  1038. * @param {BoundaryPoint} point
  1039. * @param {Object} [options]
  1040. * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false
  1041. * @param {Boolean} [options.isNotSplitEdgePoint] - default: false
  1042. * @return {Node} right node of boundaryPoint
  1043. */
  1044. var splitTree = function (root, point, options) {
  1045. // ex) [#text, <span>, <p>]
  1046. var ancestors = listAncestor(point.node, func.eq(root));
  1047. if (!ancestors.length) {
  1048. return null;
  1049. } else if (ancestors.length === 1) {
  1050. return splitNode(point, options);
  1051. }
  1052. return ancestors.reduce(function (node, parent) {
  1053. if (node === point.node) {
  1054. node = splitNode(point, options);
  1055. }
  1056. return splitNode({
  1057. node: parent,
  1058. offset: node ? dom.position(node) : nodeLength(parent)
  1059. }, options);
  1060. });
  1061. };
  1062. /**
  1063. * split point
  1064. *
  1065. * @param {Point} point
  1066. * @param {Boolean} isInline
  1067. * @return {Object}
  1068. */
  1069. var splitPoint = function (point, isInline) {
  1070. // find splitRoot, container
  1071. // - inline: splitRoot is a child of paragraph
  1072. // - block: splitRoot is a child of bodyContainer
  1073. var pred = isInline ? isPara : isBodyContainer;
  1074. var ancestors = listAncestor(point.node, pred);
  1075. var topAncestor = list.last(ancestors) || point.node;
  1076. var splitRoot, container;
  1077. if (pred(topAncestor)) {
  1078. splitRoot = ancestors[ancestors.length - 2];
  1079. container = topAncestor;
  1080. } else {
  1081. splitRoot = topAncestor;
  1082. container = splitRoot.parentNode;
  1083. }
  1084. // if splitRoot is exists, split with splitTree
  1085. var pivot = splitRoot && splitTree(splitRoot, point, {
  1086. isSkipPaddingBlankHTML: isInline,
  1087. isNotSplitEdgePoint: isInline
  1088. });
  1089. // if container is point.node, find pivot with point.offset
  1090. if (!pivot && container === point.node) {
  1091. pivot = point.node.childNodes[point.offset];
  1092. }
  1093. return {
  1094. rightNode: pivot,
  1095. container: container
  1096. };
  1097. };
  1098. var create = function (nodeName) {
  1099. return document.createElement(nodeName);
  1100. };
  1101. var createText = function (text) {
  1102. return document.createTextNode(text);
  1103. };
  1104. /**
  1105. * @method remove
  1106. *
  1107. * remove node, (isRemoveChild: remove child or not)
  1108. *
  1109. * @param {Node} node
  1110. * @param {Boolean} isRemoveChild
  1111. */
  1112. var remove = function (node, isRemoveChild) {
  1113. if (!node || !node.parentNode) { return; }
  1114. if (node.removeNode) { return node.removeNode(isRemoveChild); }
  1115. var parent = node.parentNode;
  1116. if (!isRemoveChild) {
  1117. var nodes = [];
  1118. var i, len;
  1119. for (i = 0, len = node.childNodes.length; i < len; i++) {
  1120. nodes.push(node.childNodes[i]);
  1121. }
  1122. for (i = 0, len = nodes.length; i < len; i++) {
  1123. parent.insertBefore(nodes[i], node);
  1124. }
  1125. }
  1126. parent.removeChild(node);
  1127. };
  1128. /**
  1129. * @method removeWhile
  1130. *
  1131. * @param {Node} node
  1132. * @param {Function} pred
  1133. */
  1134. var removeWhile = function (node, pred) {
  1135. while (node) {
  1136. if (isEditable(node) || !pred(node)) {
  1137. break;
  1138. }
  1139. var parent = node.parentNode;
  1140. remove(node);
  1141. node = parent;
  1142. }
  1143. };
  1144. /**
  1145. * @method replace
  1146. *
  1147. * replace node with provided nodeName
  1148. *
  1149. * @param {Node} node
  1150. * @param {String} nodeName
  1151. * @return {Node} - new node
  1152. */
  1153. var replace = function (node, nodeName) {
  1154. if (node.nodeName.toUpperCase() === nodeName.toUpperCase()) {
  1155. return node;
  1156. }
  1157. var newNode = create(nodeName);
  1158. if (node.style.cssText) {
  1159. newNode.style.cssText = node.style.cssText;
  1160. }
  1161. appendChildNodes(newNode, list.from(node.childNodes));
  1162. insertAfter(newNode, node);
  1163. remove(node);
  1164. return newNode;
  1165. };
  1166. var isTextarea = makePredByNodeName('TEXTAREA');
  1167. /**
  1168. * @param {jQuery} $node
  1169. * @param {Boolean} [stripLinebreaks] - default: false
  1170. */
  1171. var value = function ($node, stripLinebreaks) {
  1172. var val = isTextarea($node[0]) ? $node.val() : $node.html();
  1173. if (stripLinebreaks) {
  1174. return val.replace(/[\n\r]/g, '');
  1175. }
  1176. return val;
  1177. };
  1178. /**
  1179. * @method html
  1180. *
  1181. * get the HTML contents of node
  1182. *
  1183. * @param {jQuery} $node
  1184. * @param {Boolean} [isNewlineOnBlock]
  1185. */
  1186. var html = function ($node, isNewlineOnBlock) {
  1187. var markup = value($node);
  1188. if (isNewlineOnBlock) {
  1189. var regexTag = /<(\/?)(\b(?!!)[^>\s]*)(.*?)(\s*\/?>)/g;
  1190. markup = markup.replace(regexTag, function (match, endSlash, name) {
  1191. name = name.toUpperCase();
  1192. var isEndOfInlineContainer = /^DIV|^TD|^TH|^P|^LI|^H[1-7]/.test(name) &&
  1193. !!endSlash;
  1194. var isBlockNode = /^BLOCKQUOTE|^TABLE|^TBODY|^TR|^HR|^UL|^OL/.test(name);
  1195. return match + ((isEndOfInlineContainer || isBlockNode) ? '\n' : '');
  1196. });
  1197. markup = $.trim(markup);
  1198. }
  1199. return markup;
  1200. };
  1201. var posFromPlaceholder = function (placeholder) {
  1202. var $placeholder = $(placeholder);
  1203. var pos = $placeholder.offset();
  1204. var height = $placeholder.outerHeight(true); // include margin
  1205. return {
  1206. left: pos.left,
  1207. top: pos.top + height
  1208. };
  1209. };
  1210. var attachEvents = function ($node, events) {
  1211. Object.keys(events).forEach(function (key) {
  1212. $node.on(key, events[key]);
  1213. });
  1214. };
  1215. var detachEvents = function ($node, events) {
  1216. Object.keys(events).forEach(function (key) {
  1217. $node.off(key, events[key]);
  1218. });
  1219. };
  1220. return {
  1221. /** @property {String} NBSP_CHAR */
  1222. NBSP_CHAR: NBSP_CHAR,
  1223. /** @property {String} ZERO_WIDTH_NBSP_CHAR */
  1224. ZERO_WIDTH_NBSP_CHAR: ZERO_WIDTH_NBSP_CHAR,
  1225. /** @property {String} blank */
  1226. blank: blankHTML,
  1227. /** @property {String} emptyPara */
  1228. emptyPara: '<p>' + blankHTML + '</p>',
  1229. makePredByNodeName: makePredByNodeName,
  1230. isEditable: isEditable,
  1231. isControlSizing: isControlSizing,
  1232. isText: isText,
  1233. isElement: isElement,
  1234. isVoid: isVoid,
  1235. isPara: isPara,
  1236. isPurePara: isPurePara,
  1237. isHeading: isHeading,
  1238. isInline: isInline,
  1239. isBlock: func.not(isInline),
  1240. isBodyInline: isBodyInline,
  1241. isBody: isBody,
  1242. isParaInline: isParaInline,
  1243. isPre: isPre,
  1244. isList: isList,
  1245. isTable: isTable,
  1246. isCell: isCell,
  1247. isBlockquote: isBlockquote,
  1248. isBodyContainer: isBodyContainer,
  1249. isAnchor: isAnchor,
  1250. isDiv: makePredByNodeName('DIV'),
  1251. isLi: isLi,
  1252. isBR: makePredByNodeName('BR'),
  1253. isSpan: makePredByNodeName('SPAN'),
  1254. isB: makePredByNodeName('B'),
  1255. isU: makePredByNodeName('U'),
  1256. isS: makePredByNodeName('S'),
  1257. isI: makePredByNodeName('I'),
  1258. isImg: makePredByNodeName('IMG'),
  1259. isTextarea: isTextarea,
  1260. isEmpty: isEmpty,
  1261. isEmptyAnchor: func.and(isAnchor, isEmpty),
  1262. isClosestSibling: isClosestSibling,
  1263. withClosestSiblings: withClosestSiblings,
  1264. nodeLength: nodeLength,
  1265. isLeftEdgePoint: isLeftEdgePoint,
  1266. isRightEdgePoint: isRightEdgePoint,
  1267. isEdgePoint: isEdgePoint,
  1268. isLeftEdgeOf: isLeftEdgeOf,
  1269. isRightEdgeOf: isRightEdgeOf,
  1270. isLeftEdgePointOf: isLeftEdgePointOf,
  1271. isRightEdgePointOf: isRightEdgePointOf,
  1272. prevPoint: prevPoint,
  1273. nextPoint: nextPoint,
  1274. isSamePoint: isSamePoint,
  1275. isVisiblePoint: isVisiblePoint,
  1276. prevPointUntil: prevPointUntil,
  1277. nextPointUntil: nextPointUntil,
  1278. isCharPoint: isCharPoint,
  1279. walkPoint: walkPoint,
  1280. ancestor: ancestor,
  1281. singleChildAncestor: singleChildAncestor,
  1282. listAncestor: listAncestor,
  1283. lastAncestor: lastAncestor,
  1284. listNext: listNext,
  1285. listPrev: listPrev,
  1286. listDescendant: listDescendant,
  1287. commonAncestor: commonAncestor,
  1288. wrap: wrap,
  1289. insertAfter: insertAfter,
  1290. appendChildNodes: appendChildNodes,
  1291. position: position,
  1292. hasChildren: hasChildren,
  1293. makeOffsetPath: makeOffsetPath,
  1294. fromOffsetPath: fromOffsetPath,
  1295. splitTree: splitTree,
  1296. splitPoint: splitPoint,
  1297. create: create,
  1298. createText: createText,
  1299. remove: remove,
  1300. removeWhile: removeWhile,
  1301. replace: replace,
  1302. html: html,
  1303. value: value,
  1304. posFromPlaceholder: posFromPlaceholder,
  1305. attachEvents: attachEvents,
  1306. detachEvents: detachEvents
  1307. };
  1308. })();
  1309. /**
  1310. * @param {jQuery} $note
  1311. * @param {Object} options
  1312. * @return {Context}
  1313. */
  1314. var Context = function ($note, options) {
  1315. var self = this;
  1316. var ui = $.summernote.ui;
  1317. this.memos = {};
  1318. this.modules = {};
  1319. this.layoutInfo = {};
  1320. this.options = options;
  1321. /**
  1322. * create layout and initialize modules and other resources
  1323. */
  1324. this.initialize = function () {
  1325. this.layoutInfo = ui.createLayout($note, options);
  1326. this._initialize();
  1327. $note.hide();
  1328. return this;
  1329. };
  1330. /**
  1331. * destroy modules and other resources and remove layout
  1332. */
  1333. this.destroy = function () {
  1334. this._destroy();
  1335. $note.removeData('summernote');
  1336. ui.removeLayout($note, this.layoutInfo);
  1337. };
  1338. /**
  1339. * destory modules and other resources and initialize it again
  1340. */
  1341. this.reset = function () {
  1342. this.code(dom.emptyPara);
  1343. this._destroy();
  1344. this._initialize();
  1345. };
  1346. this._initialize = function () {
  1347. // add optional buttons
  1348. var buttons = $.extend({}, this.options.buttons);
  1349. Object.keys(buttons).forEach(function (key) {
  1350. self.memo('button.' + key, buttons[key]);
  1351. });
  1352. var modules = $.extend({}, this.options.modules, $.summernote.plugins || {});
  1353. // add and initialize modules
  1354. Object.keys(modules).forEach(function (key) {
  1355. self.module(key, modules[key], true);
  1356. });
  1357. Object.keys(this.modules).forEach(function (key) {
  1358. self.initializeModule(key);
  1359. });
  1360. };
  1361. this._destroy = function () {
  1362. // destroy modules with reversed order
  1363. Object.keys(this.modules).reverse().forEach(function (key) {
  1364. self.removeModule(key);
  1365. });
  1366. Object.keys(this.memos).forEach(function (key) {
  1367. self.removeMemo(key);
  1368. });
  1369. };
  1370. this.code = function (html) {
  1371. var isActivated = this.invoke('codeview.isActivated');
  1372. if (html === undefined) {
  1373. this.invoke('codeview.sync');
  1374. return isActivated ? this.layoutInfo.codable.val() : this.layoutInfo.editable.html();
  1375. } else {
  1376. if (isActivated) {
  1377. this.layoutInfo.codable.val(html);
  1378. } else {
  1379. this.layoutInfo.editable.html(html);
  1380. }
  1381. $note.val(html);
  1382. this.triggerEvent('change', html);
  1383. }
  1384. };
  1385. this.isDisabled = function () {
  1386. return this.layoutInfo.editable.attr('contenteditable') === 'false';
  1387. };
  1388. this.enable = function () {
  1389. this.layoutInfo.editable.attr('contenteditable', true);
  1390. this.invoke('toolbar.activate', true);
  1391. };
  1392. this.disable = function () {
  1393. // close codeview if codeview is opend
  1394. if (this.invoke('codeview.isActivated')) {
  1395. this.invoke('codeview.deactivate');
  1396. }
  1397. this.layoutInfo.editable.attr('contenteditable', false);
  1398. this.invoke('toolbar.deactivate', true);
  1399. };
  1400. this.triggerEvent = function () {
  1401. var namespace = list.head(arguments);
  1402. var args = list.tail(list.from(arguments));
  1403. var callback = this.options.callbacks[func.namespaceToCamel(namespace, 'on')];
  1404. if (callback) {
  1405. callback.apply($note[0], args);
  1406. }
  1407. $note.trigger('summernote.' + namespace, args);
  1408. };
  1409. this.initializeModule = function (key) {
  1410. var module = this.modules[key];
  1411. module.shouldInitialize = module.shouldInitialize || func.ok;
  1412. if (!module.shouldInitialize()) {
  1413. return;
  1414. }
  1415. // initialize module
  1416. if (module.initialize) {
  1417. module.initialize();
  1418. }
  1419. // attach events
  1420. if (module.events) {
  1421. dom.attachEvents($note, module.events);
  1422. }
  1423. };
  1424. this.module = function (key, ModuleClass, withoutIntialize) {
  1425. if (arguments.length === 1) {
  1426. return this.modules[key];
  1427. }
  1428. this.modules[key] = new ModuleClass(this);
  1429. if (!withoutIntialize) {
  1430. this.initializeModule(key);
  1431. }
  1432. };
  1433. this.removeModule = function (key) {
  1434. var module = this.modules[key];
  1435. if (module.shouldInitialize()) {
  1436. if (module.events) {
  1437. dom.detachEvents($note, module.events);
  1438. }
  1439. if (module.destroy) {
  1440. module.destroy();
  1441. }
  1442. }
  1443. delete this.modules[key];
  1444. };
  1445. this.memo = function (key, obj) {
  1446. if (arguments.length === 1) {
  1447. return this.memos[key];
  1448. }
  1449. this.memos[key] = obj;
  1450. };
  1451. this.removeMemo = function (key) {
  1452. if (this.memos[key] && this.memos[key].destroy) {
  1453. this.memos[key].destroy();
  1454. }
  1455. delete this.memos[key];
  1456. };
  1457. this.createInvokeHandler = function (namespace, value) {
  1458. return function (event) {
  1459. event.preventDefault();
  1460. self.invoke(namespace, value || $(event.target).data('value') || $(event.currentTarget).data('value'));
  1461. };
  1462. };
  1463. this.invoke = function () {
  1464. var namespace = list.head(arguments);
  1465. var args = list.tail(list.from(arguments));
  1466. var splits = namespace.split('.');
  1467. var hasSeparator = splits.length > 1;
  1468. var moduleName = hasSeparator && list.head(splits);
  1469. var methodName = hasSeparator ? list.last(splits) : list.head(splits);
  1470. var module = this.modules[moduleName || 'editor'];
  1471. if (!moduleName && this[methodName]) {
  1472. return this[methodName].apply(this, args);
  1473. } else if (module && module[methodName] && module.shouldInitialize()) {
  1474. return module[methodName].apply(module, args);
  1475. }
  1476. };
  1477. return this.initialize();
  1478. };
  1479. $.summernote = $.summernote || {
  1480. lang: {}
  1481. };
  1482. $.fn.extend({
  1483. /**
  1484. * Summernote API
  1485. *
  1486. * @param {Object|String}
  1487. * @return {this}
  1488. */
  1489. summernote: function () {
  1490. var type = $.type(list.head(arguments));
  1491. var isExternalAPICalled = type === 'string';
  1492. var hasInitOptions = type === 'object';
  1493. var options = hasInitOptions ? list.head(arguments) : {};
  1494. options = $.extend({}, $.summernote.options, options);
  1495. options.langInfo = $.extend(true, {}, $.summernote.lang['en-US'], $.summernote.lang[options.lang]);
  1496. this.each(function (idx, note) {
  1497. var $note = $(note);
  1498. if (!$note.data('summernote')) {
  1499. var context = new Context($note, options);
  1500. $note.data('summernote', context);
  1501. $note.data('summernote').triggerEvent('init', context.layoutInfo);
  1502. }
  1503. });
  1504. var $note = this.first();
  1505. if ($note.length) {
  1506. var context = $note.data('summernote');
  1507. if (isExternalAPICalled) {
  1508. return context.invoke.apply(context, list.from(arguments));
  1509. } else if (options.focus) {
  1510. context.invoke('editor.focus');
  1511. }
  1512. }
  1513. return this;
  1514. }
  1515. });
  1516. var Renderer = function (markup, children, options, callback) {
  1517. this.render = function ($parent) {
  1518. var $node = $(markup);
  1519. if (options && options.contents) {
  1520. $node.html(options.contents);
  1521. }
  1522. if (options && options.className) {
  1523. $node.addClass(options.className);
  1524. }
  1525. if (options && options.data) {
  1526. $.each(options.data, function (k, v) {
  1527. $node.attr('data-' + k, v);
  1528. });
  1529. }
  1530. if (options && options.click) {
  1531. $node.on('click', options.click);
  1532. }
  1533. if (children) {
  1534. var $container = $node.find('.note-children-container');
  1535. children.forEach(function (child) {
  1536. child.render($container.length ? $container : $node);
  1537. });
  1538. }
  1539. if (callback) {
  1540. callback($node, options);
  1541. }
  1542. if (options && options.callback) {
  1543. options.callback($node);
  1544. }
  1545. if ($parent) {
  1546. $parent.append($node);
  1547. }
  1548. return $node;
  1549. };
  1550. };
  1551. var renderer = {
  1552. create: function (markup, callback) {
  1553. return function () {
  1554. var children = $.isArray(arguments[0]) ? arguments[0] : [];
  1555. var options = typeof arguments[1] === 'object' ? arguments[1] : arguments[0];
  1556. if (options && options.children) {
  1557. children = options.children;
  1558. }
  1559. return new Renderer(markup, children, options, callback);
  1560. };
  1561. }
  1562. };
  1563. var editor = renderer.create('<div class="note-editor note-frame panel panel-default"/>');
  1564. var toolbar = renderer.create('<div class="note-toolbar panel-heading"/>');
  1565. var editingArea = renderer.create('<div class="note-editing-area"/>');
  1566. var codable = renderer.create('<textarea class="note-codable"/>');
  1567. var editable = renderer.create('<div class="note-editable panel-body" contentEditable="true"/>');
  1568. var statusbar = renderer.create([
  1569. '<div class="note-statusbar">',
  1570. ' <div class="note-resizebar">',
  1571. ' <div class="note-icon-bar"/>',
  1572. ' <div class="note-icon-bar"/>',
  1573. ' <div class="note-icon-bar"/>',
  1574. ' </div>',
  1575. '</div>'
  1576. ].join(''));
  1577. var airEditor = renderer.create('<div class="note-editor"/>');
  1578. var airEditable = renderer.create('<div class="note-editable" contentEditable="true"/>');
  1579. var buttonGroup = renderer.create('<div class="note-btn-group btn-group">');
  1580. var button = renderer.create('<button type="button" class="note-btn btn btn-default btn-sm">', function ($node, options) {
  1581. /*
  1582. if (options && options.tooltip) {
  1583. $node.attr({
  1584. title: options.tooltip
  1585. }).tooltip({
  1586. container: 'body',
  1587. trigger: 'hover',
  1588. placement: 'bottom'
  1589. });
  1590. }
  1591. */
  1592. });
  1593. var dropdown = renderer.create('<div class="dropdown-menu">', function ($node, options) {
  1594. var markup = $.isArray(options.items) ? options.items.map(function (item) {
  1595. return '<li><a href="#" data-value="' + item + '">' + item + '</a></li>';
  1596. }).join('') : options.items;
  1597. $node.html(markup);
  1598. });
  1599. var dropdownCheck = renderer.create('<div class="dropdown-menu note-check">', function ($node, options) {
  1600. var markup = $.isArray(options.items) ? options.items.map(function (item) {
  1601. return '<li><a href="#" data-value="' + item + '">' + icon(options.checkClassName) + ' ' + item + '</a></li>';
  1602. }).join('') : options.items;
  1603. $node.html(markup);
  1604. });
  1605. var palette = renderer.create('<div class="note-color-palette"/>', function ($node, options) {
  1606. var contents = [];
  1607. for (var row = 0, rowSize = options.colors.length; row < rowSize; row++) {
  1608. var eventName = options.eventName;
  1609. var colors = options.colors[row];
  1610. var buttons = [];
  1611. for (var col = 0, colSize = colors.length; col < colSize; col++) {
  1612. var color = colors[col];
  1613. buttons.push([
  1614. '<button type="button" class="note-color-btn"',
  1615. 'style="background-color:', color, '" ',
  1616. 'data-event="', eventName, '" ',
  1617. 'data-value="', color, '" ',
  1618. 'title="', color, '" ',
  1619. 'data-toggle="button" tabindex="-1"></button>'
  1620. ].join(''));
  1621. }
  1622. contents.push('<div class="note-color-row">' + buttons.join('') + '</div>');
  1623. }
  1624. $node.html(contents.join(''));
  1625. $node.find('.note-color-btn').tooltip({
  1626. container: 'body',
  1627. trigger: 'hover',
  1628. placement: 'bottom'
  1629. });
  1630. });
  1631. var dialog = renderer.create('<div class="modal" aria-hidden="false"/>', function ($node, options) {
  1632. $node.html([
  1633. '<div class="modal-dialog">',
  1634. ' <div class="modal-content">',
  1635. (options.title ?
  1636. ' <div class="modal-header">' +
  1637. ' <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>' +
  1638. ' <h4 class="modal-title">' + options.title + '</h4>' +
  1639. ' </div>' : ''
  1640. ),
  1641. ' <div class="modal-body">' + options.body + '</div>',
  1642. (options.footer ?
  1643. ' <div class="modal-footer">' + options.footer + '</div>' : ''
  1644. ),
  1645. ' </div>',
  1646. '</div>'
  1647. ].join(''));
  1648. });
  1649. var popover = renderer.create([
  1650. '<div class="note-popover popover bottom in">',
  1651. ' <div class="arrow"/>',
  1652. ' <div class="popover-content note-children-container"/>',
  1653. '</div>'
  1654. ].join(''));
  1655. var icon = function (iconClassName, tagName) {
  1656. tagName = tagName || 'i';
  1657. return '<' + tagName + ' class="' + iconClassName + '"/>';
  1658. };
  1659. var ui = {
  1660. editor: editor,
  1661. toolbar: toolbar,
  1662. editingArea: editingArea,
  1663. codable: codable,
  1664. editable: editable,
  1665. statusbar: statusbar,
  1666. airEditor: airEditor,
  1667. airEditable: airEditable,
  1668. buttonGroup: buttonGroup,
  1669. button: button,
  1670. dropdown: dropdown,
  1671. dropdownCheck: dropdownCheck,
  1672. palette: palette,
  1673. dialog: dialog,
  1674. popover: popover,
  1675. icon: icon,
  1676. toggleBtn: function ($btn, isEnable) {
  1677. $btn.toggleClass('disabled', !isEnable);
  1678. $btn.attr('disabled', !isEnable);
  1679. },
  1680. toggleBtnActive: function ($btn, isActive) {
  1681. $btn.toggleClass('active', isActive);
  1682. },
  1683. onDialogShown: function ($dialog, handler) {
  1684. $dialog.one('shown.bs.modal', handler);
  1685. },
  1686. onDialogHidden: function ($dialog, handler) {
  1687. $dialog.one('hidden.bs.modal', handler);
  1688. },
  1689. showDialog: function ($dialog) {
  1690. $dialog.modal('show');
  1691. },
  1692. hideDialog: function ($dialog) {
  1693. $dialog.modal('hide');
  1694. },
  1695. createLayout: function ($note, options) {
  1696. var $editor = (options.airMode ? ui.airEditor([
  1697. ui.editingArea([
  1698. ui.airEditable()
  1699. ])
  1700. ]) : ui.editor([
  1701. ui.toolbar(),
  1702. ui.editingArea([
  1703. ui.codable(),
  1704. ui.editable()
  1705. ]),
  1706. ui.statusbar()
  1707. ])).render();
  1708. $editor.insertAfter($note);
  1709. return {
  1710. note: $note,
  1711. editor: $editor,
  1712. toolbar: $editor.find('.note-toolbar'),
  1713. editingArea: $editor.find('.note-editing-area'),
  1714. editable: $editor.find('.note-editable'),
  1715. codable: $editor.find('.note-codable'),
  1716. statusbar: $editor.find('.note-statusbar')
  1717. };
  1718. },
  1719. removeLayout: function ($note, layoutInfo) {
  1720. $note.html(layoutInfo.editable.html());
  1721. layoutInfo.editor.remove();
  1722. $note.show();
  1723. }
  1724. };
  1725. $.extend($.summernote.lang, {
  1726. 'en-US': {
  1727. font: {
  1728. bold: 'Bold',
  1729. italic: 'Italic',
  1730. underline: 'Underline',
  1731. clear: 'Remove Font Style',
  1732. height: 'Line Height',
  1733. name: 'Font Family',
  1734. strikethrough: 'Strikethrough',
  1735. subscript: 'Subscript',
  1736. superscript: 'Superscript',
  1737. size: 'Font Size'
  1738. },
  1739. image: {
  1740. image: 'Picture',
  1741. insert: 'Insert Image',
  1742. resizeFull: 'Resize Full',
  1743. resizeHalf: 'Resize Half',
  1744. resizeQuarter: 'Resize Quarter',
  1745. floatLeft: 'Float Left',
  1746. floatRight: 'Float Right',
  1747. floatNone: 'Float None',
  1748. shapeRounded: 'Shape: Rounded',
  1749. shapeCircle: 'Shape: Circle',
  1750. shapeThumbnail: 'Shape: Thumbnail',
  1751. shapeNone: 'Shape: None',
  1752. dragImageHere: 'Drag image or text here',
  1753. dropImage: 'Drop image or Text',
  1754. selectFromFiles: 'Select from files',
  1755. maximumFileSize: 'Maximum file size',
  1756. maximumFileSizeError: 'Maximum file size exceeded.',
  1757. url: 'Image URL',
  1758. remove: 'Remove Image'
  1759. },
  1760. video: {
  1761. video: 'Video',
  1762. videoLink: 'Video Link',
  1763. insert: 'Insert Video',
  1764. url: 'Video URL?',
  1765. providers: '(YouTube, Vimeo, Vine, Instagram, DailyMotion or Youku)'
  1766. },
  1767. link: {
  1768. link: 'Link',
  1769. insert: 'Insert Link',
  1770. unlink: 'Unlink',
  1771. edit: 'Edit',
  1772. textToDisplay: 'Text to display',
  1773. url: 'To what URL should this link go?',
  1774. openInNewWindow: 'Open in new window'
  1775. },
  1776. table: {
  1777. table: 'Table'
  1778. },
  1779. hr: {
  1780. insert: 'Insert Horizontal Rule'
  1781. },
  1782. style: {
  1783. style: 'Style',
  1784. normal: 'Normal',
  1785. blockquote: 'Quote',
  1786. pre: 'Code',
  1787. h1: 'Header 1',
  1788. h2: 'Header 2',
  1789. h3: 'Header 3',
  1790. h4: 'Header 4',
  1791. h5: 'Header 5',
  1792. h6: 'Header 6'
  1793. },
  1794. lists: {
  1795. unordered: 'Unordered list',
  1796. ordered: 'Ordered list'
  1797. },
  1798. options: {
  1799. help: 'Help',
  1800. fullscreen: 'Full Screen',
  1801. codeview: 'Code View'
  1802. },
  1803. paragraph: {
  1804. paragraph: 'Paragraph',
  1805. outdent: 'Outdent',
  1806. indent: 'Indent',
  1807. left: 'Align left',
  1808. center: 'Align center',
  1809. right: 'Align right',
  1810. justify: 'Justify full'
  1811. },
  1812. color: {
  1813. recent: 'Recent Color',
  1814. more: 'More Color',
  1815. background: 'Background Color',
  1816. foreground: 'Foreground Color',
  1817. transparent: 'Transparent',
  1818. setTransparent: 'Set transparent',
  1819. reset: 'Reset',
  1820. resetToDefault: 'Reset to default'
  1821. },
  1822. shortcut: {
  1823. shortcuts: 'Keyboard shortcuts',
  1824. close: 'Close',
  1825. textFormatting: 'Text formatting',
  1826. action: 'Action',
  1827. paragraphFormatting: 'Paragraph formatting',
  1828. documentStyle: 'Document Style',
  1829. extraKeys: 'Extra keys'
  1830. },
  1831. help : {
  1832. 'insertParagraph': 'Insert Paragraph',
  1833. 'undo': 'Undoes the last command',
  1834. 'redo': 'Redoes the last command',
  1835. 'tab': 'Tab',
  1836. 'untab': 'Untab',
  1837. 'bold': 'Set a bold style',
  1838. 'italic': 'Set a italic style',
  1839. 'underline': 'Set a underline style',
  1840. 'strikethrough': 'Set a strikethrough style',
  1841. 'removeFormat': 'Clean a style',
  1842. 'justifyLeft': 'Set left align',
  1843. 'justifyCenter': 'Set center align',
  1844. 'justifyRight': 'Set right align',
  1845. 'justifyFull': 'Set full align',
  1846. 'insertUnorderedList': 'Toggle unordered list',
  1847. 'insertOrderedList': 'Toggle ordered list',
  1848. 'outdent': 'Outdent on current paragraph',
  1849. 'indent': 'Indent on current paragraph',
  1850. 'formatPara': 'Change current block\'s format as a paragraph(P tag)',
  1851. 'formatH1': 'Change current block\'s format as H1',
  1852. 'formatH2': 'Change current block\'s format as H2',
  1853. 'formatH3': 'Change current block\'s format as H3',
  1854. 'formatH4': 'Change current block\'s format as H4',
  1855. 'formatH5': 'Change current block\'s format as H5',
  1856. 'formatH6': 'Change current block\'s format as H6',
  1857. 'insertHorizontalRule': 'Insert horizontal rule',
  1858. 'linkDialog.show': 'Show Link Dialog'
  1859. },
  1860. history: {
  1861. undo: 'Undo',
  1862. redo: 'Redo'
  1863. },
  1864. specialChar: {
  1865. specialChar: 'SPECIAL CHARACTERS',
  1866. select: 'Select Special characters'
  1867. }
  1868. }
  1869. });
  1870. /**
  1871. * @class core.key
  1872. *
  1873. * Object for keycodes.
  1874. *
  1875. * @singleton
  1876. * @alternateClassName key
  1877. */
  1878. var key = (function () {
  1879. var keyMap = {
  1880. 'BACKSPACE': 8,
  1881. 'TAB': 9,
  1882. 'ENTER': 13,
  1883. 'SPACE': 32,
  1884. // Arrow
  1885. 'LEFT': 37,
  1886. 'UP': 38,
  1887. 'RIGHT': 39,
  1888. 'DOWN': 40,
  1889. // Number: 0-9
  1890. 'NUM0': 48,
  1891. 'NUM1': 49,
  1892. 'NUM2': 50,
  1893. 'NUM3': 51,
  1894. 'NUM4': 52,
  1895. 'NUM5': 53,
  1896. 'NUM6': 54,
  1897. 'NUM7': 55,
  1898. 'NUM8': 56,
  1899. // Alphabet: a-z
  1900. 'B': 66,
  1901. 'E': 69,
  1902. 'I': 73,
  1903. 'J': 74,
  1904. 'K': 75,
  1905. 'L': 76,
  1906. 'R': 82,
  1907. 'S': 83,
  1908. 'U': 85,
  1909. 'V': 86,
  1910. 'Y': 89,
  1911. 'Z': 90,
  1912. 'SLASH': 191,
  1913. 'LEFTBRACKET': 219,
  1914. 'BACKSLASH': 220,
  1915. 'RIGHTBRACKET': 221
  1916. };
  1917. return {
  1918. /**
  1919. * @method isEdit
  1920. *
  1921. * @param {Number} keyCode
  1922. * @return {Boolean}
  1923. */
  1924. isEdit: function (keyCode) {
  1925. return list.contains([
  1926. keyMap.BACKSPACE,
  1927. keyMap.TAB,
  1928. keyMap.ENTER,
  1929. keyMap.SPACe
  1930. ], keyCode);
  1931. },
  1932. /**
  1933. * @method isMove
  1934. *
  1935. * @param {Number} keyCode
  1936. * @return {Boolean}
  1937. */
  1938. isMove: function (keyCode) {
  1939. return list.contains([
  1940. keyMap.LEFT,
  1941. keyMap.UP,
  1942. keyMap.RIGHT,
  1943. keyMap.DOWN
  1944. ], keyCode);
  1945. },
  1946. /**
  1947. * @property {Object} nameFromCode
  1948. * @property {String} nameFromCode.8 "BACKSPACE"
  1949. */
  1950. nameFromCode: func.invertObject(keyMap),
  1951. code: keyMap
  1952. };
  1953. })();
  1954. var range = (function () {
  1955. /**
  1956. * return boundaryPoint from TextRange, inspired by Andy Na's HuskyRange.js
  1957. *
  1958. * @param {TextRange} textRange
  1959. * @param {Boolean} isStart
  1960. * @return {BoundaryPoint}
  1961. *
  1962. * @see http://msdn.microsoft.com/en-us/library/ie/ms535872(v=vs.85).aspx
  1963. */
  1964. var textRangeToPoint = function (textRange, isStart) {
  1965. var container = textRange.parentElement(), offset;
  1966. var tester = document.body.createTextRange(), prevContainer;
  1967. var childNodes = list.from(container.childNodes);
  1968. for (offset = 0; offset < childNodes.length; offset++) {
  1969. if (dom.isText(childNodes[offset])) {
  1970. continue;
  1971. }
  1972. tester.moveToElementText(childNodes[offset]);
  1973. if (tester.compareEndPoints('StartToStart', textRange) >= 0) {
  1974. break;
  1975. }
  1976. prevContainer = childNodes[offset];
  1977. }
  1978. if (offset !== 0 && dom.isText(childNodes[offset - 1])) {
  1979. var textRangeStart = document.body.createTextRange(), curTextNode = null;
  1980. textRangeStart.moveToElementText(prevContainer || container);
  1981. textRangeStart.collapse(!prevContainer);
  1982. curTextNode = prevContainer ? prevContainer.nextSibling : container.firstChild;
  1983. var pointTester = textRange.duplicate();
  1984. pointTester.setEndPoint('StartToStart', textRangeStart);
  1985. var textCount = pointTester.text.replace(/[\r\n]/g, '').length;
  1986. while (textCount > curTextNode.nodeValue.length && curTextNode.nextSibling) {
  1987. textCount -= curTextNode.nodeValue.length;
  1988. curTextNode = curTextNode.nextSibling;
  1989. }
  1990. /* jshint ignore:start */
  1991. var dummy = curTextNode.nodeValue; // enforce IE to re-reference curTextNode, hack
  1992. /* jshint ignore:end */
  1993. if (isStart && curTextNode.nextSibling && dom.isText(curTextNode.nextSibling) &&
  1994. textCount === curTextNode.nodeValue.length) {
  1995. textCount -= curTextNode.nodeValue.length;
  1996. curTextNode = curTextNode.nextSibling;
  1997. }
  1998. container = curTextNode;
  1999. offset = textCount;
  2000. }
  2001. return {
  2002. cont: container,
  2003. offset: offset
  2004. };
  2005. };
  2006. /**
  2007. * return TextRange from boundary point (inspired by google closure-library)
  2008. * @param {BoundaryPoint} point
  2009. * @return {TextRange}
  2010. */
  2011. var pointToTextRange = function (point) {
  2012. var textRangeInfo = function (container, offset) {
  2013. var node, isCollapseToStart;
  2014. if (dom.isText(container)) {
  2015. var prevTextNodes = dom.listPrev(container, func.not(dom.isText));
  2016. var prevContainer = list.last(prevTextNodes).previousSibling;
  2017. node = prevContainer || container.parentNode;
  2018. offset += list.sum(list.tail(prevTextNodes), dom.nodeLength);
  2019. isCollapseToStart = !prevContainer;
  2020. } else {
  2021. node = container.childNodes[offset] || container;
  2022. if (dom.isText(node)) {
  2023. return textRangeInfo(node, 0);
  2024. }
  2025. offset = 0;
  2026. isCollapseToStart = false;
  2027. }
  2028. return {
  2029. node: node,
  2030. collapseToStart: isCollapseToStart,
  2031. offset: offset
  2032. };
  2033. };
  2034. var textRange = document.body.createTextRange();
  2035. var info = textRangeInfo(point.node, point.offset);
  2036. textRange.moveToElementText(info.node);
  2037. textRange.collapse(info.collapseToStart);
  2038. textRange.moveStart('character', info.offset);
  2039. return textRange;
  2040. };
  2041. /**
  2042. * Wrapped Range
  2043. *
  2044. * @constructor
  2045. * @param {Node} sc - start container
  2046. * @param {Number} so - start offset
  2047. * @param {Node} ec - end container
  2048. * @param {Number} eo - end offset
  2049. */
  2050. var WrappedRange = function (sc, so, ec, eo) {
  2051. this.sc = sc;
  2052. this.so = so;
  2053. this.ec = ec;
  2054. this.eo = eo;
  2055. // nativeRange: get nativeRange from sc, so, ec, eo
  2056. var nativeRange = function () {
  2057. if (agent.isW3CRangeSupport) {
  2058. var w3cRange = document.createRange();
  2059. w3cRange.setStart(sc, so);
  2060. w3cRange.setEnd(ec, eo);
  2061. return w3cRange;
  2062. } else {
  2063. var textRange = pointToTextRange({
  2064. node: sc,
  2065. offset: so
  2066. });
  2067. textRange.setEndPoint('EndToEnd', pointToTextRange({
  2068. node: ec,
  2069. offset: eo
  2070. }));
  2071. return textRange;
  2072. }
  2073. };
  2074. this.getPoints = function () {
  2075. return {
  2076. sc: sc,
  2077. so: so,
  2078. ec: ec,
  2079. eo: eo
  2080. };
  2081. };
  2082. this.getStartPoint = function () {
  2083. return {
  2084. node: sc,
  2085. offset: so
  2086. };
  2087. };
  2088. this.getEndPoint = function () {
  2089. return {
  2090. node: ec,
  2091. offset: eo
  2092. };
  2093. };
  2094. /**
  2095. * select update visible range
  2096. */
  2097. this.select = function () {
  2098. var nativeRng = nativeRange();
  2099. if (agent.isW3CRangeSupport) {
  2100. var selection = document.getSelection();
  2101. if (selection.rangeCount > 0) {
  2102. selection.removeAllRanges();
  2103. }
  2104. selection.addRange(nativeRng);
  2105. } else {
  2106. nativeRng.select();
  2107. }
  2108. return this;
  2109. };
  2110. /**
  2111. * Moves the scrollbar to start container(sc) of current range
  2112. *
  2113. * @return {WrappedRange}
  2114. */
  2115. this.scrollIntoView = function ($container) {
  2116. if ($container[0].scrollTop + $container.height() < this.sc.offsetTop) {
  2117. $container[0].scrollTop += Math.abs($container[0].scrollTop + $container.height() - this.sc.offsetTop);
  2118. }
  2119. return this;
  2120. };
  2121. /**
  2122. * @return {WrappedRange}
  2123. */
  2124. this.normalize = function () {
  2125. /**
  2126. * @param {BoundaryPoint} point
  2127. * @param {Boolean} isLeftToRight
  2128. * @return {BoundaryPoint}
  2129. */
  2130. var getVisiblePoint = function (point, isLeftToRight) {
  2131. if ((dom.isVisiblePoint(point) && !dom.isEdgePoint(point)) ||
  2132. (dom.isVisiblePoint(point) && dom.isRightEdgePoint(point) && !isLeftToRight) ||
  2133. (dom.isVisiblePoint(point) && dom.isLeftEdgePoint(point) && isLeftToRight) ||
  2134. (dom.isVisiblePoint(point) && dom.isBlock(point.node) && dom.isEmpty(point.node))) {
  2135. return point;
  2136. }
  2137. // point on block's edge
  2138. var block = dom.ancestor(point.node, dom.isBlock);
  2139. if (((dom.isLeftEdgePointOf(point, block) || dom.isVoid(dom.prevPoint(point).node)) && !isLeftToRight) ||
  2140. ((dom.isRightEdgePointOf(point, block) || dom.isVoid(dom.nextPoint(point).node)) && isLeftToRight)) {
  2141. // returns point already on visible point
  2142. if (dom.isVisiblePoint(point)) {
  2143. return point;
  2144. }
  2145. // reverse direction
  2146. isLeftToRight = !isLeftToRight;
  2147. }
  2148. var nextPoint = isLeftToRight ? dom.nextPointUntil(dom.nextPoint(point), dom.isVisiblePoint) :
  2149. dom.prevPointUntil(dom.prevPoint(point), dom.isVisiblePoint);
  2150. return nextPoint || point;
  2151. };
  2152. var endPoint = getVisiblePoint(this.getEndPoint(), false);
  2153. var startPoint = this.isCollapsed() ? endPoint : getVisiblePoint(this.getStartPoint(), true);
  2154. return new WrappedRange(
  2155. startPoint.node,
  2156. startPoint.offset,
  2157. endPoint.node,
  2158. endPoint.offset
  2159. );
  2160. };
  2161. /**
  2162. * returns matched nodes on range
  2163. *
  2164. * @param {Function} [pred] - predicate function
  2165. * @param {Object} [options]
  2166. * @param {Boolean} [options.includeAncestor]
  2167. * @param {Boolean} [options.fullyContains]
  2168. * @return {Node[]}
  2169. */
  2170. this.nodes = function (pred, options) {
  2171. pred = pred || func.ok;
  2172. var includeAncestor = options && options.includeAncestor;
  2173. var fullyContains = options && options.fullyContains;
  2174. // TODO compare points and sort
  2175. var startPoint = this.getStartPoint();
  2176. var endPoint = this.getEndPoint();
  2177. var nodes = [];
  2178. var leftEdgeNodes = [];
  2179. dom.walkPoint(startPoint, endPoint, function (point) {
  2180. if (dom.isEditable(point.node)) {
  2181. return;
  2182. }
  2183. var node;
  2184. if (fullyContains) {
  2185. if (dom.isLeftEdgePoint(point)) {
  2186. leftEdgeNodes.push(point.node);
  2187. }
  2188. if (dom.isRightEdgePoint(point) && list.contains(leftEdgeNodes, point.node)) {
  2189. node = point.node;
  2190. }
  2191. } else if (includeAncestor) {
  2192. node = dom.ancestor(point.node, pred);
  2193. } else {
  2194. node = point.node;
  2195. }
  2196. if (node && pred(node)) {
  2197. nodes.push(node);
  2198. }
  2199. }, true);
  2200. return list.unique(nodes);
  2201. };
  2202. /**
  2203. * returns commonAncestor of range
  2204. * @return {Element} - commonAncestor
  2205. */
  2206. this.commonAncestor = function () {
  2207. return dom.commonAncestor(sc, ec);
  2208. };
  2209. /**
  2210. * returns expanded range by pred
  2211. *
  2212. * @param {Function} pred - predicate function
  2213. * @return {WrappedRange}
  2214. */
  2215. this.expand = function (pred) {
  2216. var startAncestor = dom.ancestor(sc, pred);
  2217. var endAncestor = dom.ancestor(ec, pred);
  2218. if (!startAncestor && !endAncestor) {
  2219. return new WrappedRange(sc, so, ec, eo);
  2220. }
  2221. var boundaryPoints = this.getPoints();
  2222. if (startAncestor) {
  2223. boundaryPoints.sc = startAncestor;
  2224. boundaryPoints.so = 0;
  2225. }
  2226. if (endAncestor) {
  2227. boundaryPoints.ec = endAncestor;
  2228. boundaryPoints.eo = dom.nodeLength(endAncestor);
  2229. }
  2230. return new WrappedRange(
  2231. boundaryPoints.sc,
  2232. boundaryPoints.so,
  2233. boundaryPoints.ec,
  2234. boundaryPoints.eo
  2235. );
  2236. };
  2237. /**
  2238. * @param {Boolean} isCollapseToStart
  2239. * @return {WrappedRange}
  2240. */
  2241. this.collapse = function (isCollapseToStart) {
  2242. if (isCollapseToStart) {
  2243. return new WrappedRange(sc, so, sc, so);
  2244. } else {
  2245. return new WrappedRange(ec, eo, ec, eo);
  2246. }
  2247. };
  2248. /**
  2249. * splitText on range
  2250. */
  2251. this.splitText = function () {
  2252. var isSameContainer = sc === ec;
  2253. var boundaryPoints = this.getPoints();
  2254. if (dom.isText(ec) && !dom.isEdgePoint(this.getEndPoint())) {
  2255. ec.splitText(eo);
  2256. }
  2257. if (dom.isText(sc) && !dom.isEdgePoint(this.getStartPoint())) {
  2258. boundaryPoints.sc = sc.splitText(so);
  2259. boundaryPoints.so = 0;
  2260. if (isSameContainer) {
  2261. boundaryPoints.ec = boundaryPoints.sc;
  2262. boundaryPoints.eo = eo - so;
  2263. }
  2264. }
  2265. return new WrappedRange(
  2266. boundaryPoints.sc,
  2267. boundaryPoints.so,
  2268. boundaryPoints.ec,
  2269. boundaryPoints.eo
  2270. );
  2271. };
  2272. /**
  2273. * delete contents on range
  2274. * @return {WrappedRange}
  2275. */
  2276. this.deleteContents = function () {
  2277. if (this.isCollapsed()) {
  2278. return this;
  2279. }
  2280. var rng = this.splitText();
  2281. var nodes = rng.nodes(null, {
  2282. fullyContains: true
  2283. });
  2284. // find new cursor point
  2285. var point = dom.prevPointUntil(rng.getStartPoint(), function (point) {
  2286. return !list.contains(nodes, point.node);
  2287. });
  2288. var emptyParents = [];
  2289. $.each(nodes, function (idx, node) {
  2290. // find empty parents
  2291. var parent = node.parentNode;
  2292. if (point.node !== parent && dom.nodeLength(parent) === 1) {
  2293. emptyParents.push(parent);
  2294. }
  2295. dom.remove(node, false);
  2296. });
  2297. // remove empty parents
  2298. $.each(emptyParents, function (idx, node) {
  2299. dom.remove(node, false);
  2300. });
  2301. return new WrappedRange(
  2302. point.node,
  2303. point.offset,
  2304. point.node,
  2305. point.offset
  2306. ).normalize();
  2307. };
  2308. /**
  2309. * makeIsOn: return isOn(pred) function
  2310. */
  2311. var makeIsOn = function (pred) {
  2312. return function () {
  2313. var ancestor = dom.ancestor(sc, pred);
  2314. return !!ancestor && (ancestor === dom.ancestor(ec, pred));
  2315. };
  2316. };
  2317. // isOnEditable: judge whether range is on editable or not
  2318. this.isOnEditable = makeIsOn(dom.isEditable);
  2319. // isOnList: judge whether range is on list node or not
  2320. this.isOnList = makeIsOn(dom.isList);
  2321. // isOnAnchor: judge whether range is on anchor node or not
  2322. this.isOnAnchor = makeIsOn(dom.isAnchor);
  2323. // isOnAnchor: judge whether range is on cell node or not
  2324. this.isOnCell = makeIsOn(dom.isCell);
  2325. /**
  2326. * @param {Function} pred
  2327. * @return {Boolean}
  2328. */
  2329. this.isLeftEdgeOf = function (pred) {
  2330. if (!dom.isLeftEdgePoint(this.getStartPoint())) {
  2331. return false;
  2332. }
  2333. var node = dom.ancestor(this.sc, pred);
  2334. return node && dom.isLeftEdgeOf(this.sc, node);
  2335. };
  2336. /**
  2337. * returns whether range was collapsed or not
  2338. */
  2339. this.isCollapsed = function () {
  2340. return sc === ec && so === eo;
  2341. };
  2342. /**
  2343. * wrap inline nodes which children of body with paragraph
  2344. *
  2345. * @return {WrappedRange}
  2346. */
  2347. this.wrapBodyInlineWithPara = function () {
  2348. if (dom.isBodyContainer(sc) && dom.isEmpty(sc)) {
  2349. sc.innerHTML = dom.emptyPara;
  2350. return new WrappedRange(sc.firstChild, 0, sc.firstChild, 0);
  2351. }
  2352. /**
  2353. * [workaround] firefox often create range on not visible point. so normalize here.
  2354. * - firefox: |<p>text</p>|
  2355. * - chrome: <p>|text|</p>
  2356. */
  2357. var rng = this.normalize();
  2358. if (dom.isParaInline(sc) || dom.isPara(sc)) {
  2359. return rng;
  2360. }
  2361. // find inline top ancestor
  2362. var topAncestor;
  2363. if (dom.isInline(rng.sc)) {
  2364. var ancestors = dom.listAncestor(rng.sc, func.not(dom.isInline));
  2365. topAncestor = list.last(ancestors);
  2366. if (!dom.isInline(topAncestor)) {
  2367. topAncestor = ancestors[ancestors.length - 2] || rng.sc.childNodes[rng.so];
  2368. }
  2369. } else {
  2370. topAncestor = rng.sc.childNodes[rng.so > 0 ? rng.so - 1 : 0];
  2371. }
  2372. // siblings not in paragraph
  2373. var inlineSiblings = dom.listPrev(topAncestor, dom.isParaInline).reverse();
  2374. inlineSiblings = inlineSiblings.concat(dom.listNext(topAncestor.nextSibling, dom.isParaInline));
  2375. // wrap with paragraph
  2376. if (inlineSiblings.length) {
  2377. var para = dom.wrap(list.head(inlineSiblings), 'p');
  2378. dom.appendChildNodes(para, list.tail(inlineSiblings));
  2379. }
  2380. return this.normalize();
  2381. };
  2382. /**
  2383. * insert node at current cursor
  2384. *
  2385. * @param {Node} node
  2386. * @return {Node}
  2387. */
  2388. this.insertNode = function (node) {
  2389. var rng = this.wrapBodyInlineWithPara().deleteContents();
  2390. var info = dom.splitPoint(rng.getStartPoint(), dom.isInline(node));
  2391. if (info.rightNode) {
  2392. info.rightNode.parentNode.insertBefore(node, info.rightNode);
  2393. } else {
  2394. info.container.appendChild(node);
  2395. }
  2396. return node;
  2397. };
  2398. /**
  2399. * insert html at current cursor
  2400. */
  2401. this.pasteHTML = function (markup) {
  2402. var contentsContainer = $('<div></div>').html(markup)[0];
  2403. var childNodes = list.from(contentsContainer.childNodes);
  2404. var rng = this.wrapBodyInlineWithPara().deleteContents();
  2405. return childNodes.reverse().map(function (childNode) {
  2406. return rng.insertNode(childNode);
  2407. }).reverse();
  2408. };
  2409. /**
  2410. * returns text in range
  2411. *
  2412. * @return {String}
  2413. */
  2414. this.toString = function () {
  2415. var nativeRng = nativeRange();
  2416. return agent.isW3CRangeSupport ? nativeRng.toString() : nativeRng.text;
  2417. };
  2418. /**
  2419. * returns range for word before cursor
  2420. *
  2421. * @param {Boolean} [findAfter] - find after cursor, default: false
  2422. * @return {WrappedRange}
  2423. */
  2424. this.getWordRange = function (findAfter) {
  2425. var endPoint = this.getEndPoint();
  2426. if (!dom.isCharPoint(endPoint)) {
  2427. return this;
  2428. }
  2429. var startPoint = dom.prevPointUntil(endPoint, function (point) {
  2430. return !dom.isCharPoint(point);
  2431. });
  2432. if (findAfter) {
  2433. endPoint = dom.nextPointUntil(endPoint, function (point) {
  2434. return !dom.isCharPoint(point);
  2435. });
  2436. }
  2437. return new WrappedRange(
  2438. startPoint.node,
  2439. startPoint.offset,
  2440. endPoint.node,
  2441. endPoint.offset
  2442. );
  2443. };
  2444. /**
  2445. * create offsetPath bookmark
  2446. *
  2447. * @param {Node} editable
  2448. */
  2449. this.bookmark = function (editable) {
  2450. return {
  2451. s: {
  2452. path: dom.makeOffsetPath(editable, sc),
  2453. offset: so
  2454. },
  2455. e: {
  2456. path: dom.makeOffsetPath(editable, ec),
  2457. offset: eo
  2458. }
  2459. };
  2460. };
  2461. /**
  2462. * create offsetPath bookmark base on paragraph
  2463. *
  2464. * @param {Node[]} paras
  2465. */
  2466. this.paraBookmark = function (paras) {
  2467. return {
  2468. s: {
  2469. path: list.tail(dom.makeOffsetPath(list.head(paras), sc)),
  2470. offset: so
  2471. },
  2472. e: {
  2473. path: list.tail(dom.makeOffsetPath(list.last(paras), ec)),
  2474. offset: eo
  2475. }
  2476. };
  2477. };
  2478. /**
  2479. * getClientRects
  2480. * @return {Rect[]}
  2481. */
  2482. this.getClientRects = function () {
  2483. var nativeRng = nativeRange();
  2484. return nativeRng.getClientRects();
  2485. };
  2486. };
  2487. /**
  2488. * @class core.range
  2489. *
  2490. * Data structure
  2491. * * BoundaryPoint: a point of dom tree
  2492. * * BoundaryPoints: two boundaryPoints corresponding to the start and the end of the Range
  2493. *
  2494. * See to http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Position
  2495. *
  2496. * @singleton
  2497. * @alternateClassName range
  2498. */
  2499. return {
  2500. /**
  2501. * @method
  2502. *
  2503. * create Range Object From arguments or Browser Selection
  2504. *
  2505. * @param {Node} sc - start container
  2506. * @param {Number} so - start offset
  2507. * @param {Node} ec - end container
  2508. * @param {Number} eo - end offset
  2509. * @return {WrappedRange}
  2510. */
  2511. create : function (sc, so, ec, eo) {
  2512. if (!arguments.length) { // from Browser Selection
  2513. if (agent.isW3CRangeSupport) {
  2514. var selection = document.getSelection();
  2515. if (!selection || selection.rangeCount === 0) {
  2516. return null;
  2517. } else if (dom.isBody(selection.anchorNode)) {
  2518. // Firefox: returns entire body as range on initialization. We won't never need it.
  2519. return null;
  2520. }
  2521. var nativeRng = selection.getRangeAt(0);
  2522. sc = nativeRng.startContainer;
  2523. so = nativeRng.startOffset;
  2524. ec = nativeRng.endContainer;
  2525. eo = nativeRng.endOffset;
  2526. } else { // IE8: TextRange
  2527. var textRange = document.selection.createRange();
  2528. var textRangeEnd = textRange.duplicate();
  2529. textRangeEnd.collapse(false);
  2530. var textRangeStart = textRange;
  2531. textRangeStart.collapse(true);
  2532. var startPoint = textRangeToPoint(textRangeStart, true),
  2533. endPoint = textRangeToPoint(textRangeEnd, false);
  2534. // same visible point case: range was collapsed.
  2535. if (dom.isText(startPoint.node) && dom.isLeftEdgePoint(startPoint) &&
  2536. dom.isTextNode(endPoint.node) && dom.isRightEdgePoint(endPoint) &&
  2537. endPoint.node.nextSibling === startPoint.node) {
  2538. startPoint = endPoint;
  2539. }
  2540. sc = startPoint.cont;
  2541. so = startPoint.offset;
  2542. ec = endPoint.cont;
  2543. eo = endPoint.offset;
  2544. }
  2545. } else if (arguments.length === 2) { //collapsed
  2546. ec = sc;
  2547. eo = so;
  2548. }
  2549. return new WrappedRange(sc, so, ec, eo);
  2550. },
  2551. /**
  2552. * @method
  2553. *
  2554. * create WrappedRange from node
  2555. *
  2556. * @param {Node} node
  2557. * @return {WrappedRange}
  2558. */
  2559. createFromNode: function (node) {
  2560. var sc = node;
  2561. var so = 0;
  2562. var ec = node;
  2563. var eo = dom.nodeLength(ec);
  2564. // browsers can't target a picture or void node
  2565. if (dom.isVoid(sc)) {
  2566. so = dom.listPrev(sc).length - 1;
  2567. sc = sc.parentNode;
  2568. }
  2569. if (dom.isBR(ec)) {
  2570. eo = dom.listPrev(ec).length - 1;
  2571. ec = ec.parentNode;
  2572. } else if (dom.isVoid(ec)) {
  2573. eo = dom.listPrev(ec).length;
  2574. ec = ec.parentNode;
  2575. }
  2576. return this.create(sc, so, ec, eo);
  2577. },
  2578. /**
  2579. * create WrappedRange from node after position
  2580. *
  2581. * @param {Node} node
  2582. * @return {WrappedRange}
  2583. */
  2584. createFromNodeBefore: function (node) {
  2585. return this.createFromNode(node).collapse(true);
  2586. },
  2587. /**
  2588. * create WrappedRange from node after position
  2589. *
  2590. * @param {Node} node
  2591. * @return {WrappedRange}
  2592. */
  2593. createFromNodeAfter: function (node) {
  2594. return this.createFromNode(node).collapse();
  2595. },
  2596. /**
  2597. * @method
  2598. *
  2599. * create WrappedRange from bookmark
  2600. *
  2601. * @param {Node} editable
  2602. * @param {Object} bookmark
  2603. * @return {WrappedRange}
  2604. */
  2605. createFromBookmark : function (editable, bookmark) {
  2606. var sc = dom.fromOffsetPath(editable, bookmark.s.path);
  2607. var so = bookmark.s.offset;
  2608. var ec = dom.fromOffsetPath(editable, bookmark.e.path);
  2609. var eo = bookmark.e.offset;
  2610. return new WrappedRange(sc, so, ec, eo);
  2611. },
  2612. /**
  2613. * @method
  2614. *
  2615. * create WrappedRange from paraBookmark
  2616. *
  2617. * @param {Object} bookmark
  2618. * @param {Node[]} paras
  2619. * @return {WrappedRange}
  2620. */
  2621. createFromParaBookmark: function (bookmark, paras) {
  2622. var so = bookmark.s.offset;
  2623. var eo = bookmark.e.offset;
  2624. var sc = dom.fromOffsetPath(list.head(paras), bookmark.s.path);
  2625. var ec = dom.fromOffsetPath(list.last(paras), bookmark.e.path);
  2626. return new WrappedRange(sc, so, ec, eo);
  2627. }
  2628. };
  2629. })();
  2630. /**
  2631. * @class core.async
  2632. *
  2633. * Async functions which returns `Promise`
  2634. *
  2635. * @singleton
  2636. * @alternateClassName async
  2637. */
  2638. var async = (function () {
  2639. /**
  2640. * @method readFileAsDataURL
  2641. *
  2642. * read contents of file as representing URL
  2643. *
  2644. * @param {File} file
  2645. * @return {Promise} - then: dataUrl
  2646. */
  2647. var readFileAsDataURL = function (file) {
  2648. return $.Deferred(function (deferred) {
  2649. $.extend(new FileReader(), {
  2650. onload: function (e) {
  2651. var dataURL = e.target.result;
  2652. deferred.resolve(dataURL);
  2653. },
  2654. onerror: function () {
  2655. deferred.reject(this);
  2656. }
  2657. }).readAsDataURL(file);
  2658. }).promise();
  2659. };
  2660. /**
  2661. * @method createImage
  2662. *
  2663. * create `<image>` from url string
  2664. *
  2665. * @param {String} url
  2666. * @return {Promise} - then: $image
  2667. */
  2668. var createImage = function (url) {
  2669. return $.Deferred(function (deferred) {
  2670. var $img = $('<img>');
  2671. $img.one('load', function () {
  2672. $img.off('error abort');
  2673. deferred.resolve($img);
  2674. }).one('error abort', function () {
  2675. $img.off('load').detach();
  2676. deferred.reject($img);
  2677. }).css({
  2678. display: 'none'
  2679. }).appendTo(document.body).attr('src', url);
  2680. }).promise();
  2681. };
  2682. return {
  2683. readFileAsDataURL: readFileAsDataURL,
  2684. createImage: createImage
  2685. };
  2686. })();
  2687. /**
  2688. * @class editing.History
  2689. *
  2690. * Editor History
  2691. *
  2692. */
  2693. var History = function ($editable) {
  2694. var stack = [], stackOffset = -1;
  2695. var editable = $editable[0];
  2696. var makeSnapshot = function () {
  2697. var rng = range.create();
  2698. var emptyBookmark = {s: {path: [], offset: 0}, e: {path: [], offset: 0}};
  2699. return {
  2700. contents: $editable.html(),
  2701. bookmark: (rng ? rng.bookmark(editable) : emptyBookmark)
  2702. };
  2703. };
  2704. var applySnapshot = function (snapshot) {
  2705. if (snapshot.contents !== null) {
  2706. $editable.html(snapshot.contents);
  2707. }
  2708. if (snapshot.bookmark !== null) {
  2709. range.createFromBookmark(editable, snapshot.bookmark).select();
  2710. }
  2711. };
  2712. /**
  2713. * @method rewind
  2714. * Rewinds the history stack back to the first snapshot taken.
  2715. * Leaves the stack intact, so that "Redo" can still be used.
  2716. */
  2717. this.rewind = function () {
  2718. // Create snap shot if not yet recorded
  2719. if ($editable.html() !== stack[stackOffset].contents) {
  2720. this.recordUndo();
  2721. }
  2722. // Return to the first available snapshot.
  2723. stackOffset = 0;
  2724. // Apply that snapshot.
  2725. applySnapshot(stack[stackOffset]);
  2726. };
  2727. /**
  2728. * @method reset
  2729. * Resets the history stack completely; reverting to an empty editor.
  2730. */
  2731. this.reset = function () {
  2732. // Clear the stack.
  2733. stack = [];
  2734. // Restore stackOffset to its original value.
  2735. stackOffset = -1;
  2736. // Clear the editable area.
  2737. $editable.html('');
  2738. // Record our first snapshot (of nothing).
  2739. this.recordUndo();
  2740. };
  2741. /**
  2742. * undo
  2743. */
  2744. this.undo = function () {
  2745. // Create snap shot if not yet recorded
  2746. if ($editable.html() !== stack[stackOffset].contents) {
  2747. this.recordUndo();
  2748. }
  2749. if (0 < stackOffset) {
  2750. stackOffset--;
  2751. applySnapshot(stack[stackOffset]);
  2752. }
  2753. };
  2754. /**
  2755. * redo
  2756. */
  2757. this.redo = function () {
  2758. if (stack.length - 1 > stackOffset) {
  2759. stackOffset++;
  2760. applySnapshot(stack[stackOffset]);
  2761. }
  2762. };
  2763. /**
  2764. * recorded undo
  2765. */
  2766. this.recordUndo = function () {
  2767. stackOffset++;
  2768. // Wash out stack after stackOffset
  2769. if (stack.length > stackOffset) {
  2770. stack = stack.slice(0, stackOffset);
  2771. }
  2772. // Create new snapshot and push it to the end
  2773. stack.push(makeSnapshot());
  2774. };
  2775. };
  2776. /**
  2777. * @class editing.Style
  2778. *
  2779. * Style
  2780. *
  2781. */
  2782. var Style = function () {
  2783. /**
  2784. * @method jQueryCSS
  2785. *
  2786. * [workaround] for old jQuery
  2787. * passing an array of style properties to .css()
  2788. * will result in an object of property-value pairs.
  2789. * (compability with version < 1.9)
  2790. *
  2791. * @private
  2792. * @param {jQuery} $obj
  2793. * @param {Array} propertyNames - An array of one or more CSS properties.
  2794. * @return {Object}
  2795. */
  2796. var jQueryCSS = function ($obj, propertyNames) {
  2797. if (agent.jqueryVersion < 1.9) {
  2798. var result = {};
  2799. $.each(propertyNames, function (idx, propertyName) {
  2800. result[propertyName] = $obj.css(propertyName);
  2801. });
  2802. return result;
  2803. }
  2804. return $obj.css.call($obj, propertyNames);
  2805. };
  2806. /**
  2807. * returns style object from node
  2808. *
  2809. * @param {jQuery} $node
  2810. * @return {Object}
  2811. */
  2812. this.fromNode = function ($node) {
  2813. var properties = ['font-family', 'font-size', 'text-align', 'list-style-type', 'line-height'];
  2814. var styleInfo = jQueryCSS($node, properties) || {};
  2815. styleInfo['font-size'] = parseInt(styleInfo['font-size'], 10);
  2816. return styleInfo;
  2817. };
  2818. /**
  2819. * paragraph level style
  2820. *
  2821. * @param {WrappedRange} rng
  2822. * @param {Object} styleInfo
  2823. */
  2824. this.stylePara = function (rng, styleInfo) {
  2825. $.each(rng.nodes(dom.isPara, {
  2826. includeAncestor: true
  2827. }), function (idx, para) {
  2828. $(para).css(styleInfo);
  2829. });
  2830. };
  2831. /**
  2832. * insert and returns styleNodes on range.
  2833. *
  2834. * @param {WrappedRange} rng
  2835. * @param {Object} [options] - options for styleNodes
  2836. * @param {String} [options.nodeName] - default: `SPAN`
  2837. * @param {Boolean} [options.expandClosestSibling] - default: `false`
  2838. * @param {Boolean} [options.onlyPartialContains] - default: `false`
  2839. * @return {Node[]}
  2840. */
  2841. this.styleNodes = function (rng, options) {
  2842. rng = rng.splitText();
  2843. var nodeName = options && options.nodeName || 'SPAN';
  2844. var expandClosestSibling = !!(options && options.expandClosestSibling);
  2845. var onlyPartialContains = !!(options && options.onlyPartialContains);
  2846. if (rng.isCollapsed()) {
  2847. return [rng.insertNode(dom.create(nodeName))];
  2848. }
  2849. var pred = dom.makePredByNodeName(nodeName);
  2850. var nodes = rng.nodes(dom.isText, {
  2851. fullyContains: true
  2852. }).map(function (text) {
  2853. return dom.singleChildAncestor(text, pred) || dom.wrap(text, nodeName);
  2854. });
  2855. if (expandClosestSibling) {
  2856. if (onlyPartialContains) {
  2857. var nodesInRange = rng.nodes();
  2858. // compose with partial contains predication
  2859. pred = func.and(pred, function (node) {
  2860. return list.contains(nodesInRange, node);
  2861. });
  2862. }
  2863. return nodes.map(function (node) {
  2864. var siblings = dom.withClosestSiblings(node, pred);
  2865. var head = list.head(siblings);
  2866. var tails = list.tail(siblings);
  2867. $.each(tails, function (idx, elem) {
  2868. dom.appendChildNodes(head, elem.childNodes);
  2869. dom.remove(elem);
  2870. });
  2871. return list.head(siblings);
  2872. });
  2873. } else {
  2874. return nodes;
  2875. }
  2876. };
  2877. /**
  2878. * get current style on cursor
  2879. *
  2880. * @param {WrappedRange} rng
  2881. * @return {Object} - object contains style properties.
  2882. */
  2883. this.current = function (rng) {
  2884. var $cont = $(!dom.isElement(rng.sc) ? rng.sc.parentNode : rng.sc);
  2885. var styleInfo = this.fromNode($cont);
  2886. // document.queryCommandState for toggle state
  2887. // [workaround] prevent Firefox nsresult: "0x80004005 (NS_ERROR_FAILURE)"
  2888. try {
  2889. styleInfo = $.extend(styleInfo, {
  2890. 'font-bold': document.queryCommandState('bold') ? 'bold' : 'normal',
  2891. 'font-italic': document.queryCommandState('italic') ? 'italic' : 'normal',
  2892. 'font-underline': document.queryCommandState('underline') ? 'underline' : 'normal',
  2893. 'font-subscript': document.queryCommandState('subscript') ? 'subscript' : 'normal',
  2894. 'font-superscript': document.queryCommandState('superscript') ? 'superscript' : 'normal',
  2895. 'font-strikethrough': document.queryCommandState('strikeThrough') ? 'strikethrough' : 'normal'
  2896. });
  2897. } catch (e) {}
  2898. // list-style-type to list-style(unordered, ordered)
  2899. if (!rng.isOnList()) {
  2900. styleInfo['list-style'] = 'none';
  2901. } else {
  2902. var orderedTypes = ['circle', 'disc', 'disc-leading-zero', 'square'];
  2903. var isUnordered = $.inArray(styleInfo['list-style-type'], orderedTypes) > -1;
  2904. styleInfo['list-style'] = isUnordered ? 'unordered' : 'ordered';
  2905. }
  2906. var para = dom.ancestor(rng.sc, dom.isPara);
  2907. if (para && para.style['line-height']) {
  2908. styleInfo['line-height'] = para.style.lineHeight;
  2909. } else {
  2910. var lineHeight = parseInt(styleInfo['line-height'], 10) / parseInt(styleInfo['font-size'], 10);
  2911. styleInfo['line-height'] = lineHeight.toFixed(1);
  2912. }
  2913. styleInfo.anchor = rng.isOnAnchor() && dom.ancestor(rng.sc, dom.isAnchor);
  2914. styleInfo.ancestors = dom.listAncestor(rng.sc, dom.isEditable);
  2915. styleInfo.range = rng;
  2916. return styleInfo;
  2917. };
  2918. };
  2919. /**
  2920. * @class editing.Bullet
  2921. *
  2922. * @alternateClassName Bullet
  2923. */
  2924. var Bullet = function () {
  2925. /**
  2926. * @method insertOrderedList
  2927. *
  2928. * toggle ordered list
  2929. *
  2930. * @type command
  2931. */
  2932. this.insertOrderedList = function () {
  2933. this.toggleList('OL');
  2934. };
  2935. /**
  2936. * @method insertUnorderedList
  2937. *
  2938. * toggle unordered list
  2939. *
  2940. * @type command
  2941. */
  2942. this.insertUnorderedList = function () {
  2943. this.toggleList('UL');
  2944. };
  2945. /**
  2946. * @method indent
  2947. *
  2948. * indent
  2949. *
  2950. * @type command
  2951. */
  2952. this.indent = function () {
  2953. var self = this;
  2954. var rng = range.create().wrapBodyInlineWithPara();
  2955. var paras = rng.nodes(dom.isPara, { includeAncestor: true });
  2956. var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
  2957. $.each(clustereds, function (idx, paras) {
  2958. var head = list.head(paras);
  2959. if (dom.isLi(head)) {
  2960. self.wrapList(paras, head.parentNode.nodeName);
  2961. } else {
  2962. $.each(paras, function (idx, para) {
  2963. $(para).css('marginLeft', function (idx, val) {
  2964. return (parseInt(val, 10) || 0) + 25;
  2965. });
  2966. });
  2967. }
  2968. });
  2969. rng.select();
  2970. };
  2971. /**
  2972. * @method outdent
  2973. *
  2974. * outdent
  2975. *
  2976. * @type command
  2977. */
  2978. this.outdent = function () {
  2979. var self = this;
  2980. var rng = range.create().wrapBodyInlineWithPara();
  2981. var paras = rng.nodes(dom.isPara, { includeAncestor: true });
  2982. var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
  2983. $.each(clustereds, function (idx, paras) {
  2984. var head = list.head(paras);
  2985. if (dom.isLi(head)) {
  2986. self.releaseList([paras]);
  2987. } else {
  2988. $.each(paras, function (idx, para) {
  2989. $(para).css('marginLeft', function (idx, val) {
  2990. val = (parseInt(val, 10) || 0);
  2991. return val > 25 ? val - 25 : '';
  2992. });
  2993. });
  2994. }
  2995. });
  2996. rng.select();
  2997. };
  2998. /**
  2999. * @method toggleList
  3000. *
  3001. * toggle list
  3002. *
  3003. * @param {String} listName - OL or UL
  3004. */
  3005. this.toggleList = function (listName) {
  3006. var self = this;
  3007. var rng = range.create().wrapBodyInlineWithPara();
  3008. var paras = rng.nodes(dom.isPara, { includeAncestor: true });
  3009. var bookmark = rng.paraBookmark(paras);
  3010. var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
  3011. // paragraph to list
  3012. if (list.find(paras, dom.isPurePara)) {
  3013. var wrappedParas = [];
  3014. $.each(clustereds, function (idx, paras) {
  3015. wrappedParas = wrappedParas.concat(self.wrapList(paras, listName));
  3016. });
  3017. paras = wrappedParas;
  3018. // list to paragraph or change list style
  3019. } else {
  3020. var diffLists = rng.nodes(dom.isList, {
  3021. includeAncestor: true
  3022. }).filter(function (listNode) {
  3023. return !$.nodeName(listNode, listName);
  3024. });
  3025. if (diffLists.length) {
  3026. $.each(diffLists, function (idx, listNode) {
  3027. dom.replace(listNode, listName);
  3028. });
  3029. } else {
  3030. paras = this.releaseList(clustereds, true);
  3031. }
  3032. }
  3033. range.createFromParaBookmark(bookmark, paras).select();
  3034. };
  3035. /**
  3036. * @method wrapList
  3037. *
  3038. * @param {Node[]} paras
  3039. * @param {String} listName
  3040. * @return {Node[]}
  3041. */
  3042. this.wrapList = function (paras, listName) {
  3043. var head = list.head(paras);
  3044. var last = list.last(paras);
  3045. var prevList = dom.isList(head.previousSibling) && head.previousSibling;
  3046. var nextList = dom.isList(last.nextSibling) && last.nextSibling;
  3047. var listNode = prevList || dom.insertAfter(dom.create(listName || 'UL'), last);
  3048. // P to LI
  3049. paras = paras.map(function (para) {
  3050. return dom.isPurePara(para) ? dom.replace(para, 'LI') : para;
  3051. });
  3052. // append to list(<ul>, <ol>)
  3053. dom.appendChildNodes(listNode, paras);
  3054. if (nextList) {
  3055. dom.appendChildNodes(listNode, list.from(nextList.childNodes));
  3056. dom.remove(nextList);
  3057. }
  3058. return paras;
  3059. };
  3060. /**
  3061. * @method releaseList
  3062. *
  3063. * @param {Array[]} clustereds
  3064. * @param {Boolean} isEscapseToBody
  3065. * @return {Node[]}
  3066. */
  3067. this.releaseList = function (clustereds, isEscapseToBody) {
  3068. var releasedParas = [];
  3069. $.each(clustereds, function (idx, paras) {
  3070. var head = list.head(paras);
  3071. var last = list.last(paras);
  3072. var headList = isEscapseToBody ? dom.lastAncestor(head, dom.isList) :
  3073. head.parentNode;
  3074. var lastList = headList.childNodes.length > 1 ? dom.splitTree(headList, {
  3075. node: last.parentNode,
  3076. offset: dom.position(last) + 1
  3077. }, {
  3078. isSkipPaddingBlankHTML: true
  3079. }) : null;
  3080. var middleList = dom.splitTree(headList, {
  3081. node: head.parentNode,
  3082. offset: dom.position(head)
  3083. }, {
  3084. isSkipPaddingBlankHTML: true
  3085. });
  3086. paras = isEscapseToBody ? dom.listDescendant(middleList, dom.isLi) :
  3087. list.from(middleList.childNodes).filter(dom.isLi);
  3088. // LI to P
  3089. if (isEscapseToBody || !dom.isList(headList.parentNode)) {
  3090. paras = paras.map(function (para) {
  3091. return dom.replace(para, 'P');
  3092. });
  3093. }
  3094. $.each(list.from(paras).reverse(), function (idx, para) {
  3095. dom.insertAfter(para, headList);
  3096. });
  3097. // remove empty lists
  3098. var rootLists = list.compact([headList, middleList, lastList]);
  3099. $.each(rootLists, function (idx, rootList) {
  3100. var listNodes = [rootList].concat(dom.listDescendant(rootList, dom.isList));
  3101. $.each(listNodes.reverse(), function (idx, listNode) {
  3102. if (!dom.nodeLength(listNode)) {
  3103. dom.remove(listNode, true);
  3104. }
  3105. });
  3106. });
  3107. releasedParas = releasedParas.concat(paras);
  3108. });
  3109. return releasedParas;
  3110. };
  3111. };
  3112. /**
  3113. * @class editing.Typing
  3114. *
  3115. * Typing
  3116. *
  3117. */
  3118. var Typing = function () {
  3119. // a Bullet instance to toggle lists off
  3120. var bullet = new Bullet();
  3121. /**
  3122. * insert tab
  3123. *
  3124. * @param {jQuery} $editable
  3125. * @param {WrappedRange} rng
  3126. * @param {Number} tabsize
  3127. */
  3128. this.insertTab = function ($editable, rng, tabsize) {
  3129. var tab = dom.createText(new Array(tabsize + 1).join(dom.NBSP_CHAR));
  3130. rng = rng.deleteContents();
  3131. rng.insertNode(tab, true);
  3132. rng = range.create(tab, tabsize);
  3133. rng.select();
  3134. };
  3135. /**
  3136. * insert paragraph
  3137. */
  3138. this.insertParagraph = function ($editable) {
  3139. var rng = range.create();
  3140. // deleteContents on range.
  3141. rng = rng.deleteContents();
  3142. // Wrap range if it needs to be wrapped by paragraph
  3143. rng = rng.wrapBodyInlineWithPara();
  3144. // finding paragraph
  3145. var splitRoot = dom.ancestor(rng.sc, dom.isPara);
  3146. var nextPara;
  3147. // on paragraph: split paragraph
  3148. if (splitRoot) {
  3149. // if it is an empty line with li
  3150. if (dom.isEmpty(splitRoot) && dom.isLi(splitRoot)) {
  3151. // toogle UL/OL and escape
  3152. bullet.toggleList(splitRoot.parentNode.nodeName);
  3153. return;
  3154. // if it is an empty line with para on blockquote
  3155. } else if (dom.isEmpty(splitRoot) && dom.isPara(splitRoot) && dom.isBlockquote(splitRoot.parentNode)) {
  3156. // escape blockquote
  3157. dom.insertAfter(splitRoot, splitRoot.parentNode);
  3158. nextPara = splitRoot;
  3159. // if new line has content (not a line break)
  3160. } else {
  3161. nextPara = dom.splitTree(splitRoot, rng.getStartPoint());
  3162. var emptyAnchors = dom.listDescendant(splitRoot, dom.isEmptyAnchor);
  3163. emptyAnchors = emptyAnchors.concat(dom.listDescendant(nextPara, dom.isEmptyAnchor));
  3164. $.each(emptyAnchors, function (idx, anchor) {
  3165. dom.remove(anchor);
  3166. });
  3167. // replace empty heading or pre with P tag
  3168. if ((dom.isHeading(nextPara) || dom.isPre(nextPara)) && dom.isEmpty(nextPara)) {
  3169. nextPara = dom.replace(nextPara, 'p');
  3170. }
  3171. }
  3172. // no paragraph: insert empty paragraph
  3173. } else {
  3174. var next = rng.sc.childNodes[rng.so];
  3175. nextPara = $(dom.emptyPara)[0];
  3176. if (next) {
  3177. rng.sc.insertBefore(nextPara, next);
  3178. } else {
  3179. rng.sc.appendChild(nextPara);
  3180. }
  3181. }
  3182. range.create(nextPara, 0).normalize().select().scrollIntoView($editable);
  3183. };
  3184. };
  3185. /**
  3186. * @class editing.Table
  3187. *
  3188. * Table
  3189. *
  3190. */
  3191. var Table = function () {
  3192. /**
  3193. * handle tab key
  3194. *
  3195. * @param {WrappedRange} rng
  3196. * @param {Boolean} isShift
  3197. */
  3198. this.tab = function (rng, isShift) {
  3199. var cell = dom.ancestor(rng.commonAncestor(), dom.isCell);
  3200. var table = dom.ancestor(cell, dom.isTable);
  3201. var cells = dom.listDescendant(table, dom.isCell);
  3202. var nextCell = list[isShift ? 'prev' : 'next'](cells, cell);
  3203. if (nextCell) {
  3204. range.create(nextCell, 0).select();
  3205. }
  3206. };
  3207. /**
  3208. * create empty table element
  3209. *
  3210. * @param {Number} rowCount
  3211. * @param {Number} colCount
  3212. * @return {Node}
  3213. */
  3214. this.createTable = function (colCount, rowCount, options) {
  3215. var tds = [], tdHTML;
  3216. for (var idxCol = 0; idxCol < colCount; idxCol++) {
  3217. tds.push('<td>' + dom.blank + '</td>');
  3218. }
  3219. tdHTML = tds.join('');
  3220. var trs = [], trHTML;
  3221. for (var idxRow = 0; idxRow < rowCount; idxRow++) {
  3222. trs.push('<tr>' + tdHTML + '</tr>');
  3223. }
  3224. trHTML = trs.join('');
  3225. var $table = $('<table>' + trHTML + '</table>');
  3226. if (options && options.tableClassName) {
  3227. $table.addClass(options.tableClassName);
  3228. }
  3229. return $table[0];
  3230. };
  3231. };
  3232. var KEY_BOGUS = 'bogus';
  3233. /**
  3234. * @class Editor
  3235. */
  3236. var Editor = function (context) {
  3237. var self = this;
  3238. var $note = context.layoutInfo.note;
  3239. var $editor = context.layoutInfo.editor;
  3240. var $editable = context.layoutInfo.editable;
  3241. var options = context.options;
  3242. var lang = options.langInfo;
  3243. var style = new Style();
  3244. var table = new Table();
  3245. var typing = new Typing();
  3246. var bullet = new Bullet();
  3247. var history = new History($editable);
  3248. this.initialize = function () {
  3249. // bind custom events
  3250. $editable.on('keydown', function (event) {
  3251. if (event.keyCode === key.code.ENTER) {
  3252. context.triggerEvent('enter', event);
  3253. }
  3254. context.triggerEvent('keydown', event);
  3255. if (options.shortcuts && !event.isDefaultPrevented()) {
  3256. self.handleKeyMap(event);
  3257. }
  3258. }).on('keyup', function (event) {
  3259. context.triggerEvent('keyup', event);
  3260. }).on('focus', function (event) {
  3261. context.triggerEvent('focus', event);
  3262. }).on('blur', function (event) {
  3263. context.triggerEvent('blur', event);
  3264. }).on('mousedown', function (event) {
  3265. context.triggerEvent('mousedown', event);
  3266. }).on('mouseup', function (event) {
  3267. context.triggerEvent('mouseup', event);
  3268. }).on('scroll', function (event) {
  3269. context.triggerEvent('scroll', event);
  3270. }).on('paste', function (event) {
  3271. context.triggerEvent('paste', event);
  3272. });
  3273. // [workaround] IE doesn't have input events for contentEditable
  3274. // - see: https://goo.gl/4bfIvA
  3275. var changeEventName = agent.isMSIE ? 'DOMCharacterDataModified DOMSubtreeModified DOMNodeInserted' : 'input';
  3276. $editable.on(changeEventName, function () {
  3277. context.triggerEvent('change', $editable.html());
  3278. });
  3279. $editor.on('focusin', function (event) {
  3280. context.triggerEvent('focusin', event);
  3281. }).on('focusout', function (event) {
  3282. context.triggerEvent('focusout', event);
  3283. });
  3284. if (!options.airMode && options.height) {
  3285. $editable.outerHeight(options.height);
  3286. }
  3287. if (!options.airMode && options.maxHeight) {
  3288. $editable.css('max-height', options.maxHeight);
  3289. }
  3290. if (!options.airMode && options.minHeight) {
  3291. $editable.css('min-height', options.minHeight);
  3292. }
  3293. $editable.html(dom.html($note) || dom.emptyPara);
  3294. history.recordUndo();
  3295. };
  3296. this.destroy = function () {
  3297. $editable.off();
  3298. };
  3299. this.handleKeyMap = function (event) {
  3300. var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc'];
  3301. var keys = [];
  3302. if (event.metaKey) { keys.push('CMD'); }
  3303. if (event.ctrlKey && !event.altKey) { keys.push('CTRL'); }
  3304. if (event.shiftKey) { keys.push('SHIFT'); }
  3305. var keyName = key.nameFromCode[event.keyCode];
  3306. if (keyName) {
  3307. keys.push(keyName);
  3308. }
  3309. var eventName = keyMap[keys.join('+')];
  3310. if (eventName) {
  3311. event.preventDefault();
  3312. context.invoke(eventName);
  3313. } else if (key.isEdit(event.keyCode)) {
  3314. this.afterCommand();
  3315. }
  3316. };
  3317. /**
  3318. * createRange
  3319. *
  3320. * create range
  3321. * @return {WrappedRange}
  3322. */
  3323. this.createRange = function () {
  3324. this.focus();
  3325. return range.create();
  3326. };
  3327. /**
  3328. * saveRange
  3329. *
  3330. * save current range
  3331. *
  3332. * @param {Boolean} [thenCollapse=false]
  3333. */
  3334. this.saveRange = function (thenCollapse) {
  3335. this.focus();
  3336. $editable.data('range', range.create());
  3337. if (thenCollapse) {
  3338. range.create().collapse().select();
  3339. }
  3340. };
  3341. /**
  3342. * restoreRange
  3343. *
  3344. * restore lately range
  3345. */
  3346. this.restoreRange = function () {
  3347. var rng = $editable.data('range');
  3348. if (rng) {
  3349. rng.select();
  3350. this.focus();
  3351. }
  3352. };
  3353. this.saveTarget = function (node) {
  3354. $editable.data('target', node);
  3355. };
  3356. this.clearTarget = function () {
  3357. $editable.removeData('target');
  3358. };
  3359. this.restoreTarget = function () {
  3360. return $editable.data('target');
  3361. };
  3362. /**
  3363. * currentStyle
  3364. *
  3365. * current style
  3366. * @return {Object|Boolean} unfocus
  3367. */
  3368. this.currentStyle = function () {
  3369. var rng = range.create();
  3370. if (rng) {
  3371. rng = rng.normalize();
  3372. }
  3373. return rng ? style.current(rng) : style.fromNode($editable);
  3374. };
  3375. /**
  3376. * style from node
  3377. *
  3378. * @param {jQuery} $node
  3379. * @return {Object}
  3380. */
  3381. this.styleFromNode = function ($node) {
  3382. return style.fromNode($node);
  3383. };
  3384. /**
  3385. * undo
  3386. */
  3387. this.undo = function () {
  3388. context.triggerEvent('before.command', $editable.html());
  3389. history.undo();
  3390. context.triggerEvent('change', $editable.html());
  3391. };
  3392. context.memo('help.undo', lang.help.undo);
  3393. /**
  3394. * redo
  3395. */
  3396. this.redo = function () {
  3397. context.triggerEvent('before.command', $editable.html());
  3398. history.redo();
  3399. context.triggerEvent('change', $editable.html());
  3400. };
  3401. context.memo('help.redo', lang.help.redo);
  3402. /**
  3403. * beforeCommand
  3404. * before command
  3405. */
  3406. var beforeCommand = this.beforeCommand = function () {
  3407. context.triggerEvent('before.command', $editable.html());
  3408. // keep focus on editable before command execution
  3409. self.focus();
  3410. };
  3411. /**
  3412. * afterCommand
  3413. * after command
  3414. * @param {Boolean} isPreventTrigger
  3415. */
  3416. var afterCommand = this.afterCommand = function (isPreventTrigger) {
  3417. history.recordUndo();
  3418. if (!isPreventTrigger) {
  3419. context.triggerEvent('change', $editable.html());
  3420. }
  3421. };
  3422. /* jshint ignore:start */
  3423. // native commands(with execCommand), generate function for execCommand
  3424. var commands = ['bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript',
  3425. 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull',
  3426. 'formatBlock', 'removeFormat',
  3427. 'backColor', 'foreColor', 'fontName'];
  3428. for (var idx = 0, len = commands.length; idx < len; idx ++) {
  3429. this[commands[idx]] = (function (sCmd) {
  3430. return function (value) {
  3431. beforeCommand();
  3432. document.execCommand(sCmd, false, value);
  3433. afterCommand(true);
  3434. };
  3435. })(commands[idx]);
  3436. context.memo('help.' + commands[idx], lang.help[commands[idx]]);
  3437. }
  3438. /* jshint ignore:end */
  3439. /**
  3440. * tab
  3441. *
  3442. * handle tab key
  3443. */
  3444. this.tab = function () {
  3445. var rng = this.createRange();
  3446. if (rng.isCollapsed() && rng.isOnCell()) {
  3447. table.tab(rng);
  3448. } else {
  3449. beforeCommand();
  3450. typing.insertTab($editable, rng, options.tabSize);
  3451. afterCommand();
  3452. }
  3453. };
  3454. context.memo('help.tab', lang.help.tab);
  3455. /**
  3456. * untab
  3457. *
  3458. * handle shift+tab key
  3459. *
  3460. */
  3461. this.untab = function () {
  3462. var rng = this.createRange();
  3463. if (rng.isCollapsed() && rng.isOnCell()) {
  3464. table.tab(rng, true);
  3465. }
  3466. };
  3467. context.memo('help.untab', lang.help.untab);
  3468. /**
  3469. * wrapCommand
  3470. *
  3471. * run given function between beforeCommand and afterCommand
  3472. */
  3473. this.wrapCommand = function (fn) {
  3474. return function () {
  3475. beforeCommand();
  3476. fn.apply(self, arguments);
  3477. afterCommand();
  3478. };
  3479. };
  3480. /**
  3481. * insertParagraph
  3482. *
  3483. * insert paragraph
  3484. */
  3485. this.insertParagraph = this.wrapCommand(function () {
  3486. typing.insertParagraph($editable);
  3487. });
  3488. context.memo('help.insertParagraph', lang.help.insertParagraph);
  3489. /**
  3490. * insertOrderedList
  3491. */
  3492. this.insertOrderedList = this.wrapCommand(function () {
  3493. bullet.insertOrderedList($editable);
  3494. });
  3495. context.memo('help.insertOrderedList', lang.help.insertOrderedList);
  3496. this.insertUnorderedList = this.wrapCommand(function () {
  3497. bullet.insertUnorderedList($editable);
  3498. });
  3499. context.memo('help.insertUnorderedList', lang.help.insertUnorderedList);
  3500. this.indent = this.wrapCommand(function () {
  3501. bullet.indent($editable);
  3502. });
  3503. context.memo('help.indent', lang.help.indent);
  3504. this.outdent = this.wrapCommand(function () {
  3505. bullet.outdent($editable);
  3506. });
  3507. context.memo('help.outdent', lang.help.outdent);
  3508. /**
  3509. * insert image
  3510. *
  3511. * @param {String} src
  3512. * @param {String|Function} param
  3513. * @return {Promise}
  3514. */
  3515. this.insertImage = function (src, param) {
  3516. return async.createImage(src, param).then(function ($image) {
  3517. beforeCommand();
  3518. if (typeof param === 'function') {
  3519. param($image);
  3520. } else {
  3521. if (typeof param === 'string') {
  3522. $image.attr('data-filename', param);
  3523. }
  3524. $image.css('width', Math.min($editable.width(), $image.width()));
  3525. }
  3526. $image.show();
  3527. range.create().insertNode($image[0]);
  3528. range.createFromNodeAfter($image[0]).select();
  3529. afterCommand();
  3530. }).fail(function () {
  3531. context.triggerEvent('image.upload.error');
  3532. });
  3533. };
  3534. /**
  3535. * insertImages
  3536. * @param {File[]} files
  3537. */
  3538. this.insertImages = function (files) {
  3539. $.each(files, function (idx, file) {
  3540. var filename = file.name;
  3541. if (options.maximumImageFileSize && options.maximumImageFileSize < file.size) {
  3542. context.triggerEvent('image.upload.error', lang.image.maximumFileSizeError);
  3543. } else {
  3544. async.readFileAsDataURL(file).then(function (dataURL) {
  3545. return self.insertImage(dataURL, filename);
  3546. }).fail(function () {
  3547. context.triggerEvent('image.upload.error');
  3548. });
  3549. }
  3550. });
  3551. };
  3552. /**
  3553. * insertImagesOrCallback
  3554. * @param {File[]} files
  3555. */
  3556. this.insertImagesOrCallback = function (files) {
  3557. var callbacks = options.callbacks;
  3558. // If onImageUpload options setted
  3559. if (callbacks.onImageUpload) {
  3560. context.triggerEvent('image.upload', files);
  3561. // else insert Image as dataURL
  3562. } else {
  3563. this.insertImages(files);
  3564. }
  3565. };
  3566. /**
  3567. * insertNode
  3568. * insert node
  3569. * @param {Node} node
  3570. */
  3571. this.insertNode = this.wrapCommand(function (node) {
  3572. range.create().insertNode(node);
  3573. range.createFromNodeAfter(node).select();
  3574. });
  3575. /**
  3576. * insert text
  3577. * @param {String} text
  3578. */
  3579. this.insertText = this.wrapCommand(function (text) {
  3580. var textNode = range.create().insertNode(dom.createText(text));
  3581. range.create(textNode, dom.nodeLength(textNode)).select();
  3582. });
  3583. /**
  3584. * return selected plain text
  3585. * @return {String} text
  3586. */
  3587. this.getSelectedText = function () {
  3588. var rng = this.createRange();
  3589. // if range on anchor, expand range with anchor
  3590. if (rng.isOnAnchor()) {
  3591. rng = range.createFromNode(dom.ancestor(rng.sc, dom.isAnchor));
  3592. }
  3593. return rng.toString();
  3594. };
  3595. /**
  3596. * paste HTML
  3597. * @param {String} markup
  3598. */
  3599. this.pasteHTML = this.wrapCommand(function (markup) {
  3600. var contents = range.create().pasteHTML(markup);
  3601. range.createFromNodeAfter(list.last(contents)).select();
  3602. });
  3603. /**
  3604. * formatBlock
  3605. *
  3606. * @param {String} tagName
  3607. */
  3608. this.formatBlock = this.wrapCommand(function (tagName) {
  3609. // [workaround] for MSIE, IE need `<`
  3610. tagName = agent.isMSIE ? '<' + tagName + '>' : tagName;
  3611. document.execCommand('FormatBlock', false, tagName);
  3612. });
  3613. this.formatPara = function () {
  3614. this.formatBlock('P');
  3615. };
  3616. context.memo('help.formatPara', lang.help.formatPara);
  3617. /* jshint ignore:start */
  3618. for (var idx = 1; idx <= 6; idx ++) {
  3619. this['formatH' + idx] = function (idx) {
  3620. return function () {
  3621. this.formatBlock('H' + idx);
  3622. };
  3623. }(idx);
  3624. context.memo('help.formatH'+idx, lang.help['formatH' + idx]);
  3625. };
  3626. /* jshint ignore:end */
  3627. /**
  3628. * fontSize
  3629. *
  3630. * @param {String} value - px
  3631. */
  3632. this.fontSize = function (value) {
  3633. this.focus();
  3634. var rng = range.create();
  3635. if (rng && rng.isCollapsed()) {
  3636. var spans = style.styleNodes(rng);
  3637. var firstSpan = list.head(spans);
  3638. $(spans).css({
  3639. 'font-size': value + 'px'
  3640. });
  3641. // [workaround] added styled bogus span for style
  3642. // - also bogus character needed for cursor position
  3643. if (firstSpan && !dom.nodeLength(firstSpan)) {
  3644. firstSpan.innerHTML = dom.ZERO_WIDTH_NBSP_CHAR;
  3645. range.createFromNodeAfter(firstSpan.firstChild).select();
  3646. $editable.data(KEY_BOGUS, firstSpan);
  3647. }
  3648. } else {
  3649. beforeCommand();
  3650. $(style.styleNodes(rng)).css({
  3651. 'font-size': value + 'px'
  3652. });
  3653. afterCommand();
  3654. }
  3655. };
  3656. /**
  3657. * insert horizontal rule
  3658. */
  3659. this.insertHorizontalRule = this.wrapCommand(function () {
  3660. var rng = range.create();
  3661. var hrNode = rng.insertNode($('<HR/>')[0]);
  3662. if (hrNode.nextSibling) {
  3663. range.create(hrNode.nextSibling, 0).normalize().select();
  3664. }
  3665. });
  3666. context.memo('help.insertHorizontalRule', lang.help.insertHorizontalRule);
  3667. /**
  3668. * remove bogus node and character
  3669. */
  3670. this.removeBogus = function () {
  3671. var bogusNode = $editable.data(KEY_BOGUS);
  3672. if (!bogusNode) {
  3673. return;
  3674. }
  3675. var textNode = list.find(list.from(bogusNode.childNodes), dom.isText);
  3676. var bogusCharIdx = textNode.nodeValue.indexOf(dom.ZERO_WIDTH_NBSP_CHAR);
  3677. if (bogusCharIdx !== -1) {
  3678. textNode.deleteData(bogusCharIdx, 1);
  3679. }
  3680. if (dom.isEmpty(bogusNode)) {
  3681. dom.remove(bogusNode);
  3682. }
  3683. $editable.removeData(KEY_BOGUS);
  3684. };
  3685. /**
  3686. * lineHeight
  3687. * @param {String} value
  3688. */
  3689. this.lineHeight = this.wrapCommand(function (value) {
  3690. style.stylePara(range.create(), {
  3691. lineHeight: value
  3692. });
  3693. });
  3694. /**
  3695. * unlink
  3696. *
  3697. * @type command
  3698. */
  3699. this.unlink = function () {
  3700. var rng = this.createRange();
  3701. if (rng.isOnAnchor()) {
  3702. var anchor = dom.ancestor(rng.sc, dom.isAnchor);
  3703. rng = range.createFromNode(anchor);
  3704. rng.select();
  3705. beforeCommand();
  3706. document.execCommand('unlink');
  3707. afterCommand();
  3708. }
  3709. };
  3710. /**
  3711. * create link (command)
  3712. *
  3713. * @param {Object} linkInfo
  3714. */
  3715. this.createLink = this.wrapCommand(function (linkInfo) {
  3716. var linkUrl = linkInfo.url;
  3717. var linkText = linkInfo.text;
  3718. var isNewWindow = linkInfo.isNewWindow;
  3719. var rng = linkInfo.range || this.createRange();
  3720. var isTextChanged = rng.toString() !== linkText;
  3721. if (options.onCreateLink) {
  3722. linkUrl = options.onCreateLink(linkUrl);
  3723. }
  3724. var anchors = [];
  3725. if (isTextChanged) {
  3726. // Create a new link when text changed.
  3727. var anchor = rng.insertNode($('<A>' + linkText + '</A>')[0]);
  3728. anchors.push(anchor);
  3729. } else {
  3730. anchors = style.styleNodes(rng, {
  3731. nodeName: 'A',
  3732. expandClosestSibling: true,
  3733. onlyPartialContains: true
  3734. });
  3735. }
  3736. $.each(anchors, function (idx, anchor) {
  3737. $(anchor).attr('href', linkUrl);
  3738. if (isNewWindow) {
  3739. $(anchor).attr('target', '_blank');
  3740. } else {
  3741. $(anchor).removeAttr('target');
  3742. }
  3743. });
  3744. var startRange = range.createFromNodeBefore(list.head(anchors));
  3745. var startPoint = startRange.getStartPoint();
  3746. var endRange = range.createFromNodeAfter(list.last(anchors));
  3747. var endPoint = endRange.getEndPoint();
  3748. range.create(
  3749. startPoint.node,
  3750. startPoint.offset,
  3751. endPoint.node,
  3752. endPoint.offset
  3753. ).select();
  3754. });
  3755. /**
  3756. * returns link info
  3757. *
  3758. * @return {Object}
  3759. * @return {WrappedRange} return.range
  3760. * @return {String} return.text
  3761. * @return {Boolean} [return.isNewWindow=true]
  3762. * @return {String} [return.url=""]
  3763. */
  3764. this.getLinkInfo = function () {
  3765. this.focus();
  3766. var rng = range.create().expand(dom.isAnchor);
  3767. // Get the first anchor on range(for edit).
  3768. var $anchor = $(list.head(rng.nodes(dom.isAnchor)));
  3769. return {
  3770. range: rng,
  3771. text: rng.toString(),
  3772. isNewWindow: $anchor.length ? $anchor.attr('target') === '_blank' : false,
  3773. url: $anchor.length ? $anchor.attr('href') : ''
  3774. };
  3775. };
  3776. /**
  3777. * setting color
  3778. *
  3779. * @param {Object} sObjColor color code
  3780. * @param {String} sObjColor.foreColor foreground color
  3781. * @param {String} sObjColor.backColor background color
  3782. */
  3783. this.color = this.wrapCommand(function (colorInfo) {
  3784. var foreColor = colorInfo.foreColor;
  3785. var backColor = colorInfo.backColor;
  3786. if (foreColor) { document.execCommand('foreColor', false, foreColor); }
  3787. if (backColor) { document.execCommand('backColor', false, backColor); }
  3788. });
  3789. /**
  3790. * insert Table
  3791. *
  3792. * @param {String} sDim dimension of table (ex : "5x5")
  3793. */
  3794. this.insertTable = this.wrapCommand(function (sDim) {
  3795. var dimension = sDim.split('x');
  3796. var rng = range.create().deleteContents();
  3797. rng.insertNode(table.createTable(dimension[0], dimension[1], options));
  3798. });
  3799. /**
  3800. * float me
  3801. *
  3802. * @param {String} value
  3803. */
  3804. this.floatMe = this.wrapCommand(function (value) {
  3805. var $target = $(this.restoreTarget());
  3806. $target.css('float', value);
  3807. });
  3808. /**
  3809. * resize overlay element
  3810. * @param {String} value
  3811. */
  3812. this.resize = this.wrapCommand(function (value) {
  3813. var $target = $(this.restoreTarget());
  3814. $target.css({
  3815. width: value * 100 + '%',
  3816. height: ''
  3817. });
  3818. });
  3819. /**
  3820. * @param {Position} pos
  3821. * @param {jQuery} $target - target element
  3822. * @param {Boolean} [bKeepRatio] - keep ratio
  3823. */
  3824. this.resizeTo = function (pos, $target, bKeepRatio) {
  3825. var imageSize;
  3826. if (bKeepRatio) {
  3827. var newRatio = pos.y / pos.x;
  3828. var ratio = $target.data('ratio');
  3829. imageSize = {
  3830. width: ratio > newRatio ? pos.x : pos.y / ratio,
  3831. height: ratio > newRatio ? pos.x * ratio : pos.y
  3832. };
  3833. } else {
  3834. imageSize = {
  3835. width: pos.x,
  3836. height: pos.y
  3837. };
  3838. }
  3839. $target.css(imageSize);
  3840. };
  3841. /**
  3842. * remove media object
  3843. */
  3844. this.removeMedia = this.wrapCommand(function () {
  3845. var $target = $(this.restoreTarget()).detach();
  3846. context.triggerEvent('media.delete', $target, $editable);
  3847. });
  3848. /**
  3849. * set focus
  3850. */
  3851. this.focus = function () {
  3852. // [workaround] Screen will move when page is scolled in IE.
  3853. // - do focus when not focused
  3854. if (!$editable.is(':focus')) {
  3855. $editable.focus();
  3856. // [workaround] for firefox bug http://goo.gl/lVfAaI
  3857. if (!$editable.is(':focus') && agent.isFF) {
  3858. range.createFromNode($editable[0])
  3859. .normalize()
  3860. .collapse()
  3861. .select();
  3862. }
  3863. }
  3864. };
  3865. /**
  3866. * returns whether contents is empty or not.
  3867. * @return {Boolean}
  3868. */
  3869. this.isEmpty = function () {
  3870. return dom.isEmpty($editable[0]) || dom.emptyPara === $editable.html();
  3871. };
  3872. };
  3873. var Clipboard = function (context) {
  3874. var self = this;
  3875. var $editable = context.layoutInfo.editable;
  3876. this.events = {
  3877. 'summernote.keydown': function (we, e) {
  3878. if (self.needKeydownHook()) {
  3879. if ((e.ctrlKey || e.metaKey) && e.keyCode === key.code.V) {
  3880. context.invoke('editor.saveRange');
  3881. self.$paste.focus();
  3882. setTimeout(function () {
  3883. self.pasteByHook();
  3884. }, 0);
  3885. }
  3886. }
  3887. }
  3888. };
  3889. this.needKeydownHook = function () {
  3890. return (agent.isMSIE && agent.browserVersion > 10) || agent.isFF;
  3891. };
  3892. this.initialize = function () {
  3893. // [workaround] getting image from clipboard
  3894. // - IE11 and Firefox: CTRL+v hook
  3895. // - Webkit: event.clipboardData
  3896. if (this.needKeydownHook()) {
  3897. this.$paste = $('<div />').attr('contenteditable', true).css({
  3898. position : 'absolute',
  3899. left : -100000,
  3900. opacity : 0
  3901. });
  3902. $editable.before(this.$paste);
  3903. this.$paste.on('paste', function (event) {
  3904. context.triggerEvent('paste', event);
  3905. });
  3906. } else {
  3907. $editable.on('paste', this.pasteByEvent);
  3908. }
  3909. };
  3910. this.destroy = function () {
  3911. if (this.needKeydownHook()) {
  3912. this.$paste.remove();
  3913. this.$paste = null;
  3914. }
  3915. };
  3916. this.pasteByHook = function () {
  3917. var node = this.$paste[0].firstChild;
  3918. if (dom.isImg(node)) {
  3919. var dataURI = node.src;
  3920. var decodedData = atob(dataURI.split(',')[1]);
  3921. var array = new Uint8Array(decodedData.length);
  3922. for (var i = 0; i < decodedData.length; i++) {
  3923. array[i] = decodedData.charCodeAt(i);
  3924. }
  3925. var blob = new Blob([array], { type : 'image/png' });
  3926. blob.name = 'clipboard.png';
  3927. context.invoke('editor.restoreRange');
  3928. context.invoke('editor.focus');
  3929. context.invoke('editor.insertImagesOrCallback', [blob]);
  3930. } else {
  3931. var pasteContent = $('<div />').html(this.$paste.html()).html();
  3932. context.invoke('editor.restoreRange');
  3933. context.invoke('editor.focus');
  3934. if (pasteContent) {
  3935. context.invoke('editor.pasteHTML', pasteContent);
  3936. }
  3937. }
  3938. this.$paste.empty();
  3939. };
  3940. /**
  3941. * paste by clipboard event
  3942. *
  3943. * @param {Event} event
  3944. */
  3945. this.pasteByEvent = function (event) {
  3946. var clipboardData = event.originalEvent.clipboardData;
  3947. if (clipboardData && clipboardData.items && clipboardData.items.length) {
  3948. var item = list.head(clipboardData.items);
  3949. if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {
  3950. context.invoke('editor.insertImagesOrCallback', [item.getAsFile()]);
  3951. }
  3952. context.invoke('editor.afterCommand');
  3953. }
  3954. };
  3955. };
  3956. var Dropzone = function (context) {
  3957. var $document = $(document);
  3958. var $editor = context.layoutInfo.editor;
  3959. var $editable = context.layoutInfo.editable;
  3960. var options = context.options;
  3961. var lang = options.langInfo;
  3962. var $dropzone = $([
  3963. '<div class="note-dropzone">',
  3964. ' <div class="note-dropzone-message"/>',
  3965. '</div>'
  3966. ].join('')).prependTo($editor);
  3967. /**
  3968. * attach Drag and Drop Events
  3969. */
  3970. this.initialize = function () {
  3971. if (options.disableDragAndDrop) {
  3972. // prevent default drop event
  3973. $document.on('drop', function (e) {
  3974. e.preventDefault();
  3975. });
  3976. } else {
  3977. this.attachDragAndDropEvent();
  3978. }
  3979. };
  3980. /**
  3981. * attach Drag and Drop Events
  3982. */
  3983. this.attachDragAndDropEvent = function () {
  3984. var collection = $(),
  3985. $dropzoneMessage = $dropzone.find('.note-dropzone-message');
  3986. // show dropzone on dragenter when dragging a object to document
  3987. // -but only if the editor is visible, i.e. has a positive width and height
  3988. $document.on('dragenter', function (e) {
  3989. var isCodeview = context.invoke('codeview.isActivated');
  3990. var hasEditorSize = $editor.width() > 0 && $editor.height() > 0;
  3991. if (!isCodeview && !collection.length && hasEditorSize) {
  3992. $editor.addClass('dragover');
  3993. $dropzone.width($editor.width());
  3994. $dropzone.height($editor.height());
  3995. $dropzoneMessage.text(lang.image.dragImageHere);
  3996. }
  3997. collection = collection.add(e.target);
  3998. }).on('dragleave', function (e) {
  3999. collection = collection.not(e.target);
  4000. if (!collection.length) {
  4001. $editor.removeClass('dragover');
  4002. }
  4003. }).on('drop', function () {
  4004. collection = $();
  4005. $editor.removeClass('dragover');
  4006. });
  4007. // change dropzone's message on hover.
  4008. $dropzone.on('dragenter', function () {
  4009. $dropzone.addClass('hover');
  4010. $dropzoneMessage.text(lang.image.dropImage);
  4011. }).on('dragleave', function () {
  4012. $dropzone.removeClass('hover');
  4013. $dropzoneMessage.text(lang.image.dragImageHere);
  4014. });
  4015. // attach dropImage
  4016. $dropzone.on('drop', function (event) {
  4017. var dataTransfer = event.originalEvent.dataTransfer;
  4018. if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
  4019. event.preventDefault();
  4020. $editable.focus();
  4021. context.invoke('editor.insertImagesOrCallback', dataTransfer.files);
  4022. } else {
  4023. $.each(dataTransfer.types, function (idx, type) {
  4024. var content = dataTransfer.getData(type);
  4025. if (type.toLowerCase().indexOf('text') > -1) {
  4026. context.invoke('editor.pasteHTML', content);
  4027. } else {
  4028. $(content).each(function () {
  4029. context.invoke('editor.insertNode', this);
  4030. });
  4031. }
  4032. });
  4033. }
  4034. }).on('dragover', false); // prevent default dragover event
  4035. };
  4036. };
  4037. var CodeMirror;
  4038. if (agent.hasCodeMirror) {
  4039. if (agent.isSupportAmd) {
  4040. require(['codemirror'], function (cm) {
  4041. CodeMirror = cm;
  4042. });
  4043. } else {
  4044. CodeMirror = window.CodeMirror;
  4045. }
  4046. }
  4047. /**
  4048. * @class Codeview
  4049. */
  4050. var Codeview = function (context) {
  4051. var $editor = context.layoutInfo.editor;
  4052. var $editable = context.layoutInfo.editable;
  4053. var $codable = context.layoutInfo.codable;
  4054. var options = context.options;
  4055. this.sync = function () {
  4056. var isCodeview = this.isActivated();
  4057. if (isCodeview && agent.hasCodeMirror) {
  4058. $codable.data('cmEditor').save();
  4059. }
  4060. };
  4061. /**
  4062. * @return {Boolean}
  4063. */
  4064. this.isActivated = function () {
  4065. return $editor.hasClass('codeview');
  4066. };
  4067. /**
  4068. * toggle codeview
  4069. */
  4070. this.toggle = function () {
  4071. if (this.isActivated()) {
  4072. this.deactivate();
  4073. } else {
  4074. this.activate();
  4075. }
  4076. context.triggerEvent('codeview.toggled');
  4077. };
  4078. /**
  4079. * activate code view
  4080. */
  4081. this.activate = function () {
  4082. $codable.val(dom.html($editable, options.prettifyHtml));
  4083. $codable.height($editable.height());
  4084. context.invoke('toolbar.updateCodeview', true);
  4085. $editor.addClass('codeview');
  4086. $codable.focus();
  4087. // activate CodeMirror as codable
  4088. if (agent.hasCodeMirror) {
  4089. var cmEditor = CodeMirror.fromTextArea($codable[0], options.codemirror);
  4090. /*
  4091. * MODIFIED: Adding support for CodeMirror callbacks.
  4092. * If we've got any callbacks for code mirror, register them.
  4093. */
  4094. if ( 'undefined' != typeof options.codemirror.callbacks ) {
  4095. for ( var key in options.codemirror.callbacks ) {
  4096. if ( options.codemirror.callbacks.hasOwnProperty( key ) ) {
  4097. if ( 'onBlur' == key ) {
  4098. var event = 'blur';
  4099. } else if ( 'onFocus' == key ) {
  4100. var event = 'focus';
  4101. } else if ( 'onChange' == key ) {
  4102. var event = 'change';
  4103. }
  4104. cmEditor.on( event, options.codemirror.callbacks[ key ], cmEditor );
  4105. }
  4106. }
  4107. }
  4108. // CodeMirror TernServer
  4109. if (options.codemirror.tern) {
  4110. var server = new CodeMirror.TernServer(options.codemirror.tern);
  4111. cmEditor.ternServer = server;
  4112. cmEditor.on('cursorActivity', function (cm) {
  4113. server.updateArgHints(cm);
  4114. });
  4115. }
  4116. // CodeMirror hasn't Padding.
  4117. cmEditor.setSize(null, $editable.outerHeight());
  4118. $codable.data('cmEditor', cmEditor);
  4119. }
  4120. };
  4121. /**
  4122. * deactivate code view
  4123. */
  4124. this.deactivate = function () {
  4125. // deactivate CodeMirror as codable
  4126. if (agent.hasCodeMirror) {
  4127. var cmEditor = $codable.data('cmEditor');
  4128. $codable.val(cmEditor.getValue());
  4129. cmEditor.toTextArea();
  4130. }
  4131. var value = dom.value($codable, options.prettifyHtml) || dom.emptyPara;
  4132. var isChange = $editable.html() !== value;
  4133. $editable.html(value);
  4134. $editable.height(options.height ? $codable.height() : 'auto');
  4135. $editor.removeClass('codeview');
  4136. if (isChange) {
  4137. context.triggerEvent('change', $editable.html(), $editable);
  4138. }
  4139. $editable.focus();
  4140. context.invoke('toolbar.updateCodeview', false);
  4141. };
  4142. this.destroy = function () {
  4143. if (this.isActivated()) {
  4144. this.deactivate();
  4145. }
  4146. };
  4147. };
  4148. var EDITABLE_PADDING = 24;
  4149. var Statusbar = function (context) {
  4150. var $document = $(document);
  4151. var $statusbar = context.layoutInfo.statusbar;
  4152. var $editable = context.layoutInfo.editable;
  4153. var options = context.options;
  4154. this.initialize = function () {
  4155. if (options.airMode || options.disableResizeEditor) {
  4156. return;
  4157. }
  4158. $statusbar.on('mousedown', function (event) {
  4159. event.preventDefault();
  4160. event.stopPropagation();
  4161. var editableTop = $editable.offset().top - $document.scrollTop();
  4162. $document.on('mousemove', function (event) {
  4163. var height = event.clientY - (editableTop + EDITABLE_PADDING);
  4164. height = (options.minheight > 0) ? Math.max(height, options.minheight) : height;
  4165. height = (options.maxHeight > 0) ? Math.min(height, options.maxHeight) : height;
  4166. $editable.height(height);
  4167. }).one('mouseup', function () {
  4168. $document.off('mousemove');
  4169. });
  4170. });
  4171. };
  4172. this.destroy = function () {
  4173. $statusbar.off();
  4174. };
  4175. };
  4176. var Fullscreen = function (context) {
  4177. var $editor = context.layoutInfo.editor;
  4178. var $toolbar = context.layoutInfo.toolbar;
  4179. var $editable = context.layoutInfo.editable;
  4180. var $codable = context.layoutInfo.codable;
  4181. var $window = $(window);
  4182. var $scrollbar = $('html, body');
  4183. /**
  4184. * toggle fullscreen
  4185. */
  4186. this.toggle = function () {
  4187. var resize = function (size) {
  4188. $editable.css('height', size.h);
  4189. $codable.css('height', size.h);
  4190. if ($codable.data('cmeditor')) {
  4191. $codable.data('cmeditor').setsize(null, size.h);
  4192. }
  4193. };
  4194. $editor.toggleClass('fullscreen');
  4195. var isFullscreen = $editor.hasClass('fullscreen');
  4196. if (isFullscreen) {
  4197. $editable.data('orgHeight', $editable.css('height'));
  4198. $window.on('resize', function () {
  4199. resize({
  4200. h: $window.height() - $toolbar.outerHeight()
  4201. });
  4202. }).trigger('resize');
  4203. $scrollbar.css('overflow', 'hidden');
  4204. } else {
  4205. $window.off('resize');
  4206. resize({
  4207. h: $editable.data('orgHeight')
  4208. });
  4209. $scrollbar.css('overflow', 'visible');
  4210. }
  4211. context.invoke('toolbar.updateFullscreen', isFullscreen);
  4212. };
  4213. };
  4214. var Handle = function (context) {
  4215. var self = this;
  4216. var $document = $(document);
  4217. var $editingArea = context.layoutInfo.editingArea;
  4218. var options = context.options;
  4219. this.events = {
  4220. 'summernote.mousedown': function (we, e) {
  4221. if (self.update(e.target)) {
  4222. e.preventDefault();
  4223. }
  4224. },
  4225. 'summernote.keyup summernote.scroll summernote.change summernote.dialog.shown': function () {
  4226. self.update();
  4227. }
  4228. };
  4229. this.initialize = function () {
  4230. this.$handle = $([
  4231. '<div class="note-handle">',
  4232. '<div class="note-control-selection">',
  4233. '<div class="note-control-selection-bg"></div>',
  4234. '<div class="note-control-holder note-control-nw"></div>',
  4235. '<div class="note-control-holder note-control-ne"></div>',
  4236. '<div class="note-control-holder note-control-sw"></div>',
  4237. '<div class="',
  4238. (options.disableResizeImage ? 'note-control-holder' : 'note-control-sizing'),
  4239. ' note-control-se"></div>',
  4240. (options.disableResizeImage ? '' : '<div class="note-control-selection-info"></div>'),
  4241. '</div>',
  4242. '</div>'
  4243. ].join('')).prependTo($editingArea);
  4244. this.$handle.on('mousedown', function (event) {
  4245. if (dom.isControlSizing(event.target)) {
  4246. event.preventDefault();
  4247. event.stopPropagation();
  4248. var $target = self.$handle.find('.note-control-selection').data('target'),
  4249. posStart = $target.offset(),
  4250. scrollTop = $document.scrollTop();
  4251. $document.on('mousemove', function (event) {
  4252. context.invoke('editor.resizeTo', {
  4253. x: event.clientX - posStart.left,
  4254. y: event.clientY - (posStart.top - scrollTop)
  4255. }, $target, !event.shiftKey);
  4256. self.update($target[0]);
  4257. }).one('mouseup', function (e) {
  4258. e.preventDefault();
  4259. $document.off('mousemove');
  4260. context.invoke('editor.afterCommand');
  4261. });
  4262. if (!$target.data('ratio')) { // original ratio.
  4263. $target.data('ratio', $target.height() / $target.width());
  4264. }
  4265. }
  4266. });
  4267. };
  4268. this.destroy = function () {
  4269. this.$handle.remove();
  4270. };
  4271. this.update = function (target) {
  4272. var isImage = dom.isImg(target);
  4273. var $selection = this.$handle.find('.note-control-selection');
  4274. context.invoke('imagePopover.update', target);
  4275. if (isImage) {
  4276. var $image = $(target);
  4277. var pos = $image.position();
  4278. // include margin
  4279. var imageSize = {
  4280. w: $image.outerWidth(true),
  4281. h: $image.outerHeight(true)
  4282. };
  4283. $selection.css({
  4284. display: 'block',
  4285. left: pos.left,
  4286. top: pos.top,
  4287. width: imageSize.w,
  4288. height: imageSize.h
  4289. }).data('target', $image); // save current image element.
  4290. var sizingText = imageSize.w + 'x' + imageSize.h;
  4291. $selection.find('.note-control-selection-info').text(sizingText);
  4292. context.invoke('editor.saveTarget', target);
  4293. } else {
  4294. this.hide();
  4295. }
  4296. return isImage;
  4297. };
  4298. /**
  4299. * hide
  4300. *
  4301. * @param {jQuery} $handle
  4302. */
  4303. this.hide = function () {
  4304. context.invoke('editor.clearTarget');
  4305. this.$handle.children().hide();
  4306. };
  4307. };
  4308. var AutoLink = function (context) {
  4309. var self = this;
  4310. var linkPattern = /^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+-]+@)(.+)$/i;
  4311. this.events = {
  4312. 'summernote.keyup': function (we, e) {
  4313. if (!e.isDefaultPrevented()) {
  4314. self.handleKeyup(e);
  4315. }
  4316. },
  4317. 'summernote.keydown': function (we, e) {
  4318. self.handleKeydown(e);
  4319. }
  4320. };
  4321. this.initialize = function () {
  4322. this.lastWordRange = null;
  4323. };
  4324. this.destroy = function () {
  4325. this.lastWordRange = null;
  4326. };
  4327. this.replace = function () {
  4328. if (!this.lastWordRange) {
  4329. return;
  4330. }
  4331. var keyword = this.lastWordRange.toString();
  4332. if (linkPattern.test(keyword)) {
  4333. var node = this.nodeFromKeyword(keyword);
  4334. this.lastWordRange.insertNode(node);
  4335. this.lastWordRange = null;
  4336. context.invoke('editor.focus');
  4337. }
  4338. };
  4339. this.nodeFromKeyword = function (keyword) {
  4340. return $('<a />').html(keyword).attr('href', keyword)[0];
  4341. };
  4342. this.handleKeydown = function (e) {
  4343. if (list.contains([key.code.ENTER, key.code.SPACE], e.keyCode)) {
  4344. var wordRange = context.invoke('editor.createRange').getWordRange();
  4345. this.lastWordRange = wordRange;
  4346. }
  4347. };
  4348. this.handleKeyup = function (e) {
  4349. if (list.contains([key.code.ENTER, key.code.SPACE], e.keyCode)) {
  4350. this.replace();
  4351. }
  4352. };
  4353. };
  4354. /**
  4355. * textarea auto sync.
  4356. */
  4357. var AutoSync = function (context) {
  4358. var $note = context.layoutInfo.note;
  4359. this.events = {
  4360. 'summernote.change': function () {
  4361. $note.val(context.invoke('code'));
  4362. }
  4363. };
  4364. this.shouldInitialize = function () {
  4365. return dom.isTextarea($note[0]);
  4366. };
  4367. };
  4368. var Placeholder = function (context) {
  4369. var self = this;
  4370. var $editingArea = context.layoutInfo.editingArea;
  4371. var options = context.options;
  4372. this.events = {
  4373. 'summernote.init summernote.change': function () {
  4374. self.update();
  4375. },
  4376. 'summernote.codeview.toggled': function () {
  4377. self.update();
  4378. }
  4379. };
  4380. this.shouldInitialize = function () {
  4381. return !!options.placeholder;
  4382. };
  4383. this.initialize = function () {
  4384. this.$placeholder = $('<div class="note-placeholder">');
  4385. this.$placeholder.on('click', function () {
  4386. context.invoke('focus');
  4387. }).text(options.placeholder).prependTo($editingArea);
  4388. };
  4389. this.destroy = function () {
  4390. this.$placeholder.remove();
  4391. };
  4392. this.update = function () {
  4393. var isShow = !context.invoke('codeview.isActivated') && context.invoke('editor.isEmpty');
  4394. this.$placeholder.toggle(isShow);
  4395. };
  4396. };
  4397. var Buttons = function (context) {
  4398. var self = this;
  4399. var ui = $.summernote.ui;
  4400. var $toolbar = context.layoutInfo.toolbar;
  4401. var options = context.options;
  4402. var lang = options.langInfo;
  4403. var invertedKeyMap = func.invertObject(options.keyMap[agent.isMac ? 'mac' : 'pc']);
  4404. var representShortcut = this.representShortcut = function (editorMethod) {
  4405. var shortcut = invertedKeyMap[editorMethod];
  4406. if (agent.isMac) {
  4407. shortcut = shortcut.replace('CMD', '⌘').replace('SHIFT', '⇧');
  4408. }
  4409. shortcut = shortcut.replace('BACKSLASH', '\\')
  4410. .replace('SLASH', '/')
  4411. .replace('LEFTBRACKET', '[')
  4412. .replace('RIGHTBRACKET', ']');
  4413. return ' (' + shortcut + ')';
  4414. };
  4415. this.initialize = function () {
  4416. this.addToolbarButtons();
  4417. this.addImagePopoverButtons();
  4418. this.addLinkPopoverButtons();
  4419. };
  4420. this.addToolbarButtons = function () {
  4421. context.memo('button.style', function () {
  4422. return ui.buttonGroup([
  4423. ui.button({
  4424. className: 'dropdown-toggle',
  4425. contents: ui.icon(options.icons.magic) + ' ' + ui.icon(options.icons.caret, 'span'),
  4426. tooltip: lang.style.style,
  4427. data: {
  4428. toggle: 'dropdown'
  4429. }
  4430. }),
  4431. ui.dropdown({
  4432. className: 'dropdown-style',
  4433. items: context.options.styleTags,
  4434. click: context.createInvokeHandler('editor.formatBlock')
  4435. })
  4436. ]).render();
  4437. });
  4438. context.memo('button.bold', function () {
  4439. return ui.button({
  4440. className: 'note-btn-bold',
  4441. contents: ui.icon(options.icons.bold),
  4442. tooltip: lang.font.bold + representShortcut('bold'),
  4443. click: context.createInvokeHandler('editor.bold')
  4444. }).render();
  4445. });
  4446. context.memo('button.italic', function () {
  4447. return ui.button({
  4448. className: 'note-btn-italic',
  4449. contents: ui.icon(options.icons.italic),
  4450. tooltip: lang.font.italic + representShortcut('italic'),
  4451. click: context.createInvokeHandler('editor.italic')
  4452. }).render();
  4453. });
  4454. context.memo('button.underline', function () {
  4455. return ui.button({
  4456. className: 'note-btn-underline',
  4457. contents: ui.icon(options.icons.underline),
  4458. tooltip: lang.font.underline + representShortcut('underline'),
  4459. click: context.createInvokeHandler('editor.underline')
  4460. }).render();
  4461. });
  4462. context.memo('button.clear', function () {
  4463. return ui.button({
  4464. contents: ui.icon(options.icons.eraser),
  4465. tooltip: lang.font.clear + representShortcut('removeFormat'),
  4466. click: context.createInvokeHandler('editor.removeFormat')
  4467. }).render();
  4468. });
  4469. context.memo('button.strikethrough', function () {
  4470. return ui.button({
  4471. contents: ui.icon(options.icons.strikethrough),
  4472. tooltip: lang.font.strikethrough + representShortcut('strikethrough'),
  4473. click: context.createInvokeHandler('editor.strikethrough')
  4474. }).render();
  4475. });
  4476. context.memo('button.superscript', function () {
  4477. return ui.button({
  4478. contents: ui.icon(options.icons.superscript),
  4479. tooltip: lang.font.superscript,
  4480. click: context.createInvokeHandler('editor.superscript')
  4481. }).render();
  4482. });
  4483. context.memo('button.subscript', function () {
  4484. return ui.button({
  4485. contents: ui.icon(options.icons.subscript),
  4486. tooltip: lang.font.subscript,
  4487. click: context.createInvokeHandler('editor.subscript')
  4488. }).render();
  4489. });
  4490. context.memo('button.fontname', function () {
  4491. return ui.buttonGroup([
  4492. ui.button({
  4493. className: 'dropdown-toggle',
  4494. contents: '<span class="note-current-fontname"/> ' + ui.icon(options.icons.caret, 'span'),
  4495. tooltip: lang.font.name,
  4496. data: {
  4497. toggle: 'dropdown'
  4498. }
  4499. }),
  4500. ui.dropdownCheck({
  4501. className: 'dropdown-fontname',
  4502. checkClassName : options.icons.menuCheck,
  4503. items: options.fontNames.filter(function (name) {
  4504. return agent.isFontInstalled(name) ||
  4505. list.contains(options.fontNamesIgnoreCheck, name);
  4506. }),
  4507. click: context.createInvokeHandler('editor.fontName')
  4508. })
  4509. ]).render();
  4510. });
  4511. context.memo('button.fontsize', function () {
  4512. return ui.buttonGroup([
  4513. ui.button({
  4514. className: 'dropdown-toggle',
  4515. contents: '<span class="note-current-fontsize"/>' + ui.icon(options.icons.caret, 'span'),
  4516. tooltip: lang.font.size,
  4517. data: {
  4518. toggle: 'dropdown'
  4519. }
  4520. }),
  4521. ui.dropdownCheck({
  4522. className: 'dropdown-fontsize',
  4523. checkClassName : options.icons.menuCheck,
  4524. items: options.fontSizes,
  4525. click: context.createInvokeHandler('editor.fontSize')
  4526. })
  4527. ]).render();
  4528. });
  4529. context.memo('button.color', function () {
  4530. return ui.buttonGroup({
  4531. className: 'note-color',
  4532. children: [
  4533. ui.button({
  4534. className : 'note-current-color-button',
  4535. contents: ui.icon(options.icons.font + ' note-recent-color'),
  4536. tooltip: lang.color.recent,
  4537. click: context.createInvokeHandler('editor.color'),
  4538. callback: function ($button) {
  4539. var $recentColor = $button.find('.note-recent-color');
  4540. $recentColor.css({
  4541. 'background-color': 'yellow'
  4542. });
  4543. $button.data('value', {
  4544. backColor: 'yellow'
  4545. });
  4546. }
  4547. }),
  4548. ui.button({
  4549. className: 'dropdown-toggle',
  4550. contents: ui.icon(options.icons.caret, 'span'),
  4551. tooltip: lang.color.more,
  4552. data: {
  4553. toggle: 'dropdown'
  4554. }
  4555. }),
  4556. ui.dropdown({
  4557. items: [
  4558. '<li>',
  4559. '<div class="btn-group">',
  4560. ' <div class="note-palette-title">' + lang.color.background + '</div>',
  4561. ' <div>',
  4562. ' <button class="note-color-reset btn btn-default" data-event="backColor" data-value="inherit">',
  4563. lang.color.transparent,
  4564. ' </button>',
  4565. ' </div>',
  4566. ' <div class="note-holder" data-event="backColor"/>',
  4567. '</div>',
  4568. '<div class="btn-group">',
  4569. ' <div class="note-palette-title">' + lang.color.foreground + '</div>',
  4570. ' <div>',
  4571. ' <button class="note-color-reset btn btn-default" data-event="removeFormat" data-value="foreColor">',
  4572. lang.color.resetToDefault,
  4573. ' </button>',
  4574. ' </div>',
  4575. ' <div class="note-holder" data-event="foreColor"/>',
  4576. '</div>',
  4577. '</li>'
  4578. ].join(''),
  4579. callback: function ($dropdown) {
  4580. $dropdown.find('.note-holder').each(function () {
  4581. var $holder = $(this);
  4582. $holder.append(ui.palette({
  4583. colors: options.colors,
  4584. eventName: $holder.data('event')
  4585. }).render());
  4586. });
  4587. },
  4588. click: function (event) {
  4589. var $button = $(event.target);
  4590. var eventName = $button.data('event');
  4591. var value = $button.data('value');
  4592. if (eventName && value) {
  4593. var key = eventName === 'backColor' ? 'background-color' : 'color';
  4594. var $color = $button.closest('.note-color').find('.note-recent-color');
  4595. var $currentButton = $button.closest('.note-color').find('.note-current-color-button');
  4596. var colorInfo = $currentButton.data('value');
  4597. colorInfo[eventName] = value;
  4598. $color.css(key, value);
  4599. $currentButton.data('value', colorInfo);
  4600. context.invoke('editor.' + eventName, value);
  4601. }
  4602. }
  4603. })
  4604. ]
  4605. }).render();
  4606. });
  4607. context.memo('button.ol', function () {
  4608. return ui.button({
  4609. contents: ui.icon(options.icons.unorderedlist),
  4610. tooltip: lang.lists.unordered + representShortcut('insertUnorderedList'),
  4611. click: context.createInvokeHandler('editor.insertUnorderedList')
  4612. }).render();
  4613. });
  4614. context.memo('button.ul', function () {
  4615. return ui.button({
  4616. contents: ui.icon(options.icons.orderedlist),
  4617. tooltip: lang.lists.ordered + representShortcut('insertOrderedList'),
  4618. click: context.createInvokeHandler('editor.insertOrderedList')
  4619. }).render();
  4620. });
  4621. context.memo('button.paragraph', function () {
  4622. return ui.buttonGroup([
  4623. ui.button({
  4624. className: 'dropdown-toggle',
  4625. contents: ui.icon(options.icons.align) + ' ' + ui.icon(options.icons.caret, 'span'),
  4626. tooltip: lang.paragraph.paragraph,
  4627. data: {
  4628. toggle: 'dropdown'
  4629. }
  4630. }),
  4631. ui.dropdown([
  4632. ui.buttonGroup({
  4633. className: 'note-align',
  4634. children: [
  4635. ui.button({
  4636. contents: ui.icon(options.icons.alignLeft),
  4637. tooltip: lang.paragraph.left + representShortcut('justifyLeft'),
  4638. click: context.createInvokeHandler('editor.justifyLeft')
  4639. }),
  4640. ui.button({
  4641. contents: ui.icon(options.icons.alignCenter),
  4642. tooltip: lang.paragraph.center + representShortcut('justifyCenter'),
  4643. click: context.createInvokeHandler('editor.justifyCenter')
  4644. }),
  4645. ui.button({
  4646. contents: ui.icon(options.icons.alignRight),
  4647. tooltip: lang.paragraph.right + representShortcut('justifyRight'),
  4648. click: context.createInvokeHandler('editor.justifyRight')
  4649. }),
  4650. ui.button({
  4651. contents: ui.icon(options.icons.alignJustify),
  4652. tooltip: lang.paragraph.justify + representShortcut('justifyFull'),
  4653. click: context.createInvokeHandler('editor.justifyFull')
  4654. })
  4655. ]
  4656. }),
  4657. ui.buttonGroup({
  4658. className: 'note-list',
  4659. children: [
  4660. ui.button({
  4661. contents: ui.icon(options.icons.outdent),
  4662. tooltip: lang.paragraph.outdent + representShortcut('outdent'),
  4663. click: context.createInvokeHandler('editor.outdent')
  4664. }),
  4665. ui.button({
  4666. contents: ui.icon(options.icons.indent),
  4667. tooltip: lang.paragraph.indent + representShortcut('indent'),
  4668. click: context.createInvokeHandler('editor.indent')
  4669. })
  4670. ]
  4671. })
  4672. ])
  4673. ]).render();
  4674. });
  4675. context.memo('button.height', function () {
  4676. return ui.buttonGroup([
  4677. ui.button({
  4678. className: 'dropdown-toggle',
  4679. contents: ui.icon(options.icons.textHeight) + ' ' + ui.icon(options.icons.caret, 'span'),
  4680. tooltip: lang.font.height,
  4681. data: {
  4682. toggle: 'dropdown'
  4683. }
  4684. }),
  4685. ui.dropdownCheck({
  4686. items: options.lineHeights,
  4687. checkClassName : options.icons.menuCheck,
  4688. className: 'dropdown-line-height',
  4689. click: context.createInvokeHandler('editor.lineHeight')
  4690. })
  4691. ]).render();
  4692. });
  4693. context.memo('button.table', function () {
  4694. return ui.buttonGroup([
  4695. ui.button({
  4696. className: 'dropdown-toggle',
  4697. contents: ui.icon(options.icons.table) + ' ' + ui.icon(options.icons.caret, 'span'),
  4698. tooltip: lang.table.table,
  4699. data: {
  4700. toggle: 'dropdown'
  4701. }
  4702. }),
  4703. ui.dropdown({
  4704. className: 'note-table',
  4705. items: [
  4706. '<div class="note-dimension-picker">',
  4707. ' <div class="note-dimension-picker-mousecatcher" data-event="insertTable" data-value="1x1"/>',
  4708. ' <div class="note-dimension-picker-highlighted"/>',
  4709. ' <div class="note-dimension-picker-unhighlighted"/>',
  4710. '</div>',
  4711. '<div class="note-dimension-display">1 x 1</div>'
  4712. ].join('')
  4713. })
  4714. ], {
  4715. callback: function ($node) {
  4716. var $catcher = $node.find('.note-dimension-picker-mousecatcher');
  4717. $catcher.css({
  4718. width: options.insertTableMaxSize.col + 'em',
  4719. height: options.insertTableMaxSize.row + 'em'
  4720. }).mousedown(context.createInvokeHandler('editor.insertTable'))
  4721. .on('mousemove', self.tableMoveHandler);
  4722. }
  4723. }).render();
  4724. });
  4725. context.memo('button.link', function () {
  4726. return ui.button({
  4727. contents: ui.icon(options.icons.link),
  4728. tooltip: lang.link.link,
  4729. click: context.createInvokeHandler('linkDialog.show')
  4730. }).render();
  4731. });
  4732. context.memo('button.picture', function () {
  4733. return ui.button({
  4734. contents: ui.icon(options.icons.picture),
  4735. tooltip: lang.image.image,
  4736. click: context.createInvokeHandler('imageDialog.show')
  4737. }).render();
  4738. });
  4739. context.memo('button.video', function () {
  4740. return ui.button({
  4741. contents: ui.icon(options.icons.video),
  4742. tooltip: lang.video.video,
  4743. click: context.createInvokeHandler('videoDialog.show')
  4744. }).render();
  4745. });
  4746. context.memo('button.hr', function () {
  4747. return ui.button({
  4748. contents: ui.icon(options.icons.minus),
  4749. tooltip: lang.hr.insert + representShortcut('insertHorizontalRule'),
  4750. click: context.createInvokeHandler('editor.insertHorizontalRule')
  4751. }).render();
  4752. });
  4753. context.memo('button.fullscreen', function () {
  4754. return ui.button({
  4755. className: 'btn-fullscreen',
  4756. contents: ui.icon(options.icons.arrowsAlt),
  4757. tooltip: lang.options.fullscreen,
  4758. click: context.createInvokeHandler('fullscreen.toggle')
  4759. }).render();
  4760. });
  4761. context.memo('button.codeview', function () {
  4762. return ui.button({
  4763. className: 'btn-codeview',
  4764. contents: ui.icon(options.icons.code),
  4765. tooltip: lang.options.codeview,
  4766. click: context.createInvokeHandler('codeview.toggle')
  4767. }).render();
  4768. });
  4769. context.memo('button.redo', function () {
  4770. return ui.button({
  4771. contents: ui.icon(options.icons.redo),
  4772. tooltip: lang.history.redo + representShortcut('redo'),
  4773. click: context.createInvokeHandler('editor.redo')
  4774. }).render();
  4775. });
  4776. context.memo('button.undo', function () {
  4777. return ui.button({
  4778. contents: ui.icon(options.icons.undo),
  4779. tooltip: lang.history.undo + representShortcut('undo'),
  4780. click: context.createInvokeHandler('editor.undo')
  4781. }).render();
  4782. });
  4783. context.memo('button.help', function () {
  4784. return ui.button({
  4785. contents: ui.icon(options.icons.question),
  4786. tooltip: lang.options.help,
  4787. click: context.createInvokeHandler('helpDialog.show')
  4788. }).render();
  4789. });
  4790. };
  4791. /**
  4792. * image : [
  4793. * ['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
  4794. * ['float', ['floatLeft', 'floatRight', 'floatNone' ]],
  4795. * ['remove', ['removeMedia']]
  4796. * ],
  4797. */
  4798. this.addImagePopoverButtons = function () {
  4799. // Image Size Buttons
  4800. context.memo('button.imageSize100', function () {
  4801. return ui.button({
  4802. contents: '<span class="note-fontsize-10">100%</span>',
  4803. tooltip: lang.image.resizeFull,
  4804. click: context.createInvokeHandler('editor.resize', '1')
  4805. }).render();
  4806. });
  4807. context.memo('button.imageSize50', function () {
  4808. return ui.button({
  4809. contents: '<span class="note-fontsize-10">50%</span>',
  4810. tooltip: lang.image.resizeHalf,
  4811. click: context.createInvokeHandler('editor.resize', '0.5')
  4812. }).render();
  4813. });
  4814. context.memo('button.imageSize25', function () {
  4815. return ui.button({
  4816. contents: '<span class="note-fontsize-10">25%</span>',
  4817. tooltip: lang.image.resizeQuarter,
  4818. click: context.createInvokeHandler('editor.resize', '0.25')
  4819. }).render();
  4820. });
  4821. // Float Buttons
  4822. context.memo('button.floatLeft', function () {
  4823. return ui.button({
  4824. contents: ui.icon(options.icons.alignLeft),
  4825. tooltip: lang.image.floatLeft,
  4826. click: context.createInvokeHandler('editor.floatMe', 'left')
  4827. }).render();
  4828. });
  4829. context.memo('button.floatRight', function () {
  4830. return ui.button({
  4831. contents: ui.icon(options.icons.alignRight),
  4832. tooltip: lang.image.floatRight,
  4833. click: context.createInvokeHandler('editor.floatMe', 'right')
  4834. }).render();
  4835. });
  4836. context.memo('button.floatNone', function () {
  4837. return ui.button({
  4838. contents: ui.icon(options.icons.alignJustify),
  4839. tooltip: lang.image.floatNone,
  4840. click: context.createInvokeHandler('editor.floatMe', 'none')
  4841. }).render();
  4842. });
  4843. // Remove Buttons
  4844. context.memo('button.removeMedia', function () {
  4845. return ui.button({
  4846. contents: ui.icon(options.icons.trash),
  4847. tooltip: lang.image.remove,
  4848. click: context.createInvokeHandler('editor.removeMedia')
  4849. }).render();
  4850. });
  4851. };
  4852. this.addLinkPopoverButtons = function () {
  4853. context.memo('button.linkDialogShow', function () {
  4854. return ui.button({
  4855. contents: ui.icon(options.icons.link),
  4856. tooltip: lang.link.edit,
  4857. click: context.createInvokeHandler('linkDialog.show')
  4858. }).render();
  4859. });
  4860. context.memo('button.unlink', function () {
  4861. return ui.button({
  4862. contents: ui.icon(options.icons.unlink),
  4863. tooltip: lang.link.unlink,
  4864. click: context.createInvokeHandler('editor.unlink')
  4865. }).render();
  4866. });
  4867. };
  4868. this.build = function ($container, groups) {
  4869. for (var groupIdx = 0, groupLen = groups.length; groupIdx < groupLen; groupIdx++) {
  4870. var group = groups[groupIdx];
  4871. var groupName = group[0];
  4872. var buttons = group[1];
  4873. var $group = ui.buttonGroup({
  4874. className: 'note-' + groupName
  4875. }).render();
  4876. for (var idx = 0, len = buttons.length; idx < len; idx++) {
  4877. var button = context.memo('button.' + buttons[idx]);
  4878. if (button) {
  4879. $group.append(typeof button === 'function' ? button(context) : button);
  4880. }
  4881. }
  4882. $group.appendTo($container);
  4883. }
  4884. };
  4885. this.updateCurrentStyle = function () {
  4886. var styleInfo = context.invoke('editor.currentStyle');
  4887. this.updateBtnStates({
  4888. '.note-btn-bold': function () {
  4889. return styleInfo['font-bold'] === 'bold';
  4890. },
  4891. '.note-btn-italic': function () {
  4892. return styleInfo['font-italic'] === 'italic';
  4893. },
  4894. '.note-btn-underline': function () {
  4895. return styleInfo['font-underline'] === 'underline';
  4896. }
  4897. });
  4898. if (styleInfo['font-family']) {
  4899. var fontNames = styleInfo['font-family'].split(',').map(function (name) {
  4900. return name.replace(/[\'\"]/g, '')
  4901. .replace(/\s+$/, '')
  4902. .replace(/^\s+/, '');
  4903. });
  4904. var fontName = list.find(fontNames, function (name) {
  4905. return agent.isFontInstalled(name) ||
  4906. list.contains(options.fontNamesIgnoreCheck, name);
  4907. });
  4908. $toolbar.find('.dropdown-fontname li a').each(function () {
  4909. // always compare string to avoid creating another func.
  4910. var isChecked = ($(this).data('value') + '') === (fontName + '');
  4911. this.className = isChecked ? 'checked' : '';
  4912. });
  4913. $toolbar.find('.note-current-fontname').text(fontName);
  4914. }
  4915. if (styleInfo['font-size']) {
  4916. var fontSize = styleInfo['font-size'];
  4917. $toolbar.find('.dropdown-fontsize li a').each(function () {
  4918. // always compare with string to avoid creating another func.
  4919. var isChecked = ($(this).data('value') + '') === (fontSize + '');
  4920. this.className = isChecked ? 'checked' : '';
  4921. });
  4922. $toolbar.find('.note-current-fontsize').text(fontSize);
  4923. }
  4924. if (styleInfo['line-height']) {
  4925. var lineHeight = styleInfo['line-height'];
  4926. $toolbar.find('.dropdown-line-height li a').each(function () {
  4927. // always compare with string to avoid creating another func.
  4928. var isChecked = ($(this).data('value') + '') === (lineHeight + '');
  4929. this.className = isChecked ? 'checked' : '';
  4930. });
  4931. }
  4932. };
  4933. this.updateBtnStates = function (infos) {
  4934. $.each(infos, function (selector, pred) {
  4935. ui.toggleBtnActive($toolbar.find(selector), pred());
  4936. });
  4937. };
  4938. this.tableMoveHandler = function (event) {
  4939. var PX_PER_EM = 18;
  4940. var $picker = $(event.target.parentNode); // target is mousecatcher
  4941. var $dimensionDisplay = $picker.next();
  4942. var $catcher = $picker.find('.note-dimension-picker-mousecatcher');
  4943. var $highlighted = $picker.find('.note-dimension-picker-highlighted');
  4944. var $unhighlighted = $picker.find('.note-dimension-picker-unhighlighted');
  4945. var posOffset;
  4946. // HTML5 with jQuery - e.offsetX is undefined in Firefox
  4947. if (event.offsetX === undefined) {
  4948. var posCatcher = $(event.target).offset();
  4949. posOffset = {
  4950. x: event.pageX - posCatcher.left,
  4951. y: event.pageY - posCatcher.top
  4952. };
  4953. } else {
  4954. posOffset = {
  4955. x: event.offsetX,
  4956. y: event.offsetY
  4957. };
  4958. }
  4959. var dim = {
  4960. c: Math.ceil(posOffset.x / PX_PER_EM) || 1,
  4961. r: Math.ceil(posOffset.y / PX_PER_EM) || 1
  4962. };
  4963. $highlighted.css({ width: dim.c + 'em', height: dim.r + 'em' });
  4964. $catcher.data('value', dim.c + 'x' + dim.r);
  4965. if (3 < dim.c && dim.c < options.insertTableMaxSize.col) {
  4966. $unhighlighted.css({ width: dim.c + 1 + 'em'});
  4967. }
  4968. if (3 < dim.r && dim.r < options.insertTableMaxSize.row) {
  4969. $unhighlighted.css({ height: dim.r + 1 + 'em'});
  4970. }
  4971. $dimensionDisplay.html(dim.c + ' x ' + dim.r);
  4972. };
  4973. };
  4974. var Toolbar = function (context) {
  4975. var ui = $.summernote.ui;
  4976. var $note = context.layoutInfo.note;
  4977. var $toolbar = context.layoutInfo.toolbar;
  4978. var options = context.options;
  4979. this.shouldInitialize = function () {
  4980. return !options.airMode;
  4981. };
  4982. this.initialize = function () {
  4983. options.toolbar = options.toolbar || [];
  4984. if (!options.toolbar.length) {
  4985. $toolbar.hide();
  4986. } else {
  4987. context.invoke('buttons.build', $toolbar, options.toolbar);
  4988. }
  4989. $note.on('summernote.keyup summernote.mouseup summernote.change', function () {
  4990. context.invoke('buttons.updateCurrentStyle');
  4991. });
  4992. context.invoke('buttons.updateCurrentStyle');
  4993. };
  4994. this.destroy = function () {
  4995. $toolbar.children().remove();
  4996. };
  4997. this.updateFullscreen = function (isFullscreen) {
  4998. ui.toggleBtnActive($toolbar.find('.btn-fullscreen'), isFullscreen);
  4999. };
  5000. this.updateCodeview = function (isCodeview) {
  5001. ui.toggleBtnActive($toolbar.find('.btn-codeview'), isCodeview);
  5002. if (isCodeview) {
  5003. this.deactivate();
  5004. } else {
  5005. this.activate();
  5006. }
  5007. };
  5008. this.activate = function (isIncludeCodeview) {
  5009. var $btn = $toolbar.find('button');
  5010. if (!isIncludeCodeview) {
  5011. $btn = $btn.not('.btn-codeview');
  5012. }
  5013. ui.toggleBtn($btn, true);
  5014. };
  5015. this.deactivate = function (isIncludeCodeview) {
  5016. var $btn = $toolbar.find('button');
  5017. if (!isIncludeCodeview) {
  5018. $btn = $btn.not('.btn-codeview');
  5019. }
  5020. ui.toggleBtn($btn, false);
  5021. };
  5022. };
  5023. var LinkDialog = function (context) {
  5024. var self = this;
  5025. var ui = $.summernote.ui;
  5026. var $editor = context.layoutInfo.editor;
  5027. var options = context.options;
  5028. var lang = options.langInfo;
  5029. this.initialize = function () {
  5030. var $container = options.dialogsInBody ? $(document.body) : $editor;
  5031. var body = '<div class="form-group">' +
  5032. '<label>' + lang.link.textToDisplay + '</label>' +
  5033. '<input class="note-link-text form-control" type="text" />' +
  5034. '</div>' +
  5035. '<div class="form-group">' +
  5036. '<label>' + lang.link.url + '</label>' +
  5037. '<input class="note-link-url form-control" type="text" value="http://" />' +
  5038. '</div>' +
  5039. (!options.disableLinkTarget ?
  5040. '<div class="checkbox">' +
  5041. '<label>' + '<input type="checkbox" checked> ' + lang.link.openInNewWindow + '</label>' +
  5042. '</div>' : ''
  5043. );
  5044. var footer = '<button href="#" class="btn btn-primary note-link-btn disabled" disabled>' + lang.link.insert + '</button>';
  5045. this.$dialog = ui.dialog({
  5046. className: 'link-dialog',
  5047. title: lang.link.insert,
  5048. body: body,
  5049. footer: footer
  5050. }).render().appendTo($container);
  5051. };
  5052. this.destroy = function () {
  5053. ui.hideDialog(this.$dialog);
  5054. this.$dialog.remove();
  5055. };
  5056. this.bindEnterKey = function ($input, $btn) {
  5057. $input.on('keypress', function (event) {
  5058. if (event.keyCode === key.code.ENTER) {
  5059. $btn.trigger('click');
  5060. }
  5061. });
  5062. };
  5063. /**
  5064. * Show link dialog and set event handlers on dialog controls.
  5065. *
  5066. * @param {Object} linkInfo
  5067. * @return {Promise}
  5068. */
  5069. this.showLinkDialog = function (linkInfo) {
  5070. return $.Deferred(function (deferred) {
  5071. var $linkText = self.$dialog.find('.note-link-text'),
  5072. $linkUrl = self.$dialog.find('.note-link-url'),
  5073. $linkBtn = self.$dialog.find('.note-link-btn'),
  5074. $openInNewWindow = self.$dialog.find('input[type=checkbox]');
  5075. ui.onDialogShown(self.$dialog, function () {
  5076. context.triggerEvent('dialog.shown');
  5077. $linkText.val(linkInfo.text);
  5078. $linkText.on('input', function () {
  5079. ui.toggleBtn($linkBtn, $linkText.val() && $linkUrl.val());
  5080. // if linktext was modified by keyup,
  5081. // stop cloning text from linkUrl
  5082. linkInfo.text = $linkText.val();
  5083. });
  5084. // if no url was given, copy text to url
  5085. if (!linkInfo.url) {
  5086. linkInfo.url = linkInfo.text || 'http://';
  5087. ui.toggleBtn($linkBtn, linkInfo.text);
  5088. }
  5089. $linkUrl.on('input', function () {
  5090. ui.toggleBtn($linkBtn, $linkText.val() && $linkUrl.val());
  5091. // display same link on `Text to display` input
  5092. // when create a new link
  5093. if (!linkInfo.text) {
  5094. $linkText.val($linkUrl.val());
  5095. }
  5096. }).val(linkInfo.url).trigger('focus');
  5097. self.bindEnterKey($linkUrl, $linkBtn);
  5098. self.bindEnterKey($linkText, $linkBtn);
  5099. $openInNewWindow.prop('checked', linkInfo.isNewWindow);
  5100. $linkBtn.one('click', function (event) {
  5101. event.preventDefault();
  5102. deferred.resolve({
  5103. range: linkInfo.range,
  5104. url: $linkUrl.val(),
  5105. text: $linkText.val(),
  5106. isNewWindow: $openInNewWindow.is(':checked')
  5107. });
  5108. self.$dialog.modal('hide');
  5109. });
  5110. });
  5111. ui.onDialogHidden(self.$dialog, function () {
  5112. // detach events
  5113. $linkText.off('input keypress');
  5114. $linkUrl.off('input keypress');
  5115. $linkBtn.off('click');
  5116. if (deferred.state() === 'pending') {
  5117. deferred.reject();
  5118. }
  5119. });
  5120. ui.showDialog(self.$dialog);
  5121. }).promise();
  5122. };
  5123. /**
  5124. * @param {Object} layoutInfo
  5125. */
  5126. this.show = function () {
  5127. var linkInfo = context.invoke('editor.getLinkInfo');
  5128. context.invoke('editor.saveRange');
  5129. this.showLinkDialog(linkInfo).then(function (linkInfo) {
  5130. context.invoke('editor.restoreRange');
  5131. context.invoke('editor.createLink', linkInfo);
  5132. }).fail(function () {
  5133. context.invoke('editor.restoreRange');
  5134. });
  5135. };
  5136. context.memo('help.linkDialog.show', options.langInfo.help['linkDialog.show']);
  5137. };
  5138. var LinkPopover = function (context) {
  5139. var self = this;
  5140. var ui = $.summernote.ui;
  5141. var options = context.options;
  5142. this.events = {
  5143. 'summernote.keyup summernote.mouseup summernote.change summernote.scroll': function () {
  5144. self.update();
  5145. },
  5146. 'summernote.dialog.shown': function () {
  5147. self.hide();
  5148. }
  5149. };
  5150. this.shouldInitialize = function () {
  5151. return !list.isEmpty(options.popover.link);
  5152. };
  5153. this.initialize = function () {
  5154. this.$popover = ui.popover({
  5155. className: 'note-link-popover',
  5156. callback: function ($node) {
  5157. var $content = $node.find('.popover-content');
  5158. $content.prepend('<span><a target="_blank"></a>&nbsp;</span>');
  5159. }
  5160. }).render().appendTo('body');
  5161. var $content = this.$popover.find('.popover-content');
  5162. context.invoke('buttons.build', $content, options.popover.link);
  5163. };
  5164. this.destroy = function () {
  5165. this.$popover.remove();
  5166. };
  5167. this.update = function () {
  5168. var rng = context.invoke('editor.createRange');
  5169. if (rng.isCollapsed() && rng.isOnAnchor()) {
  5170. var anchor = dom.ancestor(rng.sc, dom.isAnchor);
  5171. var href = $(anchor).attr('href');
  5172. this.$popover.find('a').attr('href', href).html(href);
  5173. var pos = dom.posFromPlaceholder(anchor);
  5174. this.$popover.css({
  5175. display: 'block',
  5176. left: pos.left,
  5177. top: pos.top
  5178. });
  5179. } else {
  5180. this.hide();
  5181. }
  5182. };
  5183. this.hide = function () {
  5184. this.$popover.hide();
  5185. };
  5186. };
  5187. var ImageDialog = function (context) {
  5188. var self = this;
  5189. var ui = $.summernote.ui;
  5190. var $editor = context.layoutInfo.editor;
  5191. var options = context.options;
  5192. var lang = options.langInfo;
  5193. this.initialize = function () {
  5194. var $container = options.dialogsInBody ? $(document.body) : $editor;
  5195. var imageLimitation = '';
  5196. if (options.maximumImageFileSize) {
  5197. var unit = Math.floor(Math.log(options.maximumImageFileSize) / Math.log(1024));
  5198. var readableSize = (options.maximumImageFileSize / Math.pow(1024, unit)).toFixed(2) * 1 +
  5199. ' ' + ' KMGTP'[unit] + 'B';
  5200. imageLimitation = '<small>' + lang.image.maximumFileSize + ' : ' + readableSize + '</small>';
  5201. }
  5202. var body = '<div class="form-group note-group-select-from-files">' +
  5203. '<label>' + lang.image.selectFromFiles + '</label>' +
  5204. '<input class="note-image-input form-control" type="file" name="files" accept="image/*" multiple="multiple" />' +
  5205. imageLimitation +
  5206. '</div>' +
  5207. '<div class="form-group" style="overflow:auto;">' +
  5208. '<label>' + lang.image.url + '</label>' +
  5209. '<input class="note-image-url form-control col-md-12" type="text" />' +
  5210. '</div>';
  5211. var footer = '<button href="#" class="btn btn-primary note-image-btn disabled" disabled>' + lang.image.insert + '</button>';
  5212. this.$dialog = ui.dialog({
  5213. title: lang.image.insert,
  5214. body: body,
  5215. footer: footer
  5216. }).render().appendTo($container);
  5217. };
  5218. this.destroy = function () {
  5219. ui.hideDialog(this.$dialog);
  5220. this.$dialog.remove();
  5221. };
  5222. this.bindEnterKey = function ($input, $btn) {
  5223. $input.on('keypress', function (event) {
  5224. if (event.keyCode === key.code.ENTER) {
  5225. $btn.trigger('click');
  5226. }
  5227. });
  5228. };
  5229. this.show = function () {
  5230. context.invoke('editor.saveRange');
  5231. this.showImageDialog().then(function (data) {
  5232. // [workaround] hide dialog before restore range for IE range focus
  5233. ui.hideDialog(self.$dialog);
  5234. context.invoke('editor.restoreRange');
  5235. if (typeof data === 'string') { // image url
  5236. context.invoke('editor.insertImage', data);
  5237. } else { // array of files
  5238. context.invoke('editor.insertImagesOrCallback', data);
  5239. }
  5240. }).fail(function () {
  5241. context.invoke('editor.restoreRange');
  5242. });
  5243. };
  5244. /**
  5245. * show image dialog
  5246. *
  5247. * @param {jQuery} $dialog
  5248. * @return {Promise}
  5249. */
  5250. this.showImageDialog = function () {
  5251. return $.Deferred(function (deferred) {
  5252. var $imageInput = self.$dialog.find('.note-image-input'),
  5253. $imageUrl = self.$dialog.find('.note-image-url'),
  5254. $imageBtn = self.$dialog.find('.note-image-btn');
  5255. ui.onDialogShown(self.$dialog, function () {
  5256. context.triggerEvent('dialog.shown');
  5257. // Cloning imageInput to clear element.
  5258. $imageInput.replaceWith($imageInput.clone()
  5259. .on('change', function () {
  5260. deferred.resolve(this.files || this.value);
  5261. })
  5262. .val('')
  5263. );
  5264. $imageBtn.click(function (event) {
  5265. event.preventDefault();
  5266. deferred.resolve($imageUrl.val());
  5267. });
  5268. $imageUrl.on('keyup paste', function () {
  5269. var url = $imageUrl.val();
  5270. ui.toggleBtn($imageBtn, url);
  5271. }).val('').trigger('focus');
  5272. self.bindEnterKey($imageUrl, $imageBtn);
  5273. });
  5274. ui.onDialogHidden(self.$dialog, function () {
  5275. $imageInput.off('change');
  5276. $imageUrl.off('keyup paste keypress');
  5277. $imageBtn.off('click');
  5278. if (deferred.state() === 'pending') {
  5279. deferred.reject();
  5280. }
  5281. });
  5282. ui.showDialog(self.$dialog);
  5283. });
  5284. };
  5285. };
  5286. var ImagePopover = function (context) {
  5287. var ui = $.summernote.ui;
  5288. var options = context.options;
  5289. this.shouldInitialize = function () {
  5290. return !list.isEmpty(options.popover.image);
  5291. };
  5292. this.initialize = function () {
  5293. this.$popover = ui.popover({
  5294. className: 'note-image-popover'
  5295. }).render().appendTo('body');
  5296. var $content = this.$popover.find('.popover-content');
  5297. context.invoke('buttons.build', $content, options.popover.image);
  5298. };
  5299. this.destroy = function () {
  5300. this.$popover.remove();
  5301. };
  5302. this.update = function (target) {
  5303. if (dom.isImg(target)) {
  5304. var pos = dom.posFromPlaceholder(target);
  5305. this.$popover.css({
  5306. display: 'block',
  5307. left: pos.left,
  5308. top: pos.top
  5309. });
  5310. } else {
  5311. this.hide();
  5312. }
  5313. };
  5314. this.hide = function () {
  5315. this.$popover.hide();
  5316. };
  5317. };
  5318. var VideoDialog = function (context) {
  5319. var self = this;
  5320. var ui = $.summernote.ui;
  5321. var $editor = context.layoutInfo.editor;
  5322. var options = context.options;
  5323. var lang = options.langInfo;
  5324. this.initialize = function () {
  5325. var $container = options.dialogsInBody ? $(document.body) : $editor;
  5326. var body = '<div class="form-group row-fluid">' +
  5327. '<label>' + lang.video.url + ' <small class="text-muted">' + lang.video.providers + '</small></label>' +
  5328. '<input class="note-video-url form-control span12" type="text" />' +
  5329. '</div>';
  5330. var footer = '<button href="#" class="btn btn-primary note-video-btn disabled" disabled>' + lang.video.insert + '</button>';
  5331. this.$dialog = ui.dialog({
  5332. title: lang.video.insert,
  5333. body: body,
  5334. footer: footer
  5335. }).render().appendTo($container);
  5336. };
  5337. this.destroy = function () {
  5338. ui.hideDialog(this.$dialog);
  5339. this.$dialog.remove();
  5340. };
  5341. this.bindEnterKey = function ($input, $btn) {
  5342. $input.on('keypress', function (event) {
  5343. if (event.keyCode === key.code.ENTER) {
  5344. $btn.trigger('click');
  5345. }
  5346. });
  5347. };
  5348. this.createVideoNode = function (url) {
  5349. // video url patterns(youtube, instagram, vimeo, dailymotion, youku, mp4, ogg, webm)
  5350. var ytRegExp = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
  5351. var ytMatch = url.match(ytRegExp);
  5352. var igRegExp = /\/\/instagram.com\/p\/(.[a-zA-Z0-9_-]*)/;
  5353. var igMatch = url.match(igRegExp);
  5354. var vRegExp = /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/;
  5355. var vMatch = url.match(vRegExp);
  5356. var vimRegExp = /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/;
  5357. var vimMatch = url.match(vimRegExp);
  5358. var dmRegExp = /.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/;
  5359. var dmMatch = url.match(dmRegExp);
  5360. var youkuRegExp = /\/\/v\.youku\.com\/v_show\/id_(\w+)=*\.html/;
  5361. var youkuMatch = url.match(youkuRegExp);
  5362. var mp4RegExp = /^.+.(mp4|m4v)$/;
  5363. var mp4Match = url.match(mp4RegExp);
  5364. var oggRegExp = /^.+.(ogg|ogv)$/;
  5365. var oggMatch = url.match(oggRegExp);
  5366. var webmRegExp = /^.+.(webm)$/;
  5367. var webmMatch = url.match(webmRegExp);
  5368. var $video;
  5369. if (ytMatch && ytMatch[1].length === 11) {
  5370. var youtubeId = ytMatch[1];
  5371. $video = $('<iframe>')
  5372. .attr('frameborder', 0)
  5373. .attr('src', '//www.youtube.com/embed/' + youtubeId)
  5374. .attr('width', '640').attr('height', '360');
  5375. } else if (igMatch && igMatch[0].length) {
  5376. $video = $('<iframe>')
  5377. .attr('frameborder', 0)
  5378. .attr('src', igMatch[0] + '/embed/')
  5379. .attr('width', '612').attr('height', '710')
  5380. .attr('scrolling', 'no')
  5381. .attr('allowtransparency', 'true');
  5382. } else if (vMatch && vMatch[0].length) {
  5383. $video = $('<iframe>')
  5384. .attr('frameborder', 0)
  5385. .attr('src', vMatch[0] + '/embed/simple')
  5386. .attr('width', '600').attr('height', '600')
  5387. .attr('class', 'vine-embed');
  5388. } else if (vimMatch && vimMatch[3].length) {
  5389. $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
  5390. .attr('frameborder', 0)
  5391. .attr('src', '//player.vimeo.com/video/' + vimMatch[3])
  5392. .attr('width', '640').attr('height', '360');
  5393. } else if (dmMatch && dmMatch[2].length) {
  5394. $video = $('<iframe>')
  5395. .attr('frameborder', 0)
  5396. .attr('src', '//www.dailymotion.com/embed/video/' + dmMatch[2])
  5397. .attr('width', '640').attr('height', '360');
  5398. } else if (youkuMatch && youkuMatch[1].length) {
  5399. $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
  5400. .attr('frameborder', 0)
  5401. .attr('height', '498')
  5402. .attr('width', '510')
  5403. .attr('src', '//player.youku.com/embed/' + youkuMatch[1]);
  5404. } else if (mp4Match || oggMatch || webmMatch) {
  5405. $video = $('<video controls>')
  5406. .attr('src', url)
  5407. .attr('width', '640').attr('height', '360');
  5408. } else {
  5409. // this is not a known video link. Now what, Cat? Now what?
  5410. return false;
  5411. }
  5412. $video.addClass('note-video-clip');
  5413. return $video[0];
  5414. };
  5415. this.show = function () {
  5416. var text = context.invoke('editor.getSelectedText');
  5417. context.invoke('editor.saveRange');
  5418. this.showVideoDialog(text).then(function (url) {
  5419. // [workaround] hide dialog before restore range for IE range focus
  5420. ui.hideDialog(self.$dialog);
  5421. context.invoke('editor.restoreRange');
  5422. // build node
  5423. var $node = self.createVideoNode(url);
  5424. if ($node) {
  5425. // insert video node
  5426. context.invoke('editor.insertNode', $node);
  5427. }
  5428. }).fail(function () {
  5429. context.invoke('editor.restoreRange');
  5430. });
  5431. };
  5432. /**
  5433. * show image dialog
  5434. *
  5435. * @param {jQuery} $dialog
  5436. * @return {Promise}
  5437. */
  5438. this.showVideoDialog = function (text) {
  5439. return $.Deferred(function (deferred) {
  5440. var $videoUrl = self.$dialog.find('.note-video-url'),
  5441. $videoBtn = self.$dialog.find('.note-video-btn');
  5442. ui.onDialogShown(self.$dialog, function () {
  5443. context.triggerEvent('dialog.shown');
  5444. $videoUrl.val(text).on('input', function () {
  5445. ui.toggleBtn($videoBtn, $videoUrl.val());
  5446. }).trigger('focus');
  5447. $videoBtn.click(function (event) {
  5448. event.preventDefault();
  5449. deferred.resolve($videoUrl.val());
  5450. });
  5451. self.bindEnterKey($videoUrl, $videoBtn);
  5452. });
  5453. ui.onDialogHidden(self.$dialog, function () {
  5454. $videoUrl.off('input');
  5455. $videoBtn.off('click');
  5456. if (deferred.state() === 'pending') {
  5457. deferred.reject();
  5458. }
  5459. });
  5460. ui.showDialog(self.$dialog);
  5461. });
  5462. };
  5463. };
  5464. var HelpDialog = function (context) {
  5465. var self = this;
  5466. var ui = $.summernote.ui;
  5467. var $editor = context.layoutInfo.editor;
  5468. var options = context.options;
  5469. var lang = options.langInfo;
  5470. this.createShortCutList = function () {
  5471. var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc'];
  5472. var $list = $('<div />');
  5473. Object.keys(keyMap).forEach(function (keyString) {
  5474. var $row = $('<div class="help-list-item"/>');
  5475. var command = keyMap[keyString];
  5476. var str = context.memo('help.' + command) ? context.memo('help.' + command) : command;
  5477. var $keyString = $('<label />').css({
  5478. 'width': 180,
  5479. 'max-width': 200,
  5480. 'margin-right': 10
  5481. }).html(keyString);
  5482. var $description = $('<span />').html(str);
  5483. $row.html($keyString).append($description);
  5484. $list.append($row);
  5485. });
  5486. return $list.html();
  5487. };
  5488. this.initialize = function () {
  5489. var $container = options.dialogsInBody ? $(document.body) : $editor;
  5490. var body = [
  5491. '<p class="text-center">',
  5492. '<a href="//summernote.org/" target="_blank">Summernote 0.7.1</a> · ',
  5493. '<a href="//github.com/summernote/summernote" target="_blank">Project</a> · ',
  5494. '<a href="//github.com/summernote/summernote/issues" target="_blank">Issues</a>',
  5495. '</p>'
  5496. ].join('');
  5497. this.$dialog = ui.dialog({
  5498. title: lang.options.help,
  5499. body: this.createShortCutList(),
  5500. footer: body,
  5501. callback: function ($node) {
  5502. $node.find('.modal-body').css({
  5503. 'max-height': 300,
  5504. 'overflow': 'scroll'
  5505. });
  5506. }
  5507. }).render().appendTo($container);
  5508. };
  5509. this.destroy = function () {
  5510. ui.hideDialog(this.$dialog);
  5511. this.$dialog.remove();
  5512. };
  5513. /**
  5514. * show help dialog
  5515. *
  5516. * @return {Promise}
  5517. */
  5518. this.showHelpDialog = function () {
  5519. return $.Deferred(function (deferred) {
  5520. ui.onDialogHidden(self.$dialog, function () {
  5521. context.triggerEvent('dialog.shown');
  5522. deferred.resolve();
  5523. });
  5524. ui.showDialog(self.$dialog);
  5525. }).promise();
  5526. };
  5527. this.show = function () {
  5528. context.invoke('editor.saveRange');
  5529. this.showHelpDialog().then(function () {
  5530. context.invoke('editor.restoreRange');
  5531. });
  5532. };
  5533. };
  5534. var AirPopover = function (context) {
  5535. var self = this;
  5536. var ui = $.summernote.ui;
  5537. var options = context.options;
  5538. var AIR_MODE_POPOVER_X_OFFSET = 20;
  5539. this.events = {
  5540. 'summernote.keyup summernote.mouseup summernote.scroll': function () {
  5541. self.update();
  5542. },
  5543. 'summernote.change summernote.dialog.shown': function () {
  5544. self.hide();
  5545. },
  5546. 'summernote.focusout': function (we, e) {
  5547. // [workaround] Firefox doesn't support relatedTarget on focusout
  5548. // - Ignore hide action on focus out in FF.
  5549. if (agent.isFF) {
  5550. return;
  5551. }
  5552. if (!e.relatedTarget || !dom.ancestor(e.relatedTarget, func.eq(self.$popover[0]))) {
  5553. self.hide();
  5554. }
  5555. }
  5556. };
  5557. this.shouldInitialize = function () {
  5558. return options.airMode && !list.isEmpty(options.popover.air);
  5559. };
  5560. this.initialize = function () {
  5561. this.$popover = ui.popover({
  5562. className: 'note-air-popover'
  5563. }).render().appendTo('body');
  5564. var $content = this.$popover.find('.popover-content');
  5565. context.invoke('buttons.build', $content, options.popover.air);
  5566. };
  5567. this.destroy = function () {
  5568. this.$popover.remove();
  5569. };
  5570. this.update = function () {
  5571. var styleInfo = context.invoke('editor.currentStyle');
  5572. if (styleInfo.range && !styleInfo.range.isCollapsed()) {
  5573. var rect = list.last(styleInfo.range.getClientRects());
  5574. if (rect) {
  5575. var bnd = func.rect2bnd(rect);
  5576. this.$popover.css({
  5577. display: 'block',
  5578. left: Math.max(bnd.left + bnd.width / 2, 0) - AIR_MODE_POPOVER_X_OFFSET,
  5579. top: bnd.top + bnd.height
  5580. });
  5581. }
  5582. } else {
  5583. this.hide();
  5584. }
  5585. };
  5586. this.hide = function () {
  5587. this.$popover.hide();
  5588. };
  5589. };
  5590. var HintPopover = function (context) {
  5591. var self = this;
  5592. var ui = $.summernote.ui;
  5593. var hint = context.options.hint || [];
  5594. var hints = $.isArray(hint) ? hint : [hint];
  5595. this.events = {
  5596. 'summernote.keyup': function (we, e) {
  5597. if (!e.isDefaultPrevented()) {
  5598. self.handleKeyup(e);
  5599. }
  5600. },
  5601. 'summernote.keydown' : function (we, e) {
  5602. self.handleKeydown(e);
  5603. },
  5604. 'summernote.dialog.shown': function () {
  5605. self.hide();
  5606. }
  5607. };
  5608. this.shouldInitialize = function () {
  5609. return hints.length > 0;
  5610. };
  5611. this.initialize = function () {
  5612. this.lastWordRange = null;
  5613. this.$popover = ui.popover({
  5614. className: 'note-hint-popover'
  5615. }).render().appendTo('body');
  5616. this.$content = this.$popover.find('.popover-content');
  5617. this.$content.on('click', '.note-hint-item', function () {
  5618. self.$content.find('.active').removeClass('active');
  5619. $(this).addClass('active');
  5620. self.replace();
  5621. });
  5622. };
  5623. this.destroy = function () {
  5624. this.$popover.remove();
  5625. };
  5626. this.selectItem = function ($item) {
  5627. this.$content.find('.active').removeClass('active');
  5628. $item.addClass('active');
  5629. this.$content[0].scrollTop = $item[0].offsetTop - (this.$content.innerHeight() / 2);
  5630. };
  5631. this.moveDown = function () {
  5632. var $current = this.$content.find('.note-hint-item.active');
  5633. var $next = $current.next();
  5634. if ($next.length) {
  5635. this.selectItem($next);
  5636. } else {
  5637. var $nextGroup = $current.parent().next();
  5638. if (!$nextGroup.length) {
  5639. $nextGroup = this.$content.find('.note-hint-group').first();
  5640. }
  5641. this.selectItem($nextGroup.find('.note-hint-item').first());
  5642. }
  5643. };
  5644. this.moveUp = function () {
  5645. var $current = this.$content.find('.note-hint-item.active');
  5646. var $prev = $current.prev();
  5647. if ($prev.length) {
  5648. this.selectItem($prev);
  5649. } else {
  5650. var $prevGroup = $current.parent().prev();
  5651. if (!$prevGroup.length) {
  5652. $prevGroup = this.$content.find('.note-hint-group').last();
  5653. }
  5654. this.selectItem($prevGroup.find('.note-hint-item').last());
  5655. }
  5656. };
  5657. this.replace = function () {
  5658. var $item = this.$content.find('.note-hint-item.active');
  5659. var node = this.nodeFromItem($item);
  5660. this.lastWordRange.insertNode(node);
  5661. range.createFromNode(node).collapse().select();
  5662. this.lastWordRange = null;
  5663. this.hide();
  5664. context.invoke('editor.focus');
  5665. };
  5666. this.nodeFromItem = function ($item) {
  5667. var hint = hints[$item.data('index')];
  5668. var item = $item.data('item');
  5669. var node = hint.content ? hint.content(item) : item;
  5670. if (typeof node === 'string') {
  5671. node = dom.createText(node);
  5672. }
  5673. return node;
  5674. };
  5675. this.createItemTemplates = function (hintIdx, items) {
  5676. var hint = hints[hintIdx];
  5677. return items.map(function (item, idx) {
  5678. var $item = $('<div class="note-hint-item"/>');
  5679. $item.append(hint.template ? hint.template(item) : item + '');
  5680. $item.data({
  5681. 'index': hintIdx,
  5682. 'item': item
  5683. });
  5684. if (hintIdx === 0 && idx === 0) {
  5685. $item.addClass('active');
  5686. }
  5687. return $item;
  5688. });
  5689. };
  5690. this.handleKeydown = function (e) {
  5691. if (!this.$popover.is(':visible')) {
  5692. return;
  5693. }
  5694. if (e.keyCode === key.code.ENTER) {
  5695. e.preventDefault();
  5696. this.replace();
  5697. } else if (e.keyCode === key.code.UP) {
  5698. e.preventDefault();
  5699. this.moveUp();
  5700. } else if (e.keyCode === key.code.DOWN) {
  5701. e.preventDefault();
  5702. this.moveDown();
  5703. }
  5704. };
  5705. this.searchKeyword = function (index, keyword, callback) {
  5706. var hint = hints[index];
  5707. if (hint && hint.match.test(keyword) && hint.search) {
  5708. var matches = hint.match.exec(keyword);
  5709. hint.search(matches[1], callback);
  5710. } else {
  5711. callback();
  5712. }
  5713. };
  5714. this.createGroup = function (idx, keyword) {
  5715. var $group = $('<div class="note-hint-group note-hint-group-' + idx + '"/>');
  5716. this.searchKeyword(idx, keyword, function (items) {
  5717. items = items || [];
  5718. if (items.length) {
  5719. $group.html(self.createItemTemplates(idx, items));
  5720. self.show();
  5721. }
  5722. });
  5723. return $group;
  5724. };
  5725. this.handleKeyup = function (e) {
  5726. if (list.contains([key.code.ENTER, key.code.UP, key.code.DOWN], e.keyCode)) {
  5727. if (e.keyCode === key.code.ENTER) {
  5728. if (this.$popover.is(':visible')) {
  5729. return;
  5730. }
  5731. }
  5732. } else {
  5733. var wordRange = context.invoke('editor.createRange').getWordRange();
  5734. var keyword = wordRange.toString();
  5735. if (hints.length && keyword) {
  5736. this.$content.empty();
  5737. var bnd = func.rect2bnd(list.last(wordRange.getClientRects()));
  5738. if (bnd) {
  5739. this.$popover.css({
  5740. left: bnd.left,
  5741. top: bnd.top + bnd.height
  5742. }).hide();
  5743. this.lastWordRange = wordRange;
  5744. hints.forEach(function (hint, idx) {
  5745. if (hint.match.test(keyword)) {
  5746. self.createGroup(idx, keyword).appendTo(self.$content);
  5747. }
  5748. });
  5749. }
  5750. } else {
  5751. this.hide();
  5752. }
  5753. }
  5754. };
  5755. this.show = function () {
  5756. this.$popover.show();
  5757. };
  5758. this.hide = function () {
  5759. this.$popover.hide();
  5760. };
  5761. };
  5762. $.summernote = $.extend($.summernote, {
  5763. version: '0.7.1',
  5764. ui: ui,
  5765. plugins: {},
  5766. options: {
  5767. modules: {
  5768. 'editor': Editor,
  5769. 'clipboard': Clipboard,
  5770. 'dropzone': Dropzone,
  5771. 'codeview': Codeview,
  5772. 'statusbar': Statusbar,
  5773. 'fullscreen': Fullscreen,
  5774. 'handle': Handle,
  5775. // FIXME: HintPopover must be front of autolink
  5776. // - Script error about range when Enter key is pressed on hint popover
  5777. 'hintPopover': HintPopover,
  5778. 'autoLink': AutoLink,
  5779. 'autoSync': AutoSync,
  5780. 'placeholder': Placeholder,
  5781. 'buttons' : Buttons,
  5782. 'toolbar': Toolbar,
  5783. 'linkDialog': LinkDialog,
  5784. 'linkPopover': LinkPopover,
  5785. 'imageDialog': ImageDialog,
  5786. 'imagePopover': ImagePopover,
  5787. 'videoDialog': VideoDialog,
  5788. 'helpDialog': HelpDialog,
  5789. 'airPopover': AirPopover
  5790. },
  5791. buttons: {},
  5792. lang: 'en-US',
  5793. // toolbar
  5794. toolbar: [
  5795. ['style', ['style']],
  5796. ['font', ['bold', 'underline', 'clear']],
  5797. ['fontname', ['fontname']],
  5798. ['color', ['color']],
  5799. ['para', ['ul', 'ol', 'paragraph']],
  5800. ['table', ['table']],
  5801. ['insert', ['link', 'picture', 'video']],
  5802. ['view', ['fullscreen', 'codeview', 'help']]
  5803. ],
  5804. // popover
  5805. popover: {
  5806. image: [
  5807. ['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
  5808. ['float', ['floatLeft', 'floatRight', 'floatNone']],
  5809. ['remove', ['removeMedia']]
  5810. ],
  5811. link: [
  5812. ['link', ['linkDialogShow', 'unlink']]
  5813. ],
  5814. air: [
  5815. ['color', ['color']],
  5816. ['font', ['bold', 'underline', 'clear']],
  5817. ['para', ['ul', 'paragraph']],
  5818. ['table', ['table']],
  5819. ['insert', ['link', 'picture']]
  5820. ]
  5821. },
  5822. // air mode: inline editor
  5823. airMode: false,
  5824. width: null,
  5825. height: null,
  5826. focus: false,
  5827. tabSize: 4,
  5828. styleWithSpan: true,
  5829. shortcuts: true,
  5830. textareaAutoSync: true,
  5831. direction: null,
  5832. styleTags: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
  5833. fontNames: [
  5834. 'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New',
  5835. 'Helvetica Neue', 'Helvetica', 'Impact', 'Lucida Grande',
  5836. 'Tahoma', 'Times New Roman', 'Verdana'
  5837. ],
  5838. fontSizes: ['8', '9', '10', '11', '12', '14', '18', '24', '36'],
  5839. // pallete colors(n x n)
  5840. colors: [
  5841. ['#000000', '#424242', '#636363', '#9C9C94', '#CEC6CE', '#EFEFEF', '#F7F7F7', '#FFFFFF'],
  5842. ['#FF0000', '#FF9C00', '#FFFF00', '#00FF00', '#00FFFF', '#0000FF', '#9C00FF', '#FF00FF'],
  5843. ['#F7C6CE', '#FFE7CE', '#FFEFC6', '#D6EFD6', '#CEDEE7', '#CEE7F7', '#D6D6E7', '#E7D6DE'],
  5844. ['#E79C9C', '#FFC69C', '#FFE79C', '#B5D6A5', '#A5C6CE', '#9CC6EF', '#B5A5D6', '#D6A5BD'],
  5845. ['#E76363', '#F7AD6B', '#FFD663', '#94BD7B', '#73A5AD', '#6BADDE', '#8C7BC6', '#C67BA5'],
  5846. ['#CE0000', '#E79439', '#EFC631', '#6BA54A', '#4A7B8C', '#3984C6', '#634AA5', '#A54A7B'],
  5847. ['#9C0000', '#B56308', '#BD9400', '#397B21', '#104A5A', '#085294', '#311873', '#731842'],
  5848. ['#630000', '#7B3900', '#846300', '#295218', '#083139', '#003163', '#21104A', '#4A1031']
  5849. ],
  5850. lineHeights: ['1.0', '1.2', '1.4', '1.5', '1.6', '1.8', '2.0', '3.0'],
  5851. tableClassName: 'table table-bordered',
  5852. insertTableMaxSize: {
  5853. col: 10,
  5854. row: 10
  5855. },
  5856. dialogsInBody: false,
  5857. maximumImageFileSize: null,
  5858. callbacks: {
  5859. onInit: null,
  5860. onFocus: null,
  5861. onBlur: null,
  5862. onEnter: null,
  5863. onKeyup: null,
  5864. onKeydown: null,
  5865. onSubmit: null,
  5866. onImageUpload: null,
  5867. onImageUploadError: null
  5868. },
  5869. codemirror: {
  5870. mode: 'text/html',
  5871. htmlMode: true,
  5872. lineNumbers: true
  5873. },
  5874. keyMap: {
  5875. pc: {
  5876. 'ENTER': 'insertParagraph',
  5877. 'CTRL+Z': 'undo',
  5878. 'CTRL+Y': 'redo',
  5879. 'TAB': 'tab',
  5880. 'SHIFT+TAB': 'untab',
  5881. 'CTRL+B': 'bold',
  5882. 'CTRL+I': 'italic',
  5883. 'CTRL+U': 'underline',
  5884. 'CTRL+SHIFT+S': 'strikethrough',
  5885. 'CTRL+BACKSLASH': 'removeFormat',
  5886. 'CTRL+SHIFT+L': 'justifyLeft',
  5887. 'CTRL+SHIFT+E': 'justifyCenter',
  5888. 'CTRL+SHIFT+R': 'justifyRight',
  5889. 'CTRL+SHIFT+J': 'justifyFull',
  5890. 'CTRL+SHIFT+NUM7': 'insertUnorderedList',
  5891. 'CTRL+SHIFT+NUM8': 'insertOrderedList',
  5892. 'CTRL+LEFTBRACKET': 'outdent',
  5893. 'CTRL+RIGHTBRACKET': 'indent',
  5894. 'CTRL+NUM0': 'formatPara',
  5895. 'CTRL+NUM1': 'formatH1',
  5896. 'CTRL+NUM2': 'formatH2',
  5897. 'CTRL+NUM3': 'formatH3',
  5898. 'CTRL+NUM4': 'formatH4',
  5899. 'CTRL+NUM5': 'formatH5',
  5900. 'CTRL+NUM6': 'formatH6',
  5901. 'CTRL+ENTER': 'insertHorizontalRule',
  5902. 'CTRL+K': 'linkDialog.show'
  5903. },
  5904. mac: {
  5905. 'ENTER': 'insertParagraph',
  5906. 'CMD+Z': 'undo',
  5907. 'CMD+SHIFT+Z': 'redo',
  5908. 'TAB': 'tab',
  5909. 'SHIFT+TAB': 'untab',
  5910. 'CMD+B': 'bold',
  5911. 'CMD+I': 'italic',
  5912. 'CMD+U': 'underline',
  5913. 'CMD+SHIFT+S': 'strikethrough',
  5914. 'CMD+BACKSLASH': 'removeFormat',
  5915. 'CMD+SHIFT+L': 'justifyLeft',
  5916. 'CMD+SHIFT+E': 'justifyCenter',
  5917. 'CMD+SHIFT+R': 'justifyRight',
  5918. 'CMD+SHIFT+J': 'justifyFull',
  5919. 'CMD+SHIFT+NUM7': 'insertUnorderedList',
  5920. 'CMD+SHIFT+NUM8': 'insertOrderedList',
  5921. 'CMD+LEFTBRACKET': 'outdent',
  5922. 'CMD+RIGHTBRACKET': 'indent',
  5923. 'CMD+NUM0': 'formatPara',
  5924. 'CMD+NUM1': 'formatH1',
  5925. 'CMD+NUM2': 'formatH2',
  5926. 'CMD+NUM3': 'formatH3',
  5927. 'CMD+NUM4': 'formatH4',
  5928. 'CMD+NUM5': 'formatH5',
  5929. 'CMD+NUM6': 'formatH6',
  5930. 'CMD+ENTER': 'insertHorizontalRule',
  5931. 'CMD+K': 'linkDialog.show'
  5932. }
  5933. },
  5934. icons: {
  5935. 'align': 'fa fa-align-left',
  5936. 'alignCenter': 'fa fa-align-center',
  5937. 'alignJustify': 'fa fa-align-justify',
  5938. 'alignLeft': 'fa fa-align-left',
  5939. 'alignRight': 'fa fa-align-right',
  5940. 'indent': 'fa fa-indent',
  5941. 'outdent': 'fa fa-outdent',
  5942. 'arrowsAlt': 'fa fa-arrows-alt',
  5943. 'bold': 'fa fa-bold',
  5944. 'caret': 'caret',
  5945. 'circle': 'fa fa-circle',
  5946. 'close': 'fa fa-close',
  5947. 'code': 'fa fa-code',
  5948. 'eraser': 'fa fa-eraser',
  5949. 'font': 'fa fa-font',
  5950. 'frame': 'fa fa-frame',
  5951. 'italic': 'fa fa-italic',
  5952. 'link': 'fa fa-link',
  5953. 'unlink': 'fa fa-chain-broken',
  5954. 'magic': 'fa fa-magic',
  5955. 'menuCheck': 'fa fa-check',
  5956. 'minus': 'fa fa-minus',
  5957. 'orderedlist': 'fa fa-list-ol',
  5958. 'pencil': 'fa fa-pencil',
  5959. 'picture': 'fa fa-picture-o',
  5960. 'question': 'fa fa-question',
  5961. 'redo': 'fa fa-repeat',
  5962. 'square': 'fa fa-square',
  5963. 'strikethrough': 'fa fa-strikethrough',
  5964. 'subscript': 'fa fa-subscript',
  5965. 'superscript': 'fa fa-superscript',
  5966. 'table': 'fa fa-table',
  5967. 'textHeight': 'fa fa-text-height',
  5968. 'trash': 'fa fa-trash',
  5969. 'underline': 'fa fa-underline',
  5970. 'undo': 'fa fa-undo',
  5971. 'unorderedlist': 'fa fa-list-ul',
  5972. 'video': 'fa fa-youtube-play'
  5973. }
  5974. }
  5975. });
  5976. }));