summernote.js 203 KB

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