jquery.sceditor.js 141 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634
  1. /**
  2. * SCEditor
  3. * http://www.sceditor.com/
  4. *
  5. * Copyright (C) 2011-2013, Sam Clarke (samclarke.com)
  6. *
  7. * SCEditor is licensed under the MIT license:
  8. * http://www.opensource.org/licenses/mit-license.php
  9. *
  10. * @fileoverview SCEditor - A lightweight WYSIWYG BBCode and HTML editor
  11. * @author Sam Clarke
  12. * @requires jQuery
  13. */
  14. // ==ClosureCompiler==
  15. // @output_file_name jquery.sceditor.min.js
  16. // @compilation_level SIMPLE_OPTIMIZATIONS
  17. // ==/ClosureCompiler==
  18. /*jshint smarttabs: true, scripturl: true, jquery: true, devel:true, eqnull:true, curly: false */
  19. /*global Range: true, browser*/
  20. ;(function ($, window, document) {
  21. 'use strict';
  22. /**
  23. * HTML templates used by the editor and default commands
  24. * @type {Object}
  25. * @private
  26. */
  27. var _templates = {
  28. html: '<!DOCTYPE html>' +
  29. '<html>' +
  30. '<head>' +
  31. '<style>.ie * {min-height: auto !important}</style>' +
  32. '<meta http-equiv="Content-Type" content="text/html;charset={charset}" />' +
  33. '<link rel="stylesheet" type="text/css" href="{style}" />' +
  34. '</head>' +
  35. '<body contenteditable="true" {spellcheck}></body>' +
  36. '</html>',
  37. toolbarButton: '<a class="sceditor-button sceditor-button-{name}" data-sceditor-command="{name}" unselectable="on"><div unselectable="on">{dispName}</div></a>',
  38. emoticon: '<img src="{url}" data-sceditor-emoticon="{key}" alt="{key}" title="{tooltip}" />',
  39. fontOpt: '<a class="sceditor-font-option" href="#" data-font="{font}"><font face="{font}">{font}</font></a>',
  40. sizeOpt: '<a class="sceditor-fontsize-option" data-size="{size}" href="#"><font size="{size}">{size}</font></a>',
  41. pastetext: '<div><label for="txt">{label}</label> ' +
  42. '<textarea cols="20" rows="7" id="txt"></textarea></div>' +
  43. '<div><input type="button" class="button" value="{insert}" /></div>',
  44. table: '<div><label for="rows">{rows}</label><input type="text" id="rows" value="2" /></div>' +
  45. '<div><label for="cols">{cols}</label><input type="text" id="cols" value="2" /></div>' +
  46. '<div><input type="button" class="button" value="{insert}" /></div>',
  47. image: '<div><label for="link">{url}</label> <input type="text" id="image" value="http://" /></div>' +
  48. '<div><label for="width">{width}</label> <input type="text" id="width" size="2" /></div>' +
  49. '<div><label for="height">{height}</label> <input type="text" id="height" size="2" /></div>' +
  50. '<div><input type="button" class="button" value="{insert}" /></div>',
  51. email: '<div><label for="email">{label}</label> <input type="text" id="email" /></div>' +
  52. '<div><input type="button" class="button" value="{insert}" /></div>',
  53. link: '<div><label for="link">{url}</label> <input type="text" id="link" value="http://" /></div>' +
  54. '<div><label for="des">{desc}</label> <input type="text" id="des" /></div>' +
  55. '<div><input type="button" class="button" value="{ins}" /></div>',
  56. youtubeMenu: '<div><label for="link">{label}</label> <input type="text" id="link" value="http://" /></div><div><input type="button" class="button" value="{insert}" /></div>',
  57. youtube: '<iframe width="560" height="315" src="http://www.youtube.com/embed/{id}?wmode=opaque" data-youtube-id="{id}" frameborder="0" allowfullscreen></iframe>'
  58. };
  59. /**
  60. * <p>Replaces any params in a template with the passed params.</p>
  61. *
  62. * <p>If createHTML is passed it will use jQuery to create the HTML. The
  63. * same as doing: $(editor.tmpl("html", {params...}));</p>
  64. *
  65. * @param {string} templateName
  66. * @param {Object} params
  67. * @param {Boolean} createHTML
  68. * @private
  69. */
  70. var _tmpl = function(name, params, createHTML) {
  71. var template = _templates[name];
  72. $.each(params, function(name, val) {
  73. template = template.replace(new RegExp('\\{' + name + '\\}', 'g'), val);
  74. });
  75. if(createHTML)
  76. template = $(template);
  77. return template;
  78. };
  79. /**
  80. * SCEditor - A lightweight WYSIWYG editor
  81. *
  82. * @param {Element} el The textarea to be converted
  83. * @return {Object} options
  84. * @class sceditor
  85. * @name jQuery.sceditor
  86. */
  87. $.sceditor = function (el, options) {
  88. /**
  89. * Alias of this
  90. * @private
  91. */
  92. var base = this;
  93. /**
  94. * The textarea element being replaced
  95. * @private
  96. */
  97. var original = el.get ? el.get(0) : el;
  98. var $original = $(original);
  99. /**
  100. * The div which contains the editor and toolbar
  101. * @private
  102. */
  103. var $editorContainer;
  104. /**
  105. * The editors toolbar
  106. * @private
  107. */
  108. var $toolbar;
  109. /**
  110. * The editors iframe which should be in design mode
  111. * @private
  112. */
  113. var $wysiwygEditor;
  114. var wysiwygEditor;
  115. /**
  116. * The WYSIWYG editors body element
  117. * @private
  118. */
  119. var $wysiwygBody;
  120. /**
  121. * The WYSIWYG editors document
  122. * @private
  123. */
  124. var $wysiwygDoc;
  125. /**
  126. * The editors textarea for viewing source
  127. * @private
  128. */
  129. var $sourceEditor;
  130. var sourceEditor;
  131. /**
  132. * The current dropdown
  133. * @private
  134. */
  135. var $dropdown;
  136. /**
  137. * Array of all the commands key press functions
  138. * @private
  139. * @type {Array}
  140. */
  141. var keyPressFuncs = [];
  142. /**
  143. * Store the last cursor position. Needed for IE because it forgets
  144. * @private
  145. */
  146. var lastRange;
  147. /**
  148. * The editors locale
  149. * @private
  150. */
  151. var locale;
  152. /**
  153. * Stores a cache of preloaded images
  154. * @private
  155. * @type {Array}
  156. */
  157. var preLoadCache = [];
  158. /**
  159. * The editors rangeHelper instance
  160. * @type {jQuery.sceditor.rangeHelper}
  161. * @private
  162. */
  163. var rangeHelper;
  164. /**
  165. * Tags which require the new line fix
  166. * @type {Array}
  167. * @private
  168. */
  169. var requireNewLineFix = [];
  170. /**
  171. * An array of button state handlers
  172. * @type {Array}
  173. * @private
  174. */
  175. var btnStateHandlers = [];
  176. /**
  177. * Element which gets focused to blur the editor.
  178. *
  179. * This will be null until blur() is called.
  180. * @type {HTMLElement}
  181. * @private
  182. */
  183. var $blurElm;
  184. /**
  185. * Plugin manager instance
  186. * @type {jQuery.sceditor.PluginManager}
  187. * @private
  188. */
  189. var pluginManager;
  190. /**
  191. * The current node containing the selection/caret
  192. * @type {Node}
  193. * @private
  194. */
  195. var currentNode;
  196. /**
  197. * The first block level parent of the current node
  198. * @type {node}
  199. * @private
  200. */
  201. var currentBlockNode;
  202. /**
  203. * The current node selection/caret
  204. * @type {Object}
  205. * @private
  206. */
  207. var currentSelection;
  208. /**
  209. * Used to make sure only 1 selection changed check is called every 100ms.
  210. * Helps improve performance as it is checked a lot.
  211. * @type {Boolean}
  212. * @private
  213. */
  214. var isSelectionCheckPending;
  215. /**
  216. * If content is required (equivalent to the HTML5 required attribute)
  217. * @type {Boolean}
  218. * @private
  219. */
  220. var isRequired;
  221. /**
  222. * The inline CSS style element. Will be undefined until css() is called
  223. * for the first time.
  224. * @type {HTMLElement}
  225. * @private
  226. */
  227. var inlineCss;
  228. /**
  229. * Object containing a list of shortcut handlers
  230. * @type {Object}
  231. * @private
  232. */
  233. var shortcutHandlers = {};
  234. /**
  235. * An array of all the current emoticons.
  236. *
  237. * Only used or populated when emoticonsCompat is enabled.
  238. * @type {Array}
  239. * @private
  240. */
  241. var currentEmoticons = [];
  242. /**
  243. * Private functions
  244. * @private
  245. */
  246. var init,
  247. replaceEmoticons,
  248. handleCommand,
  249. saveRange,
  250. initEditor,
  251. initPlugins,
  252. initLocale,
  253. initToolBar,
  254. initOptions,
  255. initEvents,
  256. initCommands,
  257. initResize,
  258. initEmoticons,
  259. getWysiwygDoc,
  260. handlePasteEvt,
  261. handlePasteData,
  262. handleKeyDown,
  263. handleBackSpace,
  264. handleKeyPress,
  265. handleFormReset,
  266. handleMouseDown,
  267. handleEvent,
  268. handleDocumentClick,
  269. handleWindowResize,
  270. updateToolBar,
  271. updateActiveButtons,
  272. sourceEditorSelectedText,
  273. appendNewLine,
  274. checkSelectionChanged,
  275. checkNodeChanged,
  276. autofocus,
  277. emoticonsKeyPress,
  278. emoticonsCheckWhitespace,
  279. currentStyledBlockNode;
  280. /**
  281. * All the commands supported by the editor
  282. * @name commands
  283. * @memberOf jQuery.sceditor.prototype
  284. */
  285. base.commands = $.extend(true, {}, (options.commands || $.sceditor.commands));
  286. /**
  287. * Options for this editor instance
  288. * @name opts
  289. * @memberOf jQuery.sceditor.prototype
  290. */
  291. base.opts = options = $.extend({}, $.sceditor.defaultOptions, options);
  292. /**
  293. * Creates the editor iframe and textarea
  294. * @private
  295. */
  296. init = function () {
  297. $original.data("sceditor", base);
  298. // Clone any objects in options
  299. $.each(options, function(key, val) {
  300. if($.isPlainObject(val))
  301. options[key] = $.extend(true, {}, val);
  302. });
  303. // Load locale
  304. if(options.locale && options.locale !== 'en')
  305. initLocale();
  306. $editorContainer = $('<div class="sceditor-container" />')
  307. .insertAfter($original)
  308. .css('z-index', options.zIndex);
  309. // Add IE version to the container to allow IE specific CSS
  310. // fixes without using CSS hacks or conditional comments
  311. if($.sceditor.ie)
  312. $editorContainer.addClass('ie ie' + $.sceditor.ie);
  313. isRequired = !!$original.attr('required');
  314. $original.removeAttr('required');
  315. // create the editor
  316. initPlugins();
  317. initEmoticons();
  318. initToolBar();
  319. initEditor();
  320. initCommands();
  321. initOptions();
  322. initEvents();
  323. // force into source mode if is a browser that can't handle
  324. // full editing
  325. if(!$.sceditor.isWysiwygSupported)
  326. base.toggleSourceMode();
  327. var loaded = function() {
  328. $(window).unbind('load', loaded);
  329. if(options.autofocus)
  330. autofocus();
  331. if(options.autoExpand)
  332. base.expandToContent();
  333. // Page width might have changed after CSS is loaded so
  334. // call handleWindowResize to update any % based dimensions
  335. handleWindowResize();
  336. };
  337. $(window).load(loaded);
  338. if(document.readyState && document.readyState === 'complete')
  339. loaded();
  340. updateActiveButtons();
  341. pluginManager.call('ready');
  342. };
  343. initPlugins = function() {
  344. var plugins = options.plugins;
  345. plugins = plugins ? plugins.toString().split(',') : [];
  346. pluginManager = new $.sceditor.PluginManager(base);
  347. $.each(plugins, function(idx, plugin) {
  348. pluginManager.register($.trim(plugin));
  349. });
  350. };
  351. /**
  352. * Init the locale variable with the specified locale if possible
  353. * @private
  354. * @return void
  355. */
  356. initLocale = function() {
  357. var lang;
  358. if($.sceditor.locale[options.locale])
  359. locale = $.sceditor.locale[options.locale];
  360. else
  361. {
  362. lang = options.locale.split('-');
  363. if($.sceditor.locale[lang[0]])
  364. locale = $.sceditor.locale[lang[0]];
  365. }
  366. if(locale && locale.dateFormat)
  367. options.dateFormat = locale.dateFormat;
  368. };
  369. /**
  370. * Creates the editor iframe and textarea
  371. * @private
  372. */
  373. initEditor = function () {
  374. var doc, tabIndex;
  375. // @SMF code: tabindex applied to the editor
  376. $sourceEditor = $('<textarea></textarea>').attr('tabindex', $original.attr('tabindex')).hide();
  377. $wysiwygEditor = $('<iframe frameborder="0"></iframe>').attr('tabindex', $original.attr('tabindex'));
  378. if(!options.spellcheck)
  379. $sourceEditor.attr('spellcheck', 'false');
  380. if(window.location.protocol === 'https:')
  381. $wysiwygEditor.attr('src', 'javascript:false');
  382. // add the editor to the HTML and store the editors element
  383. $editorContainer.append($wysiwygEditor).append($sourceEditor);
  384. wysiwygEditor = $wysiwygEditor[0];
  385. sourceEditor = $sourceEditor[0];
  386. base.width(options.width || $original.width());
  387. base.height(options.height || $original.height());
  388. doc = getWysiwygDoc();
  389. doc.open();
  390. doc.write(_tmpl('html', { spellcheck: options.spellcheck ? '' : 'spellcheck="false"', charset: options.charset, style: options.style }));
  391. doc.close();
  392. $wysiwygDoc = $(doc);
  393. $wysiwygBody = $(doc.body);
  394. base.readOnly(!!options.readOnly);
  395. // Add IE version class to the HTML element so can apply
  396. // conditional styling without CSS hacks
  397. if($.sceditor.ie)
  398. $wysiwygDoc.find('html').addClass('ie ie' + $.sceditor.ie);
  399. // iframe overflow fix for iOS, also fixes an IE issue with the
  400. // editor not getting focus when clicking inside
  401. if($.sceditor.ios || $.sceditor.ie)
  402. {
  403. $wysiwygBody.height('100%');
  404. if(!$.sceditor.ie)
  405. $wysiwygBody.bind('touchend', base.focus);
  406. }
  407. rangeHelper = new $.sceditor.rangeHelper(wysiwygEditor.contentWindow);
  408. // load any textarea value into the editor
  409. base.val($original.hide().val());
  410. tabIndex = $original.attr('tabindex');
  411. $sourceEditor.attr('tabindex', tabIndex);
  412. $wysiwygEditor.attr('tabindex', tabIndex);
  413. };
  414. /**
  415. * Initialises options
  416. * @private
  417. */
  418. initOptions = function() {
  419. // auto-update original textbox on blur if option set to true
  420. if(options.autoUpdate)
  421. {
  422. $wysiwygBody.bind('blur', base.updateOriginal);
  423. $sourceEditor.bind('blur', base.updateOriginal);
  424. }
  425. if(options.rtl === null)
  426. options.rtl = $sourceEditor.css('direction') === 'rtl';
  427. base.rtl(!!options.rtl);
  428. if(options.autoExpand)
  429. $wysiwygDoc.bind('keyup', base.expandToContent);
  430. if(options.resizeEnabled)
  431. initResize();
  432. $editorContainer.attr('id', options.id);
  433. base.emoticons(options.emoticonsEnabled);
  434. };
  435. /**
  436. * Initialises events
  437. * @private
  438. */
  439. initEvents = function() {
  440. $(document).click(handleDocumentClick);
  441. $(original.form)
  442. .bind('reset', handleFormReset)
  443. .submit(base.updateOriginal);
  444. $(window).bind('resize orientationChanged', handleWindowResize);
  445. $wysiwygBody
  446. .keypress(handleKeyPress)
  447. .keydown(handleKeyDown)
  448. .keydown(handleBackSpace)
  449. .keyup(appendNewLine)
  450. .bind('paste', handlePasteEvt)
  451. .bind($.sceditor.ie ? 'selectionchange' : 'keyup focus blur contextmenu mouseup touchend click', checkSelectionChanged)
  452. .bind('keydown keyup keypress focus blur contextmenu', handleEvent);
  453. if(options.emoticonsCompat && window.getSelection)
  454. $wysiwygBody.keyup(emoticonsCheckWhitespace);
  455. $sourceEditor.bind('keydown keyup keypress focus blur contextmenu', handleEvent).keydown(handleKeyDown);
  456. $wysiwygDoc
  457. .keypress(handleKeyPress)
  458. .mousedown(handleMouseDown)
  459. .bind($.sceditor.ie ? 'selectionchange' : 'focus blur contextmenu mouseup click', checkSelectionChanged)
  460. .bind('beforedeactivate keyup', saveRange)
  461. .keyup(appendNewLine)
  462. .focus(function() {
  463. lastRange = null;
  464. });
  465. $editorContainer
  466. .bind('selectionchanged', checkNodeChanged)
  467. .bind('selectionchanged', updateActiveButtons)
  468. .bind('selectionchanged', handleEvent)
  469. .bind('nodechanged', handleEvent);
  470. };
  471. /**
  472. * Creates the toolbar and appends it to the container
  473. * @private
  474. */
  475. initToolBar = function () {
  476. var $group, $button,
  477. exclude = (options.toolbarExclude || '').split(','),
  478. groups = options.toolbar.split('|');
  479. $toolbar = $('<div class="sceditor-toolbar" unselectable="on" />');
  480. $.each(groups, function(idx, group) {
  481. $group = $('<div class="sceditor-group" />');
  482. $.each(group.split(','), function(idx, button) {
  483. // The button must be a valid command and not excluded
  484. if(!base.commands[button] || $.inArray(button, exclude) > -1)
  485. return;
  486. $button = _tmpl('toolbarButton', {
  487. name: button,
  488. dispName: base._(base.commands[button].tooltip || button)
  489. }, true);
  490. $button.data('sceditor-txtmode', !!base.commands[button].txtExec);
  491. $button.data('sceditor-wysiwygmode', !!base.commands[button].exec);
  492. $button.click(function() {
  493. var $this = $(this);
  494. if(!$this.hasClass('disabled'))
  495. handleCommand($this, base.commands[button]);
  496. updateActiveButtons();
  497. return false;
  498. });
  499. if(base.commands[button].tooltip)
  500. $button.attr('title', base._(base.commands[button].tooltip));
  501. if(!base.commands[button].exec)
  502. $button.addClass('disabled');
  503. if(base.commands[button].shortcut)
  504. base.addShortcut(base.commands[button].shortcut, button);
  505. $group.append($button);
  506. });
  507. // Exclude empty groups
  508. if($group[0].firstChild)
  509. $toolbar.append($group);
  510. });
  511. // append the toolbar to the toolbarContainer option if given
  512. $(options.toolbarContainer || $editorContainer).append($toolbar);
  513. };
  514. /**
  515. * Creates an array of all the key press functions
  516. * like emoticons, ect.
  517. * @private
  518. */
  519. initCommands = function () {
  520. $.each(base.commands, function (name, cmd) {
  521. if(cmd.keyPress)
  522. keyPressFuncs.push(cmd.keyPress);
  523. if(cmd.forceNewLineAfter && $.isArray(cmd.forceNewLineAfter))
  524. requireNewLineFix = $.merge(requireNewLineFix, cmd.forceNewLineAfter);
  525. if(cmd.state)
  526. btnStateHandlers.push({ name: name, state: cmd.state });
  527. // exec string commands can be passed to queryCommandState
  528. else if(typeof cmd.exec === 'string')
  529. btnStateHandlers.push({ name: name, state: cmd.exec });
  530. });
  531. appendNewLine();
  532. };
  533. /**
  534. * Creates the resizer.
  535. * @private
  536. */
  537. initResize = function () {
  538. var minHeight, maxHeight, minWidth, maxWidth, mouseMoveFunc, mouseUpFunc,
  539. $grip = $('<div class="sceditor-grip" />'),
  540. // cover is used to cover the editor iframe so document still gets mouse move events
  541. $cover = $('<div class="sceditor-resize-cover" />'),
  542. startX = 0,
  543. startY = 0,
  544. startWidth = 0,
  545. startHeight = 0,
  546. origWidth = $editorContainer.width(),
  547. origHeight = $editorContainer.height(),
  548. dragging = false,
  549. rtl = base.rtl();
  550. minHeight = options.resizeMinHeight || origHeight / 1.5;
  551. maxHeight = options.resizeMaxHeight || origHeight * 2.5;
  552. minWidth = options.resizeMinWidth || origWidth / 1.25;
  553. maxWidth = options.resizeMaxWidth || origWidth * 1.25;
  554. mouseMoveFunc = function (e) {
  555. // iOS must use window.event
  556. if(e.type === 'touchmove')
  557. e = window.event;
  558. var newHeight = startHeight + (e.pageY - startY),
  559. newWidth = rtl ? startWidth - (e.pageX - startX) : startWidth + (e.pageX - startX);
  560. if(maxWidth > 0 && newWidth > maxWidth)
  561. newWidth = maxWidth;
  562. if(maxHeight > 0 && newHeight > maxHeight)
  563. newHeight = maxHeight;
  564. if(!options.resizeWidth || newWidth < minWidth || (maxWidth > 0 && newWidth > maxWidth))
  565. newWidth = false;
  566. if(!options.resizeHeight || newHeight < minHeight || (maxHeight > 0 && newHeight > maxHeight))
  567. newHeight = false;
  568. if(newWidth || newHeight)
  569. {
  570. base.dimensions(newWidth, newHeight);
  571. // The resize cover will not fill the container in IE6 unless a height is specified.
  572. if($.sceditor.ie < 7)
  573. $editorContainer.height(newHeight);
  574. }
  575. e.preventDefault();
  576. };
  577. mouseUpFunc = function (e) {
  578. if(!dragging)
  579. return;
  580. dragging = false;
  581. $cover.hide();
  582. $editorContainer.removeClass('resizing').height('auto');
  583. $(document).unbind('touchmove mousemove', mouseMoveFunc);
  584. $(document).unbind('touchend mouseup', mouseUpFunc);
  585. e.preventDefault();
  586. };
  587. $editorContainer.append($grip);
  588. $editorContainer.append($cover.hide());
  589. $grip.bind('touchstart mousedown', function (e) {
  590. // iOS must use window.event
  591. if(e.type === 'touchstart')
  592. e = window.event;
  593. startX = e.pageX;
  594. startY = e.pageY;
  595. startWidth = $editorContainer.width();
  596. startHeight = $editorContainer.height();
  597. dragging = true;
  598. $editorContainer.addClass('resizing');
  599. $cover.show();
  600. $(document).bind('touchmove mousemove', mouseMoveFunc);
  601. $(document).bind('touchend mouseup', mouseUpFunc);
  602. // The resize cover will not fill the container in IE6 unless a height is specified.
  603. if($.sceditor.ie < 7)
  604. $editorContainer.height(startHeight);
  605. e.preventDefault();
  606. });
  607. };
  608. /**
  609. * Prefixes and preloads the emoticon images
  610. * @private
  611. */
  612. initEmoticons = function () {
  613. var emoticon,
  614. emoticons = options.emoticons,
  615. root = options.emoticonsRoot;
  616. if(!$.isPlainObject(emoticons) || !options.emoticonsEnabled)
  617. return;
  618. $.each(emoticons, function (idx, val) {
  619. $.each(val, function (key, url) {
  620. // @SMF code: In SMF an empty entry means a new line
  621. if (url == '')
  622. emoticon = document.createElement('br');
  623. // Prefix emoticon root to emoticon urls
  624. if(root)
  625. {
  626. url = {
  627. url: root + (url.url || url),
  628. tooltip: url.tooltip || key
  629. };
  630. emoticons[idx][key] = url;
  631. }
  632. // Preload the emoticon
  633. // Idea from: http://engineeredweb.com/blog/09/12/preloading-images-jquery-and-javascript
  634. emoticon = document.createElement('img');
  635. emoticon.src = url.url || url;
  636. preLoadCache.push(emoticon);
  637. });
  638. });
  639. };
  640. /**
  641. * Autofocus the editor
  642. * @private
  643. */
  644. autofocus = function() {
  645. var rng, elm, txtPos,
  646. doc = $wysiwygDoc[0],
  647. body = $wysiwygBody[0],
  648. focusEnd = !!options.autofocusEnd;
  649. // Can't focus invisible elements
  650. if(!$editorContainer.is(':visible'))
  651. return;
  652. if(base.sourceMode())
  653. {
  654. txtPos = sourceEditor.value.length;
  655. if(sourceEditor.setSelectionRange)
  656. sourceEditor.setSelectionRange(txtPos, txtPos);
  657. else if (sourceEditor.createTextRange)
  658. {
  659. rng = sourceEditor.createTextRange();
  660. rng.moveEnd('character', txtPos);
  661. rng.moveStart('character', txtPos);
  662. rangeHelper.selectRange(rng);
  663. }
  664. }
  665. else // WYSIWYG mode
  666. {
  667. $.sceditor.dom.removeWhiteSpace(body);
  668. if(focusEnd)
  669. {
  670. if(!(elm = body.lastChild))
  671. $wysiwygBody.append((elm = doc.createElement('div')));
  672. while(elm.lastChild)
  673. {
  674. elm = elm.lastChild;
  675. if(/br/i.test(elm.nodeName) && elm.previousSibling)
  676. elm = elm.previousSibling;
  677. }
  678. }
  679. else
  680. elm = body.firstChild;
  681. if(doc.createRange)
  682. {
  683. rng = doc.createRange();
  684. if(/br/i.test(elm.nodeName))
  685. rng.setStartBefore(elm);
  686. else
  687. rng.selectNodeContents(elm);
  688. rng.collapse(false);
  689. }
  690. else
  691. {
  692. rng = body.createTextRange();
  693. rng.moveToElementText(elm.nodeType !== 3 ? elm : elm.parentNode);
  694. rng.collapse(false);
  695. }
  696. rangeHelper.selectRange(rng);
  697. if(focusEnd)
  698. {
  699. $wysiwygDoc.scrollTop(body.scrollHeight);
  700. $wysiwygBody.scrollTop(body.scrollHeight);
  701. }
  702. }
  703. base.focus();
  704. };
  705. /**
  706. * Gets if the editor is read only
  707. *
  708. * @since 1.3.5
  709. * @function
  710. * @memberOf jQuery.sceditor.prototype
  711. * @name readOnly
  712. * @return {Boolean}
  713. */
  714. /**
  715. * Sets if the editor is read only
  716. *
  717. * @param {boolean} readOnly
  718. * @since 1.3.5
  719. * @function
  720. * @memberOf jQuery.sceditor.prototype
  721. * @name readOnly^2
  722. * @return {this}
  723. */
  724. base.readOnly = function(readOnly) {
  725. if(typeof readOnly !== 'boolean')
  726. return $sourceEditor.attr('readonly') === 'readonly';
  727. $wysiwygBody[0].contentEditable = !readOnly;
  728. if(!readOnly)
  729. $sourceEditor.removeAttr('readonly');
  730. else
  731. $sourceEditor.attr('readonly', 'readonly');
  732. updateToolBar(readOnly);
  733. return this;
  734. };
  735. /**
  736. * Gets if the editor is in RTL mode
  737. *
  738. * @since 1.4.1
  739. * @function
  740. * @memberOf jQuery.sceditor.prototype
  741. * @name rtl
  742. * @return {Boolean}
  743. */
  744. /**
  745. * Sets if the editor is in RTL mode
  746. *
  747. * @param {boolean} rtl
  748. * @since 1.4.1
  749. * @function
  750. * @memberOf jQuery.sceditor.prototype
  751. * @name rtl^2
  752. * @return {this}
  753. */
  754. base.rtl = function(rtl) {
  755. var dir = rtl ? 'rtl' : 'ltr';
  756. if(typeof rtl !== 'boolean')
  757. return $sourceEditor.attr('dir') === 'rtl';
  758. $wysiwygBody.attr('dir', dir);
  759. $sourceEditor.attr('dir', dir);
  760. $editorContainer
  761. .removeClass('rtl')
  762. .removeClass('ltr')
  763. .addClass(dir);
  764. return this;
  765. };
  766. /**
  767. * Updates the toolbar to disable/enable the appropriate buttons
  768. * @private
  769. */
  770. updateToolBar = function(disable) {
  771. var inSourceMode = base.inSourceMode();
  772. $toolbar.find('.sceditor-button').removeClass('disabled').each(function () {
  773. var button = $(this);
  774. if(disable === true || (inSourceMode && !button.data('sceditor-txtmode')))
  775. button.addClass('disabled');
  776. else if (!inSourceMode && !button.data('sceditor-wysiwygmode'))
  777. button.addClass('disabled');
  778. });
  779. };
  780. /**
  781. * Gets the width of the editor in pixels
  782. *
  783. * @since 1.3.5
  784. * @function
  785. * @memberOf jQuery.sceditor.prototype
  786. * @name width
  787. * @return {int}
  788. */
  789. /**
  790. * Sets the width of the editor
  791. *
  792. * @param {int} width Width in pixels
  793. * @since 1.3.5
  794. * @function
  795. * @memberOf jQuery.sceditor.prototype
  796. * @name width^2
  797. * @return {this}
  798. */
  799. /**
  800. * Sets the width of the editor
  801. *
  802. * The saveWidth specifies if to save the width. The stored width can be
  803. * used for things like restoring from maximized state.
  804. *
  805. * @param {int} height Width in pixels
  806. * @param {boolean} [saveWidth=true] If to store the width
  807. * @since 1.4.1
  808. * @function
  809. * @memberOf jQuery.sceditor.prototype
  810. * @name width^3
  811. * @return {this}
  812. */
  813. base.width = function (width, saveWidth) {
  814. if(!width && width !== 0)
  815. return $editorContainer.width();
  816. base.dimensions(width, null, saveWidth);
  817. return this;
  818. };
  819. /**
  820. * Returns an object with the properties width and height
  821. * which are the width and height of the editor in px.
  822. *
  823. * @since 1.4.1
  824. * @function
  825. * @memberOf jQuery.sceditor.prototype
  826. * @name dimensions
  827. * @return {object}
  828. */
  829. /**
  830. * <p>Sets the width and/or height of the editor.</p>
  831. *
  832. * <p>If width or height is not numeric it is ignored.</p>
  833. *
  834. * @param {int} width Width in px
  835. * @param {int} height Height in px
  836. * @since 1.4.1
  837. * @function
  838. * @memberOf jQuery.sceditor.prototype
  839. * @name dimensions^2
  840. * @return {this}
  841. */
  842. /**
  843. * <p>Sets the width and/or height of the editor.</p>
  844. *
  845. * <p>If width or height is not numeric it is ignored.</p>
  846. *
  847. * <p>The save argument specifies if to save the new sizes.
  848. * The saved sizes can be used for things like restoring from
  849. * maximized state. This should normally be left as true.</p>
  850. *
  851. * @param {int} width Width in px
  852. * @param {int} height Height in px
  853. * @param {boolean} [save=true] If to store the new sizes
  854. * @since 1.4.1
  855. * @function
  856. * @memberOf jQuery.sceditor.prototype
  857. * @name dimensions^3
  858. * @return {this}
  859. */
  860. base.dimensions = function(width, height, save) {
  861. // IE6 & IE7 add 2 pixels to the source mode textarea height which must be ignored.
  862. // Doesn't seem to be any way to fix it with only CSS
  863. var ieBorderBox = $.sceditor.ie < 8 || document.documentMode < 8 ? 2 : 0;
  864. // set undefined width/height to boolean false
  865. width = (!width && width !== 0) ? false : width;
  866. height = (!height && height !== 0) ? false : height;
  867. if(width === false && height === false)
  868. return { width: base.width(), height: base.height() };
  869. if(typeof $wysiwygEditor.data('outerWidthOffset') === 'undefined')
  870. base.updateStyleCache();
  871. if(width !== false)
  872. {
  873. if(save !== false)
  874. options.width = width;
  875. if(height === false)
  876. {
  877. height = $editorContainer.height();
  878. save = false;
  879. }
  880. $editorContainer.width(width);
  881. if(width && width.toString().indexOf('%') > -1)
  882. width = $editorContainer.width();
  883. $wysiwygEditor.width(width - $wysiwygEditor.data('outerWidthOffset'));
  884. $sourceEditor.width(width - $sourceEditor.data('outerWidthOffset'));
  885. // Fix overflow issue with iOS not breaking words unless a width is set
  886. if($.sceditor.ios && $wysiwygBody)
  887. $wysiwygBody.width(width - $wysiwygEditor.data('outerWidthOffset') - ($wysiwygBody.outerWidth(true) - $wysiwygBody.width()));
  888. }
  889. if(height !== false)
  890. {
  891. if(save !== false)
  892. options.height = height;
  893. // Convert % based heights to px
  894. if(height && height.toString().indexOf('%') > -1)
  895. {
  896. height = $editorContainer.height(height).height();
  897. $editorContainer.height('auto');
  898. }
  899. height -= !options.toolbarContainer ? $toolbar.outerHeight(true) : 0;
  900. $wysiwygEditor.height(height - $wysiwygEditor.data('outerHeightOffset'));
  901. $sourceEditor.height(height - ieBorderBox - $sourceEditor.data('outerHeightOffset'));
  902. }
  903. return this;
  904. };
  905. /**
  906. * Updates the CSS styles cache. Shouldn't be needed unless changing the editors theme.
  907. *
  908. * @since 1.4.1
  909. * @function
  910. * @memberOf jQuery.sceditor.prototype
  911. * @name updateStyleCache
  912. * @return {int}
  913. */
  914. base.updateStyleCache = function() {
  915. // caching these improves FF resize performance
  916. $wysiwygEditor.data('outerWidthOffset', $wysiwygEditor.outerWidth(true) - $wysiwygEditor.width());
  917. $sourceEditor.data('outerWidthOffset', $sourceEditor.outerWidth(true) - $sourceEditor.width());
  918. $wysiwygEditor.data('outerHeightOffset', $wysiwygEditor.outerHeight(true) - $wysiwygEditor.height());
  919. $sourceEditor.data('outerHeightOffset', $sourceEditor.outerHeight(true) - $sourceEditor.height());
  920. };
  921. /**
  922. * Gets the height of the editor in px
  923. *
  924. * @since 1.3.5
  925. * @function
  926. * @memberOf jQuery.sceditor.prototype
  927. * @name height
  928. * @return {int}
  929. */
  930. /**
  931. * Sets the height of the editor
  932. *
  933. * @param {int} height Height in px
  934. * @since 1.3.5
  935. * @function
  936. * @memberOf jQuery.sceditor.prototype
  937. * @name height^2
  938. * @return {this}
  939. */
  940. /**
  941. * Sets the height of the editor
  942. *
  943. * The saveHeight specifies if to save the height. The stored height can be
  944. * used for things like restoring from maximized state.
  945. *
  946. * @param {int} height Height in px
  947. * @param {boolean} [saveHeight=true] If to store the height
  948. * @since 1.4.1
  949. * @function
  950. * @memberOf jQuery.sceditor.prototype
  951. * @name height^3
  952. * @return {this}
  953. */
  954. base.height = function (height, saveHeight) {
  955. if(!height && height !== 0)
  956. return $editorContainer.height();
  957. base.dimensions(null, height, saveHeight);
  958. return this;
  959. };
  960. /**
  961. * Gets if the editor is maximised or not
  962. *
  963. * @since 1.4.1
  964. * @function
  965. * @memberOf jQuery.sceditor.prototype
  966. * @name maximize
  967. * @return {boolean}
  968. */
  969. /**
  970. * Sets if the editor is maximised or not
  971. *
  972. * @param {boolean} maximize If to maximise the editor
  973. * @since 1.4.1
  974. * @function
  975. * @memberOf jQuery.sceditor.prototype
  976. * @name maximize^2
  977. * @return {this}
  978. */
  979. base.maximize = function(maximize) {
  980. if(typeof maximize === 'undefined')
  981. return $editorContainer.is('.sceditor-maximize');
  982. maximize = !!maximize;
  983. // IE 6 fix
  984. if($.sceditor.ie < 7)
  985. $('html, body').toggleClass('sceditor-maximize', maximize);
  986. $editorContainer.toggleClass('sceditor-maximize', maximize);
  987. base.width(maximize ? '100%' : options.width, false);
  988. base.height(maximize ? '100%' : options.height, false);
  989. return this;
  990. };
  991. /**
  992. * Expands the editors height to the height of it's content
  993. *
  994. * Unless ignoreMaxHeight is set to true it will not expand
  995. * higher than the maxHeight option.
  996. *
  997. * @since 1.3.5
  998. * @param {Boolean} [ignoreMaxHeight=false]
  999. * @function
  1000. * @name expandToContent
  1001. * @memberOf jQuery.sceditor.prototype
  1002. * @see #resizeToContent
  1003. */
  1004. base.expandToContent = function(ignoreMaxHeight) {
  1005. var currentHeight = $editorContainer.height(),
  1006. height = $wysiwygBody[0].scrollHeight || $wysiwygDoc[0].documentElement.scrollHeight,
  1007. padding = (currentHeight - $wysiwygEditor.height()),
  1008. maxHeight = options.resizeMaxHeight || ((options.height || $original.height()) * 2);
  1009. height += padding;
  1010. if(ignoreMaxHeight !== true && height > maxHeight)
  1011. height = maxHeight;
  1012. if(height > currentHeight)
  1013. base.height(height);
  1014. };
  1015. /**
  1016. * Destroys the editor, removing all elements and
  1017. * event handlers.
  1018. *
  1019. * Leaves only the original textarea.
  1020. *
  1021. * @function
  1022. * @name destroy
  1023. * @memberOf jQuery.sceditor.prototype
  1024. */
  1025. base.destroy = function () {
  1026. pluginManager.destroy();
  1027. rangeHelper = null;
  1028. lastRange = null;
  1029. pluginManager = null;
  1030. $(document).unbind('click', handleDocumentClick);
  1031. $(window).unbind('resize orientationChanged', handleWindowResize);
  1032. $(original.form)
  1033. .unbind('reset', handleFormReset)
  1034. .unbind('submit', base.updateOriginal);
  1035. $wysiwygBody.unbind();
  1036. $wysiwygDoc.unbind().find('*').remove();
  1037. $sourceEditor.unbind().remove();
  1038. $toolbar.remove();
  1039. $editorContainer.unbind().find('*').unbind().remove();
  1040. $editorContainer.remove();
  1041. $original
  1042. .removeData('sceditor')
  1043. .removeData('sceditorbbcode')
  1044. .show();
  1045. if(isRequired)
  1046. $original.attr('required', 'required');
  1047. };
  1048. /**
  1049. * Creates a menu item drop down
  1050. *
  1051. * @param {HTMLElement} menuItem The button to align the drop down with
  1052. * @param {string} dropDownName Used for styling the dropown, will be a class sceditor-dropDownName
  1053. * @param {HTMLElement} content The HTML content of the dropdown
  1054. * @param {bool} [ieUnselectable=true] If to add the unselectable attribute to all the contents elements. Stops IE from deselecting the text in the editor
  1055. * @function
  1056. * @name createDropDown
  1057. * @memberOf jQuery.sceditor.prototype
  1058. */
  1059. base.createDropDown = function (menuItem, dropDownName, content, ieUnselectable) {
  1060. // first click for create second click for close
  1061. var css,
  1062. onlyclose = $dropdown && $dropdown.is('.sceditor-' + dropDownName);
  1063. base.closeDropDown();
  1064. if (onlyclose) return;
  1065. // IE needs unselectable attr to stop it from unselecting the text in the editor.
  1066. // The editor can cope if IE does unselect the text it's just not nice.
  1067. if (ieUnselectable !== false)
  1068. {
  1069. $(content)
  1070. .find(':not(input,textarea)')
  1071. .filter(function() {
  1072. return this.nodeType===1;
  1073. })
  1074. .attr('unselectable', 'on');
  1075. }
  1076. css = {
  1077. top: menuItem.offset().top,
  1078. left: menuItem.offset().left,
  1079. marginTop: menuItem.outerHeight()
  1080. };
  1081. $.extend(css, options.dropDownCss);
  1082. $dropdown = $('<div class="sceditor-dropdown sceditor-' + dropDownName + '" />')
  1083. .css(css)
  1084. .append(content)
  1085. .appendTo($('body'))
  1086. .click(function (e) {
  1087. // stop clicks within the dropdown from being handled
  1088. e.stopPropagation();
  1089. });
  1090. };
  1091. /**
  1092. * Handles any document click and closes the dropdown if open
  1093. * @private
  1094. */
  1095. handleDocumentClick = function (e) {
  1096. // ignore right clicks
  1097. if(e.which !== 3)
  1098. base.closeDropDown();
  1099. };
  1100. /**
  1101. * Handles the WYSIWYG editors paste event
  1102. * @private
  1103. */
  1104. handlePasteEvt = function(e) {
  1105. var html, handlePaste,
  1106. elm = $wysiwygBody[0],
  1107. doc = $wysiwygDoc[0],
  1108. checkCount = 0,
  1109. pastearea = document.createElement('div'),
  1110. prePasteContent = doc.createDocumentFragment();
  1111. if (options.disablePasting)
  1112. return false;
  1113. if (!options.enablePasteFiltering)
  1114. return;
  1115. rangeHelper.saveRange();
  1116. document.body.appendChild(pastearea);
  1117. if (e && e.clipboardData && e.clipboardData.getData)
  1118. {
  1119. if ((html = e.clipboardData.getData('text/html')) || (html = e.clipboardData.getData('text/plain')))
  1120. {
  1121. pastearea.innerHTML = html;
  1122. handlePasteData(elm, pastearea);
  1123. return false;
  1124. }
  1125. }
  1126. while(elm.firstChild)
  1127. prePasteContent.appendChild(elm.firstChild);
  1128. // try make pastearea contenteditable and redirect to that? Might work.
  1129. // Check the tests if still exist, if not re-0create
  1130. handlePaste = function (elm, pastearea) {
  1131. if (elm.childNodes.length > 0)
  1132. {
  1133. while(elm.firstChild)
  1134. pastearea.appendChild(elm.firstChild);
  1135. while(prePasteContent.firstChild)
  1136. elm.appendChild(prePasteContent.firstChild);
  1137. handlePasteData(elm, pastearea);
  1138. }
  1139. else
  1140. {
  1141. // Allow max 25 checks before giving up.
  1142. // Needed in case an empty string is pasted or
  1143. // something goes wrong.
  1144. if(checkCount > 25)
  1145. {
  1146. while(prePasteContent.firstChild)
  1147. elm.appendChild(prePasteContent.firstChild);
  1148. rangeHelper.restoreRange();
  1149. return;
  1150. }
  1151. ++checkCount;
  1152. setTimeout(function () {
  1153. handlePaste(elm, pastearea);
  1154. }, 20);
  1155. }
  1156. };
  1157. handlePaste(elm, pastearea);
  1158. base.focus();
  1159. return true;
  1160. };
  1161. /**
  1162. * Gets the pasted data, filters it and then inserts it.
  1163. * @param {Element} elm
  1164. * @param {Element} pastearea
  1165. * @private
  1166. */
  1167. handlePasteData = function(elm, pastearea) {
  1168. // fix any invalid nesting
  1169. $.sceditor.dom.fixNesting(pastearea);
  1170. // TODO: Trigger custom paste event to allow filtering (pre and post converstion?)
  1171. var pasteddata = pastearea.innerHTML;
  1172. if(pluginManager.hasHandler('toSource'))
  1173. pasteddata = pluginManager.callOnlyFirst('toSource', pasteddata, $(pastearea));
  1174. pastearea.parentNode.removeChild(pastearea);
  1175. if(pluginManager.hasHandler('toWysiwyg'))
  1176. pasteddata = pluginManager.callOnlyFirst('toWysiwyg', pasteddata, true);
  1177. rangeHelper.restoreRange();
  1178. base.wysiwygEditorInsertHtml(pasteddata, null, true);
  1179. };
  1180. /**
  1181. * Closes any currently open drop down
  1182. *
  1183. * @param {bool} [focus=false] If to focus the editor after closing the drop down
  1184. * @function
  1185. * @name closeDropDown
  1186. * @memberOf jQuery.sceditor.prototype
  1187. */
  1188. base.closeDropDown = function (focus) {
  1189. if($dropdown) {
  1190. $dropdown.unbind().remove();
  1191. $dropdown = null;
  1192. }
  1193. if(focus === true)
  1194. base.focus();
  1195. };
  1196. /**
  1197. * Gets the WYSIWYG editors document
  1198. * @private
  1199. */
  1200. getWysiwygDoc = function () {
  1201. if (wysiwygEditor.contentDocument)
  1202. return wysiwygEditor.contentDocument;
  1203. if (wysiwygEditor.contentWindow && wysiwygEditor.contentWindow.document)
  1204. return wysiwygEditor.contentWindow.document;
  1205. if (wysiwygEditor.document)
  1206. return wysiwygEditor.document;
  1207. return null;
  1208. };
  1209. /**
  1210. * <p>Inserts HTML into WYSIWYG editor.</p>
  1211. *
  1212. * <p>If endHtml is specified, any selected text will be placed between html
  1213. * and endHtml. If there is no selected text html and endHtml will just be
  1214. * concated together.</p>
  1215. *
  1216. * @param {string} html
  1217. * @param {string} [endHtml=null]
  1218. * @param {boolean} [overrideCodeBlocking=false] If to insert the html into code tags, by default code tags only support text.
  1219. * @function
  1220. * @name wysiwygEditorInsertHtml
  1221. * @memberOf jQuery.sceditor.prototype
  1222. */
  1223. base.wysiwygEditorInsertHtml = function (html, endHtml, overrideCodeBlocking) {
  1224. var scrollTo, $marker,
  1225. marker = '<span id="sceditor-cursor">&nbsp;</span>';
  1226. base.focus();
  1227. // TODO: This code tag should be configurable and should maybe convert the HTML into text
  1228. // don't apply to code elements
  1229. if(!overrideCodeBlocking && ($(currentBlockNode).is('code') || $(currentBlockNode).parents('code').length !== 0))
  1230. return;
  1231. if(endHtml)
  1232. endHtml += marker;
  1233. else
  1234. html += marker;
  1235. rangeHelper.insertHTML(html, endHtml);
  1236. // Scroll the editor to after the inserted HTML
  1237. $marker = $wysiwygBody.find('#sceditor-cursor');
  1238. scrollTo = ($marker.offset().top + ($marker.outerHeight(true) * 2)) - $wysiwygEditor.height();
  1239. $marker.remove();
  1240. // TODO: check if already in range and don't scroll if it is
  1241. $wysiwygDoc.scrollTop(scrollTo);
  1242. $wysiwygBody.scrollTop(scrollTo);
  1243. rangeHelper.saveRange();
  1244. replaceEmoticons($wysiwygBody[0]);
  1245. rangeHelper.restoreRange();
  1246. appendNewLine();
  1247. };
  1248. /**
  1249. * Like wysiwygEditorInsertHtml except it will convert any HTML into text
  1250. * before inserting it.
  1251. *
  1252. * @param {String} text
  1253. * @param {String} [endText=null]
  1254. * @function
  1255. * @name wysiwygEditorInsertText
  1256. * @memberOf jQuery.sceditor.prototype
  1257. */
  1258. base.wysiwygEditorInsertText = function (text, endText) {
  1259. base.wysiwygEditorInsertHtml($.sceditor.escapeEntities(text), $.sceditor.escapeEntities(endText));
  1260. };
  1261. /**
  1262. * <p>Inserts text into the WYSIWYG or source editor depending on which
  1263. * mode the editor is in.</p>
  1264. *
  1265. * <p>If endText is specified any selected text will be placed between
  1266. * text and endText. If no text is selected text and endText will
  1267. * just be concated together.</p>
  1268. *
  1269. * @param {String} text
  1270. * @param {String} [endText=null]
  1271. * @since 1.3.5
  1272. * @function
  1273. * @name insertText
  1274. * @memberOf jQuery.sceditor.prototype
  1275. */
  1276. base.insertText = function (text, endText) {
  1277. if(base.inSourceMode())
  1278. base.sourceEditorInsertText(text, endText);
  1279. else
  1280. base.wysiwygEditorInsertText(text, endText);
  1281. return this;
  1282. };
  1283. /**
  1284. * <p>Like wysiwygEditorInsertHtml but inserts text into the
  1285. * source mode editor instead.</p>
  1286. *
  1287. * <p>If endText is specified any selected text will be placed between
  1288. * text and endText. If no text is selected text and endText will
  1289. * just be concated together.</p>
  1290. *
  1291. * <p>The cursor will be placed after the text param. If endText is
  1292. * specified the cursor will be placed before endText, so passing:<br />
  1293. *
  1294. * '[b]', '[/b]'</p>
  1295. *
  1296. * <p>Would cause the cursor to be placed:<br />
  1297. *
  1298. * [b]Selected text|[/b]</p>
  1299. *
  1300. * @param {String} text
  1301. * @param {String} [endText=null]
  1302. * @since 1.4.0
  1303. * @function
  1304. * @name sourceEditorInsertText
  1305. * @memberOf jQuery.sceditor.prototype
  1306. */
  1307. base.sourceEditorInsertText = function (text, endText) {
  1308. var range, start, end, txtLen, scrollTop;
  1309. scrollTop = sourceEditor.scrollTop;
  1310. sourceEditor.focus();
  1311. if(typeof sourceEditor.selectionStart !== 'undefined')
  1312. {
  1313. start = sourceEditor.selectionStart;
  1314. end = sourceEditor.selectionEnd;
  1315. txtLen = text.length;
  1316. if(endText)
  1317. text += sourceEditor.value.substring(start, end) + endText;
  1318. sourceEditor.value = sourceEditor.value.substring(0, start) + text + sourceEditor.value.substring(end, sourceEditor.value.length);
  1319. sourceEditor.selectionStart = (start + text.length) - (endText ? endText.length : 0);
  1320. sourceEditor.selectionEnd = sourceEditor.selectionStart;
  1321. }
  1322. else if(typeof document.selection.createRange !== 'undefined')
  1323. {
  1324. range = document.selection.createRange();
  1325. if(endText)
  1326. text += range.text + endText;
  1327. range.text = text;
  1328. if(endText)
  1329. range.moveEnd('character', 0-endText.length);
  1330. range.moveStart('character', range.End - range.Start);
  1331. range.select();
  1332. }
  1333. else
  1334. sourceEditor.value += text + endText;
  1335. sourceEditor.scrollTop = scrollTop;
  1336. sourceEditor.focus();
  1337. };
  1338. /**
  1339. * Gets the current instance of the rangeHelper class
  1340. * for the editor.
  1341. *
  1342. * @return jQuery.sceditor.rangeHelper
  1343. * @function
  1344. * @name getRangeHelper
  1345. * @memberOf jQuery.sceditor.prototype
  1346. */
  1347. base.getRangeHelper = function () {
  1348. return rangeHelper;
  1349. };
  1350. /**
  1351. * <p>Gets the value of the editor.</p>
  1352. *
  1353. * <p>If the editor is in WYSIWYG mode it will return the filtered
  1354. * HTML from it (converted to BBCode if using the BBCode plugin).
  1355. * It it's in Source Mode it will return the unfiltered contents
  1356. * of the source editor (if using the BBCode plugin this will be
  1357. * BBCode again).</p>
  1358. *
  1359. * @since 1.3.5
  1360. * @return {string}
  1361. * @function
  1362. * @name val
  1363. * @memberOf jQuery.sceditor.prototype
  1364. */
  1365. /**
  1366. * <p>Sets the value of the editor.</p>
  1367. *
  1368. * <p>If filter set true the val will be passed through the filter
  1369. * function. If using the BBCode plugin it will pass the val to
  1370. * the BBCode filter to convert any BBCode into HTML.</p>
  1371. *
  1372. * @param {String} val
  1373. * @param {Boolean} [filter=true]
  1374. * @return {this}
  1375. * @since 1.3.5
  1376. * @function
  1377. * @name val^2
  1378. * @memberOf jQuery.sceditor.prototype
  1379. */
  1380. base.val = function (val, filter) {
  1381. if(typeof val === "string")
  1382. {
  1383. if(base.inSourceMode())
  1384. base.setSourceEditorValue(val);
  1385. else
  1386. {
  1387. if(filter !== false && pluginManager.hasHandler('toWysiwyg'))
  1388. val = pluginManager.callOnlyFirst('toWysiwyg', val);
  1389. base.setWysiwygEditorValue(val);
  1390. }
  1391. return this;
  1392. }
  1393. return base.inSourceMode() ?
  1394. base.getSourceEditorValue(false) :
  1395. base.getWysiwygEditorValue();
  1396. };
  1397. /**
  1398. * <p>Inserts HTML/BBCode into the editor</p>
  1399. *
  1400. * <p>If end is supplied any selected text will be placed between
  1401. * start and end. If there is no selected text start and end
  1402. * will be concated together.</p>
  1403. *
  1404. * <p>If the filter param is set to true, the HTML/BBCode will be
  1405. * passed through any plugin filters. If using the BBCode plugin
  1406. * this will convert any BBCode into HTML.</p>
  1407. *
  1408. * @param {String} start
  1409. * @param {String} [end=null]
  1410. * @param {Boolean} [filter=true]
  1411. * @param {Boolean} [convertEmoticons=true] If to convert emoticons
  1412. * @return {this}
  1413. * @since 1.3.5
  1414. * @function
  1415. * @name insert
  1416. * @memberOf jQuery.sceditor.prototype
  1417. */
  1418. /**
  1419. * <p>Inserts HTML/BBCode into the editor</p>
  1420. *
  1421. * <p>If end is supplied any selected text will be placed between
  1422. * start and end. If there is no selected text start and end
  1423. * will be concated together.</p>
  1424. *
  1425. * <p>If the filter param is set to true, the HTML/BBCode will be
  1426. * passed through any plugin filters. If using the BBCode plugin
  1427. * this will convert any BBCode into HTML.</p>
  1428. *
  1429. * <p>If the allowMixed param is set to true, HTML any will not be escaped</p>
  1430. *
  1431. * @param {String} start
  1432. * @param {String} [end=null]
  1433. * @param {Boolean} [filter=true]
  1434. * @param {Boolean} [convertEmoticons=true] If to convert emoticons
  1435. * @param {Boolean} [allowMixed=false]
  1436. * @return {this}
  1437. * @since 1.4.3
  1438. * @function
  1439. * @name insert^2
  1440. * @memberOf jQuery.sceditor.prototype
  1441. */
  1442. base.insert = function (start, end, filter, convertEmoticons, allowMixed) {
  1443. if(base.inSourceMode())
  1444. base.sourceEditorInsertText(start, end);
  1445. else
  1446. {
  1447. // Add the selection between start and end
  1448. if(end)
  1449. {
  1450. var html = base.getRangeHelper().selectedHtml(),
  1451. frag = $('<div>').appendTo($('body')).hide().html(html);
  1452. if(filter !== false && pluginManager.hasHandler('toSource'))
  1453. html = pluginManager.callOnlyFirst('toSource', html, frag);
  1454. frag.remove();
  1455. start += html + end;
  1456. }
  1457. if(filter !== false && pluginManager.hasHandler('toWysiwyg'))
  1458. start = pluginManager.callOnlyFirst('toWysiwyg', start, true);
  1459. // Convert any escaped HTML back into HTML if mixed is allowed
  1460. if(filter !== false && allowMixed === true)
  1461. {
  1462. start = start.replace(/&lt;/g, '<')
  1463. .replace(/&gt;/g, '>')
  1464. .replace(/&amp;/g, '&');
  1465. }
  1466. base.wysiwygEditorInsertHtml(start);
  1467. }
  1468. return this;
  1469. };
  1470. /**
  1471. * Gets the WYSIWYG editors HTML value.
  1472. *
  1473. * If using a plugin that filters the Ht Ml like the BBCode plugin
  1474. * it will return the result of the filtering (BBCode) unless the
  1475. * filter param is set to false.
  1476. *
  1477. * @param {bool} [filter=true]
  1478. * @return {string}
  1479. * @function
  1480. * @name getWysiwygEditorValue
  1481. * @memberOf jQuery.sceditor.prototype
  1482. */
  1483. base.getWysiwygEditorValue = function(filter) {
  1484. var html, ieBookmark,
  1485. hasSelection = rangeHelper.hasSelection();
  1486. if(hasSelection)
  1487. rangeHelper.saveRange();
  1488. // IE <= 8 bookmark the current TextRange position
  1489. // and restore it after
  1490. else if(lastRange && lastRange.getBookmark)
  1491. ieBookmark = lastRange.getBookmark();
  1492. $.sceditor.dom.fixNesting($wysiwygBody[0]);
  1493. // filter the HTML and DOM through any plugins
  1494. html = $wysiwygBody.html();
  1495. if(filter !== false && pluginManager.hasHandler('toSource'))
  1496. html = pluginManager.callOnlyFirst('toSource', html, $wysiwygBody);
  1497. if(hasSelection)
  1498. {
  1499. // remove the last stored range for IE as it no longer applies
  1500. rangeHelper.restoreRange();
  1501. lastRange = null;
  1502. }
  1503. else if(ieBookmark)
  1504. {
  1505. lastRange.moveToBookmark(ieBookmark);
  1506. lastRange = null;
  1507. }
  1508. return html;
  1509. };
  1510. /**
  1511. * Gets the WYSIWYG editor's iFrame Body.
  1512. *
  1513. * @return {jQuery}
  1514. * @function
  1515. * @since 1.4.3
  1516. * @name getBody
  1517. * @memberOf jQuery.sceditor.prototype
  1518. */
  1519. base.getBody = function () {
  1520. return $wysiwygBody;
  1521. };
  1522. /**
  1523. * Gets the WYSIWYG editors container area (whole iFrame).
  1524. *
  1525. * @return {Node}
  1526. * @function
  1527. * @since 1.4.3
  1528. * @name getContentAreaContainer
  1529. * @memberOf jQuery.sceditor.prototype
  1530. */
  1531. base.getContentAreaContainer = function () {
  1532. return $wysiwygEditor;
  1533. };
  1534. /**
  1535. * Gets the text editor value
  1536. *
  1537. * If using a plugin that filters the text like the BBCode plugin
  1538. * it will return the result of the filtering which is BBCode to
  1539. * HTML so it will return HTML. If filter is set to false it will
  1540. * just return the contents of the source editor (BBCode).
  1541. *
  1542. * @param {bool} [filter=true]
  1543. * @return {string}
  1544. * @function
  1545. * @since 1.4.0
  1546. * @name getSourceEditorValue
  1547. * @memberOf jQuery.sceditor.prototype
  1548. */
  1549. base.getSourceEditorValue = function (filter) {
  1550. var val = $sourceEditor.val();
  1551. if(filter !== false && pluginManager.hasHandler('toWysiwyg'))
  1552. val = pluginManager.callOnlyFirst('toWysiwyg', val);
  1553. return val;
  1554. };
  1555. /**
  1556. * Sets the WYSIWYG HTML editor value. Should only be the HTML
  1557. * contained within the body tags
  1558. *
  1559. * @param {string} value
  1560. * @function
  1561. * @name setWysiwygEditorValue
  1562. * @memberOf jQuery.sceditor.prototype
  1563. */
  1564. base.setWysiwygEditorValue = function (value) {
  1565. if(!value)
  1566. value = '<p>' + ($.sceditor.ie ? '' : '<br />') + '</p>';
  1567. $wysiwygBody[0].innerHTML = value;
  1568. replaceEmoticons($wysiwygBody[0]);
  1569. appendNewLine();
  1570. };
  1571. /**
  1572. * Sets the text editor value
  1573. *
  1574. * @param {string} value
  1575. * @function
  1576. * @name setSourceEditorValue
  1577. * @memberOf jQuery.sceditor.prototype
  1578. */
  1579. base.setSourceEditorValue = function (value) {
  1580. $sourceEditor.val(value);
  1581. };
  1582. /**
  1583. * Updates the textarea that the editor is replacing
  1584. * with the value currently inside the editor.
  1585. *
  1586. * @function
  1587. * @name updateOriginal
  1588. * @since 1.4.0
  1589. * @memberOf jQuery.sceditor.prototype
  1590. */
  1591. base.updateOriginal = function() {
  1592. $original.val(base.val());
  1593. };
  1594. /**
  1595. * Replaces any emoticon codes in the passed HTML with their emoticon images
  1596. * @private
  1597. */
  1598. replaceEmoticons = function(node) {
  1599. // TODO: Make this tag configurable.
  1600. if(!options.emoticonsEnabled || $(node).parents('code').length)
  1601. return;
  1602. var doc = node.ownerDocument,
  1603. emoticonCodes = [],
  1604. emoticonRegex = [],
  1605. emoticons = $.extend({}, options.emoticons.more, options.emoticons.dropdown, options.emoticons.hidden);
  1606. $.each(emoticons, function (key) {
  1607. if(options.emoticonsCompat)
  1608. emoticonRegex[key] = new RegExp('(>|^|\\s|\xA0|\u2002|\u2003|\u2009|&nbsp;)' + $.sceditor.regexEscape(key) + '(\\s|$|<|\xA0|\u2002|\u2003|\u2009|&nbsp;)');
  1609. emoticonCodes.push(key);
  1610. });
  1611. (function convertEmoticons(node) {
  1612. node = node.firstChild;
  1613. while(node != null)
  1614. {
  1615. var parts, key, emoticon, parsedHtml, emoticonIdx, nextSibling, startIdx,
  1616. nodeParent = node.parentNode,
  1617. nodeValue = node.nodeValue;
  1618. // All none textnodes
  1619. if(node.nodeType !== 3)
  1620. {
  1621. // TODO: Make this tag configurable.
  1622. if(!$(node).is('code'))
  1623. convertEmoticons(node);
  1624. }
  1625. else if(nodeValue)
  1626. {
  1627. emoticonIdx = emoticonCodes.length;
  1628. while(emoticonIdx--)
  1629. {
  1630. key = emoticonCodes[emoticonIdx];
  1631. startIdx = options.emoticonsCompat ? nodeValue.search(emoticonRegex[key]) : nodeValue.indexOf(key);
  1632. if(startIdx > -1)
  1633. {
  1634. nextSibling = node.nextSibling;
  1635. emoticon = emoticons[key];
  1636. parts = nodeValue.substr(startIdx).split(key);
  1637. nodeValue = nodeValue.substr(0, startIdx) + parts.shift();
  1638. node.nodeValue = nodeValue;
  1639. parsedHtml = $.sceditor.dom.parseHTML(_tmpl('emoticon', {
  1640. key: key,
  1641. url: emoticon.url || emoticon,
  1642. tooltip: emoticon.tooltip || key
  1643. }), doc);
  1644. nodeParent.insertBefore(parsedHtml[0], nextSibling);
  1645. nodeParent.insertBefore(doc.createTextNode(parts.join(key)), nextSibling);
  1646. }
  1647. }
  1648. }
  1649. node = node.nextSibling;
  1650. }
  1651. }(node));
  1652. if(options.emoticonsCompat)
  1653. currentEmoticons = $wysiwygBody.find('img[data-sceditor-emoticon]');
  1654. };
  1655. /**
  1656. * If the editor is in source code mode
  1657. *
  1658. * @return {bool}
  1659. * @function
  1660. * @name inSourceMode
  1661. * @memberOf jQuery.sceditor.prototype
  1662. */
  1663. base.inSourceMode = function () {
  1664. return $editorContainer.hasClass('sourceMode');
  1665. };
  1666. /**
  1667. * Gets if the editor is in sourceMode
  1668. *
  1669. * @return boolean
  1670. * @function
  1671. * @name sourceMode
  1672. * @memberOf jQuery.sceditor.prototype
  1673. */
  1674. /**
  1675. * Sets if the editor is in sourceMode
  1676. *
  1677. * @param {bool} enable
  1678. * @return {this}
  1679. * @function
  1680. * @name sourceMode^2
  1681. * @memberOf jQuery.sceditor.prototype
  1682. */
  1683. base.sourceMode = function (enable) {
  1684. if(typeof enable !== 'boolean')
  1685. return base.inSourceMode();
  1686. if((base.inSourceMode() && !enable) || (!base.inSourceMode() && enable))
  1687. base.toggleSourceMode();
  1688. return this;
  1689. };
  1690. /**
  1691. * Switches between the WYSIWYG and source modes
  1692. *
  1693. * @function
  1694. * @name toggleSourceMode
  1695. * @since 1.4.0
  1696. * @memberOf jQuery.sceditor.prototype
  1697. */
  1698. base.toggleSourceMode = function () {
  1699. // don't allow switching to WYSIWYG if doesn't support it
  1700. if(!$.sceditor.isWysiwygSupported && base.inSourceMode())
  1701. return;
  1702. base.blur();
  1703. if(base.inSourceMode())
  1704. base.setWysiwygEditorValue(base.getSourceEditorValue());
  1705. else
  1706. base.setSourceEditorValue(base.getWysiwygEditorValue());
  1707. lastRange = null;
  1708. $sourceEditor.toggle();
  1709. $wysiwygEditor.toggle();
  1710. if(!base.inSourceMode())
  1711. $editorContainer.removeClass('wysiwygMode').addClass('sourceMode');
  1712. else
  1713. $editorContainer.removeClass('sourceMode').addClass('wysiwygMode');
  1714. updateToolBar();
  1715. updateActiveButtons();
  1716. };
  1717. /**
  1718. * Gets the selected text of the source editor
  1719. * @return {String}
  1720. * @private
  1721. */
  1722. sourceEditorSelectedText = function () {
  1723. sourceEditor.focus();
  1724. if(sourceEditor.selectionStart != null)
  1725. return sourceEditor.value.substring(sourceEditor.selectionStart, sourceEditor.selectionEnd);
  1726. else if(document.selection.createRange)
  1727. return document.selection.createRange().text;
  1728. };
  1729. /**
  1730. * Handles the passed command
  1731. * @private
  1732. */
  1733. handleCommand = function (caller, command) {
  1734. // check if in text mode and handle text commands
  1735. if(base.inSourceMode())
  1736. {
  1737. if(command.txtExec)
  1738. {
  1739. if($.isArray(command.txtExec))
  1740. base.sourceEditorInsertText.apply(base, command.txtExec);
  1741. else
  1742. command.txtExec.call(base, caller, sourceEditorSelectedText());
  1743. }
  1744. return;
  1745. }
  1746. if(!command.exec)
  1747. return;
  1748. if($.isFunction(command.exec))
  1749. command.exec.call(base, caller);
  1750. else
  1751. base.execCommand(command.exec, command.hasOwnProperty('execParam') ? command.execParam : null);
  1752. };
  1753. /**
  1754. * Saves the current range. Needed for IE because it forgets
  1755. * where the cursor was and what was selected
  1756. * @private
  1757. */
  1758. saveRange = function () {
  1759. /* this is only needed for IE */
  1760. if($.sceditor.ie)
  1761. lastRange = rangeHelper.selectedRange();
  1762. };
  1763. /**
  1764. * Executes a command on the WYSIWYG editor
  1765. *
  1766. * @param {String} command
  1767. * @param {String|Boolean} [param]
  1768. * @function
  1769. * @name execCommand
  1770. * @memberOf jQuery.sceditor.prototype
  1771. */
  1772. base.execCommand = function (command, param) {
  1773. var executed = false,
  1774. $parentNode = $(rangeHelper.parentNode());
  1775. base.focus();
  1776. // don't apply any commands to code elements
  1777. if($parentNode.is('code') || $parentNode.parents('code').length !== 0)
  1778. return;
  1779. try
  1780. {
  1781. executed = $wysiwygDoc[0].execCommand(command, false, param);
  1782. }
  1783. catch (e) {}
  1784. // show error if execution failed and an error message exists
  1785. if(!executed && base.commands[command] && base.commands[command].errorMessage)
  1786. alert(base._(base.commands[command].errorMessage));
  1787. };
  1788. /**
  1789. * Checks if the current selection has changed and triggers
  1790. * the selectionchanged event if it has.
  1791. *
  1792. * In browsers other than IE, it will check at most once every 100ms.
  1793. * This is because only IE has a selection changed event.
  1794. * @private
  1795. */
  1796. checkSelectionChanged = function() {
  1797. var check = function() {
  1798. // rangeHelper could be null if editor was destroyed
  1799. // before the timeout had finished
  1800. if(rangeHelper && !rangeHelper.compare(currentSelection))
  1801. {
  1802. currentSelection = rangeHelper.cloneSelected();
  1803. $editorContainer.trigger($.Event('selectionchanged'));
  1804. }
  1805. isSelectionCheckPending = false;
  1806. };
  1807. if(isSelectionCheckPending)
  1808. return;
  1809. isSelectionCheckPending = true;
  1810. // In IE, this is only called on the selectionchanged event so no need to
  1811. // limit checking as it should always be valid to do.
  1812. if($.sceditor.ie)
  1813. check();
  1814. else
  1815. setTimeout(check, 100);
  1816. };
  1817. /**
  1818. * Checks if the current node has changed and triggers
  1819. * the nodechanged event if it has
  1820. * @private
  1821. */
  1822. checkNodeChanged = function() {
  1823. // check if node has changed
  1824. var oldNode,
  1825. node = rangeHelper.parentNode();
  1826. if(currentNode !== node)
  1827. {
  1828. oldNode = currentNode;
  1829. currentNode = node;
  1830. currentBlockNode = rangeHelper.getFirstBlockParent(node);
  1831. $editorContainer.trigger($.Event('nodechanged', { oldNode: oldNode, newNode: currentNode }));
  1832. }
  1833. };
  1834. /**
  1835. * <p>Gets the current node that contains the selection/caret in WYSIWYG mode.</p>
  1836. *
  1837. * <p>Will be null in sourceMode or if there is no selection.</p>
  1838. * @return {Node}
  1839. * @function
  1840. * @name currentNode
  1841. * @memberOf jQuery.sceditor.prototype
  1842. */
  1843. base.currentNode = function() {
  1844. return currentNode;
  1845. };
  1846. /**
  1847. * <p>Gets the first block level node that contains the selection/caret in WYSIWYG mode.</p>
  1848. *
  1849. * <p>Will be null in sourceMode or if there is no selection.</p>
  1850. * @return {Node}
  1851. * @function
  1852. * @name currentBlockNode
  1853. * @memberOf jQuery.sceditor.prototype
  1854. * @since 1.4.4
  1855. */
  1856. base.currentBlockNode = function() {
  1857. return currentBlockNode;
  1858. };
  1859. /**
  1860. * Updates if buttons are active or not
  1861. * @private
  1862. */
  1863. updateActiveButtons = function(e) {
  1864. var state, stateHandler, firstBlock, $button, parent,
  1865. doc = $wysiwygDoc[0],
  1866. i = btnStateHandlers.length,
  1867. inSourceMode = base.sourceMode();
  1868. if(!base.sourceMode() && !base.readOnly())
  1869. {
  1870. parent = e ? e.newNode : rangeHelper.parentNode();
  1871. firstBlock = rangeHelper.getFirstBlockParent(parent);
  1872. while(i--)
  1873. {
  1874. state = 0;
  1875. stateHandler = btnStateHandlers[i];
  1876. $button = $toolbar.find('.sceditor-button-' + stateHandler.name);
  1877. if(inSourceMode && !$button.data('sceditor-txtmode'))
  1878. $button.addClass('disabled');
  1879. else if (!inSourceMode && !$button.data('sceditor-wysiwygmode'))
  1880. $button.addClass('disabled');
  1881. else
  1882. {
  1883. if(typeof stateHandler.state === 'string')
  1884. {
  1885. try
  1886. {
  1887. state = doc.queryCommandEnabled(stateHandler.state) ? 0 : -1;
  1888. if(state > -1)
  1889. state = doc.queryCommandState(stateHandler.state) ? 1 : 0;
  1890. }
  1891. catch(ex) {}
  1892. }
  1893. else
  1894. state = stateHandler.state.call(base, parent, firstBlock);
  1895. if(state < 0)
  1896. $button.addClass('disabled');
  1897. else
  1898. $button.removeClass('disabled');
  1899. if(state > 0)
  1900. $button.addClass('active');
  1901. else
  1902. $button.removeClass('active');
  1903. }
  1904. }
  1905. }
  1906. else
  1907. $toolbar.find('.sceditor-button').removeClass('active');
  1908. };
  1909. /**
  1910. * Handles any key press in the WYSIWYG editor
  1911. *
  1912. * @private
  1913. */
  1914. handleKeyPress = function(e) {
  1915. var $parentNode,
  1916. i = keyPressFuncs.length;
  1917. base.closeDropDown();
  1918. $parentNode = $(currentNode);
  1919. // "Fix" (OK it's a cludge) for blocklevel elements being duplicated in some browsers when
  1920. // enter is pressed instead of inserting a newline
  1921. if(e.which === 13)
  1922. {
  1923. if($parentNode.is('code,blockquote,pre') || $parentNode.parents('code,blockquote,pre').length !== 0)
  1924. {
  1925. lastRange = null;
  1926. base.wysiwygEditorInsertHtml('<br />', null, true);
  1927. return false;
  1928. }
  1929. }
  1930. // TODO: Remove keyPressFuncs, which are deprecated
  1931. // don't apply to code elements
  1932. if($parentNode.is('code') || $parentNode.parents('code').length !== 0)
  1933. return;
  1934. while(i--)
  1935. keyPressFuncs[i].call(base, e, wysiwygEditor, $sourceEditor);
  1936. };
  1937. /**
  1938. * Makes sure that if there is a code or quote tag at the
  1939. * end of the editor, that there is a new line after it.
  1940. *
  1941. * If there wasn't a new line at the end you wouldn't be able
  1942. * to enter any text after a code/quote tag
  1943. * @return {void}
  1944. * @private
  1945. */
  1946. appendNewLine = function() {
  1947. var name, requiresNewLine, div;
  1948. $.sceditor.dom.rTraverse($wysiwygBody[0], function(node) {
  1949. name = node.nodeName.toLowerCase();
  1950. // TODO: Replace requireNewLineFix with just a block level fix for any block that has styling and
  1951. // any block that isn't a plain <p> or <div>
  1952. if($.inArray(name, requireNewLineFix) > -1)
  1953. requiresNewLine = true;
  1954. // find the last non-empty text node or line break.
  1955. if((node.nodeType === 3 && !/^\s*$/.test(node.nodeValue)) || name === 'br' ||
  1956. ($.sceditor.ie && !node.firstChild && !$.sceditor.dom.isInline(node, false)))
  1957. {
  1958. // this is the last text or br node, if its in a code or quote tag
  1959. // then add a newline to the end of the editor
  1960. if(requiresNewLine)
  1961. {
  1962. div = $wysiwygBody[0].ownerDocument.createElement('div');
  1963. div.className = 'sceditor-nlf';
  1964. div.innerHTML = !$.sceditor.ie ? '<br />' : '';
  1965. $wysiwygBody[0].appendChild(div);
  1966. }
  1967. return false;
  1968. }
  1969. });
  1970. };
  1971. /**
  1972. * Handles form reset event
  1973. * @private
  1974. */
  1975. handleFormReset = function() {
  1976. base.val($original.val());
  1977. };
  1978. /**
  1979. * Handles any mousedown press in the WYSIWYG editor
  1980. * @private
  1981. */
  1982. handleMouseDown = function() {
  1983. base.closeDropDown();
  1984. lastRange = null;
  1985. };
  1986. /**
  1987. * Handles the window resize event. Needed to resize then editor
  1988. * when the window size changes in fluid designs.
  1989. * @ignore
  1990. */
  1991. handleWindowResize = function() {
  1992. var height = options.height,
  1993. width = options.width;
  1994. if(!base.maximize())
  1995. {
  1996. if(height && height.toString().indexOf("%") > -1)
  1997. base.height(height);
  1998. if(width && width.toString().indexOf("%") > -1)
  1999. base.width(width);
  2000. }
  2001. else
  2002. base.dimensions('100%', '100%', false);
  2003. };
  2004. /**
  2005. * Translates the string into the locale language.
  2006. *
  2007. * Replaces any {0}, {1}, {2}, ect. with the params provided.
  2008. *
  2009. * @param {string} str
  2010. * @param {...String} args
  2011. * @return {string}
  2012. * @function
  2013. * @name _
  2014. * @memberOf jQuery.sceditor.prototype
  2015. */
  2016. base._ = function() {
  2017. var args = arguments;
  2018. if(locale && locale[args[0]])
  2019. args[0] = locale[args[0]];
  2020. return args[0].replace(/\{(\d+)\}/g, function(str, p1) {
  2021. return typeof args[p1-0+1] !== 'undefined' ?
  2022. args[p1-0+1] :
  2023. '{' + p1 + '}';
  2024. });
  2025. };
  2026. /**
  2027. * Passes events on to any handlers
  2028. * @private
  2029. * @return void
  2030. */
  2031. handleEvent = function(e) {
  2032. var customEvent,
  2033. clone = $.extend({}, e);
  2034. // Send event to all plugins
  2035. pluginManager.call(clone.type + 'Event', e, base);
  2036. // convert the event into a custom event to send
  2037. delete clone.type;
  2038. customEvent = $.Event((e.target === sourceEditor ? 'scesrc' : 'scewys') + e.type, clone);
  2039. $editorContainer.trigger.apply($editorContainer, [customEvent, base]);
  2040. if(customEvent.isDefaultPrevented())
  2041. e.preventDefault();
  2042. if(customEvent.isImmediatePropagationStopped())
  2043. customEvent.stopImmediatePropagation();
  2044. if(customEvent.isPropagationStopped())
  2045. customEvent.stopPropagation();
  2046. };
  2047. /**
  2048. * <p>Binds a handler to the specified events</p>
  2049. *
  2050. * <p>This function only binds to a limited list of supported events.<br />
  2051. * The supported events are:
  2052. * <ul>
  2053. * <li>keyup</li>
  2054. * <li>keydown</li>
  2055. * <li>Keypress</li>
  2056. * <li>blur</li>
  2057. * <li>focus</li>
  2058. * <li>nodechanged<br />
  2059. * When the current node containing the selection changes in WYSIWYG mode</li>
  2060. * <li>contextmenu</li>
  2061. * </ul>
  2062. * </p>
  2063. *
  2064. * <p>The events param should be a string containing the event(s)
  2065. * to bind this handler to. If multiple, they should be separated
  2066. * by spaces.</p>
  2067. *
  2068. * @param {String} events
  2069. * @param {Function} handler
  2070. * @param {Boolean} excludeWysiwyg If to exclude adding this handler to the WYSIWYG editor
  2071. * @param {Boolean} excludeSource if to exclude adding this handler to the source editor
  2072. * @return {this}
  2073. * @function
  2074. * @name bind
  2075. * @memberOf jQuery.sceditor.prototype
  2076. * @since 1.4.1
  2077. */
  2078. base.bind = function(events, handler, excludeWysiwyg, excludeSource) {
  2079. var i = events.length;
  2080. events = events.split(" ");
  2081. while(i--)
  2082. {
  2083. if($.isFunction(handler))
  2084. {
  2085. // Use custom events to allow passing the instance as the 2nd argument.
  2086. // Also allows unbinding without unbinding the editors own event handlers.
  2087. if(!excludeWysiwyg)
  2088. $editorContainer.bind('scewys' + events[i], handler);
  2089. if(!excludeSource)
  2090. $editorContainer.bind('scesrc' + events[i], handler);
  2091. }
  2092. }
  2093. return this;
  2094. };
  2095. /**
  2096. * Unbinds an event that was bound using bind().
  2097. *
  2098. * @param {String} events
  2099. * @param {Function} handler
  2100. * @param {Boolean} excludeWysiwyg If to exclude unbinding this handler from the WYSIWYG editor
  2101. * @param {Boolean} excludeSource if to exclude unbinding this handler from the source editor
  2102. * @return {this}
  2103. * @function
  2104. * @name unbind
  2105. * @memberOf jQuery.sceditor.prototype
  2106. * @since 1.4.1
  2107. * @see bind
  2108. */
  2109. base.unbind = function(events, handler, excludeWysiwyg, excludeSource) {
  2110. var i = events.length;
  2111. events = events.split(" ");
  2112. while(i--)
  2113. {
  2114. if($.isFunction(handler))
  2115. {
  2116. if(!excludeWysiwyg)
  2117. $editorContainer.unbind('scewys' + events[i], handler);
  2118. if(!excludeSource)
  2119. $editorContainer.unbind('scesrc' + events[i], handler);
  2120. }
  2121. }
  2122. return this;
  2123. };
  2124. /**
  2125. * Blurs the editors input area
  2126. *
  2127. * @return {this}
  2128. * @function
  2129. * @name blur
  2130. * @memberOf jQuery.sceditor.prototype
  2131. * @since 1.3.6
  2132. */
  2133. /**
  2134. * Adds a handler to the editors blur event
  2135. *
  2136. * @param {Function} handler
  2137. * @param {Boolean} excludeWysiwyg If to exclude adding this handler to the WYSIWYG editor
  2138. * @param {Boolean} excludeSource if to exclude adding this handler to the source editor
  2139. * @return {this}
  2140. * @function
  2141. * @name blur^2
  2142. * @memberOf jQuery.sceditor.prototype
  2143. * @since 1.4.1
  2144. */
  2145. base.blur = function(handler, excludeWysiwyg, excludeSource) {
  2146. if($.isFunction(handler))
  2147. base.bind('blur', handler, excludeWysiwyg, excludeSource);
  2148. else if(!base.sourceMode())
  2149. {
  2150. // Must use an element that isn't display:hidden or visibility:hidden for iOS
  2151. // so create a special blur element to use
  2152. if(!$blurElm)
  2153. $blurElm = $('<input style="position:absolute;width:0;height:0;opacity:0;border:0;padding:0;filter:alpha(opacity=0)" type="text" />').appendTo($editorContainer);
  2154. $blurElm.removeAttr('disabled').show().focus().blur().hide().attr('disabled', 'disabled');
  2155. }
  2156. else
  2157. $sourceEditor.blur();
  2158. return this;
  2159. };
  2160. /**
  2161. * Fucuses the editors input area
  2162. *
  2163. * @return {this}
  2164. * @function
  2165. * @name focus
  2166. * @memberOf jQuery.sceditor.prototype
  2167. */
  2168. /**
  2169. * Adds an event handler to the focus event
  2170. *
  2171. * @param {Function} handler
  2172. * @param {Boolean} excludeWysiwyg If to exclude adding this handler to the WYSIWYG editor
  2173. * @param {Boolean} excludeSource if to exclude adding this handler to the source editor
  2174. * @return {this}
  2175. * @function
  2176. * @name focus^2
  2177. * @memberOf jQuery.sceditor.prototype
  2178. * @since 1.4.1
  2179. */
  2180. base.focus = function (handler, excludeWysiwyg, excludeSource) {
  2181. if($.isFunction(handler))
  2182. base.bind('focus', handler, excludeWysiwyg, excludeSource);
  2183. else
  2184. {
  2185. if(!base.inSourceMode())
  2186. {
  2187. wysiwygEditor.contentWindow.focus();
  2188. $wysiwygBody[0].focus();
  2189. // Needed for IE < 9
  2190. if(lastRange)
  2191. {
  2192. rangeHelper.selectRange(lastRange);
  2193. // remove the stored range after being set.
  2194. // If the editor loses focus it should be
  2195. // saved again.
  2196. lastRange = null;
  2197. }
  2198. }
  2199. else
  2200. sourceEditor.focus();
  2201. }
  2202. return this;
  2203. };
  2204. /**
  2205. * Adds a handler to the key down event
  2206. *
  2207. * @param {Function} handler
  2208. * @param {Boolean} excludeWysiwyg If to exclude adding this handler to the WYSIWYG editor
  2209. * @param {Boolean} excludeSource if to exclude adding this handler to the source editor
  2210. * @return {this}
  2211. * @function
  2212. * @name keyDown
  2213. * @memberOf jQuery.sceditor.prototype
  2214. * @since 1.4.1
  2215. */
  2216. base.keyDown = function(handler, excludeWysiwyg, excludeSource) {
  2217. return base.bind('keydown', handler, excludeWysiwyg, excludeSource);
  2218. };
  2219. /**
  2220. * Adds a handler to the key press event
  2221. *
  2222. * @param {Function} handler
  2223. * @param {Boolean} excludeWysiwyg If to exclude adding this handler to the WYSIWYG editor
  2224. * @param {Boolean} excludeSource if to exclude adding this handler to the source editor
  2225. * @return {this}
  2226. * @function
  2227. * @name keyPress
  2228. * @memberOf jQuery.sceditor.prototype
  2229. * @since 1.4.1
  2230. */
  2231. base.keyPress = function(handler, excludeWysiwyg, excludeSource) {
  2232. return base.bind('keypress', handler, excludeWysiwyg, excludeSource);
  2233. };
  2234. /**
  2235. * Adds a handler to the key up event
  2236. *
  2237. * @param {Function} handler
  2238. * @param {Boolean} excludeWysiwyg If to exclude adding this handler to the WYSIWYG editor
  2239. * @param {Boolean} excludeSource if to exclude adding this handler to the source editor
  2240. * @return {this}
  2241. * @function
  2242. * @name keyUp
  2243. * @memberOf jQuery.sceditor.prototype
  2244. * @since 1.4.1
  2245. */
  2246. base.keyUp = function(handler, excludeWysiwyg, excludeSource) {
  2247. return base.bind('keyup', handler, excludeWysiwyg, excludeSource);
  2248. };
  2249. /**
  2250. * <p>Adds a handler to the node changed event.</p>
  2251. *
  2252. * <p>Happends whenever the node containing the selection/caret changes in WYSIWYG mode.</p>
  2253. *
  2254. * @param {Function} handler
  2255. * @return {this}
  2256. * @function
  2257. * @name nodeChanged
  2258. * @memberOf jQuery.sceditor.prototype
  2259. * @since 1.4.1
  2260. */
  2261. base.nodeChanged = function(handler) {
  2262. return base.bind('nodechanged', handler, false, true);
  2263. };
  2264. /**
  2265. * <p>Adds a handler to the selection changed event</p>
  2266. *
  2267. * <p>Happens whenever the selection changes in WYSIWYG mode.</p>
  2268. *
  2269. * @param {Function} handler
  2270. * @return {this}
  2271. * @function
  2272. * @name selectionChanged
  2273. * @memberOf jQuery.sceditor.prototype
  2274. * @since 1.4.1
  2275. */
  2276. base.selectionChanged = function(handler) {
  2277. return base.bind('selectionchanged', handler, false, true);
  2278. };
  2279. /**
  2280. * Emoticons keypress handler
  2281. * @private
  2282. */
  2283. emoticonsKeyPress = function (e) {
  2284. var pos = 0,
  2285. curChar = String.fromCharCode(e.which);
  2286. // TODO: Make configurable
  2287. if($(currentBlockNode).is('code') || $(currentBlockNode).parents('code').length)
  2288. return;
  2289. if(!base.emoticonsCache)
  2290. {
  2291. base.emoticonsCache = [];
  2292. $.each($.extend({}, options.emoticons.more, options.emoticons.dropdown, options.emoticons.hidden), function(key, url) {
  2293. base.emoticonsCache[pos++] = [
  2294. key,
  2295. _tmpl('emoticon', {
  2296. key: key,
  2297. url: url.url || url,
  2298. tooltip: url.tooltip || key
  2299. })
  2300. ];
  2301. });
  2302. base.emoticonsCache.sort(function(a, b) {
  2303. return a[0].length - b[0].length;
  2304. });
  2305. base.longestEmoticonCode = base.emoticonsCache[base.emoticonsCache.length - 1][0].length;
  2306. }
  2307. if(base.getRangeHelper().raplaceKeyword(base.emoticonsCache, true, true, base.longestEmoticonCode, options.emoticonsCompat, curChar))
  2308. {
  2309. if(options.emoticonsCompat)
  2310. currentEmoticons = $wysiwygBody.find('img[data-sceditor-emoticon]');
  2311. return (/^\s$/.test(curChar) && options.emoticonsCompat);
  2312. }
  2313. };
  2314. /**
  2315. * Makes sure emoticons are surrounded by whitespace
  2316. * @private
  2317. */
  2318. emoticonsCheckWhitespace = function() {
  2319. if(!currentEmoticons.length)
  2320. return;
  2321. var prev, next, parent, range, previousText, rangeStartContainer,
  2322. currentBlock = base.currentBlockNode(),
  2323. rangeStart = false,
  2324. noneWsRegex = /[^\s\xA0\u2002\u2003\u2009]+/;
  2325. currentEmoticons = $.map(currentEmoticons, function(emoticon) {
  2326. // Ignore emotiocons that have been removed from DOM
  2327. if(!emoticon || !emoticon.parentNode)
  2328. return null;
  2329. if(!$.contains(currentBlock, emoticon))
  2330. return emoticon;
  2331. prev = emoticon.previousSibling;
  2332. next = emoticon.nextSibling;
  2333. previousText = prev.nodeValue;
  2334. // For IE's HTMLPhraseElement
  2335. if(previousText === null)
  2336. previousText = prev.innerText || '';
  2337. if((!prev || !noneWsRegex.test(prev.nodeValue.slice(-1))) && (!next || !noneWsRegex.test((next.nodeValue || '')[0])))
  2338. return emoticon;
  2339. parent = emoticon.parentNode;
  2340. range = rangeHelper.cloneSelected();
  2341. rangeStartContainer = range.startContainer;
  2342. previousText = previousText + $(emoticon).data('sceditor-emoticon');
  2343. // Store current caret position
  2344. if(rangeStartContainer === next)
  2345. rangeStart = previousText.length + range.startOffset;
  2346. else if(rangeStartContainer === currentBlock && currentBlock.childNodes[range.startOffset] === next)
  2347. rangeStart = previousText.length;
  2348. else if(rangeStartContainer === prev)
  2349. rangeStart = range.startOffset;
  2350. if(!next || next.nodeType !== 3)
  2351. next = parent.insertBefore(parent.ownerDocument.createTextNode(''), next);
  2352. next.insertData(0, previousText);
  2353. parent.removeChild(prev);
  2354. parent.removeChild(emoticon);
  2355. // Need to update the range starting position if it has been modified
  2356. if(rangeStart !== false)
  2357. {
  2358. range.setStart(next, rangeStart);
  2359. range.collapse(true);
  2360. rangeHelper.selectRange(range);
  2361. }
  2362. return null;
  2363. });
  2364. };
  2365. /**
  2366. * Gets if emoticons are currently enabled
  2367. * @return {boolean}
  2368. * @function
  2369. * @name emoticons
  2370. * @memberOf jQuery.sceditor.prototype
  2371. * @since 1.4.2
  2372. */
  2373. /**
  2374. * Enables/disables emoticons
  2375. *
  2376. * @param {boolean} enable
  2377. * @return {this}
  2378. * @function
  2379. * @name emoticons^2
  2380. * @memberOf jQuery.sceditor.prototype
  2381. * @since 1.4.2
  2382. */
  2383. base.emoticons = function(enable) {
  2384. if(!enable && enable !== false)
  2385. return options.emoticonsEnabled;
  2386. options.emoticonsEnabled = enable;
  2387. if(enable)
  2388. {
  2389. $wysiwygBody.keypress(emoticonsKeyPress);
  2390. if(!base.sourceMode())
  2391. {
  2392. rangeHelper.saveRange();
  2393. replaceEmoticons($wysiwygBody[0]);
  2394. currentEmoticons = $wysiwygBody.find('img[data-sceditor-emoticon]');
  2395. rangeHelper.restoreRange();
  2396. }
  2397. }
  2398. else
  2399. {
  2400. $wysiwygBody.find('img[data-sceditor-emoticon]').replaceWith(function() {
  2401. return $(this).data('sceditor-emoticon');
  2402. });
  2403. currentEmoticons = [];
  2404. $wysiwygBody.unbind('keypress', emoticonsKeyPress);
  2405. }
  2406. return this;
  2407. };
  2408. /**
  2409. * Gets the current WYSIWYG editors inline CSS
  2410. *
  2411. * @return {string}
  2412. * @function
  2413. * @name css
  2414. * @memberOf jQuery.sceditor.prototype
  2415. * @since 1.4.3
  2416. */
  2417. /**
  2418. * Sets inline CSS for the WYSIWYG editor
  2419. *
  2420. * @param {string} css
  2421. * @return {this}
  2422. * @function
  2423. * @name css^2
  2424. * @memberOf jQuery.sceditor.prototype
  2425. * @since 1.4.3
  2426. */
  2427. base.css = function(css) {
  2428. if(!inlineCss)
  2429. inlineCss = $('<style id="#inline" />').appendTo($wysiwygDoc.find('head'))[0];
  2430. if(typeof css != 'string')
  2431. return inlineCss.styleSheet ? inlineCss.styleSheet.cssText : inlineCss.innerHTML;
  2432. if(inlineCss.styleSheet)
  2433. inlineCss.styleSheet.cssText = css;
  2434. else
  2435. inlineCss.innerHTML = css;
  2436. return this;
  2437. };
  2438. /**
  2439. * Handles the keydown event, used for shortcuts
  2440. * @private
  2441. */
  2442. handleKeyDown = function(e) {
  2443. var shortcut = [],
  2444. shift_keys = {
  2445. '`':'~', '1':'!', '2':'@', '3':'#', '4':'$', '5':'%', '6':'^',
  2446. '7':'&', '8':'*', '9':'(', '0':')', '-':'_', '=':'+', ';':':',
  2447. '\'':'"', ',':'<', '.':'>', '/':'?', '\\':'|', '[':'{', ']':'}'
  2448. },
  2449. special_keys = {
  2450. 8:'backspace', 9:'tab', 13:'enter', 19:'pause', 20:'capslock', 27:'esc',
  2451. 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left',
  2452. 38:'up', 39:'right', 40:'down', 45:'insert', 46:'del', 91: 'win', 92: 'win',
  2453. 93:'select', 96:'0', 97:'1', 98:'2', 99:'3', 100:'4', 101:'5', 102:'6',
  2454. 103:'7', 104:'8', 105:'9', 106:'*', 107:'+', 109:'-', 110:'.', 111:'/',
  2455. 112:'f1', 113:'f2', 114:'f3', 115:'f4', 116:'f5', 117:'f6', 118:'f7',
  2456. 119:'f8', 120:'f9', 121:'f10', 122:'f11', 123:'f12', 144:'numlock',
  2457. 145:'scrolllock', 186:';', 187:'=', 188:',', 189:'-', 190:'.', 191:'/',
  2458. 192:'`', 219:'[', 220:'\\', 221:']', 222:'\''
  2459. },
  2460. numpad_shift_keys = {
  2461. 109:'-', 110:'del', 111:'/', 96:'0', 97:'1', 98:'2', 99:'3',
  2462. 100:'4', 101:'5', 102:'6', 103:'7', 104:'8', 105:'9'
  2463. },
  2464. which = e.which,
  2465. character = special_keys[which] || String.fromCharCode(which).toLowerCase();
  2466. if(e.ctrlKey)
  2467. shortcut.push('ctrl');
  2468. if(e.altKey)
  2469. shortcut.push('alt');
  2470. if(e.shiftKey)
  2471. {
  2472. shortcut.push('shift');
  2473. if(numpad_shift_keys[which])
  2474. character = numpad_shift_keys[which];
  2475. else if(shift_keys[character])
  2476. character = shift_keys[character];
  2477. }
  2478. // Shift is 16, ctrl is 17 and alt is 18
  2479. if(character && (which < 16 || which > 18))
  2480. shortcut.push(character);
  2481. shortcut = shortcut.join('+');
  2482. if(shortcutHandlers[shortcut])
  2483. return shortcutHandlers[shortcut].call(base);
  2484. };
  2485. /**
  2486. * Adds a shortcut handler to the editor
  2487. * @param {String} shortcut
  2488. * @param {String|Function} cmd
  2489. * @return {jQuery.sceditor}
  2490. */
  2491. base.addShortcut = function(shortcut, cmd) {
  2492. shortcut = shortcut.toLowerCase();
  2493. if(typeof cmd === "string")
  2494. {
  2495. shortcutHandlers[shortcut] = function() {
  2496. handleCommand($toolbar.find('.sceditor-button-' + cmd), base.commands[cmd]);
  2497. return false;
  2498. };
  2499. }
  2500. else
  2501. shortcutHandlers[shortcut] = cmd;
  2502. return this;
  2503. };
  2504. /**
  2505. * Removes a shortcut handler
  2506. * @param {String} shortcut
  2507. * @return {jQuery.sceditor}
  2508. */
  2509. base.removeShortcut = function(shortcut) {
  2510. delete shortcutHandlers[shortcut.toLowerCase()];
  2511. return this;
  2512. };
  2513. /**
  2514. * Handles the backspace key press
  2515. *
  2516. * Will remove block styling like quotes/code ect if at the start.
  2517. * @private
  2518. */
  2519. handleBackSpace = function(e) {
  2520. var node, offset, tmpRange, range, parent;
  2521. // 8 is the backspace key
  2522. if($.sceditor.ie || options.disableBlockRemove || e.which !== 8 || !(range = rangeHelper.selectedRange()))
  2523. return;
  2524. if(!window.getSelection)
  2525. {
  2526. node = range.parentElement();
  2527. tmpRange = $wysiwygDoc[0].selection.createRange();
  2528. // Select te entire parent and set the end as start of the current range
  2529. tmpRange.moveToElementText(node);
  2530. tmpRange.setEndPoint('EndToStart', range);
  2531. // Number of characters selected is the start offset
  2532. // relative to the parent node
  2533. offset = tmpRange.text.length;
  2534. }
  2535. else
  2536. {
  2537. node = range.startContainer;
  2538. offset = range.startOffset;
  2539. }
  2540. if(offset !== 0 || !(parent = currentStyledBlockNode()))
  2541. return;
  2542. while(node !== parent)
  2543. {
  2544. while(node.previousSibling)
  2545. {
  2546. node = node.previousSibling;
  2547. // Everything but empty text nodes before the cursor
  2548. // should prevent the style from being removed
  2549. if(node.nodeType !== 3 || node.nodeValue)
  2550. return;
  2551. }
  2552. if(!(node = node.parentNode))
  2553. return;
  2554. }
  2555. if(!parent || $(parent).is('body'))
  2556. return;
  2557. // The backspace was pressed at the start of
  2558. // the container so clear the style
  2559. base.clearBlockFormatting(parent);
  2560. return false;
  2561. };
  2562. /**
  2563. * Gets the first styled block node that contains the cursor
  2564. * @return {HTMLElement}
  2565. */
  2566. currentStyledBlockNode = function() {
  2567. var block = currentBlockNode;
  2568. while(!$.sceditor.dom.hasStyling(block))
  2569. {
  2570. if(!(block = block.parentNode) || $(block).is('body'))
  2571. return;
  2572. }
  2573. return block;
  2574. };
  2575. /**
  2576. * Clears the formatting of the passed block element.
  2577. *
  2578. * If block is false, if will clear the styling of the first
  2579. * block level element that contains the cursor.
  2580. * @param {HTMLElement} block
  2581. * @since 1.4.4
  2582. */
  2583. base.clearBlockFormatting = function(block) {
  2584. block = block || currentStyledBlockNode();
  2585. if(!block || $(block).is('body'))
  2586. return this;
  2587. rangeHelper.saveRange();
  2588. lastRange = null;
  2589. block.className = '';
  2590. $(block).attr('style', '');
  2591. if(!$(block).is('p,div'))
  2592. $.sceditor.dom.convertElement(block, 'p');
  2593. rangeHelper.restoreRange();
  2594. return this;
  2595. };
  2596. // run the initializer
  2597. init();
  2598. };
  2599. /**
  2600. * <p>Detects the version of IE is being used if any.</p>
  2601. *
  2602. * <p>Returns the IE version number or undefined if not IE.</p>
  2603. *
  2604. * <p>Source: https://gist.github.com/527683 with extra code for IE 10 detection</p>
  2605. * @function
  2606. * @name ie
  2607. * @memberOf jQuery.sceditor
  2608. * @type {int}
  2609. */
  2610. $.sceditor.ie = (function(){
  2611. var undef,
  2612. v = 3,
  2613. div = document.createElement('div'),
  2614. all = div.getElementsByTagName('i');
  2615. do {
  2616. div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->';
  2617. } while (all[0]);
  2618. // Detect IE 10 as it doesn't support conditional comments.
  2619. if((document.documentMode && document.all && window.atob))
  2620. v = 10;
  2621. // Detect IE 11
  2622. if(v === 4 && document.documentMode)
  2623. v = 11;
  2624. return v > 4 ? v : undef;
  2625. }());
  2626. /**
  2627. * <p>Detects if the browser is iOS</p>
  2628. *
  2629. * <p>Needed to fix iOS specific bugs/</p>
  2630. *
  2631. * @function
  2632. * @name ios
  2633. * @memberOf jQuery.sceditor
  2634. * @type {Boolean}
  2635. */
  2636. $.sceditor.ios = /iPhone|iPod|iPad| wosbrowser\//i.test(navigator.userAgent);
  2637. /**
  2638. * If the browser supports WYSIWYG editing (e.g. older mobile browsers).
  2639. * @function
  2640. * @name isWysiwygSupported
  2641. * @memberOf jQuery.sceditor
  2642. * @return {Boolean}
  2643. */
  2644. $.sceditor.isWysiwygSupported = (function() {
  2645. var match, isUnsupported,
  2646. contentEditable = $('<div contenteditable="true">')[0].contentEditable,
  2647. contentEditableSupported = typeof contentEditable !== 'undefined' && contentEditable !== 'inherit',
  2648. userAgent = navigator.userAgent;
  2649. if(!contentEditableSupported)
  2650. return false;
  2651. // I think blackberry supports it or will at least
  2652. // give a valid value for the contentEditable detection above
  2653. // so it's not included here.
  2654. // I hate having to use UA sniffing but some mobile browsers say they support
  2655. // contentediable/design mode when it isn't usable (i.e. you can't enter text, ect.).
  2656. // This is the only way I can think of to detect them which is also how every other
  2657. // editor I've seen deals with this
  2658. isUnsupported = /Opera Mobi|Opera Mini/i.test(userAgent);
  2659. if(/Android/i.test(userAgent))
  2660. {
  2661. isUnsupported = true;
  2662. if(/Safari/.test(userAgent))
  2663. {
  2664. // Android browser 534+ supports content editable
  2665. // This also matches Chrome which supports content editable too
  2666. match = /Safari\/(\d+)/.exec(userAgent);
  2667. isUnsupported = (!match || !match[1] ? true : match[1] < 534);
  2668. }
  2669. }
  2670. // Amazon Silk doesn't but as it uses webkit like android
  2671. // it might in a later version if it uses version >= 534
  2672. if(/ Silk\//i.test(userAgent))
  2673. {
  2674. match = /AppleWebKit\/(\d+)/.exec(userAgent);
  2675. isUnsupported = (!match || !match[1] ? true : match[1] < 534);
  2676. }
  2677. // iOS 5+ supports content editable
  2678. if($.sceditor.ios)
  2679. isUnsupported = !/OS [5-9](_\d)+ like Mac OS X/i.test(userAgent);
  2680. // FireFox does support WYSIWYG on mobiles so override
  2681. // any previous value if using FF
  2682. if(/fennec/i.test(userAgent))
  2683. isUnsupported = false;
  2684. if(/OneBrowser/i.test(userAgent))
  2685. isUnsupported = false;
  2686. // UCBrowser works but doesn't give a unique user agent
  2687. if(navigator.vendor === 'UCWEB')
  2688. isUnsupported = false;
  2689. return !isUnsupported;
  2690. }());
  2691. /**
  2692. * Escapes a string so it's safe to use in regex
  2693. *
  2694. * @param {String} str
  2695. * @return {String}
  2696. * @name regexEscape
  2697. * @memberOf jQuery.sceditor
  2698. */
  2699. $.sceditor.regexEscape = function(str) {
  2700. return str.replace(/[\$\?\[\]\.\*\(\)\|\\]/g, '\\$&');
  2701. };
  2702. /**
  2703. * Escapes all HTML entities in a string
  2704. *
  2705. * @param {String} str
  2706. * @return {String}
  2707. * @name escapeEntities
  2708. * @memberOf jQuery.sceditor
  2709. * @since 1.4.1
  2710. */
  2711. $.sceditor.escapeEntities = function(str) {
  2712. if(!str)
  2713. return str;
  2714. return str.replace(/&/g, '&amp;')
  2715. .replace(/</g, '&lt;')
  2716. .replace(/>/g, '&gt;')
  2717. .replace(/ {2}/g, ' &nbsp;')
  2718. .replace(/\r\n|\r/g, '\n')
  2719. .replace(/\n/g, '<br />');
  2720. };
  2721. /**
  2722. * Map containing the loaded SCEditor locales
  2723. * @type {Object}
  2724. * @name locale
  2725. * @memberOf jQuery.sceditor
  2726. */
  2727. $.sceditor.locale = {};
  2728. /**
  2729. * Map of all the commands for SCEditor
  2730. * @type {Object}
  2731. * @name commands
  2732. * @memberOf jQuery.sceditor
  2733. */
  2734. $.sceditor.commands = {
  2735. // START_COMMAND: Bold
  2736. bold: {
  2737. exec: 'bold',
  2738. tooltip: 'Bold',
  2739. shortcut: 'ctrl+b'
  2740. },
  2741. // END_COMMAND
  2742. // START_COMMAND: Italic
  2743. italic: {
  2744. exec: 'italic',
  2745. tooltip: 'Italic',
  2746. shortcut: 'ctrl+i'
  2747. },
  2748. // END_COMMAND
  2749. // START_COMMAND: Underline
  2750. underline: {
  2751. exec: 'underline',
  2752. tooltip: 'Underline',
  2753. shortcut: 'ctrl+u'
  2754. },
  2755. // END_COMMAND
  2756. // START_COMMAND: Strikethrough
  2757. strike: {
  2758. exec: 'strikethrough',
  2759. tooltip: 'Strikethrough'
  2760. },
  2761. // END_COMMAND
  2762. // START_COMMAND: Subscript
  2763. subscript: {
  2764. exec: 'subscript',
  2765. tooltip: 'Subscript'
  2766. },
  2767. // END_COMMAND
  2768. // START_COMMAND: Superscript
  2769. superscript: {
  2770. exec: 'superscript',
  2771. tooltip: 'Superscript'
  2772. },
  2773. // END_COMMAND
  2774. // START_COMMAND: Left
  2775. left: {
  2776. exec: 'justifyleft',
  2777. tooltip: 'Align left'
  2778. },
  2779. // END_COMMAND
  2780. // START_COMMAND: Centre
  2781. center: {
  2782. exec: 'justifycenter',
  2783. tooltip: 'Center'
  2784. },
  2785. // END_COMMAND
  2786. // START_COMMAND: Right
  2787. right: {
  2788. exec: 'justifyright',
  2789. tooltip: 'Align right'
  2790. },
  2791. // END_COMMAND
  2792. // START_COMMAND: Justify
  2793. justify: {
  2794. exec: 'justifyfull',
  2795. tooltip: 'Justify'
  2796. },
  2797. // END_COMMAND
  2798. // START_COMMAND: Font
  2799. font: {
  2800. _dropDown: function(editor, caller, callback) {
  2801. var fonts = editor.opts.fonts.split(','),
  2802. content = $('<div />'),
  2803. /** @private */
  2804. clickFunc = function () {
  2805. callback($(this).data('font'));
  2806. editor.closeDropDown(true);
  2807. return false;
  2808. };
  2809. for (var i=0; i < fonts.length; i++)
  2810. content.append(_tmpl('fontOpt', {font: fonts[i]}, true).click(clickFunc));
  2811. editor.createDropDown(caller, 'font-picker', content);
  2812. },
  2813. exec: function (caller) {
  2814. var editor = this;
  2815. $.sceditor.command.get('font')._dropDown(
  2816. editor,
  2817. caller,
  2818. function(fontName) {
  2819. editor.execCommand('fontname', fontName);
  2820. }
  2821. );
  2822. },
  2823. tooltip: 'Font Name'
  2824. },
  2825. // END_COMMAND
  2826. // START_COMMAND: Size
  2827. size: {
  2828. _dropDown: function(editor, caller, callback) {
  2829. var content = $('<div />'),
  2830. /** @private */
  2831. clickFunc = function (e) {
  2832. callback($(this).data('size'));
  2833. editor.closeDropDown(true);
  2834. e.preventDefault();
  2835. };
  2836. for (var i=1; i<= 7; i++)
  2837. content.append(_tmpl('sizeOpt', {size: i}, true).click(clickFunc));
  2838. editor.createDropDown(caller, 'fontsize-picker', content);
  2839. },
  2840. exec: function (caller) {
  2841. var editor = this;
  2842. $.sceditor.command.get('size')._dropDown(
  2843. editor,
  2844. caller,
  2845. function(fontSize) {
  2846. editor.execCommand('fontsize', fontSize);
  2847. }
  2848. );
  2849. },
  2850. tooltip: 'Font Size'
  2851. },
  2852. // END_COMMAND
  2853. // START_COMMAND: Colour
  2854. color: {
  2855. _dropDown: function(editor, caller, callback) {
  2856. var i, x, color, colors,
  2857. genColor = {r: 255, g: 255, b: 255},
  2858. content = $('<div />'),
  2859. colorColumns = editor.opts.colors?editor.opts.colors.split('|'):new Array(21),
  2860. // IE is slow at string concation so use an array
  2861. html = [],
  2862. cmd = $.sceditor.command.get('color');
  2863. if(!cmd._htmlCache)
  2864. {
  2865. for (i=0; i < colorColumns.length; ++i)
  2866. {
  2867. colors = colorColumns[i]?colorColumns[i].split(','):new Array(21);
  2868. html.push('<div class="sceditor-color-column">');
  2869. for (x=0; x < colors.length; ++x)
  2870. {
  2871. // use pre defined colour if can otherwise use the generated color
  2872. color = colors[x] || "#" + genColor.r.toString(16) + genColor.g.toString(16) + genColor.b.toString(16);
  2873. html.push('<a href="#" class="sceditor-color-option" style="background-color: '+color+'" data-color="'+color+'"></a>');
  2874. // calculate the next generated color
  2875. if(x%5===0)
  2876. {
  2877. genColor.g -= 51;
  2878. genColor.b = 255;
  2879. }
  2880. else
  2881. genColor.b -= 51;
  2882. }
  2883. html.push('</div>');
  2884. // calculate the next generated color
  2885. if(i%5===0)
  2886. {
  2887. genColor.r -= 51;
  2888. genColor.g = 255;
  2889. genColor.b = 255;
  2890. }
  2891. else
  2892. {
  2893. genColor.g = 255;
  2894. genColor.b = 255;
  2895. }
  2896. }
  2897. cmd._htmlCache = html.join('');
  2898. }
  2899. content.append(cmd._htmlCache)
  2900. .find('a')
  2901. .click(function (e) {
  2902. callback($(this).attr('data-color'));
  2903. editor.closeDropDown(true);
  2904. e.preventDefault();
  2905. });
  2906. editor.createDropDown(caller, 'color-picker', content);
  2907. },
  2908. exec: function (caller) {
  2909. var editor = this;
  2910. $.sceditor.command.get('color')._dropDown(
  2911. editor,
  2912. caller,
  2913. function(color) {
  2914. editor.execCommand('forecolor', color);
  2915. }
  2916. );
  2917. },
  2918. tooltip: 'Font Color'
  2919. },
  2920. // END_COMMAND
  2921. // START_COMMAND: Remove Format
  2922. removeformat: {
  2923. exec: 'removeformat',
  2924. tooltip: 'Remove Formatting'
  2925. },
  2926. // END_COMMAND
  2927. // START_COMMAND: Cut
  2928. cut: {
  2929. exec: 'cut',
  2930. tooltip: 'Cut',
  2931. errorMessage: 'Your browser does not allow the cut command. Please use the keyboard shortcut Ctrl/Cmd-X'
  2932. },
  2933. // END_COMMAND
  2934. // START_COMMAND: Copy
  2935. copy: {
  2936. exec: 'copy',
  2937. tooltip: 'Copy',
  2938. errorMessage: 'Your browser does not allow the copy command. Please use the keyboard shortcut Ctrl/Cmd-C'
  2939. },
  2940. // END_COMMAND
  2941. // START_COMMAND: Paste
  2942. paste: {
  2943. exec: 'paste',
  2944. tooltip: 'Paste',
  2945. errorMessage: 'Your browser does not allow the paste command. Please use the keyboard shortcut Ctrl/Cmd-V'
  2946. },
  2947. // END_COMMAND
  2948. // START_COMMAND: Paste Text
  2949. pastetext: {
  2950. exec: function (caller) {
  2951. var val,
  2952. editor = this,
  2953. content = _tmpl('pastetext', {
  2954. label: editor._('Paste your text inside the following box:'),
  2955. insert: editor._('Insert')
  2956. }, true);
  2957. content.find('.button').click(function (e) {
  2958. val = content.find('#txt').val();
  2959. if(val)
  2960. editor.wysiwygEditorInsertText(val);
  2961. editor.closeDropDown(true);
  2962. e.preventDefault();
  2963. });
  2964. editor.createDropDown(caller, 'pastetext', content);
  2965. },
  2966. tooltip: 'Paste Text'
  2967. },
  2968. // END_COMMAND
  2969. // START_COMMAND: Bullet List
  2970. bulletlist: {
  2971. exec: 'insertunorderedlist',
  2972. tooltip: 'Bullet list'
  2973. },
  2974. // END_COMMAND
  2975. // START_COMMAND: Ordered List
  2976. orderedlist: {
  2977. exec: 'insertorderedlist',
  2978. tooltip: 'Numbered list'
  2979. },
  2980. // END_COMMAND
  2981. // START_COMMAND: Table
  2982. table: {
  2983. exec: function (caller) {
  2984. var editor = this,
  2985. content = _tmpl('table', {
  2986. rows: editor._('Rows:'),
  2987. cols: editor._('Cols:'),
  2988. insert: editor._('Insert')
  2989. }, true);
  2990. content.find('.button').click(function (e) {
  2991. var rows = content.find('#rows').val() - 0,
  2992. cols = content.find('#cols').val() - 0,
  2993. html = '<table>';
  2994. if(rows < 1 || cols < 1)
  2995. return;
  2996. for (var row=0; row < rows; row++) {
  2997. html += '<tr>';
  2998. for (var col=0; col < cols; col++)
  2999. html += '<td>' + ($.sceditor.ie ? '' : '<br />') + '</td>';
  3000. html += '</tr>';
  3001. }
  3002. html += '</table>';
  3003. editor.wysiwygEditorInsertHtml(html);
  3004. editor.closeDropDown(true);
  3005. e.preventDefault();
  3006. });
  3007. editor.createDropDown(caller, 'inserttable', content);
  3008. },
  3009. tooltip: 'Insert a table'
  3010. },
  3011. // END_COMMAND
  3012. // START_COMMAND: Horizontal Rule
  3013. horizontalrule: {
  3014. exec: 'inserthorizontalrule',
  3015. tooltip: 'Insert a horizontal rule'
  3016. },
  3017. // END_COMMAND
  3018. // START_COMMAND: Code
  3019. code: {
  3020. forceNewLineAfter: ['code'],
  3021. exec: function () {
  3022. this.wysiwygEditorInsertHtml('<code>', '<br /></code>');
  3023. },
  3024. tooltip: 'Code'
  3025. },
  3026. // END_COMMAND
  3027. // START_COMMAND: Image
  3028. image: {
  3029. exec: function (caller) {
  3030. var editor = this,
  3031. content = _tmpl('image', {
  3032. url: editor._('URL:'),
  3033. width: editor._('Width (optional):'),
  3034. height: editor._('Height (optional):'),
  3035. insert: editor._('Insert')
  3036. }, true);
  3037. content.find('.button').click(function (e) {
  3038. var val = content.find('#image').val(),
  3039. width = content.find('#width').val(),
  3040. height = content.find('#height').val(),
  3041. attrs = '';
  3042. if(width)
  3043. attrs += ' width="' + width + '"';
  3044. if(height)
  3045. attrs += ' height="' + height + '"';
  3046. if(val && val !== 'http://')
  3047. editor.wysiwygEditorInsertHtml('<img' + attrs + ' src="' + val + '" />');
  3048. editor.closeDropDown(true);
  3049. e.preventDefault();
  3050. });
  3051. editor.createDropDown(caller, 'insertimage', content);
  3052. },
  3053. tooltip: 'Insert an image'
  3054. },
  3055. // END_COMMAND
  3056. // START_COMMAND: E-mail
  3057. email: {
  3058. exec: function (caller) {
  3059. var editor = this,
  3060. content = _tmpl('email', {
  3061. label: editor._('E-mail:'),
  3062. insert: editor._('Insert')
  3063. }, true);
  3064. content.find('.button').click(function (e) {
  3065. var val = content.find('#email').val();
  3066. if(val)
  3067. {
  3068. // needed for IE to reset the last range
  3069. editor.focus();
  3070. if(!editor.getRangeHelper().selectedHtml())
  3071. editor.wysiwygEditorInsertHtml('<a href="' + 'mailto:' + val + '">' + val + '</a>');
  3072. else
  3073. editor.execCommand('createlink', 'mailto:' + val);
  3074. }
  3075. editor.closeDropDown(true);
  3076. e.preventDefault();
  3077. });
  3078. editor.createDropDown(caller, 'insertemail', content);
  3079. },
  3080. tooltip: 'Insert an email'
  3081. },
  3082. // END_COMMAND
  3083. // START_COMMAND: Link
  3084. link: {
  3085. exec: function (caller) {
  3086. var editor = this,
  3087. content = _tmpl('link', {
  3088. url: editor._('URL:'),
  3089. desc: editor._('Description (optional):'),
  3090. ins: editor._('Insert')
  3091. }, true);
  3092. content.find('.button').click(function (e) {
  3093. var val = content.find('#link').val(),
  3094. description = content.find('#des').val();
  3095. if(val && val !== 'http://') {
  3096. // needed for IE to reset the last range
  3097. editor.focus();
  3098. if(!editor.getRangeHelper().selectedHtml() || description)
  3099. {
  3100. if(!description)
  3101. description = val;
  3102. // @SMF code: otherwise all the urls become iurl
  3103. editor.wysiwygEditorInsertHtml('<a target="_blank" href="' + val + '">' + description + '</a>');
  3104. }
  3105. else
  3106. editor.execCommand('createlink', val);
  3107. }
  3108. editor.closeDropDown(true);
  3109. e.preventDefault();
  3110. });
  3111. editor.createDropDown(caller, 'insertlink', content);
  3112. },
  3113. tooltip: 'Insert a link'
  3114. },
  3115. // END_COMMAND
  3116. // START_COMMAND: Unlink
  3117. unlink: {
  3118. state: function() {
  3119. var $current = $(this.currentNode());
  3120. return $current.is('a') || $current.parents('a').length > 0 ? 0 : -1;
  3121. },
  3122. exec: function() {
  3123. var $current = $(this.currentNode()),
  3124. $anchor = $current.is('a') ? $current : $current.parents('a').first();
  3125. if($anchor.length)
  3126. $anchor.replaceWith($anchor.contents());
  3127. },
  3128. tooltip: 'Unlink'
  3129. },
  3130. // END_COMMAND
  3131. // START_COMMAND: Quote
  3132. quote: {
  3133. forceNewLineAfter: ['blockquote'],
  3134. exec: function (caller, html, author) {
  3135. var before = '<blockquote>',
  3136. end = '</blockquote>';
  3137. // if there is HTML passed set end to null so any selected
  3138. // text is replaced
  3139. if(html)
  3140. {
  3141. author = (author ? '<cite>' + author + '</cite>' : '');
  3142. before = before + author + html + end;
  3143. end = null;
  3144. }
  3145. // if not add a newline to the end of the inserted quote
  3146. else if(this.getRangeHelper().selectedHtml() === '')
  3147. end = $.sceditor.ie ? '' : '<br />' + end;
  3148. this.wysiwygEditorInsertHtml(before, end);
  3149. },
  3150. tooltip: 'Insert a Quote'
  3151. },
  3152. // END_COMMAND
  3153. // START_COMMAND: Emoticons
  3154. emoticon: {
  3155. exec: function (caller) {
  3156. var editor = this;
  3157. var createContent = function(includeMore) {
  3158. var emoticonsCompat = editor.opts.emoticonsCompat,
  3159. rangeHelper = editor.getRangeHelper(),
  3160. startSpace = emoticonsCompat && rangeHelper.getOuterText(true, 1) !== ' ' ? ' ' : '',
  3161. endSpace = emoticonsCompat && rangeHelper.getOuterText(false, 1) !== ' ' ? ' ' : '',
  3162. $content = $('<div />'),
  3163. $line = $('<div />').appendTo($content),
  3164. emoticons = $.extend({}, editor.opts.emoticons.dropdown, includeMore ? editor.opts.emoticons.more : {}),
  3165. perLine = 0;
  3166. $.each(emoticons, function() {
  3167. perLine++;
  3168. });
  3169. perLine = Math.sqrt(perLine);
  3170. $.each(emoticons, function(code, emoticon) {
  3171. $line.append(
  3172. $('<img />').attr({
  3173. src: emoticon.url || emoticon,
  3174. alt: code,
  3175. title: emoticon.tooltip || code
  3176. }).click(function() {
  3177. editor.insert(startSpace + $(this).attr('alt') + endSpace, null, false).closeDropDown(true);
  3178. return false;
  3179. })
  3180. );
  3181. if($line.children().length >= perLine)
  3182. $line = $('<div />').appendTo($content);
  3183. });
  3184. if(!includeMore)
  3185. {
  3186. $content.append(
  3187. $(editor._('<a class="sceditor-more">{0}</a>', editor._('More'))).click(function () {
  3188. editor.createDropDown(caller, 'more-emoticons', createContent(true));
  3189. return false;
  3190. })
  3191. );
  3192. }
  3193. return $content;
  3194. };
  3195. editor.createDropDown(caller, 'emoticons', createContent(false));
  3196. },
  3197. txtExec: function(caller) {
  3198. $.sceditor.command.get('emoticon').exec.call(this, caller);
  3199. },
  3200. tooltip: 'Insert an emoticon'
  3201. },
  3202. // END_COMMAND
  3203. // START_COMMAND: YouTube
  3204. youtube: {
  3205. _dropDown: function (editor, caller, handleIdFunc) {
  3206. var matches,
  3207. content = _tmpl('youtubeMenu', {
  3208. label: editor._('Video URL:'),
  3209. insert: editor._('Insert')
  3210. }, true);
  3211. content.find('.button').click(function (e) {
  3212. var val = content.find('#link').val().replace('http://', '');
  3213. if (val !== '') {
  3214. matches = val.match(/(?:v=|v\/|embed\/|youtu.be\/)(.{11})/);
  3215. if (matches)
  3216. val = matches[1];
  3217. if (/^[a-zA-Z0-9_\-]{11}$/.test(val))
  3218. handleIdFunc(val);
  3219. else
  3220. alert('Invalid YouTube video');
  3221. }
  3222. editor.closeDropDown(true);
  3223. e.preventDefault();
  3224. });
  3225. editor.createDropDown(caller, 'insertlink', content);
  3226. },
  3227. exec: function (caller) {
  3228. var editor = this;
  3229. $.sceditor.command.get('youtube')._dropDown(
  3230. editor,
  3231. caller,
  3232. function(id) {
  3233. editor.wysiwygEditorInsertHtml(_tmpl('youtube', { id: id }));
  3234. }
  3235. );
  3236. },
  3237. tooltip: 'Insert a YouTube video'
  3238. },
  3239. // END_COMMAND
  3240. // START_COMMAND: Date
  3241. date: {
  3242. _date: function (editor) {
  3243. var now = new Date(),
  3244. year = now.getYear(),
  3245. month = now.getMonth()+1,
  3246. day = now.getDate();
  3247. if(year < 2000)
  3248. year = 1900 + year;
  3249. if(month < 10)
  3250. month = '0' + month;
  3251. if(day < 10)
  3252. day = '0' + day;
  3253. return editor.opts.dateFormat.replace(/year/i, year).replace(/month/i, month).replace(/day/i, day);
  3254. },
  3255. exec: function () {
  3256. this.insertText($.sceditor.command.get('date')._date(this));
  3257. },
  3258. txtExec: function () {
  3259. this.insertText($.sceditor.command.get('date')._date(this));
  3260. },
  3261. tooltip: 'Insert current date'
  3262. },
  3263. // END_COMMAND
  3264. // START_COMMAND: Time
  3265. time: {
  3266. _time: function () {
  3267. var now = new Date(),
  3268. hours = now.getHours(),
  3269. mins = now.getMinutes(),
  3270. secs = now.getSeconds();
  3271. if(hours < 10)
  3272. hours = '0' + hours;
  3273. if(mins < 10)
  3274. mins = '0' + mins;
  3275. if(secs < 10)
  3276. secs = '0' + secs;
  3277. return hours + ':' + mins + ':' + secs;
  3278. },
  3279. exec: function () {
  3280. this.insertText($.sceditor.command.get('time')._time());
  3281. },
  3282. txtExec: function () {
  3283. this.insertText($.sceditor.command.get('time')._time());
  3284. },
  3285. tooltip: 'Insert current time'
  3286. },
  3287. // END_COMMAND
  3288. // START_COMMAND: Ltr
  3289. ltr: {
  3290. state: function(parents, firstBlock) {
  3291. return firstBlock && firstBlock.style.direction === 'ltr';
  3292. },
  3293. exec: function() {
  3294. var editor = this,
  3295. elm = editor.getRangeHelper().getFirstBlockParent(),
  3296. $elm = $(elm);
  3297. editor.focus();
  3298. if(!elm || $elm.is('body'))
  3299. {
  3300. editor.execCommand('formatBlock', 'p');
  3301. elm = editor.getRangeHelper().getFirstBlockParent();
  3302. $elm = $(elm);
  3303. if(!elm || $elm.is('body'))
  3304. return;
  3305. }
  3306. if($elm.css('direction') === 'ltr')
  3307. $elm.css('direction', '');
  3308. else
  3309. $elm.css('direction', 'ltr');
  3310. },
  3311. tooltip: 'Left-to-Right'
  3312. },
  3313. // END_COMMAND
  3314. // START_COMMAND: Rtl
  3315. rtl: {
  3316. state: function(parents, firstBlock) {
  3317. return firstBlock && firstBlock.style.direction === 'rtl';
  3318. },
  3319. exec: function() {
  3320. var editor = this,
  3321. elm = editor.getRangeHelper().getFirstBlockParent(),
  3322. $elm = $(elm);
  3323. editor.focus();
  3324. if(!elm || $elm.is('body'))
  3325. {
  3326. editor.execCommand('formatBlock', 'p');
  3327. elm = editor.getRangeHelper().getFirstBlockParent();
  3328. $elm = $(elm);
  3329. if(!elm || $elm.is('body'))
  3330. return;
  3331. }
  3332. if($elm.css('direction') === 'rtl')
  3333. $elm.css('direction', '');
  3334. else
  3335. $elm.css('direction', 'rtl');
  3336. },
  3337. tooltip: 'Right-to-Left'
  3338. },
  3339. // END_COMMAND
  3340. // START_COMMAND: Print
  3341. print: {
  3342. exec: 'print',
  3343. tooltip: 'Print'
  3344. },
  3345. // END_COMMAND
  3346. // START_COMMAND: Maximize
  3347. maximize: {
  3348. state: function() {
  3349. return this.maximize();
  3350. },
  3351. exec: function () {
  3352. this.maximize(!this.maximize());
  3353. },
  3354. txtExec: function () {
  3355. this.maximize(!this.maximize());
  3356. },
  3357. tooltip: 'Maximize',
  3358. shortcut: 'ctrl+shift+m'
  3359. },
  3360. // END_COMMAND
  3361. // START_COMMAND: Source
  3362. source: {
  3363. exec: function () {
  3364. this.toggleSourceMode();
  3365. },
  3366. txtExec: function () {
  3367. this.toggleSourceMode();
  3368. },
  3369. tooltip: 'View source',
  3370. shortcut: 'ctrl+shift+s'
  3371. },
  3372. // END_COMMAND
  3373. // this is here so that commands above can be removed
  3374. // without having to remove the , after the last one.
  3375. // Needed for IE.
  3376. ignore: {}
  3377. };
  3378. /**
  3379. * Range helper class
  3380. * @class rangeHelper
  3381. * @name jQuery.sceditor.rangeHelper
  3382. */
  3383. $.sceditor.rangeHelper = function(w, d) {
  3384. var win, doc, init, _createMarker, _isOwner,
  3385. isW3C = true,
  3386. startMarker = 'sceditor-start-marker',
  3387. endMarker = 'sceditor-end-marker',
  3388. characterStr = 'character', // Used to improve minification
  3389. base = this;
  3390. /**
  3391. * @constructor
  3392. * @param Window window
  3393. * @param Document document
  3394. * @private
  3395. */
  3396. init = function (window, document) {
  3397. doc = document || window.contentDocument || window.document;
  3398. win = window;
  3399. isW3C = !!window.getSelection;
  3400. }(w, d);
  3401. /**
  3402. * <p>Inserts HTML into the current range replacing any selected
  3403. * text.</p>
  3404. *
  3405. * <p>If endHTML is specified the selected contents will be put between
  3406. * html and endHTML. If there is nothing selected html and endHTML are
  3407. * just concated together.</p>
  3408. *
  3409. * @param {string} html
  3410. * @param {string} endHTML
  3411. * @return False on fail
  3412. * @function
  3413. * @name insertHTML
  3414. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3415. */
  3416. base.insertHTML = function(html, endHTML) {
  3417. var node, div,
  3418. range = base.selectedRange();
  3419. if(endHTML)
  3420. html += base.selectedHtml() + endHTML;
  3421. if(isW3C)
  3422. {
  3423. div = doc.createElement('div');
  3424. node = doc.createDocumentFragment();
  3425. div.innerHTML = html;
  3426. while(div.firstChild)
  3427. node.appendChild(div.firstChild);
  3428. base.insertNode(node);
  3429. }
  3430. else
  3431. {
  3432. if(!range)
  3433. return false;
  3434. range.pasteHTML(html);
  3435. }
  3436. };
  3437. /**
  3438. * <p>The same as insertHTML except with DOM nodes instead</p>
  3439. *
  3440. * <p><strong>Warning:</strong> the nodes must belong to the
  3441. * document they are being inserted into. Some browsers
  3442. * will throw exceptions if they don't.</p>
  3443. *
  3444. * @param {Node} node
  3445. * @param {Node} endNode
  3446. * @return False on fail
  3447. * @function
  3448. * @name insertNode
  3449. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3450. */
  3451. base.insertNode = function(node, endNode) {
  3452. if(isW3C)
  3453. {
  3454. var selection, selectAfter,
  3455. toInsert = doc.createDocumentFragment(),
  3456. range = base.selectedRange();
  3457. if(!range)
  3458. return false;
  3459. toInsert.appendChild(node);
  3460. if(endNode)
  3461. {
  3462. toInsert.appendChild(range.extractContents());
  3463. toInsert.appendChild(endNode);
  3464. }
  3465. selectAfter = toInsert.lastChild;
  3466. // If the last child is undefined then there is nothing to insert so return
  3467. if(!selectAfter)
  3468. return;
  3469. range.deleteContents();
  3470. range.insertNode(toInsert);
  3471. selection = doc.createRange();
  3472. selection.setStartAfter(selectAfter);
  3473. base.selectRange(selection);
  3474. }
  3475. else
  3476. base.insertHTML(node.outerHTML, endNode?endNode.outerHTML:null);
  3477. };
  3478. /**
  3479. * <p>Clones the selected Range</p>
  3480. *
  3481. * <p>IE <= 8 will return a TextRange, all other browsers
  3482. * will return a Range object.</p>
  3483. *
  3484. * @return {Range|TextRange}
  3485. * @function
  3486. * @name cloneSelected
  3487. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3488. */
  3489. base.cloneSelected = function() {
  3490. var range = base.selectedRange();
  3491. if(range)
  3492. return isW3C ? range.cloneRange() : range.duplicate();
  3493. };
  3494. /**
  3495. * <p>Gets the selected Range</p>
  3496. *
  3497. * <p>IE <= 8 will return a TextRange, all other browsers
  3498. * will return a Range object.</p>
  3499. *
  3500. * @return {Range|TextRange}
  3501. * @function
  3502. * @name selectedRange
  3503. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3504. */
  3505. base.selectedRange = function() {
  3506. var range, firstChild,
  3507. sel = isW3C ? win.getSelection() : doc.selection;
  3508. if(!sel)
  3509. return;
  3510. // When creating a new range, set the start to the body
  3511. // element to avoid errors in FF.
  3512. if(sel.getRangeAt && sel.rangeCount <= 0)
  3513. {
  3514. firstChild = doc.body;
  3515. while(firstChild.firstChild)
  3516. firstChild = firstChild.firstChild;
  3517. range = doc.createRange();
  3518. range.setStart(firstChild, 0);
  3519. sel.addRange(range);
  3520. }
  3521. if(isW3C)
  3522. range = sel.getRangeAt(0);
  3523. if(!isW3C && sel.type !== 'Control')
  3524. range = sel.createRange();
  3525. // IE fix to make sure only return selections that are part of the WYSIWYG iframe
  3526. return _isOwner(range) ? range : null;
  3527. };
  3528. /**
  3529. * Checks if an IE TextRange range belongs to
  3530. * this document or not.
  3531. *
  3532. * Returns true if the range isn't an IE range or
  3533. * if the range is null.
  3534. *
  3535. * @private
  3536. */
  3537. _isOwner = function(range) {
  3538. var parent;
  3539. // IE fix to make sure only return selections that are part of the WYSIWYG iframe
  3540. return (range && range.parentElement && (parent = range.parentElement())) ?
  3541. parent.ownerDocument === doc :
  3542. true;
  3543. };
  3544. /**
  3545. * Gets if there is currently a selection
  3546. *
  3547. * @return {Boolean}
  3548. * @function
  3549. * @name hasSelection
  3550. * @since 1.4.4
  3551. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3552. */
  3553. base.hasSelection = function() {
  3554. var range,
  3555. sel = isW3C ? win.getSelection() : doc.selection;
  3556. if(isW3C || !sel)
  3557. return sel && sel.rangeCount > 0;
  3558. range = sel.createRange();
  3559. return range && _isOwner(range);
  3560. };
  3561. /**
  3562. * Gets the currently selected HTML
  3563. *
  3564. * @return {string}
  3565. * @function
  3566. * @name selectedHtml
  3567. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3568. */
  3569. base.selectedHtml = function() {
  3570. var div,
  3571. range = base.selectedRange();
  3572. if(!range)
  3573. return '';
  3574. // IE < 9
  3575. if(!isW3C && range.text !== '' && range.htmlText)
  3576. return range.htmlText;
  3577. // IE9+ and all other browsers
  3578. if(isW3C)
  3579. {
  3580. div = doc.createElement('div');
  3581. div.appendChild(range.cloneContents());
  3582. return div.innerHTML;
  3583. }
  3584. return '';
  3585. };
  3586. /**
  3587. * Gets the parent node of the selected contents in the range
  3588. *
  3589. * @return {HTMLElement}
  3590. * @function
  3591. * @name parentNode
  3592. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3593. */
  3594. base.parentNode = function() {
  3595. var range = base.selectedRange();
  3596. if(range)
  3597. return range.parentElement ? range.parentElement() : range.commonAncestorContainer;
  3598. };
  3599. /**
  3600. * Gets the first block level parent of the selected
  3601. * contents of the range.
  3602. *
  3603. * @return {HTMLElement}
  3604. * @function
  3605. * @name getFirstBlockParent
  3606. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3607. */
  3608. /**
  3609. * Gets the first block level parent of the selected
  3610. * contents of the range.
  3611. *
  3612. * @param {Node} n The element to get the first block level parent from
  3613. * @return {HTMLElement}
  3614. * @function
  3615. * @name getFirstBlockParent^2
  3616. * @since 1.4.1
  3617. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3618. */
  3619. base.getFirstBlockParent = function(n) {
  3620. var func = function(node) {
  3621. if(!$.sceditor.dom.isInline(node, true))
  3622. return node;
  3623. node = node ? node.parentNode : null;
  3624. return node ? func(node) : null;
  3625. };
  3626. return func(n || base.parentNode());
  3627. };
  3628. /**
  3629. * Inserts a node at either the start or end of the current selection
  3630. *
  3631. * @param {Bool} start
  3632. * @param {Node} node
  3633. * @function
  3634. * @name insertNodeAt
  3635. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3636. */
  3637. base.insertNodeAt = function(start, node) {
  3638. var currentRange = base.selectedRange(),
  3639. range = base.cloneSelected();
  3640. if(!range)
  3641. return false;
  3642. range.collapse(start);
  3643. if(range.insertNode)
  3644. range.insertNode(node);
  3645. else
  3646. range.pasteHTML(node.outerHTML);
  3647. // Reselect the current range.
  3648. // Fixes issue with Chrome losing the selection. Issue#82
  3649. base.selectRange(currentRange);
  3650. };
  3651. /**
  3652. * Creates a marker node
  3653. *
  3654. * @param {String} id
  3655. * @return {Node}
  3656. * @private
  3657. */
  3658. _createMarker = function(id) {
  3659. base.removeMarker(id);
  3660. var marker = doc.createElement('span');
  3661. marker.id = id;
  3662. marker.style.lineHeight = '0';
  3663. marker.style.display = 'none';
  3664. marker.className = 'sceditor-selection sceditor-ignore';
  3665. marker.innerHTML = ' ';
  3666. return marker;
  3667. };
  3668. /**
  3669. * Inserts start/end markers for the current selection
  3670. * which can be used by restoreRange to re-select the
  3671. * range.
  3672. *
  3673. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3674. * @function
  3675. * @name insertMarkers
  3676. */
  3677. base.insertMarkers = function() {
  3678. base.insertNodeAt(true, _createMarker(startMarker));
  3679. base.insertNodeAt(false, _createMarker(endMarker));
  3680. };
  3681. /**
  3682. * Gets the marker with the specified ID
  3683. *
  3684. * @param {String} id
  3685. * @return {Node}
  3686. * @function
  3687. * @name getMarker
  3688. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3689. */
  3690. base.getMarker = function(id) {
  3691. return doc.getElementById(id);
  3692. };
  3693. /**
  3694. * Removes the marker with the specified ID
  3695. *
  3696. * @param {String} id
  3697. * @function
  3698. * @name removeMarker
  3699. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3700. */
  3701. base.removeMarker = function(id) {
  3702. var marker = base.getMarker(id);
  3703. if(marker)
  3704. marker.parentNode.removeChild(marker);
  3705. };
  3706. /**
  3707. * Removes the start/end markers
  3708. *
  3709. * @function
  3710. * @name removeMarkers
  3711. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3712. */
  3713. base.removeMarkers = function() {
  3714. base.removeMarker(startMarker);
  3715. base.removeMarker(endMarker);
  3716. };
  3717. /**
  3718. * Saves the current range location. Alias of insertMarkers()
  3719. *
  3720. * @function
  3721. * @name saveRage
  3722. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3723. */
  3724. base.saveRange = function() {
  3725. base.insertMarkers();
  3726. };
  3727. /**
  3728. * Select the specified range
  3729. *
  3730. * @param {Range|TextRange} range
  3731. * @function
  3732. * @name selectRange
  3733. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3734. */
  3735. base.selectRange = function(range) {
  3736. if(isW3C)
  3737. {
  3738. win.getSelection().removeAllRanges();
  3739. win.getSelection().addRange(range);
  3740. }
  3741. else
  3742. range.select();
  3743. };
  3744. /**
  3745. * Restores the last range saved by saveRange() or insertMarkers()
  3746. *
  3747. * @function
  3748. * @name restoreRange
  3749. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3750. */
  3751. base.restoreRange = function() {
  3752. var marker,
  3753. range = base.selectedRange(),
  3754. start = base.getMarker(startMarker),
  3755. end = base.getMarker(endMarker);
  3756. if(!start || !end || !range)
  3757. return false;
  3758. if(!isW3C)
  3759. {
  3760. range = doc.body.createTextRange();
  3761. marker = doc.body.createTextRange();
  3762. marker.moveToElementText(start);
  3763. range.setEndPoint('StartToStart', marker);
  3764. range.moveStart(characterStr, 0);
  3765. marker.moveToElementText(end);
  3766. range.setEndPoint('EndToStart', marker);
  3767. range.moveEnd(characterStr, 0);
  3768. base.selectRange(range);
  3769. }
  3770. else
  3771. {
  3772. range = doc.createRange();
  3773. range.setStartBefore(start);
  3774. range.setEndAfter(end);
  3775. base.selectRange(range);
  3776. }
  3777. base.removeMarkers();
  3778. };
  3779. /**
  3780. * Selects the text left and right of the current selection
  3781. * @param {int} left
  3782. * @param {int} right
  3783. * @since 1.4.3
  3784. * @function
  3785. * @name selectOuterText
  3786. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3787. */
  3788. base.selectOuterText = function(left, right) {
  3789. var range = base.cloneSelected();
  3790. if(!range)
  3791. return false;
  3792. range.collapse(false);
  3793. if(!isW3C)
  3794. {
  3795. range.moveStart(characterStr, 0-left);
  3796. range.moveEnd(characterStr, right);
  3797. }
  3798. else
  3799. {
  3800. range.setStart(range.startContainer, range.startOffset-left);
  3801. range.setEnd(range.endContainer, range.endOffset+right);
  3802. }
  3803. base.selectRange(range);
  3804. };
  3805. /**
  3806. * Gets the text left or right of the current selection
  3807. * @param {Boolean} before
  3808. * @param {Int} length
  3809. * @since 1.4.3
  3810. * @function
  3811. * @name selectOuterText
  3812. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3813. */
  3814. base.getOuterText = function(before, length) {
  3815. var ret = '',
  3816. range = base.cloneSelected();
  3817. if(!range)
  3818. return '';
  3819. range.collapse(false);
  3820. if(before)
  3821. {
  3822. if(!isW3C)
  3823. {
  3824. range.moveStart(characterStr, 0-length);
  3825. ret = range.text;
  3826. }
  3827. else
  3828. {
  3829. ret = range.startContainer.textContent.substr(0, range.startOffset);
  3830. ret = ret.substr(Math.max(0, ret.length - length));
  3831. }
  3832. }
  3833. else
  3834. {
  3835. if(!isW3C)
  3836. {
  3837. range.moveEnd(characterStr, length);
  3838. ret = range.text;
  3839. }
  3840. else
  3841. ret = range.startContainer.textContent.substr(range.startOffset, length);
  3842. }
  3843. return ret;
  3844. };
  3845. /**
  3846. * Replaces keywords with values based on the current caret position
  3847. *
  3848. * @param {Array} keywords
  3849. * @param {Boolean} includeAfter If to include the text after the current caret position or just text before
  3850. * @param {Boolean} keywordsSorted If the keywords array is pre sorted shortest to longest
  3851. * @param {Int} longestKeyword Length of the longest keyword
  3852. * @param {Boolean} requireWhiteSpace If the key must be surrounded by whitespace
  3853. * @param {String} currrentChar If this is being called from a keypress event, this should be set to the pressed character
  3854. * @return {Boolean}
  3855. * @function
  3856. * @name raplaceKeyword
  3857. * @memberOf jQuery.sceditor.rangeHelper.prototype
  3858. */
  3859. base.raplaceKeyword = function(keywords, includeAfter, keywordsSorted, longestKeyword, requireWhiteSpace, currrentChar) {
  3860. if(!keywordsSorted)
  3861. {
  3862. keywords.sort(function(a, b){
  3863. return a.length - b.length;
  3864. });
  3865. }
  3866. var beforeStr, str, keywordIdx, numberCharsLeft, keywordRegex, startIdx, keyword,
  3867. i = keywords.length,
  3868. maxKeyLen = longestKeyword || keywords[i-1][0].length;
  3869. if(requireWhiteSpace)
  3870. {
  3871. // requireWhiteSpace doesn't work with textRanges as they select text on the
  3872. // other side of elements causing space-img-key to match when it shouldn't.
  3873. if(!isW3C)
  3874. return false;
  3875. ++maxKeyLen;
  3876. }
  3877. beforeStr = base.getOuterText(true, maxKeyLen);
  3878. str = beforeStr + (currrentChar != null ? currrentChar : '');
  3879. if(includeAfter)
  3880. str += base.getOuterText(false, maxKeyLen);
  3881. while(i--)
  3882. {
  3883. keyword = keywords[i][0];
  3884. keywordRegex = new RegExp('(?:[\\s\xA0\u2002\u2003\u2009])' + $.sceditor.regexEscape(keyword) + '(?=[\\s\xA0\u2002\u2003\u2009])');
  3885. startIdx = beforeStr.length - 1 - keyword.length;
  3886. if(requireWhiteSpace)
  3887. --startIdx;
  3888. startIdx = Math.max(0, startIdx);
  3889. if((keywordIdx = requireWhiteSpace ? str.substr(startIdx).search(keywordRegex) : str.indexOf(keyword, startIdx)) > -1)
  3890. {
  3891. if(requireWhiteSpace)
  3892. keywordIdx += startIdx + 1;
  3893. // Make sure the substr is between beforeStr and after not entirely in one or the other
  3894. if(keywordIdx > beforeStr.length || (keywordIdx + keyword.length + (requireWhiteSpace ? 1 : 0)) < beforeStr.length)
  3895. continue;
  3896. numberCharsLeft = beforeStr.length - keywordIdx;
  3897. base.selectOuterText(numberCharsLeft, keyword.length - numberCharsLeft - (currrentChar != null && /^\S/.test(currrentChar) ? 1 : 0));
  3898. base.insertHTML(keywords[i][1]);
  3899. return true;
  3900. }
  3901. }
  3902. return false;
  3903. };
  3904. /**
  3905. * Compares two ranges.
  3906. * @param {Range|TextRange} rangeA
  3907. * @param {Range|TextRange} rangeB If undefined it will be set to the current selected range
  3908. * @return {Boolean}
  3909. */
  3910. base.compare = function(rangeA, rangeB) {
  3911. if(!rangeB)
  3912. rangeB = base.selectedRange();
  3913. if(!rangeA || !rangeB)
  3914. return !rangeA && !rangeB;
  3915. if(!isW3C)
  3916. {
  3917. return _isOwner(rangeA) && _isOwner(rangeB) &&
  3918. rangeA.compareEndPoints('EndToEnd', rangeB) === 0 &&
  3919. rangeA.compareEndPoints('StartToStart', rangeB) === 0;
  3920. }
  3921. return rangeA.compareBoundaryPoints(Range.END_TO_END, rangeB) === 0 &&
  3922. rangeA.compareBoundaryPoints(Range.START_TO_START, rangeB) === 0;
  3923. };
  3924. };
  3925. /**
  3926. * Static DOM helper class
  3927. * @class dom
  3928. * @name jQuery.sceditor.dom
  3929. */
  3930. $.sceditor.dom =
  3931. /** @lends jQuery.sceditor.dom */
  3932. {
  3933. /**
  3934. * Loop all child nodes of the passed node
  3935. *
  3936. * The function should accept 1 parameter being the node.
  3937. * If the function returns false the loop will be exited.
  3938. *
  3939. * @param {HTMLElement} node
  3940. * @param {function} func Function that is called for every node, should accept 1 param for the node
  3941. * @param {bool} innermostFirst If the innermost node should be passed to the function before it's parents
  3942. * @param {bool} siblingsOnly If to only traverse the nodes siblings
  3943. * @param {bool} reverse If to traverse the nodes in reverse
  3944. */
  3945. traverse: function(node, func, innermostFirst, siblingsOnly, reverse) {
  3946. if(node)
  3947. {
  3948. node = reverse ? node.lastChild : node.firstChild;
  3949. while(node != null)
  3950. {
  3951. var next = reverse ? node.previousSibling : node.nextSibling;
  3952. if(!innermostFirst && func(node) === false)
  3953. return false;
  3954. // traverse all children
  3955. if(!siblingsOnly && this.traverse(node, func, innermostFirst, siblingsOnly, reverse) === false)
  3956. return false;
  3957. if(innermostFirst && func(node) === false)
  3958. return false;
  3959. // move to next child
  3960. node = next;
  3961. }
  3962. }
  3963. },
  3964. /**
  3965. * Like traverse but loops in reverse
  3966. * @see traverse
  3967. */
  3968. rTraverse: function(node, func, innermostFirst, siblingsOnly) {
  3969. this.traverse(node, func, innermostFirst, siblingsOnly, true);
  3970. },
  3971. /**
  3972. * Parses HTML
  3973. * @param
  3974. * @since 1.4.4
  3975. * @return {Array}
  3976. */
  3977. parseHTML: function(html, context) {
  3978. var ret = [],
  3979. tmp = (context || document).createElement('div');
  3980. tmp.innerHTML = html;
  3981. $.merge(ret, tmp.childNodes);
  3982. return ret;
  3983. },
  3984. /**
  3985. * Checks if an element is not a p or div element and if it has any styling.
  3986. * @param {HTMLElement} elm
  3987. * @return {Boolean}
  3988. * @since 1.4.4
  3989. */
  3990. hasStyling: function(elm) {
  3991. var $elm = $(elm);
  3992. return elm && (!$elm.is('p,div') || elm.className || $elm.attr('style') || !$.isEmptyObject($elm.data()));
  3993. },
  3994. /**
  3995. * Converts an element from one type to another.
  3996. *
  3997. * For example it can convert the element <b> to <strong>
  3998. * @param {HTMLElement} elm
  3999. * @param {String} newElement
  4000. * @return {HTMLElement}
  4001. * @since 1.4.4
  4002. */
  4003. convertElement: function(elm, newElement) {
  4004. var child, attr,
  4005. i = elm.attributes.length,
  4006. newTag = elm.ownerDocument.createElement(newElement);
  4007. while(i--)
  4008. {
  4009. attr = elm.attributes[i];
  4010. // IE < 8 returns all possible attribtues, not just specified ones
  4011. if(!$.sceditor.ie || attr.specified)
  4012. {
  4013. // IE < 8 doesn't return the CSS for the style attribute
  4014. if($.sceditor.ie < 8 && /style/i.test(attr.name))
  4015. elm.style.cssText = elm.style.cssText;
  4016. else
  4017. newTag.setAttribute(attr.name, attr.value);
  4018. }
  4019. }
  4020. while((child = elm.firstChild))
  4021. newTag.appendChild(child);
  4022. elm.parentNode.replaceChild(newTag, elm);
  4023. return newTag;
  4024. },
  4025. /**
  4026. * List of block level elements separated by bars (|)
  4027. * @type {string}
  4028. */
  4029. blockLevelList: '|body|hr|p|div|h1|h2|h3|h4|h5|h6|address|pre|form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|blockquote|center|',
  4030. /**
  4031. * Checks if an element is inline
  4032. *
  4033. * @return {bool}
  4034. */
  4035. isInline: function(elm, includeCodeAsBlock) {
  4036. if(!elm || elm.nodeType !== 1)
  4037. return true;
  4038. elm = elm.tagName.toLowerCase();
  4039. if(elm === 'code')
  4040. return !includeCodeAsBlock;
  4041. return $.sceditor.dom.blockLevelList.indexOf('|' + elm + '|') < 0;
  4042. },
  4043. /**
  4044. * <p>Copys the CSS from 1 node to another.</p>
  4045. *
  4046. * <p>Only copies CSS defined on the element e.g. style attr.</p>
  4047. *
  4048. * @param {HTMLElement} from
  4049. * @param {HTMLElement} to
  4050. */
  4051. copyCSS: function(from, to) {
  4052. to.style.cssText = from.style.cssText + to.style.cssText;
  4053. },
  4054. /**
  4055. * Fixes block level elements inside in inline elements.
  4056. *
  4057. * @param {HTMLElement} node
  4058. */
  4059. fixNesting: function(node) {
  4060. var base = this,
  4061. getLastInlineParent = function(node) {
  4062. while(base.isInline(node.parentNode, true))
  4063. node = node.parentNode;
  4064. return node;
  4065. };
  4066. base.traverse(node, function(node) {
  4067. // if node is an element, and it is blocklevel and the parent isn't block level
  4068. // then it needs fixing
  4069. if(node.nodeType === 1 && !base.isInline(node, true) && base.isInline(node.parentNode, true))
  4070. {
  4071. var parent = getLastInlineParent(node),
  4072. rParent = parent.parentNode,
  4073. before = base.extractContents(parent, node),
  4074. middle = node;
  4075. // copy current styling so when moved out of the parent
  4076. // it still has the same styling
  4077. base.copyCSS(parent, middle);
  4078. rParent.insertBefore(before, parent);
  4079. rParent.insertBefore(middle, parent);
  4080. }
  4081. });
  4082. },
  4083. /**
  4084. * Finds the common parent of two nodes
  4085. *
  4086. * @param {HTMLElement} node1
  4087. * @param {HTMLElement} node2
  4088. * @return {HTMLElement}
  4089. */
  4090. findCommonAncestor: function(node1, node2) {
  4091. // not as fast as making two arrays of parents and comparing
  4092. // but is a lot smaller and as it's currently only used with
  4093. // fixing invalid nesting so it doesn't need to be very fast
  4094. return $(node1).parents().has($(node2)).first();
  4095. },
  4096. getSibling: function(node, previous) {
  4097. var sibling;
  4098. if(!node)
  4099. return null;
  4100. if((sibling = node[previous ? 'previousSibling' : 'nextSibling']))
  4101. return sibling;
  4102. return $.sceditor.dom.getSibling(node.parentNode, previous);
  4103. },
  4104. /**
  4105. * Removes unused whitespace from the root and all it's children
  4106. *
  4107. * @name removeWhiteSpace^1
  4108. * @param HTMLElement root
  4109. * @return void
  4110. */
  4111. /**
  4112. * Removes unused whitespace from the root and all it's children.
  4113. *
  4114. * If preserveNewLines is true, new line characters will not be removed
  4115. *
  4116. * @name removeWhiteSpace^2
  4117. * @param HTMLElement root
  4118. * @param Boolean preserveNewLines
  4119. * @return void
  4120. * @since 1.4.3
  4121. */
  4122. removeWhiteSpace: function(root, preserveNewLines) {
  4123. var nodeValue, nodeType, next, previous, cssWS, nextNode, trimStart, sibling,
  4124. getSibling = $.sceditor.dom.getSibling,
  4125. isInline = $.sceditor.dom.isInline,
  4126. node = root.firstChild,
  4127. whitespace = /[\t ]+/g,
  4128. witespaceAndLines = /[\t\n\r ]+/g;
  4129. while(node)
  4130. {
  4131. nextNode = node.nextSibling;
  4132. nodeValue = node.nodeValue;
  4133. nodeType = node.nodeType;
  4134. // 1 = element
  4135. if(nodeType === 1 && node.firstChild)
  4136. {
  4137. cssWS = $(node).css('whiteSpace');
  4138. // pre || pre-wrap with any vendor prefix
  4139. if(!/pre(?:\-wrap)?$/i.test(cssWS))
  4140. $.sceditor.dom.removeWhiteSpace(node, /line$/i.test(cssWS));
  4141. }
  4142. // 3 = textnode
  4143. if(nodeType === 3 && nodeValue)
  4144. {
  4145. next = getSibling(node);
  4146. previous = getSibling(node, true);
  4147. sibling = previous;
  4148. trimStart = false;
  4149. while($(sibling).hasClass('sceditor-ignore'))
  4150. sibling = getSibling(sibling, true);
  4151. // If last sibling is not inline or is a textnode ending in whitespace,
  4152. // the start whitespace should be stripped
  4153. if(isInline(node) && sibling)
  4154. {
  4155. while(sibling.lastChild)
  4156. sibling = sibling.lastChild;
  4157. trimStart = sibling.nodeType === 3 ? /[\t\n\r ]$/.test(sibling.nodeValue) : !isInline(sibling);
  4158. }
  4159. if(!isInline(node) || !previous || !isInline(previous) || trimStart)
  4160. nodeValue = nodeValue.replace(/^[\t\n\r ]+/, '');
  4161. if(!isInline(node) || !next || !isInline(next))
  4162. nodeValue = nodeValue.replace(/[\t\n\r ]+$/, '');
  4163. // Remove empty text nodes
  4164. if(!nodeValue.length)
  4165. root.removeChild(node);
  4166. else
  4167. node.nodeValue = nodeValue.replace(preserveNewLines ? whitespace : witespaceAndLines, ' ');
  4168. }
  4169. node = nextNode;
  4170. }
  4171. },
  4172. /**
  4173. * Extracts all the nodes between the start and end nodes
  4174. *
  4175. * @param {HTMLElement} startNode The node to start extracting at
  4176. * @param {HTMLElement} endNode The node to stop extracting at
  4177. * @return {DocumentFragment}
  4178. */
  4179. extractContents: function(startNode, endNode) {
  4180. var base = this,
  4181. $commonAncestor = base.findCommonAncestor(startNode, endNode),
  4182. commonAncestor = !$commonAncestor ? null : $commonAncestor[0],
  4183. startReached = false,
  4184. endReached = false;
  4185. return (function extract(root) {
  4186. var df = startNode.ownerDocument.createDocumentFragment();
  4187. base.traverse(root, function(node) {
  4188. // if end has been reached exit loop
  4189. if(endReached || (node === endNode && startReached))
  4190. {
  4191. endReached = true;
  4192. return false;
  4193. }
  4194. if(node === startNode)
  4195. startReached = true;
  4196. var c, n;
  4197. if(startReached)
  4198. {
  4199. // if the start has been reached and this elm contains
  4200. // the end node then clone it
  4201. if(jQuery.contains(node, endNode) && node.nodeType === 1)
  4202. {
  4203. c = extract(node);
  4204. n = node.cloneNode(false);
  4205. n.appendChild(c);
  4206. df.appendChild(n);
  4207. }
  4208. // otherwise just move it
  4209. else
  4210. df.appendChild(node);
  4211. }
  4212. // if this node contains the start node then add it
  4213. else if(jQuery.contains(node, startNode) && node.nodeType === 1)
  4214. {
  4215. c = extract(node);
  4216. n = node.cloneNode(false);
  4217. n.appendChild(c);
  4218. df.appendChild(n);
  4219. }
  4220. }, false);
  4221. return df;
  4222. }(commonAncestor));
  4223. }
  4224. };
  4225. /**
  4226. * Object containing SCEditor plugins
  4227. * @type {Object}
  4228. * @name plugins
  4229. * @memberOf jQuery.sceditor
  4230. */
  4231. $.sceditor.plugins = {};
  4232. /**
  4233. * Plugin Manager class
  4234. * @class PluginManager
  4235. * @name jQuery.sceditor.PluginManager
  4236. */
  4237. $.sceditor.PluginManager = function(owner) {
  4238. /**
  4239. * Alias of this
  4240. * @private
  4241. * @type {Object}
  4242. */
  4243. var base = this;
  4244. /**
  4245. * Array of all currently registered plugins
  4246. * @type {Array}
  4247. * @private
  4248. */
  4249. var plugins = [];
  4250. /**
  4251. * Editor instance this plugin manager belongs to
  4252. * @type {jQuery.sceditor}
  4253. * @private
  4254. */
  4255. var editorInstance = owner;
  4256. /**
  4257. * Changes a signals name from "name" into "signalName".
  4258. * @param {String} signal
  4259. * @return {String}
  4260. * @private
  4261. */
  4262. var formatSignalName = function(signal) {
  4263. return 'signal' + signal.charAt(0).toUpperCase() + signal.slice(1);
  4264. };
  4265. /**
  4266. * Calls handlers for a signal
  4267. * @see call()
  4268. * @see callOnlyFirst()
  4269. * @param {Array} args
  4270. * @param {Boolean} returnAtFirst
  4271. * @return {Mixed}
  4272. * @private
  4273. */
  4274. var callHandlers = function(args, returnAtFirst) {
  4275. args = [].slice.call(args);
  4276. var i = plugins.length,
  4277. signal = formatSignalName(args.shift());
  4278. while(i--)
  4279. {
  4280. if(signal in plugins[i])
  4281. {
  4282. if(returnAtFirst)
  4283. return plugins[i][signal].apply(editorInstance, args);
  4284. plugins[i][signal].apply(editorInstance, args);
  4285. }
  4286. }
  4287. };
  4288. /**
  4289. * Calls all handlers for the passed signal
  4290. * @param {String} signal
  4291. * @param {...String} args
  4292. * @return {Void}
  4293. * @function
  4294. * @name call
  4295. * @memberOf jQuery.sceditor.PluginManager.prototype
  4296. */
  4297. base.call = function() {
  4298. callHandlers(arguments, false);
  4299. };
  4300. /**
  4301. * Calls the first handler for a signal, and returns the result
  4302. * @param {String} signal
  4303. * @param {...String} args
  4304. * @return {Mixed} The result of calling the handler
  4305. * @function
  4306. * @name callOnlyFirst
  4307. * @memberOf jQuery.sceditor.PluginManager.prototype
  4308. */
  4309. base.callOnlyFirst = function() {
  4310. return callHandlers(arguments, true);
  4311. };
  4312. /**
  4313. * <p>Returns an object which has callNext and hasNext methods.</p>
  4314. *
  4315. * <p>callNext takes arguments to pass to the handler and returns the
  4316. * result of calling the handler</p>
  4317. *
  4318. * <p>hasNext checks if there is another handler</p>
  4319. *
  4320. * @param {String} signal
  4321. * @return {Object} Object with hasNext and callNext methods
  4322. * @function
  4323. * @name iter
  4324. * @memberOf jQuery.sceditor.PluginManager.prototype
  4325. */
  4326. base.iter = function(signal) {
  4327. signal = formatSignalName(signal);
  4328. return (function () {
  4329. var i = plugins.length;
  4330. return {
  4331. callNext: function(args) {
  4332. while(i--)
  4333. if(plugins[i] && signal in plugins[i])
  4334. return plugins[i].apply(editorInstance, args);
  4335. },
  4336. hasNext: function() {
  4337. var j = i;
  4338. while(j--)
  4339. if(plugins[j] && signal in plugins[j])
  4340. return true;
  4341. return false;
  4342. }
  4343. };
  4344. }());
  4345. };
  4346. /**
  4347. * Checks if a signal has a handler
  4348. * @param {String} signal
  4349. * @return {Boolean}
  4350. * @function
  4351. * @name hasHandler
  4352. * @memberOf jQuery.sceditor.PluginManager.prototype
  4353. */
  4354. base.hasHandler = function(signal) {
  4355. var i = plugins.length;
  4356. signal = formatSignalName(signal);
  4357. while(i--)
  4358. if(signal in plugins[i])
  4359. return true;
  4360. return false;
  4361. };
  4362. /**
  4363. * Checks if the plugin exists in jQuery.sceditor.plugins
  4364. * @param {String} plugin
  4365. * @return {Boolean}
  4366. * @function
  4367. * @name exists
  4368. * @memberOf jQuery.sceditor.PluginManager.prototype
  4369. */
  4370. base.exsists = function(plugin) {
  4371. if(plugin in $.sceditor.plugins)
  4372. {
  4373. plugin = $.sceditor.plugins[plugin];
  4374. return typeof plugin === 'function' && typeof plugin.prototype === 'object';
  4375. }
  4376. return false;
  4377. };
  4378. /**
  4379. * Checks if the passed plugin is currently registered.
  4380. * @param {String} plugin
  4381. * @return {Boolean}
  4382. * @function
  4383. * @name isRegistered
  4384. * @memberOf jQuery.sceditor.PluginManager.prototype
  4385. */
  4386. base.isRegistered = function(plugin) {
  4387. var i = plugins.length;
  4388. if(!base.exsists(plugin))
  4389. return false;
  4390. while(i--)
  4391. if(plugins[i] instanceof $.sceditor.plugins[plugin])
  4392. return true;
  4393. return false;
  4394. };
  4395. /**
  4396. * Registers a plugin to receive signals
  4397. * @param {String} plugin
  4398. * @return {Boolean}
  4399. * @function
  4400. * @name register
  4401. * @memberOf jQuery.sceditor.PluginManager.prototype
  4402. */
  4403. base.register = function(plugin) {
  4404. if(!base.exsists(plugin))
  4405. return false;
  4406. plugin = new $.sceditor.plugins[plugin]();
  4407. plugins.push(plugin);
  4408. if('init' in plugin)
  4409. plugin.init.apply(editorInstance);
  4410. return true;
  4411. };
  4412. /**
  4413. * Deregisters a plugin.
  4414. * @param {String} plugin
  4415. * @return {Boolean}
  4416. * @function
  4417. * @name deregister
  4418. * @memberOf jQuery.sceditor.PluginManager.prototype
  4419. */
  4420. base.deregister = function(plugin) {
  4421. var removedPlugin,
  4422. i = plugins.length,
  4423. ret = false;
  4424. if(!base.isRegistered(plugin))
  4425. return false;
  4426. while(i--)
  4427. {
  4428. if(plugins[i] instanceof $.sceditor.plugins[plugin])
  4429. {
  4430. removedPlugin = plugins.splice(i, 1)[0];
  4431. ret = true;
  4432. if('destroy' in removedPlugin)
  4433. removedPlugin.destroy.apply(editorInstance);
  4434. }
  4435. }
  4436. return ret;
  4437. };
  4438. /**
  4439. * <p>Clears all plugins and removes the owner reference.</p>
  4440. *
  4441. * <p>Calling any functions on this object after calling destroy will cause a JS error.</p>
  4442. * @return {Void}
  4443. * @function
  4444. * @name destroy
  4445. * @memberOf jQuery.sceditor.PluginManager.prototype
  4446. */
  4447. base.destroy = function() {
  4448. var i = plugins.length;
  4449. while(i--)
  4450. if('destroy' in plugins[i])
  4451. plugins[i].destroy.apply(editorInstance);
  4452. plugins = null;
  4453. editorInstance = null;
  4454. };
  4455. };
  4456. /**
  4457. * Static command helper class
  4458. * @class command
  4459. * @name jQuery.sceditor.command
  4460. */
  4461. $.sceditor.command =
  4462. /** @lends jQuery.sceditor.command */
  4463. {
  4464. /**
  4465. * Gets a command
  4466. *
  4467. * @param {String} name
  4468. * @return {Object|null}
  4469. * @since v1.3.5
  4470. */
  4471. get: function(name) {
  4472. return $.sceditor.commands[name] || null;
  4473. },
  4474. /**
  4475. * <p>Adds a command to the editor or updates an existing
  4476. * command if a command with the specified name already exists.</p>
  4477. *
  4478. * <p>Once a command is add it can be included in the toolbar by
  4479. * adding it's name to the toolbar option in the constructor. It
  4480. * can also be executed manually by calling {@link jQuery.sceditor.execCommand}</p>
  4481. *
  4482. * @example
  4483. * $.sceditor.command.set("hello",
  4484. * {
  4485. * exec: function() {
  4486. * alert("Hello World!");
  4487. * }
  4488. * });
  4489. *
  4490. * @param {String} name
  4491. * @param {Object} cmd
  4492. * @return {this|false} Returns false if name or cmd is false
  4493. * @since v1.3.5
  4494. */
  4495. set: function(name, cmd) {
  4496. if(!name || !cmd)
  4497. return false;
  4498. // merge any existing command properties
  4499. cmd = $.extend($.sceditor.commands[name] || {}, cmd);
  4500. cmd.remove = function() { $.sceditor.command.remove(name); };
  4501. $.sceditor.commands[name] = cmd;
  4502. return this;
  4503. },
  4504. /**
  4505. * Removes a command
  4506. *
  4507. * @param {String} name
  4508. * @return {this}
  4509. * @since v1.3.5
  4510. */
  4511. remove: function(name) {
  4512. if($.sceditor.commands[name])
  4513. delete $.sceditor.commands[name];
  4514. return this;
  4515. }
  4516. };
  4517. /**
  4518. * Default options for SCEditor
  4519. * @type {Object}
  4520. * @class defaultOptions
  4521. * @name jQuery.sceditor.defaultOptions
  4522. */
  4523. $.sceditor.defaultOptions = {
  4524. /** @lends jQuery.sceditor.defaultOptions */
  4525. /**
  4526. * Toolbar buttons order and groups. Should be comma separated and have a bar | to separate groups
  4527. * @type {String}
  4528. */
  4529. toolbar: 'bold,italic,underline,strike,subscript,superscript|left,center,right,justify|' +
  4530. 'font,size,color,removeformat|cut,copy,paste,pastetext|bulletlist,orderedlist|' +
  4531. 'table|code,quote|horizontalrule,image,email,link,unlink|emoticon,youtube,date,time|' +
  4532. 'ltr,rtl|print,maximize,source',
  4533. /**
  4534. * Comma separated list of commands to excludes from the toolbar
  4535. * @type {String}
  4536. */
  4537. toolbarExclude: null,
  4538. /**
  4539. * Stylesheet to include in the WYSIWYG editor. Will style the WYSIWYG elements
  4540. * @type {String}
  4541. */
  4542. style: 'jquery.sceditor.default.css',
  4543. /**
  4544. * Comma separated list of fonts for the font selector
  4545. * @type {String}
  4546. */
  4547. fonts: 'Arial,Arial Black,Comic Sans MS,Courier New,Georgia,Impact,Sans-serif,Serif,Times New Roman,Trebuchet MS,Verdana',
  4548. /**
  4549. * Colors should be comma separated and have a bar | to signal a new column.
  4550. *
  4551. * If null the colors will be auto generated.
  4552. * @type {string}
  4553. */
  4554. colors: null,
  4555. /**
  4556. * The locale to use.
  4557. * @type {String}
  4558. */
  4559. locale: 'en',
  4560. /**
  4561. * The Charset to use
  4562. * @type {String}
  4563. */
  4564. charset: 'utf-8',
  4565. /**
  4566. * Compatibility mode for emoticons.
  4567. *
  4568. * Helps if you have emoticons such as :/ which would put an emoticon inside http://
  4569. *
  4570. * This mode requires emoticons to be surrounded by whitespace or end of line chars.
  4571. * This mode has limited As You Type emoticon conversion support. It will not replace
  4572. * AYT for end of line chars, only emoticons surrounded by whitespace. They will still
  4573. * be replaced correctly when loaded just not AYT.
  4574. * @type {Boolean}
  4575. */
  4576. emoticonsCompat: false,
  4577. /**
  4578. * If to enable emoticons. Can be changes at runtime using the emoticons() method.
  4579. * @type {Boolean}
  4580. * @since 1.4.2
  4581. */
  4582. emoticonsEnabled: true,
  4583. /**
  4584. * Emoticon root URL
  4585. * @type {String}
  4586. */
  4587. emoticonsRoot: '',
  4588. emoticons: {
  4589. dropdown: {
  4590. ':)': 'emoticons/smile.png',
  4591. ':angel:': 'emoticons/angel.png',
  4592. ':angry:': 'emoticons/angry.png',
  4593. '8-)': 'emoticons/cool.png',
  4594. ":'(": 'emoticons/cwy.png',
  4595. ':ermm:': 'emoticons/ermm.png',
  4596. ':D': 'emoticons/grin.png',
  4597. '<3': 'emoticons/heart.png',
  4598. ':(': 'emoticons/sad.png',
  4599. ':O': 'emoticons/shocked.png',
  4600. ':P': 'emoticons/tongue.png',
  4601. ';)': 'emoticons/wink.png'
  4602. },
  4603. more: {
  4604. ':alien:': 'emoticons/alien.png',
  4605. ':blink:': 'emoticons/blink.png',
  4606. ':blush:': 'emoticons/blush.png',
  4607. ':cheerful:': 'emoticons/cheerful.png',
  4608. ':devil:': 'emoticons/devil.png',
  4609. ':dizzy:': 'emoticons/dizzy.png',
  4610. ':getlost:': 'emoticons/getlost.png',
  4611. ':happy:': 'emoticons/happy.png',
  4612. ':kissing:': 'emoticons/kissing.png',
  4613. ':ninja:': 'emoticons/ninja.png',
  4614. ':pinch:': 'emoticons/pinch.png',
  4615. ':pouty:': 'emoticons/pouty.png',
  4616. ':sick:': 'emoticons/sick.png',
  4617. ':sideways:': 'emoticons/sideways.png',
  4618. ':silly:': 'emoticons/silly.png',
  4619. ':sleeping:': 'emoticons/sleeping.png',
  4620. ':unsure:': 'emoticons/unsure.png',
  4621. ':woot:': 'emoticons/w00t.png',
  4622. ':wassat:': 'emoticons/wassat.png'
  4623. },
  4624. hidden: {
  4625. ':whistling:': 'emoticons/whistling.png',
  4626. ':love:': 'emoticons/wub.png'
  4627. }
  4628. },
  4629. /**
  4630. * Width of the editor. Set to null for automatic with
  4631. * @type {int}
  4632. */
  4633. width: null,
  4634. /**
  4635. * Height of the editor including toolbar. Set to null for automatic height
  4636. * @type {int}
  4637. */
  4638. height: null,
  4639. /**
  4640. * If to allow the editor to be resized
  4641. * @type {Boolean}
  4642. */
  4643. resizeEnabled: true,
  4644. /**
  4645. * Min resize to width, set to null for half textarea width or -1 for unlimited
  4646. * @type {int}
  4647. */
  4648. resizeMinWidth: null,
  4649. /**
  4650. * Min resize to height, set to null for half textarea height or -1 for unlimited
  4651. * @type {int}
  4652. */
  4653. resizeMinHeight: null,
  4654. /**
  4655. * Max resize to height, set to null for double textarea height or -1 for unlimited
  4656. * @type {int}
  4657. */
  4658. resizeMaxHeight: null,
  4659. /**
  4660. * Max resize to width, set to null for double textarea width or -1 for unlimited
  4661. * @type {int}
  4662. */
  4663. resizeMaxWidth: null,
  4664. /**
  4665. * If resizing by height is enabled
  4666. * @type {Boolean}
  4667. */
  4668. resizeHeight: true,
  4669. /**
  4670. * If resizing by width is enabled
  4671. * @type {Boolean}
  4672. */
  4673. resizeWidth: true,
  4674. getHtmlHandler: null,
  4675. getTextHandler: null,
  4676. /**
  4677. * Date format, will be overridden if locale specifies one.
  4678. *
  4679. * The words year, month and day will be replaced with the users current year, month and day.
  4680. * @type {String}
  4681. */
  4682. dateFormat: 'year-month-day',
  4683. /**
  4684. * Element to inset the toolbar into.
  4685. * @type {HTMLElement}
  4686. */
  4687. toolbarContainer: null,
  4688. /**
  4689. * If to enable paste filtering. This is currently experimental, please report any issues.
  4690. * @type {Boolean}
  4691. */
  4692. enablePasteFiltering: false,
  4693. /**
  4694. * If to completely disable pasting into the editor
  4695. * @type {Boolean}
  4696. */
  4697. disablePasting: false,
  4698. /**
  4699. * If the editor is read only.
  4700. * @type {Boolean}
  4701. */
  4702. readOnly: false,
  4703. /**
  4704. * If to set the editor to right-to-left mode.
  4705. *
  4706. * If set to null the direction will be automatically detected.
  4707. * @type {Boolean}
  4708. */
  4709. rtl: false,
  4710. /**
  4711. * If to auto focus the editor on page load
  4712. * @type {Boolean}
  4713. */
  4714. autofocus: false,
  4715. /**
  4716. * If to auto focus the editor to the end of the content
  4717. * @type {Boolean}
  4718. */
  4719. autofocusEnd: true,
  4720. /**
  4721. * If to auto expand the editor to fix the content
  4722. * @type {Boolean}
  4723. */
  4724. autoExpand: false,
  4725. /**
  4726. * If to auto update original textbox on blur
  4727. * @type {Boolean}
  4728. */
  4729. autoUpdate: false,
  4730. /**
  4731. * If to enable the browsers built in spell checker
  4732. * @type {Boolean}
  4733. */
  4734. spellcheck: true,
  4735. /**
  4736. * If to run the source editor when there is no WYSIWYG support. Only really applies to mobile OS's.
  4737. * @type {Boolean}
  4738. */
  4739. runWithoutWysiwygSupport: false,
  4740. /**
  4741. * Optional ID to give the editor.
  4742. * @type {String}
  4743. */
  4744. id: null,
  4745. /**
  4746. * Comma separated list of plugins
  4747. * @type {String}
  4748. */
  4749. plugins: '',
  4750. /**
  4751. * z-index to set the editor container to. Needed for jQuery UI dialog.
  4752. * @type {Int}
  4753. */
  4754. zIndex: null,
  4755. /**
  4756. * If to trim the BBCode. Removes any spaces at the start and end of the BBCode string.
  4757. * @type {Boolean}
  4758. */
  4759. bbcodeTrim: false,
  4760. /**
  4761. * If to disable removing block level elements by pressing backspace at the start of them
  4762. * @type {Boolean}
  4763. */
  4764. disableBlockRemove: false,
  4765. /**
  4766. * BBCode parser options, only applies if using the editor in BBCode mode.
  4767. *
  4768. * See $.sceditor.BBCodeParser.defaults for list of valid options
  4769. * @type {Object}
  4770. */
  4771. parserOptions: { },
  4772. /**
  4773. * CSS that will be added to the to dropdown menu (eg. z-index)
  4774. * @type {Object}
  4775. */
  4776. dropDownCss: { }
  4777. };
  4778. /**
  4779. * Creates an instance of sceditor on all textareas
  4780. * matched by the jQuery selector.
  4781. *
  4782. * If options is set to "state" it will return bool value
  4783. * indicating if the editor has been initilised on the
  4784. * matched textarea(s). If there is only one textarea
  4785. * it will return the bool value for that textarea.
  4786. * If more than one textarea is matched it will
  4787. * return an array of bool values for each textarea.
  4788. *
  4789. * If options is set to "instance" it will return the
  4790. * current editor instance for the textarea(s). Like the
  4791. * state option, if only one textarea is matched this will
  4792. * return just the instance for that textarea. If more than
  4793. * one textarea is matched it will return an array of
  4794. * instances each textarea.
  4795. *
  4796. * @param {Object|String} options Should either be an Object of options or the strings "state" or "instance"
  4797. * @return {this|Array|jQuery.sceditor|Bool}
  4798. */
  4799. $.fn.sceditor = function (options) {
  4800. var $this,
  4801. ret = [];
  4802. options = options || {};
  4803. if(!options.runWithoutWysiwygSupport && !$.sceditor.isWysiwygSupported)
  4804. return;
  4805. this.each(function () {
  4806. $this = this.jquery ? this : $(this);
  4807. // Don't allow the editor to be initilised on it's own source editor
  4808. if($this.parents('.sceditor-container').length > 0)
  4809. return;
  4810. // Add state of instance to ret if that is what options is set to
  4811. if(options === 'state')
  4812. ret.push(!!$this.data('sceditor'));
  4813. else if(options === 'instance')
  4814. ret.push($this.data('sceditor'));
  4815. else if(!$this.data('sceditor'))
  4816. (new $.sceditor(this, options));
  4817. });
  4818. // If nothing in the ret array then must be init so return this
  4819. if(!ret.length)
  4820. return this;
  4821. return ret.length === 1 ? ret[0] : $(ret);
  4822. };
  4823. })(jQuery, window, document);