12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521 |
- /**
- * @license wysihtml5 v0.3.0
- * https://github.com/xing/wysihtml5
- *
- * Author: Christopher Blum (https://github.com/tiff)
- *
- * Copyright (C) 2012 XING AG
- * Licensed under the MIT license (MIT)
- *
- */
- var wysihtml5 = {
- version: "0.3.0",
-
- // namespaces
- commands: {},
- dom: {},
- quirks: {},
- toolbar: {},
- lang: {},
- selection: {},
- views: {},
-
- INVISIBLE_SPACE: "\uFEFF",
-
- EMPTY_FUNCTION: function() {},
-
- ELEMENT_NODE: 1,
- TEXT_NODE: 3,
-
- BACKSPACE_KEY: 8,
- ENTER_KEY: 13,
- ESCAPE_KEY: 27,
- SPACE_KEY: 32,
- DELETE_KEY: 46
- };/**
- * @license Rangy, a cross-browser JavaScript range and selection library
- * http://code.google.com/p/rangy/
- *
- * Copyright 2011, Tim Down
- * Licensed under the MIT license.
- * Version: 1.2.2
- * Build date: 13 November 2011
- */
- window['rangy'] = (function() {
- var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
- var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
- "commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"];
- var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
- "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
- "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
- var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
- // Subset of TextRange's full set of methods that we're interested in
- var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark",
- "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"];
- /*----------------------------------------------------------------------------------------------------------------*/
- // Trio of functions taken from Peter Michaux's article:
- // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
- function isHostMethod(o, p) {
- var t = typeof o[p];
- return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
- }
- function isHostObject(o, p) {
- return !!(typeof o[p] == OBJECT && o[p]);
- }
- function isHostProperty(o, p) {
- return typeof o[p] != UNDEFINED;
- }
- // Creates a convenience function to save verbose repeated calls to tests functions
- function createMultiplePropertyTest(testFunc) {
- return function(o, props) {
- var i = props.length;
- while (i--) {
- if (!testFunc(o, props[i])) {
- return false;
- }
- }
- return true;
- };
- }
- // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
- var areHostMethods = createMultiplePropertyTest(isHostMethod);
- var areHostObjects = createMultiplePropertyTest(isHostObject);
- var areHostProperties = createMultiplePropertyTest(isHostProperty);
- function isTextRange(range) {
- return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
- }
- var api = {
- version: "1.2.2",
- initialized: false,
- supported: true,
- util: {
- isHostMethod: isHostMethod,
- isHostObject: isHostObject,
- isHostProperty: isHostProperty,
- areHostMethods: areHostMethods,
- areHostObjects: areHostObjects,
- areHostProperties: areHostProperties,
- isTextRange: isTextRange
- },
- features: {},
- modules: {},
- config: {
- alertOnWarn: false,
- preferTextRange: false
- }
- };
- function fail(reason) {
- window.alert("Rangy not supported in your browser. Reason: " + reason);
- api.initialized = true;
- api.supported = false;
- }
- api.fail = fail;
- function warn(msg) {
- var warningMessage = "Rangy warning: " + msg;
- if (api.config.alertOnWarn) {
- window.alert(warningMessage);
- } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) {
- window.console.log(warningMessage);
- }
- }
- api.warn = warn;
- if ({}.hasOwnProperty) {
- api.util.extend = function(o, props) {
- for (var i in props) {
- if (props.hasOwnProperty(i)) {
- o[i] = props[i];
- }
- }
- };
- } else {
- fail("hasOwnProperty not supported");
- }
- var initListeners = [];
- var moduleInitializers = [];
- // Initialization
- function init() {
- if (api.initialized) {
- return;
- }
- var testRange;
- var implementsDomRange = false, implementsTextRange = false;
- // First, perform basic feature tests
- if (isHostMethod(document, "createRange")) {
- testRange = document.createRange();
- if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
- implementsDomRange = true;
- }
- testRange.detach();
- }
- var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];
- if (body && isHostMethod(body, "createTextRange")) {
- testRange = body.createTextRange();
- if (isTextRange(testRange)) {
- implementsTextRange = true;
- }
- }
- if (!implementsDomRange && !implementsTextRange) {
- fail("Neither Range nor TextRange are implemented");
- }
- api.initialized = true;
- api.features = {
- implementsDomRange: implementsDomRange,
- implementsTextRange: implementsTextRange
- };
- // Initialize modules and call init listeners
- var allListeners = moduleInitializers.concat(initListeners);
- for (var i = 0, len = allListeners.length; i < len; ++i) {
- try {
- allListeners[i](api);
- } catch (ex) {
- if (isHostObject(window, "console") && isHostMethod(window.console, "log")) {
- window.console.log("Init listener threw an exception. Continuing.", ex);
- }
- }
- }
- }
- // Allow external scripts to initialize this library in case it's loaded after the document has loaded
- api.init = init;
- // Execute listener immediately if already initialized
- api.addInitListener = function(listener) {
- if (api.initialized) {
- listener(api);
- } else {
- initListeners.push(listener);
- }
- };
- var createMissingNativeApiListeners = [];
- api.addCreateMissingNativeApiListener = function(listener) {
- createMissingNativeApiListeners.push(listener);
- };
- function createMissingNativeApi(win) {
- win = win || window;
- init();
- // Notify listeners
- for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) {
- createMissingNativeApiListeners[i](win);
- }
- }
- api.createMissingNativeApi = createMissingNativeApi;
- /**
- * @constructor
- */
- function Module(name) {
- this.name = name;
- this.initialized = false;
- this.supported = false;
- }
- Module.prototype.fail = function(reason) {
- this.initialized = true;
- this.supported = false;
- throw new Error("Module '" + this.name + "' failed to load: " + reason);
- };
- Module.prototype.warn = function(msg) {
- api.warn("Module " + this.name + ": " + msg);
- };
- Module.prototype.createError = function(msg) {
- return new Error("Error in Rangy " + this.name + " module: " + msg);
- };
- api.createModule = function(name, initFunc) {
- var module = new Module(name);
- api.modules[name] = module;
- moduleInitializers.push(function(api) {
- initFunc(api, module);
- module.initialized = true;
- module.supported = true;
- });
- };
- api.requireModules = function(modules) {
- for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) {
- moduleName = modules[i];
- module = api.modules[moduleName];
- if (!module || !(module instanceof Module)) {
- throw new Error("Module '" + moduleName + "' not found");
- }
- if (!module.supported) {
- throw new Error("Module '" + moduleName + "' not supported");
- }
- }
- };
- /*----------------------------------------------------------------------------------------------------------------*/
- // Wait for document to load before running tests
- var docReady = false;
- var loadHandler = function(e) {
- if (!docReady) {
- docReady = true;
- if (!api.initialized) {
- init();
- }
- }
- };
- // Test whether we have window and document objects that we will need
- if (typeof window == UNDEFINED) {
- fail("No window found");
- return;
- }
- if (typeof document == UNDEFINED) {
- fail("No document found");
- return;
- }
- if (isHostMethod(document, "addEventListener")) {
- document.addEventListener("DOMContentLoaded", loadHandler, false);
- }
- // Add a fallback in case the DOMContentLoaded event isn't supported
- if (isHostMethod(window, "addEventListener")) {
- window.addEventListener("load", loadHandler, false);
- } else if (isHostMethod(window, "attachEvent")) {
- window.attachEvent("onload", loadHandler);
- } else {
- fail("Window does not have required addEventListener or attachEvent method");
- }
- return api;
- })();
- rangy.createModule("DomUtil", function(api, module) {
- var UNDEF = "undefined";
- var util = api.util;
- // Perform feature tests
- if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
- module.fail("document missing a Node creation method");
- }
- if (!util.isHostMethod(document, "getElementsByTagName")) {
- module.fail("document missing getElementsByTagName method");
- }
- var el = document.createElement("div");
- if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
- !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
- module.fail("Incomplete Element implementation");
- }
- // innerHTML is required for Range's createContextualFragment method
- if (!util.isHostProperty(el, "innerHTML")) {
- module.fail("Element is missing innerHTML property");
- }
- var textNode = document.createTextNode("test");
- if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
- !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
- !util.areHostProperties(textNode, ["data"]))) {
- module.fail("Incomplete Text Node implementation");
- }
- /*----------------------------------------------------------------------------------------------------------------*/
- // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
- // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
- // contains just the document as a single element and the value searched for is the document.
- var arrayContains = /*Array.prototype.indexOf ?
- function(arr, val) {
- return arr.indexOf(val) > -1;
- }:*/
- function(arr, val) {
- var i = arr.length;
- while (i--) {
- if (arr[i] === val) {
- return true;
- }
- }
- return false;
- };
- // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
- function isHtmlNamespace(node) {
- var ns;
- return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
- }
- function parentElement(node) {
- var parent = node.parentNode;
- return (parent.nodeType == 1) ? parent : null;
- }
- function getNodeIndex(node) {
- var i = 0;
- while( (node = node.previousSibling) ) {
- i++;
- }
- return i;
- }
- function getNodeLength(node) {
- var childNodes;
- return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0);
- }
- function getCommonAncestor(node1, node2) {
- var ancestors = [], n;
- for (n = node1; n; n = n.parentNode) {
- ancestors.push(n);
- }
- for (n = node2; n; n = n.parentNode) {
- if (arrayContains(ancestors, n)) {
- return n;
- }
- }
- return null;
- }
- function isAncestorOf(ancestor, descendant, selfIsAncestor) {
- var n = selfIsAncestor ? descendant : descendant.parentNode;
- while (n) {
- if (n === ancestor) {
- return true;
- } else {
- n = n.parentNode;
- }
- }
- return false;
- }
- function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
- var p, n = selfIsAncestor ? node : node.parentNode;
- while (n) {
- p = n.parentNode;
- if (p === ancestor) {
- return n;
- }
- n = p;
- }
- return null;
- }
- function isCharacterDataNode(node) {
- var t = node.nodeType;
- return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
- }
- function insertAfter(node, precedingNode) {
- var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
- if (nextNode) {
- parent.insertBefore(node, nextNode);
- } else {
- parent.appendChild(node);
- }
- return node;
- }
- // Note that we cannot use splitText() because it is bugridden in IE 9.
- function splitDataNode(node, index) {
- var newNode = node.cloneNode(false);
- newNode.deleteData(0, index);
- node.deleteData(index, node.length - index);
- insertAfter(newNode, node);
- return newNode;
- }
- function getDocument(node) {
- if (node.nodeType == 9) {
- return node;
- } else if (typeof node.ownerDocument != UNDEF) {
- return node.ownerDocument;
- } else if (typeof node.document != UNDEF) {
- return node.document;
- } else if (node.parentNode) {
- return getDocument(node.parentNode);
- } else {
- throw new Error("getDocument: no document found for node");
- }
- }
- function getWindow(node) {
- var doc = getDocument(node);
- if (typeof doc.defaultView != UNDEF) {
- return doc.defaultView;
- } else if (typeof doc.parentWindow != UNDEF) {
- return doc.parentWindow;
- } else {
- throw new Error("Cannot get a window object for node");
- }
- }
- function getIframeDocument(iframeEl) {
- if (typeof iframeEl.contentDocument != UNDEF) {
- return iframeEl.contentDocument;
- } else if (typeof iframeEl.contentWindow != UNDEF) {
- return iframeEl.contentWindow.document;
- } else {
- throw new Error("getIframeWindow: No Document object found for iframe element");
- }
- }
- function getIframeWindow(iframeEl) {
- if (typeof iframeEl.contentWindow != UNDEF) {
- return iframeEl.contentWindow;
- } else if (typeof iframeEl.contentDocument != UNDEF) {
- return iframeEl.contentDocument.defaultView;
- } else {
- throw new Error("getIframeWindow: No Window object found for iframe element");
- }
- }
- function getBody(doc) {
- return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
- }
- function getRootContainer(node) {
- var parent;
- while ( (parent = node.parentNode) ) {
- node = parent;
- }
- return node;
- }
- function comparePoints(nodeA, offsetA, nodeB, offsetB) {
- // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
- var nodeC, root, childA, childB, n;
- if (nodeA == nodeB) {
- // Case 1: nodes are the same
- return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
- } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
- // Case 2: node C (container B or an ancestor) is a child node of A
- return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
- } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
- // Case 3: node C (container A or an ancestor) is a child node of B
- return getNodeIndex(nodeC) < offsetB ? -1 : 1;
- } else {
- // Case 4: containers are siblings or descendants of siblings
- root = getCommonAncestor(nodeA, nodeB);
- childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
- childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
- if (childA === childB) {
- // This shouldn't be possible
- throw new Error("comparePoints got to case 4 and childA and childB are the same!");
- } else {
- n = root.firstChild;
- while (n) {
- if (n === childA) {
- return -1;
- } else if (n === childB) {
- return 1;
- }
- n = n.nextSibling;
- }
- throw new Error("Should not be here!");
- }
- }
- }
- function fragmentFromNodeChildren(node) {
- var fragment = getDocument(node).createDocumentFragment(), child;
- while ( (child = node.firstChild) ) {
- fragment.appendChild(child);
- }
- return fragment;
- }
- function inspectNode(node) {
- if (!node) {
- return "[No node]";
- }
- if (isCharacterDataNode(node)) {
- return '"' + node.data + '"';
- } else if (node.nodeType == 1) {
- var idAttr = node.id ? ' id="' + node.id + '"' : "";
- return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]";
- } else {
- return node.nodeName;
- }
- }
- /**
- * @constructor
- */
- function NodeIterator(root) {
- this.root = root;
- this._next = root;
- }
- NodeIterator.prototype = {
- _current: null,
- hasNext: function() {
- return !!this._next;
- },
- next: function() {
- var n = this._current = this._next;
- var child, next;
- if (this._current) {
- child = n.firstChild;
- if (child) {
- this._next = child;
- } else {
- next = null;
- while ((n !== this.root) && !(next = n.nextSibling)) {
- n = n.parentNode;
- }
- this._next = next;
- }
- }
- return this._current;
- },
- detach: function() {
- this._current = this._next = this.root = null;
- }
- };
- function createIterator(root) {
- return new NodeIterator(root);
- }
- /**
- * @constructor
- */
- function DomPosition(node, offset) {
- this.node = node;
- this.offset = offset;
- }
- DomPosition.prototype = {
- equals: function(pos) {
- return this.node === pos.node & this.offset == pos.offset;
- },
- inspect: function() {
- return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
- }
- };
- /**
- * @constructor
- */
- function DOMException(codeName) {
- this.code = this[codeName];
- this.codeName = codeName;
- this.message = "DOMException: " + this.codeName;
- }
- DOMException.prototype = {
- INDEX_SIZE_ERR: 1,
- HIERARCHY_REQUEST_ERR: 3,
- WRONG_DOCUMENT_ERR: 4,
- NO_MODIFICATION_ALLOWED_ERR: 7,
- NOT_FOUND_ERR: 8,
- NOT_SUPPORTED_ERR: 9,
- INVALID_STATE_ERR: 11
- };
- DOMException.prototype.toString = function() {
- return this.message;
- };
- api.dom = {
- arrayContains: arrayContains,
- isHtmlNamespace: isHtmlNamespace,
- parentElement: parentElement,
- getNodeIndex: getNodeIndex,
- getNodeLength: getNodeLength,
- getCommonAncestor: getCommonAncestor,
- isAncestorOf: isAncestorOf,
- getClosestAncestorIn: getClosestAncestorIn,
- isCharacterDataNode: isCharacterDataNode,
- insertAfter: insertAfter,
- splitDataNode: splitDataNode,
- getDocument: getDocument,
- getWindow: getWindow,
- getIframeWindow: getIframeWindow,
- getIframeDocument: getIframeDocument,
- getBody: getBody,
- getRootContainer: getRootContainer,
- comparePoints: comparePoints,
- inspectNode: inspectNode,
- fragmentFromNodeChildren: fragmentFromNodeChildren,
- createIterator: createIterator,
- DomPosition: DomPosition
- };
- api.DOMException = DOMException;
- });rangy.createModule("DomRange", function(api, module) {
- api.requireModules( ["DomUtil"] );
- var dom = api.dom;
- var DomPosition = dom.DomPosition;
- var DOMException = api.DOMException;
-
- /*----------------------------------------------------------------------------------------------------------------*/
- // Utility functions
- function isNonTextPartiallySelected(node, range) {
- return (node.nodeType != 3) &&
- (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true));
- }
- function getRangeDocument(range) {
- return dom.getDocument(range.startContainer);
- }
- function dispatchEvent(range, type, args) {
- var listeners = range._listeners[type];
- if (listeners) {
- for (var i = 0, len = listeners.length; i < len; ++i) {
- listeners[i].call(range, {target: range, args: args});
- }
- }
- }
- function getBoundaryBeforeNode(node) {
- return new DomPosition(node.parentNode, dom.getNodeIndex(node));
- }
- function getBoundaryAfterNode(node) {
- return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1);
- }
- function insertNodeAtPosition(node, n, o) {
- var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
- if (dom.isCharacterDataNode(n)) {
- if (o == n.length) {
- dom.insertAfter(node, n);
- } else {
- n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o));
- }
- } else if (o >= n.childNodes.length) {
- n.appendChild(node);
- } else {
- n.insertBefore(node, n.childNodes[o]);
- }
- return firstNodeInserted;
- }
- function cloneSubtree(iterator) {
- var partiallySelected;
- for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
- partiallySelected = iterator.isPartiallySelectedSubtree();
- node = node.cloneNode(!partiallySelected);
- if (partiallySelected) {
- subIterator = iterator.getSubtreeIterator();
- node.appendChild(cloneSubtree(subIterator));
- subIterator.detach(true);
- }
- if (node.nodeType == 10) { // DocumentType
- throw new DOMException("HIERARCHY_REQUEST_ERR");
- }
- frag.appendChild(node);
- }
- return frag;
- }
- function iterateSubtree(rangeIterator, func, iteratorState) {
- var it, n;
- iteratorState = iteratorState || { stop: false };
- for (var node, subRangeIterator; node = rangeIterator.next(); ) {
- //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node));
- if (rangeIterator.isPartiallySelectedSubtree()) {
- // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the
- // node selected by the Range.
- if (func(node) === false) {
- iteratorState.stop = true;
- return;
- } else {
- subRangeIterator = rangeIterator.getSubtreeIterator();
- iterateSubtree(subRangeIterator, func, iteratorState);
- subRangeIterator.detach(true);
- if (iteratorState.stop) {
- return;
- }
- }
- } else {
- // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
- // descendant
- it = dom.createIterator(node);
- while ( (n = it.next()) ) {
- if (func(n) === false) {
- iteratorState.stop = true;
- return;
- }
- }
- }
- }
- }
- function deleteSubtree(iterator) {
- var subIterator;
- while (iterator.next()) {
- if (iterator.isPartiallySelectedSubtree()) {
- subIterator = iterator.getSubtreeIterator();
- deleteSubtree(subIterator);
- subIterator.detach(true);
- } else {
- iterator.remove();
- }
- }
- }
- function extractSubtree(iterator) {
- for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
- if (iterator.isPartiallySelectedSubtree()) {
- node = node.cloneNode(false);
- subIterator = iterator.getSubtreeIterator();
- node.appendChild(extractSubtree(subIterator));
- subIterator.detach(true);
- } else {
- iterator.remove();
- }
- if (node.nodeType == 10) { // DocumentType
- throw new DOMException("HIERARCHY_REQUEST_ERR");
- }
- frag.appendChild(node);
- }
- return frag;
- }
- function getNodesInRange(range, nodeTypes, filter) {
- //log.info("getNodesInRange, " + nodeTypes.join(","));
- var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
- var filterExists = !!filter;
- if (filterNodeTypes) {
- regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
- }
- var nodes = [];
- iterateSubtree(new RangeIterator(range, false), function(node) {
- if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) {
- nodes.push(node);
- }
- });
- return nodes;
- }
- function inspect(range) {
- var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
- return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
- dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
- }
- /*----------------------------------------------------------------------------------------------------------------*/
- // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
- /**
- * @constructor
- */
- function RangeIterator(range, clonePartiallySelectedTextNodes) {
- this.range = range;
- this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
- if (!range.collapsed) {
- this.sc = range.startContainer;
- this.so = range.startOffset;
- this.ec = range.endContainer;
- this.eo = range.endOffset;
- var root = range.commonAncestorContainer;
- if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) {
- this.isSingleCharacterDataNode = true;
- this._first = this._last = this._next = this.sc;
- } else {
- this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ?
- this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true);
- this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ?
- this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true);
- }
- }
- }
- RangeIterator.prototype = {
- _current: null,
- _next: null,
- _first: null,
- _last: null,
- isSingleCharacterDataNode: false,
- reset: function() {
- this._current = null;
- this._next = this._first;
- },
- hasNext: function() {
- return !!this._next;
- },
- next: function() {
- // Move to next node
- var current = this._current = this._next;
- if (current) {
- this._next = (current !== this._last) ? current.nextSibling : null;
- // Check for partially selected text nodes
- if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
- if (current === this.ec) {
- (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
- }
- if (this._current === this.sc) {
- (current = current.cloneNode(true)).deleteData(0, this.so);
- }
- }
- }
- return current;
- },
- remove: function() {
- var current = this._current, start, end;
- if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
- start = (current === this.sc) ? this.so : 0;
- end = (current === this.ec) ? this.eo : current.length;
- if (start != end) {
- current.deleteData(start, end - start);
- }
- } else {
- if (current.parentNode) {
- current.parentNode.removeChild(current);
- } else {
- }
- }
- },
- // Checks if the current node is partially selected
- isPartiallySelectedSubtree: function() {
- var current = this._current;
- return isNonTextPartiallySelected(current, this.range);
- },
- getSubtreeIterator: function() {
- var subRange;
- if (this.isSingleCharacterDataNode) {
- subRange = this.range.cloneRange();
- subRange.collapse();
- } else {
- subRange = new Range(getRangeDocument(this.range));
- var current = this._current;
- var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current);
- if (dom.isAncestorOf(current, this.sc, true)) {
- startContainer = this.sc;
- startOffset = this.so;
- }
- if (dom.isAncestorOf(current, this.ec, true)) {
- endContainer = this.ec;
- endOffset = this.eo;
- }
- updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
- }
- return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
- },
- detach: function(detachRange) {
- if (detachRange) {
- this.range.detach();
- }
- this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
- }
- };
- /*----------------------------------------------------------------------------------------------------------------*/
- // Exceptions
- /**
- * @constructor
- */
- function RangeException(codeName) {
- this.code = this[codeName];
- this.codeName = codeName;
- this.message = "RangeException: " + this.codeName;
- }
- RangeException.prototype = {
- BAD_BOUNDARYPOINTS_ERR: 1,
- INVALID_NODE_TYPE_ERR: 2
- };
- RangeException.prototype.toString = function() {
- return this.message;
- };
- /*----------------------------------------------------------------------------------------------------------------*/
- /**
- * Currently iterates through all nodes in the range on creation until I think of a decent way to do it
- * TODO: Look into making this a proper iterator, not requiring preloading everything first
- * @constructor
- */
- function RangeNodeIterator(range, nodeTypes, filter) {
- this.nodes = getNodesInRange(range, nodeTypes, filter);
- this._next = this.nodes[0];
- this._position = 0;
- }
- RangeNodeIterator.prototype = {
- _current: null,
- hasNext: function() {
- return !!this._next;
- },
- next: function() {
- this._current = this._next;
- this._next = this.nodes[ ++this._position ];
- return this._current;
- },
- detach: function() {
- this._current = this._next = this.nodes = null;
- }
- };
- var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
- var rootContainerNodeTypes = [2, 9, 11];
- var readonlyNodeTypes = [5, 6, 10, 12];
- var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
- var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
- function createAncestorFinder(nodeTypes) {
- return function(node, selfIsAncestor) {
- var t, n = selfIsAncestor ? node : node.parentNode;
- while (n) {
- t = n.nodeType;
- if (dom.arrayContains(nodeTypes, t)) {
- return n;
- }
- n = n.parentNode;
- }
- return null;
- };
- }
- var getRootContainer = dom.getRootContainer;
- var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
- var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
- var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
- function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
- if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
- throw new RangeException("INVALID_NODE_TYPE_ERR");
- }
- }
- function assertNotDetached(range) {
- if (!range.startContainer) {
- throw new DOMException("INVALID_STATE_ERR");
- }
- }
- function assertValidNodeType(node, invalidTypes) {
- if (!dom.arrayContains(invalidTypes, node.nodeType)) {
- throw new RangeException("INVALID_NODE_TYPE_ERR");
- }
- }
- function assertValidOffset(node, offset) {
- if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
- throw new DOMException("INDEX_SIZE_ERR");
- }
- }
- function assertSameDocumentOrFragment(node1, node2) {
- if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
- throw new DOMException("WRONG_DOCUMENT_ERR");
- }
- }
- function assertNodeNotReadOnly(node) {
- if (getReadonlyAncestor(node, true)) {
- throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
- }
- }
- function assertNode(node, codeName) {
- if (!node) {
- throw new DOMException(codeName);
- }
- }
- function isOrphan(node) {
- return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true);
- }
- function isValidOffset(node, offset) {
- return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length);
- }
- function assertRangeValid(range) {
- assertNotDetached(range);
- if (isOrphan(range.startContainer) || isOrphan(range.endContainer) ||
- !isValidOffset(range.startContainer, range.startOffset) ||
- !isValidOffset(range.endContainer, range.endOffset)) {
- throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");
- }
- }
- /*----------------------------------------------------------------------------------------------------------------*/
- // Test the browser's innerHTML support to decide how to implement createContextualFragment
- var styleEl = document.createElement("style");
- var htmlParsingConforms = false;
- try {
- styleEl.innerHTML = "<b>x</b>";
- htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
- } catch (e) {
- // IE 6 and 7 throw
- }
- api.features.htmlParsingConforms = htmlParsingConforms;
- var createContextualFragment = htmlParsingConforms ?
- // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
- // discussion and base code for this implementation at issue 67.
- // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
- // Thanks to Aleks Williams.
- function(fragmentStr) {
- // "Let node the context object's start's node."
- var node = this.startContainer;
- var doc = dom.getDocument(node);
- // "If the context object's start's node is null, raise an INVALID_STATE_ERR
- // exception and abort these steps."
- if (!node) {
- throw new DOMException("INVALID_STATE_ERR");
- }
- // "Let element be as follows, depending on node's interface:"
- // Document, Document Fragment: null
- var el = null;
- // "Element: node"
- if (node.nodeType == 1) {
- el = node;
- // "Text, Comment: node's parentElement"
- } else if (dom.isCharacterDataNode(node)) {
- el = dom.parentElement(node);
- }
- // "If either element is null or element's ownerDocument is an HTML document
- // and element's local name is "html" and element's namespace is the HTML
- // namespace"
- if (el === null || (
- el.nodeName == "HTML"
- && dom.isHtmlNamespace(dom.getDocument(el).documentElement)
- && dom.isHtmlNamespace(el)
- )) {
- // "let element be a new Element with "body" as its local name and the HTML
- // namespace as its namespace.""
- el = doc.createElement("body");
- } else {
- el = el.cloneNode(false);
- }
- // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
- // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
- // "In either case, the algorithm must be invoked with fragment as the input
- // and element as the context element."
- el.innerHTML = fragmentStr;
- // "If this raises an exception, then abort these steps. Otherwise, let new
- // children be the nodes returned."
- // "Let fragment be a new DocumentFragment."
- // "Append all new children to fragment."
- // "Return fragment."
- return dom.fragmentFromNodeChildren(el);
- } :
- // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
- // previous versions of Rangy used (with the exception of using a body element rather than a div)
- function(fragmentStr) {
- assertNotDetached(this);
- var doc = getRangeDocument(this);
- var el = doc.createElement("body");
- el.innerHTML = fragmentStr;
- return dom.fragmentFromNodeChildren(el);
- };
- /*----------------------------------------------------------------------------------------------------------------*/
- var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
- "commonAncestorContainer"];
- var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
- var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
- function RangePrototype() {}
- RangePrototype.prototype = {
- attachListener: function(type, listener) {
- this._listeners[type].push(listener);
- },
- compareBoundaryPoints: function(how, range) {
- assertRangeValid(this);
- assertSameDocumentOrFragment(this.startContainer, range.startContainer);
- var nodeA, offsetA, nodeB, offsetB;
- var prefixA = (how == e2s || how == s2s) ? "start" : "end";
- var prefixB = (how == s2e || how == s2s) ? "start" : "end";
- nodeA = this[prefixA + "Container"];
- offsetA = this[prefixA + "Offset"];
- nodeB = range[prefixB + "Container"];
- offsetB = range[prefixB + "Offset"];
- return dom.comparePoints(nodeA, offsetA, nodeB, offsetB);
- },
- insertNode: function(node) {
- assertRangeValid(this);
- assertValidNodeType(node, insertableNodeTypes);
- assertNodeNotReadOnly(this.startContainer);
- if (dom.isAncestorOf(node, this.startContainer, true)) {
- throw new DOMException("HIERARCHY_REQUEST_ERR");
- }
- // No check for whether the container of the start of the Range is of a type that does not allow
- // children of the type of node: the browser's DOM implementation should do this for us when we attempt
- // to add the node
- var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
- this.setStartBefore(firstNodeInserted);
- },
- cloneContents: function() {
- assertRangeValid(this);
- var clone, frag;
- if (this.collapsed) {
- return getRangeDocument(this).createDocumentFragment();
- } else {
- if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) {
- clone = this.startContainer.cloneNode(true);
- clone.data = clone.data.slice(this.startOffset, this.endOffset);
- frag = getRangeDocument(this).createDocumentFragment();
- frag.appendChild(clone);
- return frag;
- } else {
- var iterator = new RangeIterator(this, true);
- clone = cloneSubtree(iterator);
- iterator.detach();
- }
- return clone;
- }
- },
- canSurroundContents: function() {
- assertRangeValid(this);
- assertNodeNotReadOnly(this.startContainer);
- assertNodeNotReadOnly(this.endContainer);
- // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
- // no non-text nodes.
- var iterator = new RangeIterator(this, true);
- var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
- (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
- iterator.detach();
- return !boundariesInvalid;
- },
- surroundContents: function(node) {
- assertValidNodeType(node, surroundNodeTypes);
- if (!this.canSurroundContents()) {
- throw new RangeException("BAD_BOUNDARYPOINTS_ERR");
- }
- // Extract the contents
- var content = this.extractContents();
- // Clear the children of the node
- if (node.hasChildNodes()) {
- while (node.lastChild) {
- node.removeChild(node.lastChild);
- }
- }
- // Insert the new node and add the extracted contents
- insertNodeAtPosition(node, this.startContainer, this.startOffset);
- node.appendChild(content);
- this.selectNode(node);
- },
- cloneRange: function() {
- assertRangeValid(this);
- var range = new Range(getRangeDocument(this));
- var i = rangeProperties.length, prop;
- while (i--) {
- prop = rangeProperties[i];
- range[prop] = this[prop];
- }
- return range;
- },
- toString: function() {
- assertRangeValid(this);
- var sc = this.startContainer;
- if (sc === this.endContainer && dom.isCharacterDataNode(sc)) {
- return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
- } else {
- var textBits = [], iterator = new RangeIterator(this, true);
- iterateSubtree(iterator, function(node) {
- // Accept only text or CDATA nodes, not comments
- if (node.nodeType == 3 || node.nodeType == 4) {
- textBits.push(node.data);
- }
- });
- iterator.detach();
- return textBits.join("");
- }
- },
- // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
- // been removed from Mozilla.
- compareNode: function(node) {
- assertRangeValid(this);
- var parent = node.parentNode;
- var nodeIndex = dom.getNodeIndex(node);
- if (!parent) {
- throw new DOMException("NOT_FOUND_ERR");
- }
- var startComparison = this.comparePoint(parent, nodeIndex),
- endComparison = this.comparePoint(parent, nodeIndex + 1);
- if (startComparison < 0) { // Node starts before
- return (endComparison > 0) ? n_b_a : n_b;
- } else {
- return (endComparison > 0) ? n_a : n_i;
- }
- },
- comparePoint: function(node, offset) {
- assertRangeValid(this);
- assertNode(node, "HIERARCHY_REQUEST_ERR");
- assertSameDocumentOrFragment(node, this.startContainer);
- if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
- return -1;
- } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
- return 1;
- }
- return 0;
- },
- createContextualFragment: createContextualFragment,
- toHtml: function() {
- assertRangeValid(this);
- var container = getRangeDocument(this).createElement("div");
- container.appendChild(this.cloneContents());
- return container.innerHTML;
- },
- // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
- // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
- intersectsNode: function(node, touchingIsIntersecting) {
- assertRangeValid(this);
- assertNode(node, "NOT_FOUND_ERR");
- if (dom.getDocument(node) !== getRangeDocument(this)) {
- return false;
- }
- var parent = node.parentNode, offset = dom.getNodeIndex(node);
- assertNode(parent, "NOT_FOUND_ERR");
- var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset),
- endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
- return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
- },
- isPointInRange: function(node, offset) {
- assertRangeValid(this);
- assertNode(node, "HIERARCHY_REQUEST_ERR");
- assertSameDocumentOrFragment(node, this.startContainer);
- return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
- (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
- },
- // The methods below are non-standard and invented by me.
- // Sharing a boundary start-to-end or end-to-start does not count as intersection.
- intersectsRange: function(range, touchingIsIntersecting) {
- assertRangeValid(this);
- if (getRangeDocument(range) != getRangeDocument(this)) {
- throw new DOMException("WRONG_DOCUMENT_ERR");
- }
- var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset),
- endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset);
- return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
- },
- intersection: function(range) {
- if (this.intersectsRange(range)) {
- var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
- endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
- var intersectionRange = this.cloneRange();
- if (startComparison == -1) {
- intersectionRange.setStart(range.startContainer, range.startOffset);
- }
- if (endComparison == 1) {
- intersectionRange.setEnd(range.endContainer, range.endOffset);
- }
- return intersectionRange;
- }
- return null;
- },
- union: function(range) {
- if (this.intersectsRange(range, true)) {
- var unionRange = this.cloneRange();
- if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
- unionRange.setStart(range.startContainer, range.startOffset);
- }
- if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
- unionRange.setEnd(range.endContainer, range.endOffset);
- }
- return unionRange;
- } else {
- throw new RangeException("Ranges do not intersect");
- }
- },
- containsNode: function(node, allowPartial) {
- if (allowPartial) {
- return this.intersectsNode(node, false);
- } else {
- return this.compareNode(node) == n_i;
- }
- },
- containsNodeContents: function(node) {
- return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0;
- },
- containsRange: function(range) {
- return this.intersection(range).equals(range);
- },
- containsNodeText: function(node) {
- var nodeRange = this.cloneRange();
- nodeRange.selectNode(node);
- var textNodes = nodeRange.getNodes([3]);
- if (textNodes.length > 0) {
- nodeRange.setStart(textNodes[0], 0);
- var lastTextNode = textNodes.pop();
- nodeRange.setEnd(lastTextNode, lastTextNode.length);
- var contains = this.containsRange(nodeRange);
- nodeRange.detach();
- return contains;
- } else {
- return this.containsNodeContents(node);
- }
- },
- createNodeIterator: function(nodeTypes, filter) {
- assertRangeValid(this);
- return new RangeNodeIterator(this, nodeTypes, filter);
- },
- getNodes: function(nodeTypes, filter) {
- assertRangeValid(this);
- return getNodesInRange(this, nodeTypes, filter);
- },
- getDocument: function() {
- return getRangeDocument(this);
- },
- collapseBefore: function(node) {
- assertNotDetached(this);
- this.setEndBefore(node);
- this.collapse(false);
- },
- collapseAfter: function(node) {
- assertNotDetached(this);
- this.setStartAfter(node);
- this.collapse(true);
- },
- getName: function() {
- return "DomRange";
- },
- equals: function(range) {
- return Range.rangesEqual(this, range);
- },
- inspect: function() {
- return inspect(this);
- }
- };
- function copyComparisonConstantsToObject(obj) {
- obj.START_TO_START = s2s;
- obj.START_TO_END = s2e;
- obj.END_TO_END = e2e;
- obj.END_TO_START = e2s;
- obj.NODE_BEFORE = n_b;
- obj.NODE_AFTER = n_a;
- obj.NODE_BEFORE_AND_AFTER = n_b_a;
- obj.NODE_INSIDE = n_i;
- }
- function copyComparisonConstants(constructor) {
- copyComparisonConstantsToObject(constructor);
- copyComparisonConstantsToObject(constructor.prototype);
- }
- function createRangeContentRemover(remover, boundaryUpdater) {
- return function() {
- assertRangeValid(this);
- var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
- var iterator = new RangeIterator(this, true);
- // Work out where to position the range after content removal
- var node, boundary;
- if (sc !== root) {
- node = dom.getClosestAncestorIn(sc, root, true);
- boundary = getBoundaryAfterNode(node);
- sc = boundary.node;
- so = boundary.offset;
- }
- // Check none of the range is read-only
- iterateSubtree(iterator, assertNodeNotReadOnly);
- iterator.reset();
- // Remove the content
- var returnValue = remover(iterator);
- iterator.detach();
- // Move to the new position
- boundaryUpdater(this, sc, so, sc, so);
- return returnValue;
- };
- }
- function createPrototypeRange(constructor, boundaryUpdater, detacher) {
- function createBeforeAfterNodeSetter(isBefore, isStart) {
- return function(node) {
- assertNotDetached(this);
- assertValidNodeType(node, beforeAfterNodeTypes);
- assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
- var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
- (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
- };
- }
- function setRangeStart(range, node, offset) {
- var ec = range.endContainer, eo = range.endOffset;
- if (node !== range.startContainer || offset !== range.startOffset) {
- // Check the root containers of the range and the new boundary, and also check whether the new boundary
- // is after the current end. In either case, collapse the range to the new position
- if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) {
- ec = node;
- eo = offset;
- }
- boundaryUpdater(range, node, offset, ec, eo);
- }
- }
- function setRangeEnd(range, node, offset) {
- var sc = range.startContainer, so = range.startOffset;
- if (node !== range.endContainer || offset !== range.endOffset) {
- // Check the root containers of the range and the new boundary, and also check whether the new boundary
- // is after the current end. In either case, collapse the range to the new position
- if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) {
- sc = node;
- so = offset;
- }
- boundaryUpdater(range, sc, so, node, offset);
- }
- }
- function setRangeStartAndEnd(range, node, offset) {
- if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) {
- boundaryUpdater(range, node, offset, node, offset);
- }
- }
- constructor.prototype = new RangePrototype();
- api.util.extend(constructor.prototype, {
- setStart: function(node, offset) {
- assertNotDetached(this);
- assertNoDocTypeNotationEntityAncestor(node, true);
- assertValidOffset(node, offset);
- setRangeStart(this, node, offset);
- },
- setEnd: function(node, offset) {
- assertNotDetached(this);
- assertNoDocTypeNotationEntityAncestor(node, true);
- assertValidOffset(node, offset);
- setRangeEnd(this, node, offset);
- },
- setStartBefore: createBeforeAfterNodeSetter(true, true),
- setStartAfter: createBeforeAfterNodeSetter(false, true),
- setEndBefore: createBeforeAfterNodeSetter(true, false),
- setEndAfter: createBeforeAfterNodeSetter(false, false),
- collapse: function(isStart) {
- assertRangeValid(this);
- if (isStart) {
- boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
- } else {
- boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
- }
- },
- selectNodeContents: function(node) {
- // This doesn't seem well specified: the spec talks only about selecting the node's contents, which
- // could be taken to mean only its children. However, browsers implement this the same as selectNode for
- // text nodes, so I shall do likewise
- assertNotDetached(this);
- assertNoDocTypeNotationEntityAncestor(node, true);
- boundaryUpdater(this, node, 0, node, dom.getNodeLength(node));
- },
- selectNode: function(node) {
- assertNotDetached(this);
- assertNoDocTypeNotationEntityAncestor(node, false);
- assertValidNodeType(node, beforeAfterNodeTypes);
- var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
- boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
- },
- extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
- deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
- canSurroundContents: function() {
- assertRangeValid(this);
- assertNodeNotReadOnly(this.startContainer);
- assertNodeNotReadOnly(this.endContainer);
- // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
- // no non-text nodes.
- var iterator = new RangeIterator(this, true);
- var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
- (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
- iterator.detach();
- return !boundariesInvalid;
- },
- detach: function() {
- detacher(this);
- },
- splitBoundaries: function() {
- assertRangeValid(this);
- var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
- var startEndSame = (sc === ec);
- if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
- dom.splitDataNode(ec, eo);
- }
- if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) {
- sc = dom.splitDataNode(sc, so);
- if (startEndSame) {
- eo -= so;
- ec = sc;
- } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) {
- eo++;
- }
- so = 0;
- }
- boundaryUpdater(this, sc, so, ec, eo);
- },
- normalizeBoundaries: function() {
- assertRangeValid(this);
- var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
- var mergeForward = function(node) {
- var sibling = node.nextSibling;
- if (sibling && sibling.nodeType == node.nodeType) {
- ec = node;
- eo = node.length;
- node.appendData(sibling.data);
- sibling.parentNode.removeChild(sibling);
- }
- };
- var mergeBackward = function(node) {
- var sibling = node.previousSibling;
- if (sibling && sibling.nodeType == node.nodeType) {
- sc = node;
- var nodeLength = node.length;
- so = sibling.length;
- node.insertData(0, sibling.data);
- sibling.parentNode.removeChild(sibling);
- if (sc == ec) {
- eo += so;
- ec = sc;
- } else if (ec == node.parentNode) {
- var nodeIndex = dom.getNodeIndex(node);
- if (eo == nodeIndex) {
- ec = node;
- eo = nodeLength;
- } else if (eo > nodeIndex) {
- eo--;
- }
- }
- }
- };
- var normalizeStart = true;
- if (dom.isCharacterDataNode(ec)) {
- if (ec.length == eo) {
- mergeForward(ec);
- }
- } else {
- if (eo > 0) {
- var endNode = ec.childNodes[eo - 1];
- if (endNode && dom.isCharacterDataNode(endNode)) {
- mergeForward(endNode);
- }
- }
- normalizeStart = !this.collapsed;
- }
- if (normalizeStart) {
- if (dom.isCharacterDataNode(sc)) {
- if (so == 0) {
- mergeBackward(sc);
- }
- } else {
- if (so < sc.childNodes.length) {
- var startNode = sc.childNodes[so];
- if (startNode && dom.isCharacterDataNode(startNode)) {
- mergeBackward(startNode);
- }
- }
- }
- } else {
- sc = ec;
- so = eo;
- }
- boundaryUpdater(this, sc, so, ec, eo);
- },
- collapseToPoint: function(node, offset) {
- assertNotDetached(this);
- assertNoDocTypeNotationEntityAncestor(node, true);
- assertValidOffset(node, offset);
- setRangeStartAndEnd(this, node, offset);
- }
- });
- copyComparisonConstants(constructor);
- }
- /*----------------------------------------------------------------------------------------------------------------*/
- // Updates commonAncestorContainer and collapsed after boundary change
- function updateCollapsedAndCommonAncestor(range) {
- range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
- range.commonAncestorContainer = range.collapsed ?
- range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
- }
- function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
- var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset);
- var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset);
- range.startContainer = startContainer;
- range.startOffset = startOffset;
- range.endContainer = endContainer;
- range.endOffset = endOffset;
- updateCollapsedAndCommonAncestor(range);
- dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved});
- }
- function detach(range) {
- assertNotDetached(range);
- range.startContainer = range.startOffset = range.endContainer = range.endOffset = null;
- range.collapsed = range.commonAncestorContainer = null;
- dispatchEvent(range, "detach", null);
- range._listeners = null;
- }
- /**
- * @constructor
- */
- function Range(doc) {
- this.startContainer = doc;
- this.startOffset = 0;
- this.endContainer = doc;
- this.endOffset = 0;
- this._listeners = {
- boundarychange: [],
- detach: []
- };
- updateCollapsedAndCommonAncestor(this);
- }
- createPrototypeRange(Range, updateBoundaries, detach);
- api.rangePrototype = RangePrototype.prototype;
- Range.rangeProperties = rangeProperties;
- Range.RangeIterator = RangeIterator;
- Range.copyComparisonConstants = copyComparisonConstants;
- Range.createPrototypeRange = createPrototypeRange;
- Range.inspect = inspect;
- Range.getRangeDocument = getRangeDocument;
- Range.rangesEqual = function(r1, r2) {
- return r1.startContainer === r2.startContainer &&
- r1.startOffset === r2.startOffset &&
- r1.endContainer === r2.endContainer &&
- r1.endOffset === r2.endOffset;
- };
- api.DomRange = Range;
- api.RangeException = RangeException;
- });rangy.createModule("WrappedRange", function(api, module) {
- api.requireModules( ["DomUtil", "DomRange"] );
- /**
- * @constructor
- */
- var WrappedRange;
- var dom = api.dom;
- var DomPosition = dom.DomPosition;
- var DomRange = api.DomRange;
- /*----------------------------------------------------------------------------------------------------------------*/
- /*
- This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
- method. For example, in the following (where pipes denote the selection boundaries):
- <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
- var range = document.selection.createRange();
- alert(range.parentElement().id); // Should alert "ul" but alerts "b"
- This method returns the common ancestor node of the following:
- - the parentElement() of the textRange
- - the parentElement() of the textRange after calling collapse(true)
- - the parentElement() of the textRange after calling collapse(false)
- */
- function getTextRangeContainerElement(textRange) {
- var parentEl = textRange.parentElement();
- var range = textRange.duplicate();
- range.collapse(true);
- var startEl = range.parentElement();
- range = textRange.duplicate();
- range.collapse(false);
- var endEl = range.parentElement();
- var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
- return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
- }
- function textRangeIsCollapsed(textRange) {
- return textRange.compareEndPoints("StartToEnd", textRange) == 0;
- }
- // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as
- // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has
- // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling
- // for inputs and images, plus optimizations.
- function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) {
- var workingRange = textRange.duplicate();
- workingRange.collapse(isStart);
- var containerElement = workingRange.parentElement();
- // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
- // check for that
- // TODO: Find out when. Workaround for wholeRangeContainerElement may break this
- if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) {
- containerElement = wholeRangeContainerElement;
- }
- // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
- // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
- if (!containerElement.canHaveHTML) {
- return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
- }
- var workingNode = dom.getDocument(containerElement).createElement("span");
- var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
- var previousNode, nextNode, boundaryPosition, boundaryNode;
- // Move the working range through the container's children, starting at the end and working backwards, until the
- // working range reaches or goes past the boundary we're interested in
- do {
- containerElement.insertBefore(workingNode, workingNode.previousSibling);
- workingRange.moveToElementText(workingNode);
- } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 &&
- workingNode.previousSibling);
- // We've now reached or gone past the boundary of the text range we're interested in
- // so have identified the node we want
- boundaryNode = workingNode.nextSibling;
- if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) {
- // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the
- // node containing the text range's boundary, so we move the end of the working range to the boundary point
- // and measure the length of its text to get the boundary's offset within the node.
- workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
- var offset;
- if (/[\r\n]/.test(boundaryNode.data)) {
- /*
- For the particular case of a boundary within a text node containing line breaks (within a <pre> element,
- for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts:
- - Each line break is represented as \r in the text node's data/nodeValue properties
- - Each line break is represented as \r\n in the TextRange's 'text' property
- - The 'text' property of the TextRange does not contain trailing line breaks
- To get round the problem presented by the final fact above, we can use the fact that TextRange's
- moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily
- the same as the number of characters it was instructed to move. The simplest approach is to use this to
- store the characters moved when moving both the start and end of the range to the start of the document
- body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).
- However, this is extremely slow when the document is large and the range is near the end of it. Clearly
- doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same
- problem.
- Another approach that works is to use moveStart() to move the start boundary of the range up to the end
- boundary one character at a time and incrementing a counter with the value returned by the moveStart()
- call. However, the check for whether the start boundary has reached the end boundary is expensive, so
- this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of
- the range within the document).
- The method below is a hybrid of the two methods above. It uses the fact that a string containing the
- TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the
- text of the TextRange, so the start of the range is moved that length initially and then a character at
- a time to make up for any trailing line breaks not contained in the 'text' property. This has good
- performance in most situations compared to the previous two methods.
- */
- var tempRange = workingRange.duplicate();
- var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
- offset = tempRange.moveStart("character", rangeLength);
- while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
- offset++;
- tempRange.moveStart("character", 1);
- }
- } else {
- offset = workingRange.text.length;
- }
- boundaryPosition = new DomPosition(boundaryNode, offset);
- } else {
- // If the boundary immediately follows a character data node and this is the end boundary, we should favour
- // a position within that, and likewise for a start boundary preceding a character data node
- previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
- nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
- if (nextNode && dom.isCharacterDataNode(nextNode)) {
- boundaryPosition = new DomPosition(nextNode, 0);
- } else if (previousNode && dom.isCharacterDataNode(previousNode)) {
- boundaryPosition = new DomPosition(previousNode, previousNode.length);
- } else {
- boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
- }
- }
- // Clean up
- workingNode.parentNode.removeChild(workingNode);
- return boundaryPosition;
- }
- // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.
- // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
- // (http://code.google.com/p/ierange/)
- function createBoundaryTextRange(boundaryPosition, isStart) {
- var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
- var doc = dom.getDocument(boundaryPosition.node);
- var workingNode, childNodes, workingRange = doc.body.createTextRange();
- var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node);
- if (nodeIsDataNode) {
- boundaryNode = boundaryPosition.node;
- boundaryParent = boundaryNode.parentNode;
- } else {
- childNodes = boundaryPosition.node.childNodes;
- boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
- boundaryParent = boundaryPosition.node;
- }
- // Position the range immediately before the node containing the boundary
- workingNode = doc.createElement("span");
- // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the
- // element rather than immediately before or after it, which is what we want
- workingNode.innerHTML = "&#feff;";
- // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
- // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
- if (boundaryNode) {
- boundaryParent.insertBefore(workingNode, boundaryNode);
- } else {
- boundaryParent.appendChild(workingNode);
- }
- workingRange.moveToElementText(workingNode);
- workingRange.collapse(!isStart);
- // Clean up
- boundaryParent.removeChild(workingNode);
- // Move the working range to the text offset, if required
- if (nodeIsDataNode) {
- workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
- }
- return workingRange;
- }
- /*----------------------------------------------------------------------------------------------------------------*/
- if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {
- // This is a wrapper around the browser's native DOM Range. It has two aims:
- // - Provide workarounds for specific browser bugs
- // - provide convenient extensions, which are inherited from Rangy's DomRange
- (function() {
- var rangeProto;
- var rangeProperties = DomRange.rangeProperties;
- var canSetRangeStartAfterEnd;
- function updateRangeProperties(range) {
- var i = rangeProperties.length, prop;
- while (i--) {
- prop = rangeProperties[i];
- range[prop] = range.nativeRange[prop];
- }
- }
- function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {
- var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
- var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
- // Always set both boundaries for the benefit of IE9 (see issue 35)
- if (startMoved || endMoved) {
- range.setEnd(endContainer, endOffset);
- range.setStart(startContainer, startOffset);
- }
- }
- function detach(range) {
- range.nativeRange.detach();
- range.detached = true;
- var i = rangeProperties.length, prop;
- while (i--) {
- prop = rangeProperties[i];
- range[prop] = null;
- }
- }
- var createBeforeAfterNodeSetter;
- WrappedRange = function(range) {
- if (!range) {
- throw new Error("Range must be specified");
- }
- this.nativeRange = range;
- updateRangeProperties(this);
- };
- DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);
- rangeProto = WrappedRange.prototype;
- rangeProto.selectNode = function(node) {
- this.nativeRange.selectNode(node);
- updateRangeProperties(this);
- };
- rangeProto.deleteContents = function() {
- this.nativeRange.deleteContents();
- updateRangeProperties(this);
- };
- rangeProto.extractContents = function() {
- var frag = this.nativeRange.extractContents();
- updateRangeProperties(this);
- return frag;
- };
- rangeProto.cloneContents = function() {
- return this.nativeRange.cloneContents();
- };
- // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still
- // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for
- // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of
- // insertNode, which works but is almost certainly slower than the native implementation.
- /*
- rangeProto.insertNode = function(node) {
- this.nativeRange.insertNode(node);
- updateRangeProperties(this);
- };
- */
- rangeProto.surroundContents = function(node) {
- this.nativeRange.surroundContents(node);
- updateRangeProperties(this);
- };
- rangeProto.collapse = function(isStart) {
- this.nativeRange.collapse(isStart);
- updateRangeProperties(this);
- };
- rangeProto.cloneRange = function() {
- return new WrappedRange(this.nativeRange.cloneRange());
- };
- rangeProto.refresh = function() {
- updateRangeProperties(this);
- };
- rangeProto.toString = function() {
- return this.nativeRange.toString();
- };
- // Create test range and node for feature detection
- var testTextNode = document.createTextNode("test");
- dom.getBody(document).appendChild(testTextNode);
- var range = document.createRange();
- /*--------------------------------------------------------------------------------------------------------*/
- // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
- // correct for it
- range.setStart(testTextNode, 0);
- range.setEnd(testTextNode, 0);
- try {
- range.setStart(testTextNode, 1);
- canSetRangeStartAfterEnd = true;
- rangeProto.setStart = function(node, offset) {
- this.nativeRange.setStart(node, offset);
- updateRangeProperties(this);
- };
- rangeProto.setEnd = function(node, offset) {
- this.nativeRange.setEnd(node, offset);
- updateRangeProperties(this);
- };
- createBeforeAfterNodeSetter = function(name) {
- return function(node) {
- this.nativeRange[name](node);
- updateRangeProperties(this);
- };
- };
- } catch(ex) {
- canSetRangeStartAfterEnd = false;
- rangeProto.setStart = function(node, offset) {
- try {
- this.nativeRange.setStart(node, offset);
- } catch (ex) {
- this.nativeRange.setEnd(node, offset);
- this.nativeRange.setStart(node, offset);
- }
- updateRangeProperties(this);
- };
- rangeProto.setEnd = function(node, offset) {
- try {
- this.nativeRange.setEnd(node, offset);
- } catch (ex) {
- this.nativeRange.setStart(node, offset);
- this.nativeRange.setEnd(node, offset);
- }
- updateRangeProperties(this);
- };
- createBeforeAfterNodeSetter = function(name, oppositeName) {
- return function(node) {
- try {
- this.nativeRange[name](node);
- } catch (ex) {
- this.nativeRange[oppositeName](node);
- this.nativeRange[name](node);
- }
- updateRangeProperties(this);
- };
- };
- }
- rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
- rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
- rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
- rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
- /*--------------------------------------------------------------------------------------------------------*/
- // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to
- // the 0th character of the text node
- range.selectNodeContents(testTextNode);
- if (range.startContainer == testTextNode && range.endContainer == testTextNode &&
- range.startOffset == 0 && range.endOffset == testTextNode.length) {
- rangeProto.selectNodeContents = function(node) {
- this.nativeRange.selectNodeContents(node);
- updateRangeProperties(this);
- };
- } else {
- rangeProto.selectNodeContents = function(node) {
- this.setStart(node, 0);
- this.setEnd(node, DomRange.getEndOffset(node));
- };
- }
- /*--------------------------------------------------------------------------------------------------------*/
- // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants
- // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
- range.selectNodeContents(testTextNode);
- range.setEnd(testTextNode, 3);
- var range2 = document.createRange();
- range2.selectNodeContents(testTextNode);
- range2.setEnd(testTextNode, 4);
- range2.setStart(testTextNode, 2);
- if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &
- range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
- // This is the wrong way round, so correct for it
- rangeProto.compareBoundaryPoints = function(type, range) {
- range = range.nativeRange || range;
- if (type == range.START_TO_END) {
- type = range.END_TO_START;
- } else if (type == range.END_TO_START) {
- type = range.START_TO_END;
- }
- return this.nativeRange.compareBoundaryPoints(type, range);
- };
- } else {
- rangeProto.compareBoundaryPoints = function(type, range) {
- return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
- };
- }
- /*--------------------------------------------------------------------------------------------------------*/
- // Test for existence of createContextualFragment and delegate to it if it exists
- if (api.util.isHostMethod(range, "createContextualFragment")) {
- rangeProto.createContextualFragment = function(fragmentStr) {
- return this.nativeRange.createContextualFragment(fragmentStr);
- };
- }
- /*--------------------------------------------------------------------------------------------------------*/
- // Clean up
- dom.getBody(document).removeChild(testTextNode);
- range.detach();
- range2.detach();
- })();
- api.createNativeRange = function(doc) {
- doc = doc || document;
- return doc.createRange();
- };
- } else if (api.features.implementsTextRange) {
- // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
- // prototype
- WrappedRange = function(textRange) {
- this.textRange = textRange;
- this.refresh();
- };
- WrappedRange.prototype = new DomRange(document);
- WrappedRange.prototype.refresh = function() {
- var start, end;
- // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
- var rangeContainerElement = getTextRangeContainerElement(this.textRange);
- if (textRangeIsCollapsed(this.textRange)) {
- end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true);
- } else {
- start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
- end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);
- }
- this.setStart(start.node, start.offset);
- this.setEnd(end.node, end.offset);
- };
- DomRange.copyComparisonConstants(WrappedRange);
- // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work
- var globalObj = (function() { return this; })();
- if (typeof globalObj.Range == "undefined") {
- globalObj.Range = WrappedRange;
- }
- api.createNativeRange = function(doc) {
- doc = doc || document;
- return doc.body.createTextRange();
- };
- }
- if (api.features.implementsTextRange) {
- WrappedRange.rangeToTextRange = function(range) {
- if (range.collapsed) {
- var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
- return tr;
- //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
- } else {
- var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
- var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
- var textRange = dom.getDocument(range.startContainer).body.createTextRange();
- textRange.setEndPoint("StartToStart", startRange);
- textRange.setEndPoint("EndToEnd", endRange);
- return textRange;
- }
- };
- }
- WrappedRange.prototype.getName = function() {
- return "WrappedRange";
- };
- api.WrappedRange = WrappedRange;
- api.createRange = function(doc) {
- doc = doc || document;
- return new WrappedRange(api.createNativeRange(doc));
- };
- api.createRangyRange = function(doc) {
- doc = doc || document;
- return new DomRange(doc);
- };
- api.createIframeRange = function(iframeEl) {
- return api.createRange(dom.getIframeDocument(iframeEl));
- };
- api.createIframeRangyRange = function(iframeEl) {
- return api.createRangyRange(dom.getIframeDocument(iframeEl));
- };
- api.addCreateMissingNativeApiListener(function(win) {
- var doc = win.document;
- if (typeof doc.createRange == "undefined") {
- doc.createRange = function() {
- return api.createRange(this);
- };
- }
- doc = win = null;
- });
- });rangy.createModule("WrappedSelection", function(api, module) {
- // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range
- // spec (http://html5.org/specs/dom-range.html)
- api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
- api.config.checkSelectionRanges = true;
- var BOOLEAN = "boolean",
- windowPropertyName = "_rangySelection",
- dom = api.dom,
- util = api.util,
- DomRange = api.DomRange,
- WrappedRange = api.WrappedRange,
- DOMException = api.DOMException,
- DomPosition = dom.DomPosition,
- getSelection,
- selectionIsCollapsed,
- CONTROL = "Control";
- function getWinSelection(winParam) {
- return (winParam || window).getSelection();
- }
- function getDocSelection(winParam) {
- return (winParam || window).document.selection;
- }
- // Test for the Range/TextRange and Selection features required
- // Test for ability to retrieve selection
- var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),
- implementsDocSelection = api.util.isHostObject(document, "selection");
- var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
- if (useDocumentSelection) {
- getSelection = getDocSelection;
- api.isSelectionValid = function(winParam) {
- var doc = (winParam || window).document, nativeSel = doc.selection;
- // Check whether the selection TextRange is actually contained within the correct document
- return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);
- };
- } else if (implementsWinGetSelection) {
- getSelection = getWinSelection;
- api.isSelectionValid = function() {
- return true;
- };
- } else {
- module.fail("Neither document.selection or window.getSelection() detected.");
- }
- api.getNativeSelection = getSelection;
- var testSelection = getSelection();
- var testRange = api.createNativeRange(document);
- var body = dom.getBody(document);
- // Obtaining a range from a selection
- var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] &&
- util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));
- api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
- // Test for existence of native selection extend() method
- var selectionHasExtend = util.isHostMethod(testSelection, "extend");
- api.features.selectionHasExtend = selectionHasExtend;
- // Test if rangeCount exists
- var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");
- api.features.selectionHasRangeCount = selectionHasRangeCount;
- var selectionSupportsMultipleRanges = false;
- var collapsedNonEditableSelectionsSupported = true;
- if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
- typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {
- (function() {
- var iframe = document.createElement("iframe");
- body.appendChild(iframe);
- var iframeDoc = dom.getIframeDocument(iframe);
- iframeDoc.open();
- iframeDoc.write("<html><head></head><body>12</body></html>");
- iframeDoc.close();
- var sel = dom.getIframeWindow(iframe).getSelection();
- var docEl = iframeDoc.documentElement;
- var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;
- // Test whether the native selection will allow a collapsed selection within a non-editable element
- var r1 = iframeDoc.createRange();
- r1.setStart(textNode, 1);
- r1.collapse(true);
- sel.addRange(r1);
- collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
- sel.removeAllRanges();
- // Test whether the native selection is capable of supporting multiple ranges
- var r2 = r1.cloneRange();
- r1.setStart(textNode, 0);
- r2.setEnd(textNode, 2);
- sel.addRange(r1);
- sel.addRange(r2);
- selectionSupportsMultipleRanges = (sel.rangeCount == 2);
- // Clean up
- r1.detach();
- r2.detach();
- body.removeChild(iframe);
- })();
- }
- api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
- api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
- // ControlRanges
- var implementsControlRange = false, testControlRange;
- if (body && util.isHostMethod(body, "createControlRange")) {
- testControlRange = body.createControlRange();
- if (util.areHostProperties(testControlRange, ["item", "add"])) {
- implementsControlRange = true;
- }
- }
- api.features.implementsControlRange = implementsControlRange;
- // Selection collapsedness
- if (selectionHasAnchorAndFocus) {
- selectionIsCollapsed = function(sel) {
- return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
- };
- } else {
- selectionIsCollapsed = function(sel) {
- return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
- };
- }
- function updateAnchorAndFocusFromRange(sel, range, backwards) {
- var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end";
- sel.anchorNode = range[anchorPrefix + "Container"];
- sel.anchorOffset = range[anchorPrefix + "Offset"];
- sel.focusNode = range[focusPrefix + "Container"];
- sel.focusOffset = range[focusPrefix + "Offset"];
- }
- function updateAnchorAndFocusFromNativeSelection(sel) {
- var nativeSel = sel.nativeSelection;
- sel.anchorNode = nativeSel.anchorNode;
- sel.anchorOffset = nativeSel.anchorOffset;
- sel.focusNode = nativeSel.focusNode;
- sel.focusOffset = nativeSel.focusOffset;
- }
- function updateEmptySelection(sel) {
- sel.anchorNode = sel.focusNode = null;
- sel.anchorOffset = sel.focusOffset = 0;
- sel.rangeCount = 0;
- sel.isCollapsed = true;
- sel._ranges.length = 0;
- }
- function getNativeRange(range) {
- var nativeRange;
- if (range instanceof DomRange) {
- nativeRange = range._selectionNativeRange;
- if (!nativeRange) {
- nativeRange = api.createNativeRange(dom.getDocument(range.startContainer));
- nativeRange.setEnd(range.endContainer, range.endOffset);
- nativeRange.setStart(range.startContainer, range.startOffset);
- range._selectionNativeRange = nativeRange;
- range.attachListener("detach", function() {
- this._selectionNativeRange = null;
- });
- }
- } else if (range instanceof WrappedRange) {
- nativeRange = range.nativeRange;
- } else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
- nativeRange = range;
- }
- return nativeRange;
- }
- function rangeContainsSingleElement(rangeNodes) {
- if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
- return false;
- }
- for (var i = 1, len = rangeNodes.length; i < len; ++i) {
- if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
- return false;
- }
- }
- return true;
- }
- function getSingleElementFromRange(range) {
- var nodes = range.getNodes();
- if (!rangeContainsSingleElement(nodes)) {
- throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
- }
- return nodes[0];
- }
- function isTextRange(range) {
- return !!range && typeof range.text != "undefined";
- }
- function updateFromTextRange(sel, range) {
- // Create a Range from the selected TextRange
- var wrappedRange = new WrappedRange(range);
- sel._ranges = [wrappedRange];
- updateAnchorAndFocusFromRange(sel, wrappedRange, false);
- sel.rangeCount = 1;
- sel.isCollapsed = wrappedRange.collapsed;
- }
- function updateControlSelection(sel) {
- // Update the wrapped selection based on what's now in the native selection
- sel._ranges.length = 0;
- if (sel.docSelection.type == "None") {
- updateEmptySelection(sel);
- } else {
- var controlRange = sel.docSelection.createRange();
- if (isTextRange(controlRange)) {
- // This case (where the selection type is "Control" and calling createRange() on the selection returns
- // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
- // ControlRange have been removed from the ControlRange and removed from the document.
- updateFromTextRange(sel, controlRange);
- } else {
- sel.rangeCount = controlRange.length;
- var range, doc = dom.getDocument(controlRange.item(0));
- for (var i = 0; i < sel.rangeCount; ++i) {
- range = api.createRange(doc);
- range.selectNode(controlRange.item(i));
- sel._ranges.push(range);
- }
- sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
- updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
- }
- }
- }
- function addRangeToControlSelection(sel, range) {
- var controlRange = sel.docSelection.createRange();
- var rangeElement = getSingleElementFromRange(range);
- // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
- // contained by the supplied range
- var doc = dom.getDocument(controlRange.item(0));
- var newControlRange = dom.getBody(doc).createControlRange();
- for (var i = 0, len = controlRange.length; i < len; ++i) {
- newControlRange.add(controlRange.item(i));
- }
- try {
- newControlRange.add(rangeElement);
- } catch (ex) {
- throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
- }
- newControlRange.select();
- // Update the wrapped selection based on what's now in the native selection
- updateControlSelection(sel);
- }
- var getSelectionRangeAt;
- if (util.isHostMethod(testSelection, "getRangeAt")) {
- getSelectionRangeAt = function(sel, index) {
- try {
- return sel.getRangeAt(index);
- } catch(ex) {
- return null;
- }
- };
- } else if (selectionHasAnchorAndFocus) {
- getSelectionRangeAt = function(sel) {
- var doc = dom.getDocument(sel.anchorNode);
- var range = api.createRange(doc);
- range.setStart(sel.anchorNode, sel.anchorOffset);
- range.setEnd(sel.focusNode, sel.focusOffset);
- // Handle the case when the selection was selected backwards (from the end to the start in the
- // document)
- if (range.collapsed !== this.isCollapsed) {
- range.setStart(sel.focusNode, sel.focusOffset);
- range.setEnd(sel.anchorNode, sel.anchorOffset);
- }
- return range;
- };
- }
- /**
- * @constructor
- */
- function WrappedSelection(selection, docSelection, win) {
- this.nativeSelection = selection;
- this.docSelection = docSelection;
- this._ranges = [];
- this.win = win;
- this.refresh();
- }
- api.getSelection = function(win) {
- win = win || window;
- var sel = win[windowPropertyName];
- var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
- if (sel) {
- sel.nativeSelection = nativeSel;
- sel.docSelection = docSel;
- sel.refresh(win);
- } else {
- sel = new WrappedSelection(nativeSel, docSel, win);
- win[windowPropertyName] = sel;
- }
- return sel;
- };
- api.getIframeSelection = function(iframeEl) {
- return api.getSelection(dom.getIframeWindow(iframeEl));
- };
- var selProto = WrappedSelection.prototype;
- function createControlSelection(sel, ranges) {
- // Ensure that the selection becomes of type "Control"
- var doc = dom.getDocument(ranges[0].startContainer);
- var controlRange = dom.getBody(doc).createControlRange();
- for (var i = 0, el; i < rangeCount; ++i) {
- el = getSingleElementFromRange(ranges[i]);
- try {
- controlRange.add(el);
- } catch (ex) {
- throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");
- }
- }
- controlRange.select();
- // Update the wrapped selection based on what's now in the native selection
- updateControlSelection(sel);
- }
- // Selecting a range
- if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
- selProto.removeAllRanges = function() {
- this.nativeSelection.removeAllRanges();
- updateEmptySelection(this);
- };
- var addRangeBackwards = function(sel, range) {
- var doc = DomRange.getRangeDocument(range);
- var endRange = api.createRange(doc);
- endRange.collapseToPoint(range.endContainer, range.endOffset);
- sel.nativeSelection.addRange(getNativeRange(endRange));
- sel.nativeSelection.extend(range.startContainer, range.startOffset);
- sel.refresh();
- };
- if (selectionHasRangeCount) {
- selProto.addRange = function(range, backwards) {
- if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
- addRangeToControlSelection(this, range);
- } else {
- if (backwards && selectionHasExtend) {
- addRangeBackwards(this, range);
- } else {
- var previousRangeCount;
- if (selectionSupportsMultipleRanges) {
- previousRangeCount = this.rangeCount;
- } else {
- this.removeAllRanges();
- previousRangeCount = 0;
- }
- this.nativeSelection.addRange(getNativeRange(range));
- // Check whether adding the range was successful
- this.rangeCount = this.nativeSelection.rangeCount;
- if (this.rangeCount == previousRangeCount + 1) {
- // The range was added successfully
- // Check whether the range that we added to the selection is reflected in the last range extracted from
- // the selection
- if (api.config.checkSelectionRanges) {
- var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
- if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) {
- // Happens in WebKit with, for example, a selection placed at the start of a text node
- range = new WrappedRange(nativeRange);
- }
- }
- this._ranges[this.rangeCount - 1] = range;
- updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection));
- this.isCollapsed = selectionIsCollapsed(this);
- } else {
- // The range was not added successfully. The simplest thing is to refresh
- this.refresh();
- }
- }
- }
- };
- } else {
- selProto.addRange = function(range, backwards) {
- if (backwards && selectionHasExtend) {
- addRangeBackwards(this, range);
- } else {
- this.nativeSelection.addRange(getNativeRange(range));
- this.refresh();
- }
- };
- }
- selProto.setRanges = function(ranges) {
- if (implementsControlRange && ranges.length > 1) {
- createControlSelection(this, ranges);
- } else {
- this.removeAllRanges();
- for (var i = 0, len = ranges.length; i < len; ++i) {
- this.addRange(ranges[i]);
- }
- }
- };
- } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") &&
- implementsControlRange && useDocumentSelection) {
- selProto.removeAllRanges = function() {
- // Added try/catch as fix for issue #21
- try {
- this.docSelection.empty();
- // Check for empty() not working (issue #24)
- if (this.docSelection.type != "None") {
- // Work around failure to empty a control selection by instead selecting a TextRange and then
- // calling empty()
- var doc;
- if (this.anchorNode) {
- doc = dom.getDocument(this.anchorNode);
- } else if (this.docSelection.type == CONTROL) {
- var controlRange = this.docSelection.createRange();
- if (controlRange.length) {
- doc = dom.getDocument(controlRange.item(0)).body.createTextRange();
- }
- }
- if (doc) {
- var textRange = doc.body.createTextRange();
- textRange.select();
- this.docSelection.empty();
- }
- }
- } catch(ex) {}
- updateEmptySelection(this);
- };
- selProto.addRange = function(range) {
- if (this.docSelection.type == CONTROL) {
- addRangeToControlSelection(this, range);
- } else {
- WrappedRange.rangeToTextRange(range).select();
- this._ranges[0] = range;
- this.rangeCount = 1;
- this.isCollapsed = this._ranges[0].collapsed;
- updateAnchorAndFocusFromRange(this, range, false);
- }
- };
- selProto.setRanges = function(ranges) {
- this.removeAllRanges();
- var rangeCount = ranges.length;
- if (rangeCount > 1) {
- createControlSelection(this, ranges);
- } else if (rangeCount) {
- this.addRange(ranges[0]);
- }
- };
- } else {
- module.fail("No means of selecting a Range or TextRange was found");
- return false;
- }
- selProto.getRangeAt = function(index) {
- if (index < 0 || index >= this.rangeCount) {
- throw new DOMException("INDEX_SIZE_ERR");
- } else {
- return this._ranges[index];
- }
- };
- var refreshSelection;
- if (useDocumentSelection) {
- refreshSelection = function(sel) {
- var range;
- if (api.isSelectionValid(sel.win)) {
- range = sel.docSelection.createRange();
- } else {
- range = dom.getBody(sel.win.document).createTextRange();
- range.collapse(true);
- }
- if (sel.docSelection.type == CONTROL) {
- updateControlSelection(sel);
- } else if (isTextRange(range)) {
- updateFromTextRange(sel, range);
- } else {
- updateEmptySelection(sel);
- }
- };
- } else if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") {
- refreshSelection = function(sel) {
- if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
- updateControlSelection(sel);
- } else {
- sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
- if (sel.rangeCount) {
- for (var i = 0, len = sel.rangeCount; i < len; ++i) {
- sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
- }
- updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection));
- sel.isCollapsed = selectionIsCollapsed(sel);
- } else {
- updateEmptySelection(sel);
- }
- }
- };
- } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) {
- refreshSelection = function(sel) {
- var range, nativeSel = sel.nativeSelection;
- if (nativeSel.anchorNode) {
- range = getSelectionRangeAt(nativeSel, 0);
- sel._ranges = [range];
- sel.rangeCount = 1;
- updateAnchorAndFocusFromNativeSelection(sel);
- sel.isCollapsed = selectionIsCollapsed(sel);
- } else {
- updateEmptySelection(sel);
- }
- };
- } else {
- module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
- return false;
- }
- selProto.refresh = function(checkForChanges) {
- var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
- refreshSelection(this);
- if (checkForChanges) {
- var i = oldRanges.length;
- if (i != this._ranges.length) {
- return false;
- }
- while (i--) {
- if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {
- return false;
- }
- }
- return true;
- }
- };
- // Removal of a single range
- var removeRangeManually = function(sel, range) {
- var ranges = sel.getAllRanges(), removed = false;
- sel.removeAllRanges();
- for (var i = 0, len = ranges.length; i < len; ++i) {
- if (removed || range !== ranges[i]) {
- sel.addRange(ranges[i]);
- } else {
- // According to the draft WHATWG Range spec, the same range may be added to the selection multiple
- // times. removeRange should only remove the first instance, so the following ensures only the first
- // instance is removed
- removed = true;
- }
- }
- if (!sel.rangeCount) {
- updateEmptySelection(sel);
- }
- };
- if (implementsControlRange) {
- selProto.removeRange = function(range) {
- if (this.docSelection.type == CONTROL) {
- var controlRange = this.docSelection.createRange();
- var rangeElement = getSingleElementFromRange(range);
- // Create a new ControlRange containing all the elements in the selected ControlRange minus the
- // element contained by the supplied range
- var doc = dom.getDocument(controlRange.item(0));
- var newControlRange = dom.getBody(doc).createControlRange();
- var el, removed = false;
- for (var i = 0, len = controlRange.length; i < len; ++i) {
- el = controlRange.item(i);
- if (el !== rangeElement || removed) {
- newControlRange.add(controlRange.item(i));
- } else {
- removed = true;
- }
- }
- newControlRange.select();
- // Update the wrapped selection based on what's now in the native selection
- updateControlSelection(this);
- } else {
- removeRangeManually(this, range);
- }
- };
- } else {
- selProto.removeRange = function(range) {
- removeRangeManually(this, range);
- };
- }
- // Detecting if a selection is backwards
- var selectionIsBackwards;
- if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) {
- selectionIsBackwards = function(sel) {
- var backwards = false;
- if (sel.anchorNode) {
- backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
- }
- return backwards;
- };
- selProto.isBackwards = function() {
- return selectionIsBackwards(this);
- };
- } else {
- selectionIsBackwards = selProto.isBackwards = function() {
- return false;
- };
- }
- // Selection text
- // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation
- selProto.toString = function() {
- var rangeTexts = [];
- for (var i = 0, len = this.rangeCount; i < len; ++i) {
- rangeTexts[i] = "" + this._ranges[i];
- }
- return rangeTexts.join("");
- };
- function assertNodeInSameDocument(sel, node) {
- if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) {
- throw new DOMException("WRONG_DOCUMENT_ERR");
- }
- }
- // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used
- selProto.collapse = function(node, offset) {
- assertNodeInSameDocument(this, node);
- var range = api.createRange(dom.getDocument(node));
- range.collapseToPoint(node, offset);
- this.removeAllRanges();
- this.addRange(range);
- this.isCollapsed = true;
- };
- selProto.collapseToStart = function() {
- if (this.rangeCount) {
- var range = this._ranges[0];
- this.collapse(range.startContainer, range.startOffset);
- } else {
- throw new DOMException("INVALID_STATE_ERR");
- }
- };
- selProto.collapseToEnd = function() {
- if (this.rangeCount) {
- var range = this._ranges[this.rangeCount - 1];
- this.collapse(range.endContainer, range.endOffset);
- } else {
- throw new DOMException("INVALID_STATE_ERR");
- }
- };
- // The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is
- // never used by Rangy.
- selProto.selectAllChildren = function(node) {
- assertNodeInSameDocument(this, node);
- var range = api.createRange(dom.getDocument(node));
- range.selectNodeContents(node);
- this.removeAllRanges();
- this.addRange(range);
- };
- selProto.deleteFromDocument = function() {
- // Sepcial behaviour required for Control selections
- if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
- var controlRange = this.docSelection.createRange();
- var element;
- while (controlRange.length) {
- element = controlRange.item(0);
- controlRange.remove(element);
- element.parentNode.removeChild(element);
- }
- this.refresh();
- } else if (this.rangeCount) {
- var ranges = this.getAllRanges();
- this.removeAllRanges();
- for (var i = 0, len = ranges.length; i < len; ++i) {
- ranges[i].deleteContents();
- }
- // The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each
- // range. Firefox moves the selection to where the final selected range was, so we emulate that
- this.addRange(ranges[len - 1]);
- }
- };
- // The following are non-standard extensions
- selProto.getAllRanges = function() {
- return this._ranges.slice(0);
- };
- selProto.setSingleRange = function(range) {
- this.setRanges( [range] );
- };
- selProto.containsNode = function(node, allowPartial) {
- for (var i = 0, len = this._ranges.length; i < len; ++i) {
- if (this._ranges[i].containsNode(node, allowPartial)) {
- return true;
- }
- }
- return false;
- };
- selProto.toHtml = function() {
- var html = "";
- if (this.rangeCount) {
- var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div");
- for (var i = 0, len = this._ranges.length; i < len; ++i) {
- container.appendChild(this._ranges[i].cloneContents());
- }
- html = container.innerHTML;
- }
- return html;
- };
- function inspect(sel) {
- var rangeInspects = [];
- var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
- var focus = new DomPosition(sel.focusNode, sel.focusOffset);
- var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
- if (typeof sel.rangeCount != "undefined") {
- for (var i = 0, len = sel.rangeCount; i < len; ++i) {
- rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
- }
- }
- return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
- ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
- }
- selProto.getName = function() {
- return "WrappedSelection";
- };
- selProto.inspect = function() {
- return inspect(this);
- };
- selProto.detach = function() {
- this.win[windowPropertyName] = null;
- this.win = this.anchorNode = this.focusNode = null;
- };
- WrappedSelection.inspect = inspect;
- api.Selection = WrappedSelection;
- api.selectionPrototype = selProto;
- api.addCreateMissingNativeApiListener(function(win) {
- if (typeof win.getSelection == "undefined") {
- win.getSelection = function() {
- return api.getSelection(this);
- };
- }
- win = null;
- });
- });
- /*
- Base.js, version 1.1a
- Copyright 2006-2010, Dean Edwards
- License: http://www.opensource.org/licenses/mit-license.php
- */
- var Base = function() {
- // dummy
- };
- Base.extend = function(_instance, _static) { // subclass
- var extend = Base.prototype.extend;
-
- // build the prototype
- Base._prototyping = true;
- var proto = new this;
- extend.call(proto, _instance);
- proto.base = function() {
- // call this method from any other method to invoke that method's ancestor
- };
- delete Base._prototyping;
-
- // create the wrapper for the constructor function
- //var constructor = proto.constructor.valueOf(); //-dean
- var constructor = proto.constructor;
- var klass = proto.constructor = function() {
- if (!Base._prototyping) {
- if (this._constructing || this.constructor == klass) { // instantiation
- this._constructing = true;
- constructor.apply(this, arguments);
- delete this._constructing;
- } else if (arguments[0] != null) { // casting
- return (arguments[0].extend || extend).call(arguments[0], proto);
- }
- }
- };
-
- // build the class interface
- klass.ancestor = this;
- klass.extend = this.extend;
- klass.forEach = this.forEach;
- klass.implement = this.implement;
- klass.prototype = proto;
- klass.toString = this.toString;
- klass.valueOf = function(type) {
- //return (type == "object") ? klass : constructor; //-dean
- return (type == "object") ? klass : constructor.valueOf();
- };
- extend.call(klass, _static);
- // class initialisation
- if (typeof klass.init == "function") klass.init();
- return klass;
- };
- Base.prototype = {
- extend: function(source, value) {
- if (arguments.length > 1) { // extending with a name/value pair
- var ancestor = this[source];
- if (ancestor && (typeof value == "function") && // overriding a method?
- // the valueOf() comparison is to avoid circular references
- (!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&
- /\bbase\b/.test(value)) {
- // get the underlying method
- var method = value.valueOf();
- // override
- value = function() {
- var previous = this.base || Base.prototype.base;
- this.base = ancestor;
- var returnValue = method.apply(this, arguments);
- this.base = previous;
- return returnValue;
- };
- // point to the underlying method
- value.valueOf = function(type) {
- return (type == "object") ? value : method;
- };
- value.toString = Base.toString;
- }
- this[source] = value;
- } else if (source) { // extending with an object literal
- var extend = Base.prototype.extend;
- // if this object has a customised extend method then use it
- if (!Base._prototyping && typeof this != "function") {
- extend = this.extend || extend;
- }
- var proto = {toSource: null};
- // do the "toString" and other methods manually
- var hidden = ["constructor", "toString", "valueOf"];
- // if we are prototyping then include the constructor
- var i = Base._prototyping ? 0 : 1;
- while (key = hidden[i++]) {
- if (source[key] != proto[key]) {
- extend.call(this, key, source[key]);
- }
- }
- // copy each of the source object's properties to this object
- for (var key in source) {
- if (!proto[key]) extend.call(this, key, source[key]);
- }
- }
- return this;
- }
- };
- // initialise
- Base = Base.extend({
- constructor: function() {
- this.extend(arguments[0]);
- }
- }, {
- ancestor: Object,
- version: "1.1",
-
- forEach: function(object, block, context) {
- for (var key in object) {
- if (this.prototype[key] === undefined) {
- block.call(context, object[key], key, object);
- }
- }
- },
-
- implement: function() {
- for (var i = 0; i < arguments.length; i++) {
- if (typeof arguments[i] == "function") {
- // if it's a function, call it
- arguments[i](this.prototype);
- } else {
- // add the interface using the extend method
- this.prototype.extend(arguments[i]);
- }
- }
- return this;
- },
-
- toString: function() {
- return String(this.valueOf());
- }
- });/**
- * Detect browser support for specific features
- */
- wysihtml5.browser = (function() {
- var userAgent = navigator.userAgent,
- testElement = document.createElement("div"),
- // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect
- isIE = userAgent.indexOf("MSIE") !== -1 && userAgent.indexOf("Opera") === -1,
- isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1,
- isWebKit = userAgent.indexOf("AppleWebKit/") !== -1,
- isChrome = userAgent.indexOf("Chrome/") !== -1,
- isOpera = userAgent.indexOf("Opera/") !== -1;
-
- function iosVersion(userAgent) {
- return ((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [, 0])[1];
- }
-
- return {
- // Static variable needed, publicly accessible, to be able override it in unit tests
- USER_AGENT: userAgent,
-
- /**
- * Exclude browsers that are not capable of displaying and handling
- * contentEditable as desired:
- * - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable
- * - IE < 8 create invalid markup and crash randomly from time to time
- *
- * @return {Boolean}
- */
- supported: function() {
- var userAgent = this.USER_AGENT.toLowerCase(),
- // Essential for making html elements editable
- hasContentEditableSupport = "contentEditable" in testElement,
- // Following methods are needed in order to interact with the contentEditable area
- hasEditingApiSupport = document.execCommand && document.queryCommandSupported && document.queryCommandState,
- // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+
- hasQuerySelectorSupport = document.querySelector && document.querySelectorAll,
- // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05)
- isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1;
-
- return hasContentEditableSupport
- && hasEditingApiSupport
- && hasQuerySelectorSupport
- && !isIncompatibleMobileBrowser;
- },
-
- isTouchDevice: function() {
- return this.supportsEvent("touchmove");
- },
-
- isIos: function() {
- var userAgent = this.USER_AGENT.toLowerCase();
- return userAgent.indexOf("webkit") !== -1 && userAgent.indexOf("mobile") !== -1;
- },
-
- /**
- * Whether the browser supports sandboxed iframes
- * Currently only IE 6+ offers such feature <iframe security="restricted">
- *
- * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx
- * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx
- *
- * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage)
- */
- supportsSandboxedIframes: function() {
- return isIE;
- },
- /**
- * IE6+7 throw a mixed content warning when the src of an iframe
- * is empty/unset or about:blank
- * window.querySelector is implemented as of IE8
- */
- throwsMixedContentWarningWhenIframeSrcIsEmpty: function() {
- return !("querySelector" in document);
- },
- /**
- * Whether the caret is correctly displayed in contentEditable elements
- * Firefox sometimes shows a huge caret in the beginning after focusing
- */
- displaysCaretInEmptyContentEditableCorrectly: function() {
- return !isGecko;
- },
- /**
- * Opera and IE are the only browsers who offer the css value
- * in the original unit, thx to the currentStyle object
- * All other browsers provide the computed style in px via window.getComputedStyle
- */
- hasCurrentStyleProperty: function() {
- return "currentStyle" in testElement;
- },
- /**
- * Whether the browser inserts a <br> when pressing enter in a contentEditable element
- */
- insertsLineBreaksOnReturn: function() {
- return isGecko;
- },
- supportsPlaceholderAttributeOn: function(element) {
- return "placeholder" in element;
- },
- supportsEvent: function(eventName) {
- return "on" + eventName in testElement || (function() {
- testElement.setAttribute("on" + eventName, "return;");
- return typeof(testElement["on" + eventName]) === "function";
- })();
- },
- /**
- * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe
- */
- supportsEventsInIframeCorrectly: function() {
- return !isOpera;
- },
- /**
- * Chrome & Safari only fire the ondrop/ondragend/... events when the ondragover event is cancelled
- * with event.preventDefault
- * Firefox 3.6 fires those events anyway, but the mozilla doc says that the dragover/dragenter event needs
- * to be cancelled
- */
- firesOnDropOnlyWhenOnDragOverIsCancelled: function() {
- return isWebKit || isGecko;
- },
-
- /**
- * Whether the browser supports the event.dataTransfer property in a proper way
- */
- supportsDataTransfer: function() {
- try {
- // Firefox doesn't support dataTransfer in a safe way, it doesn't strip script code in the html payload (like Chrome does)
- return isWebKit && (window.Clipboard || window.DataTransfer).prototype.getData;
- } catch(e) {
- return false;
- }
- },
- /**
- * Everything below IE9 doesn't know how to treat HTML5 tags
- *
- * @param {Object} context The document object on which to check HTML5 support
- *
- * @example
- * wysihtml5.browser.supportsHTML5Tags(document);
- */
- supportsHTML5Tags: function(context) {
- var element = context.createElement("div"),
- html5 = "<article>foo</article>";
- element.innerHTML = html5;
- return element.innerHTML.toLowerCase() === html5;
- },
- /**
- * Checks whether a document supports a certain queryCommand
- * In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree
- * in oder to report correct results
- *
- * @param {Object} doc Document object on which to check for a query command
- * @param {String} command The query command to check for
- * @return {Boolean}
- *
- * @example
- * wysihtml5.browser.supportsCommand(document, "bold");
- */
- supportsCommand: (function() {
- // Following commands are supported but contain bugs in some browsers
- var buggyCommands = {
- // formatBlock fails with some tags (eg. <blockquote>)
- "formatBlock": isIE,
- // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets
- // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>)
- // IE and Opera act a bit different here as they convert the entire content of the current block element into a list
- "insertUnorderedList": isIE || isOpera || isWebKit,
- "insertOrderedList": isIE || isOpera || isWebKit
- };
-
- // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands
- var supported = {
- "insertHTML": isGecko
- };
- return function(doc, command) {
- var isBuggy = buggyCommands[command];
- if (!isBuggy) {
- // Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled
- try {
- return doc.queryCommandSupported(command);
- } catch(e1) {}
- try {
- return doc.queryCommandEnabled(command);
- } catch(e2) {
- return !!supported[command];
- }
- }
- return false;
- };
- })(),
- /**
- * IE: URLs starting with:
- * www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://,
- * nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url:
- * will automatically be auto-linked when either the user inserts them via copy&paste or presses the
- * space bar when the caret is directly after such an url.
- * This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll
- * (related blog post on msdn
- * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx).
- */
- doesAutoLinkingInContentEditable: function() {
- return isIE;
- },
- /**
- * As stated above, IE auto links urls typed into contentEditable elements
- * Since IE9 it's possible to prevent this behavior
- */
- canDisableAutoLinking: function() {
- return this.supportsCommand(document, "AutoUrlDetect");
- },
- /**
- * IE leaves an empty paragraph in the contentEditable element after clearing it
- * Chrome/Safari sometimes an empty <div>
- */
- clearsContentEditableCorrectly: function() {
- return isGecko || isOpera || isWebKit;
- },
- /**
- * IE gives wrong results for getAttribute
- */
- supportsGetAttributeCorrectly: function() {
- var td = document.createElement("td");
- return td.getAttribute("rowspan") != "1";
- },
- /**
- * When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them.
- * Chrome and Safari both don't support this
- */
- canSelectImagesInContentEditable: function() {
- return isGecko || isIE || isOpera;
- },
- /**
- * When the caret is in an empty list (<ul><li>|</li></ul>) which is the first child in an contentEditable container
- * pressing backspace doesn't remove the entire list as done in other browsers
- */
- clearsListsInContentEditableCorrectly: function() {
- return isGecko || isIE || isWebKit;
- },
- /**
- * All browsers except Safari and Chrome automatically scroll the range/caret position into view
- */
- autoScrollsToCaret: function() {
- return !isWebKit;
- },
- /**
- * Check whether the browser automatically closes tags that don't need to be opened
- */
- autoClosesUnclosedTags: function() {
- var clonedTestElement = testElement.cloneNode(false),
- returnValue,
- innerHTML;
- clonedTestElement.innerHTML = "<p><div></div>";
- innerHTML = clonedTestElement.innerHTML.toLowerCase();
- returnValue = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>";
- // Cache result by overwriting current function
- this.autoClosesUnclosedTags = function() { return returnValue; };
- return returnValue;
- },
- /**
- * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists
- */
- supportsNativeGetElementsByClassName: function() {
- return String(document.getElementsByClassName).indexOf("[native code]") !== -1;
- },
- /**
- * As of now (19.04.2011) only supported by Firefox 4 and Chrome
- * See https://developer.mozilla.org/en/DOM/Selection/modify
- */
- supportsSelectionModify: function() {
- return "getSelection" in window && "modify" in window.getSelection();
- },
-
- /**
- * Whether the browser supports the classList object for fast className manipulation
- * See https://developer.mozilla.org/en/DOM/element.classList
- */
- supportsClassList: function() {
- return "classList" in testElement;
- },
-
- /**
- * Opera needs a white space after a <br> in order to position the caret correctly
- */
- needsSpaceAfterLineBreak: function() {
- return isOpera;
- },
-
- /**
- * Whether the browser supports the speech api on the given element
- * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
- *
- * @example
- * var input = document.createElement("input");
- * if (wysihtml5.browser.supportsSpeechApiOn(input)) {
- * // ...
- * }
- */
- supportsSpeechApiOn: function(input) {
- var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [, 0];
- return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input);
- },
-
- /**
- * IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest
- * See https://connect.microsoft.com/ie/feedback/details/650112
- * or try the POC http://tifftiff.de/ie9_crash/
- */
- crashesWhenDefineProperty: function(property) {
- return isIE && (property === "XMLHttpRequest" || property === "XDomainRequest");
- },
-
- /**
- * IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element
- */
- doesAsyncFocus: function() {
- return isIE;
- },
-
- /**
- * In IE it's impssible for the user and for the selection library to set the caret after an <img> when it's the lastChild in the document
- */
- hasProblemsSettingCaretAfterImg: function() {
- return isIE;
- },
-
- hasUndoInContextMenu: function() {
- return isGecko || isChrome || isOpera;
- }
- };
- })();wysihtml5.lang.array = function(arr) {
- return {
- /**
- * Check whether a given object exists in an array
- *
- * @example
- * wysihtml5.lang.array([1, 2]).contains(1);
- * // => true
- */
- contains: function(needle) {
- if (arr.indexOf) {
- return arr.indexOf(needle) !== -1;
- } else {
- for (var i=0, length=arr.length; i<length; i++) {
- if (arr[i] === needle) { return true; }
- }
- return false;
- }
- },
-
- /**
- * Substract one array from another
- *
- * @example
- * wysihtml5.lang.array([1, 2, 3, 4]).without([3, 4]);
- * // => [1, 2]
- */
- without: function(arrayToSubstract) {
- arrayToSubstract = wysihtml5.lang.array(arrayToSubstract);
- var newArr = [],
- i = 0,
- length = arr.length;
- for (; i<length; i++) {
- if (!arrayToSubstract.contains(arr[i])) {
- newArr.push(arr[i]);
- }
- }
- return newArr;
- },
-
- /**
- * Return a clean native array
- *
- * Following will convert a Live NodeList to a proper Array
- * @example
- * var childNodes = wysihtml5.lang.array(document.body.childNodes).get();
- */
- get: function() {
- var i = 0,
- length = arr.length,
- newArray = [];
- for (; i<length; i++) {
- newArray.push(arr[i]);
- }
- return newArray;
- }
- };
- };wysihtml5.lang.Dispatcher = Base.extend(
- /** @scope wysihtml5.lang.Dialog.prototype */ {
- observe: function(eventName, handler) {
- this.events = this.events || {};
- this.events[eventName] = this.events[eventName] || [];
- this.events[eventName].push(handler);
- return this;
- },
- on: function() {
- return this.observe.apply(this, wysihtml5.lang.array(arguments).get());
- },
- fire: function(eventName, payload) {
- this.events = this.events || {};
- var handlers = this.events[eventName] || [],
- i = 0;
- for (; i<handlers.length; i++) {
- handlers[i].call(this, payload);
- }
- return this;
- },
- stopObserving: function(eventName, handler) {
- this.events = this.events || {};
- var i = 0,
- handlers,
- newHandlers;
- if (eventName) {
- handlers = this.events[eventName] || [],
- newHandlers = [];
- for (; i<handlers.length; i++) {
- if (handlers[i] !== handler && handler) {
- newHandlers.push(handlers[i]);
- }
- }
- this.events[eventName] = newHandlers;
- } else {
- // Clean up all events
- this.events = {};
- }
- return this;
- }
- });wysihtml5.lang.object = function(obj) {
- return {
- /**
- * @example
- * wysihtml5.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get();
- * // => { foo: 1, bar: 2, baz: 3 }
- */
- merge: function(otherObj) {
- for (var i in otherObj) {
- obj[i] = otherObj[i];
- }
- return this;
- },
-
- get: function() {
- return obj;
- },
-
- /**
- * @example
- * wysihtml5.lang.object({ foo: 1 }).clone();
- * // => { foo: 1 }
- */
- clone: function() {
- var newObj = {},
- i;
- for (i in obj) {
- newObj[i] = obj[i];
- }
- return newObj;
- },
-
- /**
- * @example
- * wysihtml5.lang.object([]).isArray();
- * // => true
- */
- isArray: function() {
- return Object.prototype.toString.call(obj) === "[object Array]";
- }
- };
- };(function() {
- var WHITE_SPACE_START = /^\s+/,
- WHITE_SPACE_END = /\s+$/;
- wysihtml5.lang.string = function(str) {
- str = String(str);
- return {
- /**
- * @example
- * wysihtml5.lang.string(" foo ").trim();
- * // => "foo"
- */
- trim: function() {
- return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, "");
- },
-
- /**
- * @example
- * wysihtml5.lang.string("Hello #{name}").interpolate({ name: "Christopher" });
- * // => "Hello Christopher"
- */
- interpolate: function(vars) {
- for (var i in vars) {
- str = this.replace("#{" + i + "}").by(vars[i]);
- }
- return str;
- },
-
- /**
- * @example
- * wysihtml5.lang.string("Hello Tom").replace("Tom").with("Hans");
- * // => "Hello Hans"
- */
- replace: function(search) {
- return {
- by: function(replace) {
- return str.split(search).join(replace);
- }
- }
- }
- };
- };
- })();/**
- * Find urls in descendant text nodes of an element and auto-links them
- * Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/
- *
- * @param {Element} element Container element in which to search for urls
- *
- * @example
- * <div id="text-container">Please click here: www.google.com</div>
- * <script>wysihtml5.dom.autoLink(document.getElementById("text-container"));</script>
- */
- (function(wysihtml5) {
- var /**
- * Don't auto-link urls that are contained in the following elements:
- */
- IGNORE_URLS_IN = wysihtml5.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]),
- /**
- * revision 1:
- * /(\S+\.{1}[^\s\,\.\!]+)/g
- *
- * revision 2:
- * /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim
- *
- * put this in the beginning if you don't wan't to match within a word
- * (^|[\>\(\{\[\s\>])
- */
- URL_REG_EXP = /((https?:\/\/|www\.)[^\s<]{3,})/gi,
- TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i,
- MAX_DISPLAY_LENGTH = 100,
- BRACKETS = { ")": "(", "]": "[", "}": "{" };
-
- function autoLink(element) {
- if (_hasParentThatShouldBeIgnored(element)) {
- return element;
- }
- if (element === element.ownerDocument.documentElement) {
- element = element.ownerDocument.body;
- }
- return _parseNode(element);
- }
-
- /**
- * This is basically a rebuild of
- * the rails auto_link_urls text helper
- */
- function _convertUrlsToLinks(str) {
- return str.replace(URL_REG_EXP, function(match, url) {
- var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "",
- opening = BRACKETS[punctuation];
- url = url.replace(TRAILING_CHAR_REG_EXP, "");
- if (url.split(opening).length > url.split(punctuation).length) {
- url = url + punctuation;
- punctuation = "";
- }
- var realUrl = url,
- displayUrl = url;
- if (url.length > MAX_DISPLAY_LENGTH) {
- displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "...";
- }
- // Add http prefix if necessary
- if (realUrl.substr(0, 4) === "www.") {
- realUrl = "http://" + realUrl;
- }
-
- return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation;
- });
- }
-
- /**
- * Creates or (if already cached) returns a temp element
- * for the given document object
- */
- function _getTempElement(context) {
- var tempElement = context._wysihtml5_tempElement;
- if (!tempElement) {
- tempElement = context._wysihtml5_tempElement = context.createElement("div");
- }
- return tempElement;
- }
-
- /**
- * Replaces the original text nodes with the newly auto-linked dom tree
- */
- function _wrapMatchesInNode(textNode) {
- var parentNode = textNode.parentNode,
- tempElement = _getTempElement(parentNode.ownerDocument);
-
- // We need to insert an empty/temporary <span /> to fix IE quirks
- // Elsewise IE would strip white space in the beginning
- tempElement.innerHTML = "<span></span>" + _convertUrlsToLinks(textNode.data);
- tempElement.removeChild(tempElement.firstChild);
-
- while (tempElement.firstChild) {
- // inserts tempElement.firstChild before textNode
- parentNode.insertBefore(tempElement.firstChild, textNode);
- }
- parentNode.removeChild(textNode);
- }
-
- function _hasParentThatShouldBeIgnored(node) {
- var nodeName;
- while (node.parentNode) {
- node = node.parentNode;
- nodeName = node.nodeName;
- if (IGNORE_URLS_IN.contains(nodeName)) {
- return true;
- } else if (nodeName === "body") {
- return false;
- }
- }
- return false;
- }
-
- function _parseNode(element) {
- if (IGNORE_URLS_IN.contains(element.nodeName)) {
- return;
- }
-
- if (element.nodeType === wysihtml5.TEXT_NODE && element.data.match(URL_REG_EXP)) {
- _wrapMatchesInNode(element);
- return;
- }
-
- var childNodes = wysihtml5.lang.array(element.childNodes).get(),
- childNodesLength = childNodes.length,
- i = 0;
-
- for (; i<childNodesLength; i++) {
- _parseNode(childNodes[i]);
- }
-
- return element;
- }
-
- wysihtml5.dom.autoLink = autoLink;
-
- // Reveal url reg exp to the outside
- wysihtml5.dom.autoLink.URL_REG_EXP = URL_REG_EXP;
- })(wysihtml5);(function(wysihtml5) {
- var supportsClassList = wysihtml5.browser.supportsClassList(),
- api = wysihtml5.dom;
-
- api.addClass = function(element, className) {
- if (supportsClassList) {
- return element.classList.add(className);
- }
- if (api.hasClass(element, className)) {
- return;
- }
- element.className += " " + className;
- };
-
- api.removeClass = function(element, className) {
- if (supportsClassList) {
- return element.classList.remove(className);
- }
-
- element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " ");
- };
-
- api.hasClass = function(element, className) {
- if (supportsClassList) {
- return element.classList.contains(className);
- }
-
- var elementClassName = element.className;
- return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
- };
- })(wysihtml5);
- wysihtml5.dom.contains = (function() {
- var documentElement = document.documentElement;
- if (documentElement.contains) {
- return function(container, element) {
- if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
- element = element.parentNode;
- }
- return container !== element && container.contains(element);
- };
- } else if (documentElement.compareDocumentPosition) {
- return function(container, element) {
- // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition
- return !!(container.compareDocumentPosition(element) & 16);
- };
- }
- })();/**
- * Converts an HTML fragment/element into a unordered/ordered list
- *
- * @param {Element} element The element which should be turned into a list
- * @param {String} listType The list type in which to convert the tree (either "ul" or "ol")
- * @return {Element} The created list
- *
- * @example
- * <!-- Assume the following dom: -->
- * <span id="pseudo-list">
- * eminem<br>
- * dr. dre
- * <div>50 Cent</div>
- * </span>
- *
- * <script>
- * wysihtml5.dom.convertToList(document.getElementById("pseudo-list"), "ul");
- * </script>
- *
- * <!-- Will result in: -->
- * <ul>
- * <li>eminem</li>
- * <li>dr. dre</li>
- * <li>50 Cent</li>
- * </ul>
- */
- wysihtml5.dom.convertToList = (function() {
- function _createListItem(doc, list) {
- var listItem = doc.createElement("li");
- list.appendChild(listItem);
- return listItem;
- }
-
- function _createList(doc, type) {
- return doc.createElement(type);
- }
-
- function convertToList(element, listType) {
- if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") {
- // Already a list
- return element;
- }
-
- var doc = element.ownerDocument,
- list = _createList(doc, listType),
- lineBreaks = element.querySelectorAll("br"),
- lineBreaksLength = lineBreaks.length,
- childNodes,
- childNodesLength,
- childNode,
- lineBreak,
- parentNode,
- isBlockElement,
- isLineBreak,
- currentListItem,
- i;
-
- // First find <br> at the end of inline elements and move them behind them
- for (i=0; i<lineBreaksLength; i++) {
- lineBreak = lineBreaks[i];
- while ((parentNode = lineBreak.parentNode) && parentNode !== element && parentNode.lastChild === lineBreak) {
- if (wysihtml5.dom.getStyle("display").from(parentNode) === "block") {
- parentNode.removeChild(lineBreak);
- break;
- }
- wysihtml5.dom.insert(lineBreak).after(lineBreak.parentNode);
- }
- }
-
- childNodes = wysihtml5.lang.array(element.childNodes).get();
- childNodesLength = childNodes.length;
-
- for (i=0; i<childNodesLength; i++) {
- currentListItem = currentListItem || _createListItem(doc, list);
- childNode = childNodes[i];
- isBlockElement = wysihtml5.dom.getStyle("display").from(childNode) === "block";
- isLineBreak = childNode.nodeName === "BR";
-
- if (isBlockElement) {
- // Append blockElement to current <li> if empty, otherwise create a new one
- currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem;
- currentListItem.appendChild(childNode);
- currentListItem = null;
- continue;
- }
-
- if (isLineBreak) {
- // Only create a new list item in the next iteration when the current one has already content
- currentListItem = currentListItem.firstChild ? null : currentListItem;
- continue;
- }
-
- currentListItem.appendChild(childNode);
- }
-
- element.parentNode.replaceChild(list, element);
- return list;
- }
-
- return convertToList;
- })();/**
- * Copy a set of attributes from one element to another
- *
- * @param {Array} attributesToCopy List of attributes which should be copied
- * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
- * copy the attributes from., this again returns an object which provides a method named "to" which can be invoked
- * with the element where to copy the attributes to (see example)
- *
- * @example
- * var textarea = document.querySelector("textarea"),
- * div = document.querySelector("div[contenteditable=true]"),
- * anotherDiv = document.querySelector("div.preview");
- * wysihtml5.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv);
- *
- */
- wysihtml5.dom.copyAttributes = function(attributesToCopy) {
- return {
- from: function(elementToCopyFrom) {
- return {
- to: function(elementToCopyTo) {
- var attribute,
- i = 0,
- length = attributesToCopy.length;
- for (; i<length; i++) {
- attribute = attributesToCopy[i];
- if (typeof(elementToCopyFrom[attribute]) !== "undefined" && elementToCopyFrom[attribute] !== "") {
- elementToCopyTo[attribute] = elementToCopyFrom[attribute];
- }
- }
- return { andTo: arguments.callee };
- }
- };
- }
- };
- };/**
- * Copy a set of styles from one element to another
- * Please note that this only works properly across browsers when the element from which to copy the styles
- * is in the dom
- *
- * Interesting article on how to copy styles
- *
- * @param {Array} stylesToCopy List of styles which should be copied
- * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
- * copy the styles from., this again returns an object which provides a method named "to" which can be invoked
- * with the element where to copy the styles to (see example)
- *
- * @example
- * var textarea = document.querySelector("textarea"),
- * div = document.querySelector("div[contenteditable=true]"),
- * anotherDiv = document.querySelector("div.preview");
- * wysihtml5.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv);
- *
- */
- (function(dom) {
-
- /**
- * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set
- * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then
- * its computed css width will be 198px
- */
- var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"];
-
- var shouldIgnoreBoxSizingBorderBox = function(element) {
- if (hasBoxSizingBorderBox(element)) {
- return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth;
- }
- return false;
- };
-
- var hasBoxSizingBorderBox = function(element) {
- var i = 0,
- length = BOX_SIZING_PROPERTIES.length;
- for (; i<length; i++) {
- if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") {
- return BOX_SIZING_PROPERTIES[i];
- }
- }
- };
-
- dom.copyStyles = function(stylesToCopy) {
- return {
- from: function(element) {
- if (shouldIgnoreBoxSizingBorderBox(element)) {
- stylesToCopy = wysihtml5.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES);
- }
-
- var cssText = "",
- length = stylesToCopy.length,
- i = 0,
- property;
- for (; i<length; i++) {
- property = stylesToCopy[i];
- cssText += property + ":" + dom.getStyle(property).from(element) + ";";
- }
-
- return {
- to: function(element) {
- dom.setStyles(cssText).on(element);
- return { andTo: arguments.callee };
- }
- };
- }
- };
- };
- })(wysihtml5.dom);/**
- * Event Delegation
- *
- * @example
- * wysihtml5.dom.delegate(document.body, "a", "click", function() {
- * // foo
- * });
- */
- (function(wysihtml5) {
-
- wysihtml5.dom.delegate = function(container, selector, eventName, handler) {
- return wysihtml5.dom.observe(container, eventName, function(event) {
- var target = event.target,
- match = wysihtml5.lang.array(container.querySelectorAll(selector));
-
- while (target && target !== container) {
- if (match.contains(target)) {
- handler.call(target, event);
- break;
- }
- target = target.parentNode;
- }
- });
- };
-
- })(wysihtml5);/**
- * Returns the given html wrapped in a div element
- *
- * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly
- * when inserted via innerHTML
- *
- * @param {String} html The html which should be wrapped in a dom element
- * @param {Obejct} [context] Document object of the context the html belongs to
- *
- * @example
- * wysihtml5.dom.getAsDom("<article>foo</article>");
- */
- wysihtml5.dom.getAsDom = (function() {
-
- var _innerHTMLShiv = function(html, context) {
- var tempElement = context.createElement("div");
- tempElement.style.display = "none";
- context.body.appendChild(tempElement);
- // IE throws an exception when trying to insert <frameset></frameset> via innerHTML
- try { tempElement.innerHTML = html; } catch(e) {}
- context.body.removeChild(tempElement);
- return tempElement;
- };
-
- /**
- * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element
- */
- var _ensureHTML5Compatibility = function(context) {
- if (context._wysihtml5_supportsHTML5Tags) {
- return;
- }
- for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) {
- context.createElement(HTML5_ELEMENTS[i]);
- }
- context._wysihtml5_supportsHTML5Tags = true;
- };
-
-
- /**
- * List of html5 tags
- * taken from http://simon.html5.org/html5-elements
- */
- var HTML5_ELEMENTS = [
- "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption",
- "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress",
- "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr"
- ];
-
- return function(html, context) {
- context = context || document;
- var tempElement;
- if (typeof(html) === "object" && html.nodeType) {
- tempElement = context.createElement("div");
- tempElement.appendChild(html);
- } else if (wysihtml5.browser.supportsHTML5Tags(context)) {
- tempElement = context.createElement("div");
- tempElement.innerHTML = html;
- } else {
- _ensureHTML5Compatibility(context);
- tempElement = _innerHTMLShiv(html, context);
- }
- return tempElement;
- };
- })();/**
- * Walks the dom tree from the given node up until it finds a match
- * Designed for optimal performance.
- *
- * @param {Element} node The from which to check the parent nodes
- * @param {Object} matchingSet Object to match against (possible properties: nodeName, className, classRegExp)
- * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50)
- * @return {null|Element} Returns the first element that matched the desiredNodeName(s)
- * @example
- * var listElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: ["MENU", "UL", "OL"] });
- * // ... or ...
- * var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" });
- * // ... or ...
- * var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g });
- */
- wysihtml5.dom.getParentElement = (function() {
-
- function _isSameNodeName(nodeName, desiredNodeNames) {
- if (!desiredNodeNames || !desiredNodeNames.length) {
- return true;
- }
-
- if (typeof(desiredNodeNames) === "string") {
- return nodeName === desiredNodeNames;
- } else {
- return wysihtml5.lang.array(desiredNodeNames).contains(nodeName);
- }
- }
-
- function _isElement(node) {
- return node.nodeType === wysihtml5.ELEMENT_NODE;
- }
-
- function _hasClassName(element, className, classRegExp) {
- var classNames = (element.className || "").match(classRegExp) || [];
- if (!className) {
- return !!classNames.length;
- }
- return classNames[classNames.length - 1] === className;
- }
-
- function _getParentElementWithNodeName(node, nodeName, levels) {
- while (levels-- && node && node.nodeName !== "BODY") {
- if (_isSameNodeName(node.nodeName, nodeName)) {
- return node;
- }
- node = node.parentNode;
- }
- return null;
- }
-
- function _getParentElementWithNodeNameAndClassName(node, nodeName, className, classRegExp, levels) {
- while (levels-- && node && node.nodeName !== "BODY") {
- if (_isElement(node) &&
- _isSameNodeName(node.nodeName, nodeName) &&
- _hasClassName(node, className, classRegExp)) {
- return node;
- }
- node = node.parentNode;
- }
- return null;
- }
-
- return function(node, matchingSet, levels) {
- levels = levels || 50; // Go max 50 nodes upwards from current node
- if (matchingSet.className || matchingSet.classRegExp) {
- return _getParentElementWithNodeNameAndClassName(
- node, matchingSet.nodeName, matchingSet.className, matchingSet.classRegExp, levels
- );
- } else {
- return _getParentElementWithNodeName(
- node, matchingSet.nodeName, levels
- );
- }
- };
- })();
- /**
- * Get element's style for a specific css property
- *
- * @param {Element} element The element on which to retrieve the style
- * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...)
- *
- * @example
- * wysihtml5.dom.getStyle("display").from(document.body);
- * // => "block"
- */
- wysihtml5.dom.getStyle = (function() {
- var stylePropertyMapping = {
- "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat"
- },
- REG_EXP_CAMELIZE = /\-[a-z]/g;
-
- function camelize(str) {
- return str.replace(REG_EXP_CAMELIZE, function(match) {
- return match.charAt(1).toUpperCase();
- });
- }
-
- return function(property) {
- return {
- from: function(element) {
- if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
- return;
- }
-
- var doc = element.ownerDocument,
- camelizedProperty = stylePropertyMapping[property] || camelize(property),
- style = element.style,
- currentStyle = element.currentStyle,
- styleValue = style[camelizedProperty];
- if (styleValue) {
- return styleValue;
- }
-
- // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant
- // window.getComputedStyle, since it returns css property values in their original unit:
- // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle
- // gives you the original "50%".
- // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio
- if (currentStyle) {
- try {
- return currentStyle[camelizedProperty];
- } catch(e) {
- //ie will occasionally fail for unknown reasons. swallowing exception
- }
- }
- var win = doc.defaultView || doc.parentWindow,
- needsOverflowReset = (property === "height" || property === "width") && element.nodeName === "TEXTAREA",
- originalOverflow,
- returnValue;
- if (win.getComputedStyle) {
- // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars
- // therfore we remove and restore the scrollbar and calculate the value in between
- if (needsOverflowReset) {
- originalOverflow = style.overflow;
- style.overflow = "hidden";
- }
- returnValue = win.getComputedStyle(element, null).getPropertyValue(property);
- if (needsOverflowReset) {
- style.overflow = originalOverflow || "";
- }
- return returnValue;
- }
- }
- };
- };
- })();/**
- * High performant way to check whether an element with a specific tag name is in the given document
- * Optimized for being heavily executed
- * Unleashes the power of live node lists
- *
- * @param {Object} doc The document object of the context where to check
- * @param {String} tagName Upper cased tag name
- * @example
- * wysihtml5.dom.hasElementWithTagName(document, "IMG");
- */
- wysihtml5.dom.hasElementWithTagName = (function() {
- var LIVE_CACHE = {},
- DOCUMENT_IDENTIFIER = 1;
-
- function _getDocumentIdentifier(doc) {
- return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
- }
-
- return function(doc, tagName) {
- var key = _getDocumentIdentifier(doc) + ":" + tagName,
- cacheEntry = LIVE_CACHE[key];
- if (!cacheEntry) {
- cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName);
- }
-
- return cacheEntry.length > 0;
- };
- })();/**
- * High performant way to check whether an element with a specific class name is in the given document
- * Optimized for being heavily executed
- * Unleashes the power of live node lists
- *
- * @param {Object} doc The document object of the context where to check
- * @param {String} tagName Upper cased tag name
- * @example
- * wysihtml5.dom.hasElementWithClassName(document, "foobar");
- */
- (function(wysihtml5) {
- var LIVE_CACHE = {},
- DOCUMENT_IDENTIFIER = 1;
- function _getDocumentIdentifier(doc) {
- return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
- }
-
- wysihtml5.dom.hasElementWithClassName = function(doc, className) {
- // getElementsByClassName is not supported by IE<9
- // but is sometimes mocked via library code (which then doesn't return live node lists)
- if (!wysihtml5.browser.supportsNativeGetElementsByClassName()) {
- return !!doc.querySelector("." + className);
- }
- var key = _getDocumentIdentifier(doc) + ":" + className,
- cacheEntry = LIVE_CACHE[key];
- if (!cacheEntry) {
- cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className);
- }
- return cacheEntry.length > 0;
- };
- })(wysihtml5);
- wysihtml5.dom.insert = function(elementToInsert) {
- return {
- after: function(element) {
- element.parentNode.insertBefore(elementToInsert, element.nextSibling);
- },
-
- before: function(element) {
- element.parentNode.insertBefore(elementToInsert, element);
- },
-
- into: function(element) {
- element.appendChild(elementToInsert);
- }
- };
- };wysihtml5.dom.insertCSS = function(rules) {
- rules = rules.join("\n");
-
- return {
- into: function(doc) {
- var head = doc.head || doc.getElementsByTagName("head")[0],
- styleElement = doc.createElement("style");
- styleElement.type = "text/css";
- if (styleElement.styleSheet) {
- styleElement.styleSheet.cssText = rules;
- } else {
- styleElement.appendChild(doc.createTextNode(rules));
- }
- if (head) {
- head.appendChild(styleElement);
- }
- }
- };
- };/**
- * Method to set dom events
- *
- * @example
- * wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... });
- */
- wysihtml5.dom.observe = function(element, eventNames, handler) {
- eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames;
-
- var handlerWrapper,
- eventName,
- i = 0,
- length = eventNames.length;
-
- for (; i<length; i++) {
- eventName = eventNames[i];
- if (element.addEventListener) {
- element.addEventListener(eventName, handler, false);
- } else {
- handlerWrapper = function(event) {
- if (!("target" in event)) {
- event.target = event.srcElement;
- }
- event.preventDefault = event.preventDefault || function() {
- this.returnValue = false;
- };
- event.stopPropagation = event.stopPropagation || function() {
- this.cancelBubble = true;
- };
- handler.call(element, event);
- };
- element.attachEvent("on" + eventName, handlerWrapper);
- }
- }
-
- return {
- stop: function() {
- var eventName,
- i = 0,
- length = eventNames.length;
- for (; i<length; i++) {
- eventName = eventNames[i];
- if (element.removeEventListener) {
- element.removeEventListener(eventName, handler, false);
- } else {
- element.detachEvent("on" + eventName, handlerWrapper);
- }
- }
- }
- };
- };
- /**
- * HTML Sanitizer
- * Rewrites the HTML based on given rules
- *
- * @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized
- * @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will
- * be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the
- * desired substitution.
- * @param {Object} context Document object in which to parse the html, needed to sandbox the parsing
- *
- * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element.
- *
- * @example
- * var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>';
- * wysihtml5.dom.parse(userHTML, {
- * tags {
- * p: "div", // Rename p tags to div tags
- * font: "span" // Rename font tags to span tags
- * div: true, // Keep them, also possible (same result when passing: "div" or true)
- * script: undefined // Remove script elements
- * }
- * });
- * // => <div><div><span>foo bar</span></div></div>
- *
- * var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>';
- * wysihtml5.dom.parse(userHTML);
- * // => '<span><span><span><span>I'm a table!</span></span></span></span>'
- *
- * var userHTML = '<div>foobar<br>foobar</div>';
- * wysihtml5.dom.parse(userHTML, {
- * tags: {
- * div: undefined,
- * br: true
- * }
- * });
- * // => ''
- *
- * var userHTML = '<div class="red">foo</div><div class="pink">bar</div>';
- * wysihtml5.dom.parse(userHTML, {
- * classes: {
- * red: 1,
- * green: 1
- * },
- * tags: {
- * div: {
- * rename_tag: "p"
- * }
- * }
- * });
- * // => '<p class="red">foo</p><p>bar</p>'
- */
- wysihtml5.dom.parse = (function() {
-
- /**
- * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML
- * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the
- * node isn't closed
- *
- * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML.
- */
- var NODE_TYPE_MAPPING = {
- "1": _handleElement,
- "3": _handleText
- },
- // Rename unknown tags to this
- DEFAULT_NODE_NAME = "span",
- WHITE_SPACE_REG_EXP = /\s+/,
- defaultRules = { tags: {}, classes: {} },
- currentRules = {};
-
- /**
- * Iterates over all childs of the element, recreates them, appends them into a document fragment
- * which later replaces the entire body content
- */
- function parse(elementOrHtml, rules, context, cleanUp) {
- wysihtml5.lang.object(currentRules).merge(defaultRules).merge(rules).get();
-
- context = context || elementOrHtml.ownerDocument || document;
- var fragment = context.createDocumentFragment(),
- isString = typeof(elementOrHtml) === "string",
- element,
- newNode,
- firstChild;
-
- if (isString) {
- element = wysihtml5.dom.getAsDom(elementOrHtml, context);
- } else {
- element = elementOrHtml;
- }
-
- while (element.firstChild) {
- firstChild = element.firstChild;
- element.removeChild(firstChild);
- newNode = _convert(firstChild, cleanUp);
- if (newNode) {
- fragment.appendChild(newNode);
- }
- }
-
- // Clear element contents
- element.innerHTML = "";
-
- // Insert new DOM tree
- element.appendChild(fragment);
-
- return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element;
- }
-
- function _convert(oldNode, cleanUp) {
- var oldNodeType = oldNode.nodeType,
- oldChilds = oldNode.childNodes,
- oldChildsLength = oldChilds.length,
- newNode,
- method = NODE_TYPE_MAPPING[oldNodeType],
- i = 0;
-
- newNode = method && method(oldNode);
-
- if (!newNode) {
- return null;
- }
-
- for (i=0; i<oldChildsLength; i++) {
- newChild = _convert(oldChilds[i], cleanUp);
- if (newChild) {
- newNode.appendChild(newChild);
- }
- }
-
- // Cleanup senseless <span> elements
- if (cleanUp &&
- newNode.childNodes.length <= 1 &&
- newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME &&
- !newNode.attributes.length) {
- return newNode.firstChild;
- }
-
- return newNode;
- }
-
- function _handleElement(oldNode) {
- var rule,
- newNode,
- endTag,
- tagRules = currentRules.tags,
- nodeName = oldNode.nodeName.toLowerCase(),
- scopeName = oldNode.scopeName;
-
- /**
- * We already parsed that element
- * ignore it! (yes, this sometimes happens in IE8 when the html is invalid)
- */
- if (oldNode._wysihtml5) {
- return null;
- }
- oldNode._wysihtml5 = 1;
-
- if (oldNode.className === "wysihtml5-temp") {
- return null;
- }
-
- /**
- * IE is the only browser who doesn't include the namespace in the
- * nodeName, that's why we have to prepend it by ourselves
- * scopeName is a proprietary IE feature
- * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx
- */
- if (scopeName && scopeName != "HTML") {
- nodeName = scopeName + ":" + nodeName;
- }
-
- /**
- * Repair node
- * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags
- * A <p> doesn't need to be closed according HTML4-5 spec, we simply replace it with a <div> to preserve its content and layout
- */
- if ("outerHTML" in oldNode) {
- if (!wysihtml5.browser.autoClosesUnclosedTags() &&
- oldNode.nodeName === "P" &&
- oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") {
- nodeName = "div";
- }
- }
-
- if (nodeName in tagRules) {
- rule = tagRules[nodeName];
- if (!rule || rule.remove) {
- return null;
- }
-
- rule = typeof(rule) === "string" ? { rename_tag: rule } : rule;
- } else if (oldNode.firstChild) {
- rule = { rename_tag: DEFAULT_NODE_NAME };
- } else {
- // Remove empty unknown elements
- return null;
- }
-
- newNode = oldNode.ownerDocument.createElement(rule.rename_tag || nodeName);
- _handleAttributes(oldNode, newNode, rule);
-
- oldNode = null;
- return newNode;
- }
-
- function _handleAttributes(oldNode, newNode, rule) {
- var attributes = {}, // fresh new set of attributes to set on newNode
- setClass = rule.set_class, // classes to set
- addClass = rule.add_class, // add classes based on existing attributes
- setAttributes = rule.set_attributes, // attributes to set on the current node
- checkAttributes = rule.check_attributes, // check/convert values of attributes
- allowedClasses = currentRules.classes,
- i = 0,
- classes = [],
- newClasses = [],
- newUniqueClasses = [],
- oldClasses = [],
- classesLength,
- newClassesLength,
- currentClass,
- newClass,
- attributeName,
- newAttributeValue,
- method;
-
- if (setAttributes) {
- attributes = wysihtml5.lang.object(setAttributes).clone();
- }
-
- if (checkAttributes) {
- for (attributeName in checkAttributes) {
- method = attributeCheckMethods[checkAttributes[attributeName]];
- if (!method) {
- continue;
- }
- newAttributeValue = method(_getAttribute(oldNode, attributeName));
- if (typeof(newAttributeValue) === "string") {
- attributes[attributeName] = newAttributeValue;
- }
- }
- }
-
- if (setClass) {
- classes.push(setClass);
- }
-
- if (addClass) {
- for (attributeName in addClass) {
- method = addClassMethods[addClass[attributeName]];
- if (!method) {
- continue;
- }
- newClass = method(_getAttribute(oldNode, attributeName));
- if (typeof(newClass) === "string") {
- classes.push(newClass);
- }
- }
- }
-
- // make sure that wysihtml5 temp class doesn't get stripped out
- allowedClasses["_wysihtml5-temp-placeholder"] = 1;
-
- // add old classes last
- oldClasses = oldNode.getAttribute("class");
- if (oldClasses) {
- classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
- }
- classesLength = classes.length;
- for (; i<classesLength; i++) {
- currentClass = classes[i];
- if (allowedClasses[currentClass]) {
- newClasses.push(currentClass);
- }
- }
-
- // remove duplicate entries and preserve class specificity
- newClassesLength = newClasses.length;
- while (newClassesLength--) {
- currentClass = newClasses[newClassesLength];
- if (!wysihtml5.lang.array(newUniqueClasses).contains(currentClass)) {
- newUniqueClasses.unshift(currentClass);
- }
- }
-
- if (newUniqueClasses.length) {
- attributes["class"] = newUniqueClasses.join(" ");
- }
-
- // set attributes on newNode
- for (attributeName in attributes) {
- // Setting attributes can cause a js error in IE under certain circumstances
- // eg. on a <img> under https when it's new attribute value is non-https
- // TODO: Investigate this further and check for smarter handling
- try {
- newNode.setAttribute(attributeName, attributes[attributeName]);
- } catch(e) {}
- }
-
- // IE8 sometimes loses the width/height attributes when those are set before the "src"
- // so we make sure to set them again
- if (attributes.src) {
- if (typeof(attributes.width) !== "undefined") {
- newNode.setAttribute("width", attributes.width);
- }
- if (typeof(attributes.height) !== "undefined") {
- newNode.setAttribute("height", attributes.height);
- }
- }
- }
-
- /**
- * IE gives wrong results for hasAttribute/getAttribute, for example:
- * var td = document.createElement("td");
- * td.getAttribute("rowspan"); // => "1" in IE
- *
- * Therefore we have to check the element's outerHTML for the attribute
- */
- var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly();
- function _getAttribute(node, attributeName) {
- attributeName = attributeName.toLowerCase();
- var nodeName = node.nodeName;
- if (nodeName == "IMG" && attributeName == "src" && _isLoadedImage(node) === true) {
- // Get 'src' attribute value via object property since this will always contain the
- // full absolute url (http://...)
- // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host
- // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url)
- return node.src;
- } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) {
- // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML
- var outerHTML = node.outerHTML.toLowerCase(),
- // TODO: This might not work for attributes without value: <input disabled>
- hasAttribute = outerHTML.indexOf(" " + attributeName + "=") != -1;
-
- return hasAttribute ? node.getAttribute(attributeName) : null;
- } else{
- return node.getAttribute(attributeName);
- }
- }
-
- /**
- * Check whether the given node is a proper loaded image
- * FIXME: Returns undefined when unknown (Chrome, Safari)
- */
- function _isLoadedImage(node) {
- try {
- return node.complete && !node.mozMatchesSelector(":-moz-broken");
- } catch(e) {
- if (node.complete && node.readyState === "complete") {
- return true;
- }
- }
- }
-
- function _handleText(oldNode) {
- return oldNode.ownerDocument.createTextNode(oldNode.data);
- }
-
-
- // ------------ attribute checks ------------ \\
- var attributeCheckMethods = {
- url: (function() {
- var REG_EXP = /^https?:\/\//i;
- return function(attributeValue) {
- if (!attributeValue || !attributeValue.match(REG_EXP)) {
- return null;
- }
- return attributeValue.replace(REG_EXP, function(match) {
- return match.toLowerCase();
- });
- };
- })(),
-
- alt: (function() {
- var REG_EXP = /[^ a-z0-9_\-]/gi;
- return function(attributeValue) {
- if (!attributeValue) {
- return "";
- }
- return attributeValue.replace(REG_EXP, "");
- };
- })(),
-
- numbers: (function() {
- var REG_EXP = /\D/g;
- return function(attributeValue) {
- attributeValue = (attributeValue || "").replace(REG_EXP, "");
- return attributeValue || null;
- };
- })()
- };
-
- // ------------ class converter (converts an html attribute to a class name) ------------ \\
- var addClassMethods = {
- align_img: (function() {
- var mapping = {
- left: "wysiwyg-float-left",
- right: "wysiwyg-float-right"
- };
- return function(attributeValue) {
- return mapping[String(attributeValue).toLowerCase()];
- };
- })(),
-
- align_text: (function() {
- var mapping = {
- left: "wysiwyg-text-align-left",
- right: "wysiwyg-text-align-right",
- center: "wysiwyg-text-align-center",
- justify: "wysiwyg-text-align-justify"
- };
- return function(attributeValue) {
- return mapping[String(attributeValue).toLowerCase()];
- };
- })(),
-
- clear_br: (function() {
- var mapping = {
- left: "wysiwyg-clear-left",
- right: "wysiwyg-clear-right",
- both: "wysiwyg-clear-both",
- all: "wysiwyg-clear-both"
- };
- return function(attributeValue) {
- return mapping[String(attributeValue).toLowerCase()];
- };
- })(),
-
- size_font: (function() {
- var mapping = {
- "1": "wysiwyg-font-size-xx-small",
- "2": "wysiwyg-font-size-small",
- "3": "wysiwyg-font-size-medium",
- "4": "wysiwyg-font-size-large",
- "5": "wysiwyg-font-size-x-large",
- "6": "wysiwyg-font-size-xx-large",
- "7": "wysiwyg-font-size-xx-large",
- "-": "wysiwyg-font-size-smaller",
- "+": "wysiwyg-font-size-larger"
- };
- return function(attributeValue) {
- return mapping[String(attributeValue).charAt(0)];
- };
- })()
- };
-
- return parse;
- })();/**
- * Checks for empty text node childs and removes them
- *
- * @param {Element} node The element in which to cleanup
- * @example
- * wysihtml5.dom.removeEmptyTextNodes(element);
- */
- wysihtml5.dom.removeEmptyTextNodes = function(node) {
- var childNode,
- childNodes = wysihtml5.lang.array(node.childNodes).get(),
- childNodesLength = childNodes.length,
- i = 0;
- for (; i<childNodesLength; i++) {
- childNode = childNodes[i];
- if (childNode.nodeType === wysihtml5.TEXT_NODE && childNode.data === "") {
- childNode.parentNode.removeChild(childNode);
- }
- }
- };
- /**
- * Renames an element (eg. a <div> to a <p>) and keeps its childs
- *
- * @param {Element} element The list element which should be renamed
- * @param {Element} newNodeName The desired tag name
- *
- * @example
- * <!-- Assume the following dom: -->
- * <ul id="list">
- * <li>eminem</li>
- * <li>dr. dre</li>
- * <li>50 Cent</li>
- * </ul>
- *
- * <script>
- * wysihtml5.dom.renameElement(document.getElementById("list"), "ol");
- * </script>
- *
- * <!-- Will result in: -->
- * <ol>
- * <li>eminem</li>
- * <li>dr. dre</li>
- * <li>50 Cent</li>
- * </ol>
- */
- wysihtml5.dom.renameElement = function(element, newNodeName) {
- var newElement = element.ownerDocument.createElement(newNodeName),
- firstChild;
- while (firstChild = element.firstChild) {
- newElement.appendChild(firstChild);
- }
- wysihtml5.dom.copyAttributes(["align", "className"]).from(element).to(newElement);
- element.parentNode.replaceChild(newElement, element);
- return newElement;
- };/**
- * Takes an element, removes it and replaces it with it's childs
- *
- * @param {Object} node The node which to replace with it's child nodes
- * @example
- * <div id="foo">
- * <span>hello</span>
- * </div>
- * <script>
- * // Remove #foo and replace with it's children
- * wysihtml5.dom.replaceWithChildNodes(document.getElementById("foo"));
- * </script>
- */
- wysihtml5.dom.replaceWithChildNodes = function(node) {
- if (!node.parentNode) {
- return;
- }
-
- if (!node.firstChild) {
- node.parentNode.removeChild(node);
- return;
- }
-
- var fragment = node.ownerDocument.createDocumentFragment();
- while (node.firstChild) {
- fragment.appendChild(node.firstChild);
- }
- node.parentNode.replaceChild(fragment, node);
- node = fragment = null;
- };
- /**
- * Unwraps an unordered/ordered list
- *
- * @param {Element} element The list element which should be unwrapped
- *
- * @example
- * <!-- Assume the following dom: -->
- * <ul id="list">
- * <li>eminem</li>
- * <li>dr. dre</li>
- * <li>50 Cent</li>
- * </ul>
- *
- * <script>
- * wysihtml5.dom.resolveList(document.getElementById("list"));
- * </script>
- *
- * <!-- Will result in: -->
- * eminem<br>
- * dr. dre<br>
- * 50 Cent<br>
- */
- (function(dom) {
- function _isBlockElement(node) {
- return dom.getStyle("display").from(node) === "block";
- }
-
- function _isLineBreak(node) {
- return node.nodeName === "BR";
- }
-
- function _appendLineBreak(element) {
- var lineBreak = element.ownerDocument.createElement("br");
- element.appendChild(lineBreak);
- }
-
- function resolveList(list) {
- if (list.nodeName !== "MENU" && list.nodeName !== "UL" && list.nodeName !== "OL") {
- return;
- }
-
- var doc = list.ownerDocument,
- fragment = doc.createDocumentFragment(),
- previousSibling = list.previousElementSibling || list.previousSibling,
- firstChild,
- lastChild,
- isLastChild,
- shouldAppendLineBreak,
- listItem;
-
- if (previousSibling && !_isBlockElement(previousSibling)) {
- _appendLineBreak(fragment);
- }
-
- while (listItem = list.firstChild) {
- lastChild = listItem.lastChild;
- while (firstChild = listItem.firstChild) {
- isLastChild = firstChild === lastChild;
- // This needs to be done before appending it to the fragment, as it otherwise will loose style information
- shouldAppendLineBreak = isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild);
- fragment.appendChild(firstChild);
- if (shouldAppendLineBreak) {
- _appendLineBreak(fragment);
- }
- }
-
- listItem.parentNode.removeChild(listItem);
- }
- list.parentNode.replaceChild(fragment, list);
- }
-
- dom.resolveList = resolveList;
- })(wysihtml5.dom);/**
- * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way
- *
- * Browser Compatibility:
- * - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted"
- * - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...)
- *
- * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons:
- * - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'")
- * - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...)
- * - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire
- * - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe
- * can do anything as if the sandbox attribute wasn't set
- *
- * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready
- * @param {Object} [config] Optional parameters
- *
- * @example
- * new wysihtml5.dom.Sandbox(function(sandbox) {
- * sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">';
- * });
- */
- (function(wysihtml5) {
- var /**
- * Default configuration
- */
- doc = document,
- /**
- * Properties to unset/protect on the window object
- */
- windowProperties = [
- "parent", "top", "opener", "frameElement", "frames",
- "localStorage", "globalStorage", "sessionStorage", "indexedDB"
- ],
- /**
- * Properties on the window object which are set to an empty function
- */
- windowProperties2 = [
- "open", "close", "openDialog", "showModalDialog",
- "alert", "confirm", "prompt",
- "openDatabase", "postMessage",
- "XMLHttpRequest", "XDomainRequest"
- ],
- /**
- * Properties to unset/protect on the document object
- */
- documentProperties = [
- "referrer",
- "write", "open", "close"
- ];
-
- wysihtml5.dom.Sandbox = Base.extend(
- /** @scope wysihtml5.dom.Sandbox.prototype */ {
- constructor: function(readyCallback, config) {
- this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION;
- this.config = wysihtml5.lang.object({}).merge(config).get();
- this.iframe = this._createIframe();
- },
-
- insertInto: function(element) {
- if (typeof(element) === "string") {
- element = doc.getElementById(element);
- }
-
- element.appendChild(this.iframe);
- },
- getIframe: function() {
- return this.iframe;
- },
- getWindow: function() {
- this._readyError();
- },
- getDocument: function() {
- this._readyError();
- },
- destroy: function() {
- var iframe = this.getIframe();
- iframe.parentNode.removeChild(iframe);
- },
- _readyError: function() {
- throw new Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet");
- },
- /**
- * Creates the sandbox iframe
- *
- * Some important notes:
- * - We can't use HTML5 sandbox for now:
- * setting it causes that the iframe's dom can't be accessed from the outside
- * Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom
- * But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired.
- * In order to make this happen we need to set the "allow-scripts" flag.
- * A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all.
- * - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document)
- * - IE needs to have the security="restricted" attribute set before the iframe is
- * inserted into the dom tree
- * - Believe it or not but in IE "security" in document.createElement("iframe") is false, even
- * though it supports it
- * - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore
- * - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely
- * on the onreadystatechange event
- */
- _createIframe: function() {
- var that = this,
- iframe = doc.createElement("iframe");
- iframe.className = "wysihtml5-sandbox";
- wysihtml5.dom.setAttributes({
- "security": "restricted",
- "allowtransparency": "true",
- "frameborder": 0,
- "width": 0,
- "height": 0,
- "marginwidth": 0,
- "marginheight": 0
- }).on(iframe);
- // Setting the src like this prevents ssl warnings in IE6
- if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
- iframe.src = "javascript:'<html></html>'";
- }
- iframe.onload = function() {
- iframe.onreadystatechange = iframe.onload = null;
- that._onLoadIframe(iframe);
- };
- iframe.onreadystatechange = function() {
- if (/loaded|complete/.test(iframe.readyState)) {
- iframe.onreadystatechange = iframe.onload = null;
- that._onLoadIframe(iframe);
- }
- };
- return iframe;
- },
- /**
- * Callback for when the iframe has finished loading
- */
- _onLoadIframe: function(iframe) {
- // don't resume when the iframe got unloaded (eg. by removing it from the dom)
- if (!wysihtml5.dom.contains(doc.documentElement, iframe)) {
- return;
- }
- var that = this,
- iframeWindow = iframe.contentWindow,
- iframeDocument = iframe.contentWindow.document,
- charset = doc.characterSet || doc.charset || "utf-8",
- sandboxHtml = this._getHtml({
- charset: charset,
- stylesheets: this.config.stylesheets
- });
- // Create the basic dom tree including proper DOCTYPE and charset
- iframeDocument.open("text/html", "replace");
- iframeDocument.write(sandboxHtml);
- iframeDocument.close();
- this.getWindow = function() { return iframe.contentWindow; };
- this.getDocument = function() { return iframe.contentWindow.document; };
- // Catch js errors and pass them to the parent's onerror event
- // addEventListener("error") doesn't work properly in some browsers
- // TODO: apparently this doesn't work in IE9!
- iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {
- throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber);
- };
- if (!wysihtml5.browser.supportsSandboxedIframes()) {
- // Unset a bunch of sensitive variables
- // Please note: This isn't hack safe!
- // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information
- // IE is secure though, which is the most important thing, since IE is the only browser, who
- // takes over scripts & styles into contentEditable elements when copied from external websites
- // or applications (Microsoft Word, ...)
- var i, length;
- for (i=0, length=windowProperties.length; i<length; i++) {
- this._unset(iframeWindow, windowProperties[i]);
- }
- for (i=0, length=windowProperties2.length; i<length; i++) {
- this._unset(iframeWindow, windowProperties2[i], wysihtml5.EMPTY_FUNCTION);
- }
- for (i=0, length=documentProperties.length; i<length; i++) {
- this._unset(iframeDocument, documentProperties[i]);
- }
- // This doesn't work in Safari 5
- // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit
- this._unset(iframeDocument, "cookie", "", true);
- }
- this.loaded = true;
- // Trigger the callback
- setTimeout(function() { that.callback(that); }, 0);
- },
- _getHtml: function(templateVars) {
- var stylesheets = templateVars.stylesheets,
- html = "",
- i = 0,
- length;
- stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets;
- if (stylesheets) {
- length = stylesheets.length;
- for (; i<length; i++) {
- html += '<link rel="stylesheet" href="' + stylesheets[i] + '">';
- }
- }
- templateVars.stylesheets = html;
- return wysihtml5.lang.string(
- '<!DOCTYPE html><html><head>'
- + '<meta charset="#{charset}">#{stylesheets}</head>'
- + '<body></body></html>'
- ).interpolate(templateVars);
- },
- /**
- * Method to unset/override existing variables
- * @example
- * // Make cookie unreadable and unwritable
- * this._unset(document, "cookie", "", true);
- */
- _unset: function(object, property, value, setter) {
- try { object[property] = value; } catch(e) {}
- try { object.__defineGetter__(property, function() { return value; }); } catch(e) {}
- if (setter) {
- try { object.__defineSetter__(property, function() {}); } catch(e) {}
- }
- if (!wysihtml5.browser.crashesWhenDefineProperty(property)) {
- try {
- var config = {
- get: function() { return value; }
- };
- if (setter) {
- config.set = function() {};
- }
- Object.defineProperty(object, property, config);
- } catch(e) {}
- }
- }
- });
- })(wysihtml5);
- (function() {
- var mapping = {
- "className": "class"
- };
- wysihtml5.dom.setAttributes = function(attributes) {
- return {
- on: function(element) {
- for (var i in attributes) {
- element.setAttribute(mapping[i] || i, attributes[i]);
- }
- }
- }
- };
- })();wysihtml5.dom.setStyles = function(styles) {
- return {
- on: function(element) {
- var style = element.style;
- if (typeof(styles) === "string") {
- style.cssText += ";" + styles;
- return;
- }
- for (var i in styles) {
- if (i === "float") {
- style.cssFloat = styles[i];
- style.styleFloat = styles[i];
- } else {
- style[i] = styles[i];
- }
- }
- }
- };
- };/**
- * Simulate HTML5 placeholder attribute
- *
- * Needed since
- * - div[contentEditable] elements don't support it
- * - older browsers (such as IE8 and Firefox 3.6) don't support it at all
- *
- * @param {Object} parent Instance of main wysihtml5.Editor class
- * @param {Element} view Instance of wysihtml5.views.* class
- * @param {String} placeholderText
- *
- * @example
- * wysihtml.dom.simulatePlaceholder(this, composer, "Foobar");
- */
- (function(dom) {
- dom.simulatePlaceholder = function(editor, view, placeholderText) {
- var CLASS_NAME = "placeholder",
- unset = function() {
- if (view.hasPlaceholderSet()) {
- view.clear();
- }
- dom.removeClass(view.element, CLASS_NAME);
- },
- set = function() {
- if (view.isEmpty()) {
- view.setValue(placeholderText);
- dom.addClass(view.element, CLASS_NAME);
- }
- };
- editor
- .observe("set_placeholder", set)
- .observe("unset_placeholder", unset)
- .observe("focus:composer", unset)
- .observe("paste:composer", unset)
- .observe("blur:composer", set);
- set();
- };
- })(wysihtml5.dom);
- (function(dom) {
- var documentElement = document.documentElement;
- if ("textContent" in documentElement) {
- dom.setTextContent = function(element, text) {
- element.textContent = text;
- };
- dom.getTextContent = function(element) {
- return element.textContent;
- };
- } else if ("innerText" in documentElement) {
- dom.setTextContent = function(element, text) {
- element.innerText = text;
- };
- dom.getTextContent = function(element) {
- return element.innerText;
- };
- } else {
- dom.setTextContent = function(element, text) {
- element.nodeValue = text;
- };
- dom.getTextContent = function(element) {
- return element.nodeValue;
- };
- }
- })(wysihtml5.dom);
- /**
- * Fix most common html formatting misbehaviors of browsers implementation when inserting
- * content via copy & paste contentEditable
- *
- * @author Christopher Blum
- */
- wysihtml5.quirks.cleanPastedHTML = (function() {
- // TODO: We probably need more rules here
- var defaultRules = {
- // When pasting underlined links <a> into a contentEditable, IE thinks, it has to insert <u> to keep the styling
- "a u": wysihtml5.dom.replaceWithChildNodes
- };
-
- function cleanPastedHTML(elementOrHtml, rules, context) {
- rules = rules || defaultRules;
- context = context || elementOrHtml.ownerDocument || document;
-
- var element,
- isString = typeof(elementOrHtml) === "string",
- method,
- matches,
- matchesLength,
- i,
- j = 0;
- if (isString) {
- element = wysihtml5.dom.getAsDom(elementOrHtml, context);
- } else {
- element = elementOrHtml;
- }
-
- for (i in rules) {
- matches = element.querySelectorAll(i);
- method = rules[i];
- matchesLength = matches.length;
- for (; j<matchesLength; j++) {
- method(matches[j]);
- }
- }
-
- matches = elementOrHtml = rules = null;
-
- return isString ? element.innerHTML : element;
- }
-
- return cleanPastedHTML;
- })();/**
- * IE and Opera leave an empty paragraph in the contentEditable element after clearing it
- *
- * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
- * @exaple
- * wysihtml5.quirks.ensureProperClearing(myContentEditableElement);
- */
- (function(wysihtml5) {
- var dom = wysihtml5.dom;
-
- wysihtml5.quirks.ensureProperClearing = (function() {
- var clearIfNecessary = function(event) {
- var element = this;
- setTimeout(function() {
- var innerHTML = element.innerHTML.toLowerCase();
- if (innerHTML == "<p> </p>" ||
- innerHTML == "<p> </p><p> </p>") {
- element.innerHTML = "";
- }
- }, 0);
- };
- return function(composer) {
- dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary);
- };
- })();
- /**
- * In Opera when the caret is in the first and only item of a list (<ul><li>|</li></ul>) and the list is the first child of the contentEditable element, it's impossible to delete the list by hitting backspace
- *
- * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
- * @exaple
- * wysihtml5.quirks.ensureProperClearing(myContentEditableElement);
- */
- wysihtml5.quirks.ensureProperClearingOfLists = (function() {
- var ELEMENTS_THAT_CONTAIN_LI = ["OL", "UL", "MENU"];
- var clearIfNecessary = function(element, contentEditableElement) {
- if (!contentEditableElement.firstChild || !wysihtml5.lang.array(ELEMENTS_THAT_CONTAIN_LI).contains(contentEditableElement.firstChild.nodeName)) {
- return;
- }
- var list = dom.getParentElement(element, { nodeName: ELEMENTS_THAT_CONTAIN_LI });
- if (!list) {
- return;
- }
- var listIsFirstChildOfContentEditable = list == contentEditableElement.firstChild;
- if (!listIsFirstChildOfContentEditable) {
- return;
- }
- var hasOnlyOneListItem = list.childNodes.length <= 1;
- if (!hasOnlyOneListItem) {
- return;
- }
- var onlyListItemIsEmpty = list.firstChild ? list.firstChild.innerHTML === "" : true;
- if (!onlyListItemIsEmpty) {
- return;
- }
- list.parentNode.removeChild(list);
- };
- return function(composer) {
- dom.observe(composer.element, "keydown", function(event) {
- if (event.keyCode !== wysihtml5.BACKSPACE_KEY) {
- return;
- }
- var element = composer.selection.getSelectedNode();
- clearIfNecessary(element, composer.element);
- });
- };
- })();
- })(wysihtml5);
- // See https://bugzilla.mozilla.org/show_bug.cgi?id=664398
- //
- // In Firefox this:
- // var d = document.createElement("div");
- // d.innerHTML ='<a href="~"></a>';
- // d.innerHTML;
- // will result in:
- // <a href="%7E"></a>
- // which is wrong
- (function(wysihtml5) {
- var TILDE_ESCAPED = "%7E";
- wysihtml5.quirks.getCorrectInnerHTML = function(element) {
- var innerHTML = element.innerHTML;
- if (innerHTML.indexOf(TILDE_ESCAPED) === -1) {
- return innerHTML;
- }
-
- var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"),
- url,
- urlToSearch,
- length,
- i;
- for (i=0, length=elementsWithTilde.length; i<length; i++) {
- url = elementsWithTilde[i].href || elementsWithTilde[i].src;
- urlToSearch = wysihtml5.lang.string(url).replace("~").by(TILDE_ESCAPED);
- innerHTML = wysihtml5.lang.string(innerHTML).replace(urlToSearch).by(url);
- }
- return innerHTML;
- };
- })(wysihtml5);/**
- * Some browsers don't insert line breaks when hitting return in a contentEditable element
- * - Opera & IE insert new <p> on return
- * - Chrome & Safari insert new <div> on return
- * - Firefox inserts <br> on return (yippie!)
- *
- * @param {Element} element
- *
- * @example
- * wysihtml5.quirks.insertLineBreakOnReturn(element);
- */
- (function(wysihtml5) {
- var dom = wysihtml5.dom,
- USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"],
- LIST_TAGS = ["UL", "OL", "MENU"];
-
- wysihtml5.quirks.insertLineBreakOnReturn = function(composer) {
- function unwrap(selectedNode) {
- var parentElement = dom.getParentElement(selectedNode, { nodeName: ["P", "DIV"] }, 2);
- if (!parentElement) {
- return;
- }
- var invisibleSpace = document.createTextNode(wysihtml5.INVISIBLE_SPACE);
- dom.insert(invisibleSpace).before(parentElement);
- dom.replaceWithChildNodes(parentElement);
- composer.selection.selectNode(invisibleSpace);
- }
- function keyDown(event) {
- var keyCode = event.keyCode;
- if (event.shiftKey || (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY)) {
- return;
- }
- var element = event.target,
- selectedNode = composer.selection.getSelectedNode(),
- blockElement = dom.getParentElement(selectedNode, { nodeName: USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS }, 4);
- if (blockElement) {
- // Some browsers create <p> elements after leaving a list
- // check after keydown of backspace and return whether a <p> got inserted and unwrap it
- if (blockElement.nodeName === "LI" && (keyCode === wysihtml5.ENTER_KEY || keyCode === wysihtml5.BACKSPACE_KEY)) {
- setTimeout(function() {
- var selectedNode = composer.selection.getSelectedNode(),
- list,
- div;
- if (!selectedNode) {
- return;
- }
- list = dom.getParentElement(selectedNode, {
- nodeName: LIST_TAGS
- }, 2);
- if (list) {
- return;
- }
- unwrap(selectedNode);
- }, 0);
- } else if (blockElement.nodeName.match(/H[1-6]/) && keyCode === wysihtml5.ENTER_KEY) {
- setTimeout(function() {
- unwrap(composer.selection.getSelectedNode());
- }, 0);
- }
- return;
- }
- if (keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) {
- composer.commands.exec("insertLineBreak");
- event.preventDefault();
- }
- }
-
- // keypress doesn't fire when you hit backspace
- dom.observe(composer.element.ownerDocument, "keydown", keyDown);
- };
- })(wysihtml5);/**
- * Force rerendering of a given element
- * Needed to fix display misbehaviors of IE
- *
- * @param {Element} element The element object which needs to be rerendered
- * @example
- * wysihtml5.quirks.redraw(document.body);
- */
- (function(wysihtml5) {
- var CLASS_NAME = "wysihtml5-quirks-redraw";
-
- wysihtml5.quirks.redraw = function(element) {
- wysihtml5.dom.addClass(element, CLASS_NAME);
- wysihtml5.dom.removeClass(element, CLASS_NAME);
-
- // Following hack is needed for firefox to make sure that image resize handles are properly removed
- try {
- var doc = element.ownerDocument;
- doc.execCommand("italic", false, null);
- doc.execCommand("italic", false, null);
- } catch(e) {}
- };
- })(wysihtml5);/**
- * Selection API
- *
- * @example
- * var selection = new wysihtml5.Selection(editor);
- */
- (function(wysihtml5) {
- var dom = wysihtml5.dom;
-
- function _getCumulativeOffsetTop(element) {
- var top = 0;
- if (element.parentNode) {
- do {
- top += element.offsetTop || 0;
- element = element.offsetParent;
- } while (element);
- }
- return top;
- }
-
- wysihtml5.Selection = Base.extend(
- /** @scope wysihtml5.Selection.prototype */ {
- constructor: function(editor) {
- // Make sure that our external range library is initialized
- window.rangy.init();
-
- this.editor = editor;
- this.composer = editor.composer;
- this.doc = this.composer.doc;
- },
-
- /**
- * Get the current selection as a bookmark to be able to later restore it
- *
- * @return {Object} An object that represents the current selection
- */
- getBookmark: function() {
- var range = this.getRange();
- return range && range.cloneRange();
- },
- /**
- * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark
- *
- * @param {Object} bookmark An object that represents the current selection
- */
- setBookmark: function(bookmark) {
- if (!bookmark) {
- return;
- }
- this.setSelection(bookmark);
- },
- /**
- * Set the caret in front of the given node
- *
- * @param {Object} node The element or text node where to position the caret in front of
- * @example
- * selection.setBefore(myElement);
- */
- setBefore: function(node) {
- var range = rangy.createRange(this.doc);
- range.setStartBefore(node);
- range.setEndBefore(node);
- return this.setSelection(range);
- },
- /**
- * Set the caret after the given node
- *
- * @param {Object} node The element or text node where to position the caret in front of
- * @example
- * selection.setBefore(myElement);
- */
- setAfter: function(node) {
- var range = rangy.createRange(this.doc);
- range.setStartAfter(node);
- range.setEndAfter(node);
- return this.setSelection(range);
- },
- /**
- * Ability to select/mark nodes
- *
- * @param {Element} node The node/element to select
- * @example
- * selection.selectNode(document.getElementById("my-image"));
- */
- selectNode: function(node) {
- var range = rangy.createRange(this.doc),
- isElement = node.nodeType === wysihtml5.ELEMENT_NODE,
- canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"),
- content = isElement ? node.innerHTML : node.data,
- isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE),
- displayStyle = dom.getStyle("display").from(node),
- isBlockElement = (displayStyle === "block" || displayStyle === "list-item");
- if (isEmpty && isElement && canHaveHTML) {
- // Make sure that caret is visible in node by inserting a zero width no breaking space
- try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
- }
- if (canHaveHTML) {
- range.selectNodeContents(node);
- } else {
- range.selectNode(node);
- }
- if (canHaveHTML && isEmpty && isElement) {
- range.collapse(isBlockElement);
- } else if (canHaveHTML && isEmpty) {
- range.setStartAfter(node);
- range.setEndAfter(node);
- }
- this.setSelection(range);
- },
- /**
- * Get the node which contains the selection
- *
- * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange"
- * @return {Object} The node that contains the caret
- * @example
- * var nodeThatContainsCaret = selection.getSelectedNode();
- */
- getSelectedNode: function(controlRange) {
- var selection,
- range;
- if (controlRange && this.doc.selection && this.doc.selection.type === "Control") {
- range = this.doc.selection.createRange();
- if (range && range.length) {
- return range.item(0);
- }
- }
- selection = this.getSelection(this.doc);
- if (selection.focusNode === selection.anchorNode) {
- return selection.focusNode;
- } else {
- range = this.getRange(this.doc);
- return range ? range.commonAncestorContainer : this.doc.body;
- }
- },
- executeAndRestore: function(method, restoreScrollPosition) {
- var body = this.doc.body,
- oldScrollTop = restoreScrollPosition && body.scrollTop,
- oldScrollLeft = restoreScrollPosition && body.scrollLeft,
- className = "_wysihtml5-temp-placeholder",
- placeholderHTML = '<span class="' + className + '">' + wysihtml5.INVISIBLE_SPACE + '</span>',
- range = this.getRange(this.doc),
- newRange;
-
- // Nothing selected, execute and say goodbye
- if (!range) {
- method(body, body);
- return;
- }
-
- var node = range.createContextualFragment(placeholderHTML);
- range.insertNode(node);
-
- // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder
- try {
- method(range.startContainer, range.endContainer);
- } catch(e3) {
- setTimeout(function() { throw e3; }, 0);
- }
-
- caretPlaceholder = this.doc.querySelector("." + className);
- if (caretPlaceholder) {
- newRange = rangy.createRange(this.doc);
- newRange.selectNode(caretPlaceholder);
- newRange.deleteContents();
- this.setSelection(newRange);
- } else {
- // fallback for when all hell breaks loose
- body.focus();
- }
- if (restoreScrollPosition) {
- body.scrollTop = oldScrollTop;
- body.scrollLeft = oldScrollLeft;
- }
- // Remove it again, just to make sure that the placeholder is definitely out of the dom tree
- try {
- caretPlaceholder.parentNode.removeChild(caretPlaceholder);
- } catch(e4) {}
- },
- /**
- * Different approach of preserving the selection (doesn't modify the dom)
- * Takes all text nodes in the selection and saves the selection position in the first and last one
- */
- executeAndRestoreSimple: function(method) {
- var range = this.getRange(),
- body = this.doc.body,
- newRange,
- firstNode,
- lastNode,
- textNodes,
- rangeBackup;
- // Nothing selected, execute and say goodbye
- if (!range) {
- method(body, body);
- return;
- }
- textNodes = range.getNodes([3]);
- firstNode = textNodes[0] || range.startContainer;
- lastNode = textNodes[textNodes.length - 1] || range.endContainer;
- rangeBackup = {
- collapsed: range.collapsed,
- startContainer: firstNode,
- startOffset: firstNode === range.startContainer ? range.startOffset : 0,
- endContainer: lastNode,
- endOffset: lastNode === range.endContainer ? range.endOffset : lastNode.length
- };
- try {
- method(range.startContainer, range.endContainer);
- } catch(e) {
- setTimeout(function() { throw e; }, 0);
- }
- newRange = rangy.createRange(this.doc);
- try { newRange.setStart(rangeBackup.startContainer, rangeBackup.startOffset); } catch(e1) {}
- try { newRange.setEnd(rangeBackup.endContainer, rangeBackup.endOffset); } catch(e2) {}
- try { this.setSelection(newRange); } catch(e3) {}
- },
- /**
- * Insert html at the caret position and move the cursor after the inserted html
- *
- * @param {String} html HTML string to insert
- * @example
- * selection.insertHTML("<p>foobar</p>");
- */
- insertHTML: function(html) {
- var range = rangy.createRange(this.doc),
- node = range.createContextualFragment(html),
- lastChild = node.lastChild;
- this.insertNode(node);
- if (lastChild) {
- this.setAfter(lastChild);
- }
- },
- /**
- * Insert a node at the caret position and move the cursor behind it
- *
- * @param {Object} node HTML string to insert
- * @example
- * selection.insertNode(document.createTextNode("foobar"));
- */
- insertNode: function(node) {
- var range = this.getRange();
- if (range) {
- range.insertNode(node);
- }
- },
- /**
- * Wraps current selection with the given node
- *
- * @param {Object} node The node to surround the selected elements with
- */
- surround: function(node) {
- var range = this.getRange();
- if (!range) {
- return;
- }
- try {
- // This only works when the range boundaries are not overlapping other elements
- range.surroundContents(node);
- this.selectNode(node);
- } catch(e) {
- // fallback
- node.appendChild(range.extractContents());
- range.insertNode(node);
- }
- },
- /**
- * Scroll the current caret position into the view
- * FIXME: This is a bit hacky, there might be a smarter way of doing this
- *
- * @example
- * selection.scrollIntoView();
- */
- scrollIntoView: function() {
- var doc = this.doc,
- hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight,
- tempElement = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() {
- var element = doc.createElement("span");
- // The element needs content in order to be able to calculate it's position properly
- element.innerHTML = wysihtml5.INVISIBLE_SPACE;
- return element;
- })(),
- offsetTop;
- if (hasScrollBars) {
- this.insertNode(tempElement);
- offsetTop = _getCumulativeOffsetTop(tempElement);
- tempElement.parentNode.removeChild(tempElement);
- if (offsetTop > doc.body.scrollTop) {
- doc.body.scrollTop = offsetTop;
- }
- }
- },
- /**
- * Select line where the caret is in
- */
- selectLine: function() {
- if (wysihtml5.browser.supportsSelectionModify()) {
- this._selectLine_W3C();
- } else if (this.doc.selection) {
- this._selectLine_MSIE();
- }
- },
- /**
- * See https://developer.mozilla.org/en/DOM/Selection/modify
- */
- _selectLine_W3C: function() {
- var win = this.doc.defaultView,
- selection = win.getSelection();
- selection.modify("extend", "left", "lineboundary");
- selection.modify("extend", "right", "lineboundary");
- },
- _selectLine_MSIE: function() {
- var range = this.doc.selection.createRange(),
- rangeTop = range.boundingTop,
- rangeHeight = range.boundingHeight,
- scrollWidth = this.doc.body.scrollWidth,
- rangeBottom,
- rangeEnd,
- measureNode,
- i,
- j;
- if (!range.moveToPoint) {
- return;
- }
- if (rangeTop === 0) {
- // Don't know why, but when the selection ends at the end of a line
- // range.boundingTop is 0
- measureNode = this.doc.createElement("span");
- this.insertNode(measureNode);
- rangeTop = measureNode.offsetTop;
- measureNode.parentNode.removeChild(measureNode);
- }
- rangeTop += 1;
- for (i=-10; i<scrollWidth; i+=2) {
- try {
- range.moveToPoint(i, rangeTop);
- break;
- } catch(e1) {}
- }
- // Investigate the following in order to handle multi line selections
- // rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0);
- rangeBottom = rangeTop;
- rangeEnd = this.doc.selection.createRange();
- for (j=scrollWidth; j>=0; j--) {
- try {
- rangeEnd.moveToPoint(j, rangeBottom);
- break;
- } catch(e2) {}
- }
- range.setEndPoint("EndToEnd", rangeEnd);
- range.select();
- },
- getText: function() {
- var selection = this.getSelection();
- return selection ? selection.toString() : "";
- },
- getNodes: function(nodeType, filter) {
- var range = this.getRange();
- if (range) {
- return range.getNodes([nodeType], filter);
- } else {
- return [];
- }
- },
-
- getRange: function() {
- var selection = this.getSelection();
- return selection && selection.rangeCount && selection.getRangeAt(0);
- },
- getSelection: function() {
- return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow);
- },
- setSelection: function(range) {
- var win = this.doc.defaultView || this.doc.parentWindow,
- selection = rangy.getSelection(win);
- return selection.setSingleRange(range);
- }
- });
-
- })(wysihtml5);
- /**
- * Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license.
- * http://code.google.com/p/rangy/
- *
- * changed in order to be able ...
- * - to use custom tags
- * - to detect and replace similar css classes via reg exp
- */
- (function(wysihtml5, rangy) {
- var defaultTagName = "span";
-
- var REG_EXP_WHITE_SPACE = /\s+/g;
-
- function hasClass(el, cssClass, regExp) {
- if (!el.className) {
- return false;
- }
-
- var matchingClassNames = el.className.match(regExp) || [];
- return matchingClassNames[matchingClassNames.length - 1] === cssClass;
- }
- function addClass(el, cssClass, regExp) {
- if (el.className) {
- removeClass(el, regExp);
- el.className += " " + cssClass;
- } else {
- el.className = cssClass;
- }
- }
- function removeClass(el, regExp) {
- if (el.className) {
- el.className = el.className.replace(regExp, "");
- }
- }
-
- function hasSameClasses(el1, el2) {
- return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " ");
- }
- function replaceWithOwnChildren(el) {
- var parent = el.parentNode;
- while (el.firstChild) {
- parent.insertBefore(el.firstChild, el);
- }
- parent.removeChild(el);
- }
- function elementsHaveSameNonClassAttributes(el1, el2) {
- if (el1.attributes.length != el2.attributes.length) {
- return false;
- }
- for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
- attr1 = el1.attributes[i];
- name = attr1.name;
- if (name != "class") {
- attr2 = el2.attributes.getNamedItem(name);
- if (attr1.specified != attr2.specified) {
- return false;
- }
- if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) {
- return false;
- }
- }
- }
- return true;
- }
- function isSplitPoint(node, offset) {
- if (rangy.dom.isCharacterDataNode(node)) {
- if (offset == 0) {
- return !!node.previousSibling;
- } else if (offset == node.length) {
- return !!node.nextSibling;
- } else {
- return true;
- }
- }
- return offset > 0 && offset < node.childNodes.length;
- }
- function splitNodeAt(node, descendantNode, descendantOffset) {
- var newNode;
- if (rangy.dom.isCharacterDataNode(descendantNode)) {
- if (descendantOffset == 0) {
- descendantOffset = rangy.dom.getNodeIndex(descendantNode);
- descendantNode = descendantNode.parentNode;
- } else if (descendantOffset == descendantNode.length) {
- descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1;
- descendantNode = descendantNode.parentNode;
- } else {
- newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset);
- }
- }
- if (!newNode) {
- newNode = descendantNode.cloneNode(false);
- if (newNode.id) {
- newNode.removeAttribute("id");
- }
- var child;
- while ((child = descendantNode.childNodes[descendantOffset])) {
- newNode.appendChild(child);
- }
- rangy.dom.insertAfter(newNode, descendantNode);
- }
- return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode));
- }
-
- function Merge(firstNode) {
- this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE);
- this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
- this.textNodes = [this.firstTextNode];
- }
- Merge.prototype = {
- doMerge: function() {
- var textBits = [], textNode, parent, text;
- for (var i = 0, len = this.textNodes.length; i < len; ++i) {
- textNode = this.textNodes[i];
- parent = textNode.parentNode;
- textBits[i] = textNode.data;
- if (i) {
- parent.removeChild(textNode);
- if (!parent.hasChildNodes()) {
- parent.parentNode.removeChild(parent);
- }
- }
- }
- this.firstTextNode.data = text = textBits.join("");
- return text;
- },
- getLength: function() {
- var i = this.textNodes.length, len = 0;
- while (i--) {
- len += this.textNodes[i].length;
- }
- return len;
- },
- toString: function() {
- var textBits = [];
- for (var i = 0, len = this.textNodes.length; i < len; ++i) {
- textBits[i] = "'" + this.textNodes[i].data + "'";
- }
- return "[Merge(" + textBits.join(",") + ")]";
- }
- };
- function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize) {
- this.tagNames = tagNames || [defaultTagName];
- this.cssClass = cssClass || "";
- this.similarClassRegExp = similarClassRegExp;
- this.normalize = normalize;
- this.applyToAnyTagName = false;
- }
- HTMLApplier.prototype = {
- getAncestorWithClass: function(node) {
- var cssClassMatch;
- while (node) {
- cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : true;
- if (node.nodeType == wysihtml5.ELEMENT_NODE && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) {
- return node;
- }
- node = node.parentNode;
- }
- return false;
- },
- // Normalizes nodes after applying a CSS class to a Range.
- postApply: function(textNodes, range) {
- var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
- var merges = [], currentMerge;
- var rangeStartNode = firstNode, rangeEndNode = lastNode;
- var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
- var textNode, precedingTextNode;
- for (var i = 0, len = textNodes.length; i < len; ++i) {
- textNode = textNodes[i];
- precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false);
- if (precedingTextNode) {
- if (!currentMerge) {
- currentMerge = new Merge(precedingTextNode);
- merges.push(currentMerge);
- }
- currentMerge.textNodes.push(textNode);
- if (textNode === firstNode) {
- rangeStartNode = currentMerge.firstTextNode;
- rangeStartOffset = rangeStartNode.length;
- }
- if (textNode === lastNode) {
- rangeEndNode = currentMerge.firstTextNode;
- rangeEndOffset = currentMerge.getLength();
- }
- } else {
- currentMerge = null;
- }
- }
- // Test whether the first node after the range needs merging
- var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true);
- if (nextTextNode) {
- if (!currentMerge) {
- currentMerge = new Merge(lastNode);
- merges.push(currentMerge);
- }
- currentMerge.textNodes.push(nextTextNode);
- }
- // Do the merges
- if (merges.length) {
- for (i = 0, len = merges.length; i < len; ++i) {
- merges[i].doMerge();
- }
- // Set the range boundaries
- range.setStart(rangeStartNode, rangeStartOffset);
- range.setEnd(rangeEndNode, rangeEndOffset);
- }
- },
-
- getAdjacentMergeableTextNode: function(node, forward) {
- var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE);
- var el = isTextNode ? node.parentNode : node;
- var adjacentNode;
- var propName = forward ? "nextSibling" : "previousSibling";
- if (isTextNode) {
- // Can merge if the node's previous/next sibling is a text node
- adjacentNode = node[propName];
- if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) {
- return adjacentNode;
- }
- } else {
- // Compare element with its sibling
- adjacentNode = el[propName];
- if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) {
- return adjacentNode[forward ? "firstChild" : "lastChild"];
- }
- }
- return null;
- },
-
- areElementsMergeable: function(el1, el2) {
- return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase())
- && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase())
- && hasSameClasses(el1, el2)
- && elementsHaveSameNonClassAttributes(el1, el2);
- },
- createContainer: function(doc) {
- var el = doc.createElement(this.tagNames[0]);
- if (this.cssClass) {
- el.className = this.cssClass;
- }
- return el;
- },
- applyToTextNode: function(textNode) {
- var parent = textNode.parentNode;
- if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
- if (this.cssClass) {
- addClass(parent, this.cssClass, this.similarClassRegExp);
- }
- } else {
- var el = this.createContainer(rangy.dom.getDocument(textNode));
- textNode.parentNode.insertBefore(el, textNode);
- el.appendChild(textNode);
- }
- },
- isRemovable: function(el) {
- return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) && wysihtml5.lang.string(el.className).trim() == this.cssClass;
- },
- undoToTextNode: function(textNode, range, ancestorWithClass) {
- if (!range.containsNode(ancestorWithClass)) {
- // Split out the portion of the ancestor from which we can remove the CSS class
- var ancestorRange = range.cloneRange();
- ancestorRange.selectNode(ancestorWithClass);
- if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) {
- splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset);
- range.setEndAfter(ancestorWithClass);
- }
- if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) {
- ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset);
- }
- }
-
- if (this.similarClassRegExp) {
- removeClass(ancestorWithClass, this.similarClassRegExp);
- }
- if (this.isRemovable(ancestorWithClass)) {
- replaceWithOwnChildren(ancestorWithClass);
- }
- },
- applyToRange: function(range) {
- var textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
- if (!textNodes.length) {
- try {
- var node = this.createContainer(range.endContainer.ownerDocument);
- range.surroundContents(node);
- this.selectNode(range, node);
- return;
- } catch(e) {}
- }
-
- range.splitBoundaries();
- textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
-
- if (textNodes.length) {
- var textNode;
- for (var i = 0, len = textNodes.length; i < len; ++i) {
- textNode = textNodes[i];
- if (!this.getAncestorWithClass(textNode)) {
- this.applyToTextNode(textNode);
- }
- }
-
- range.setStart(textNodes[0], 0);
- textNode = textNodes[textNodes.length - 1];
- range.setEnd(textNode, textNode.length);
-
- if (this.normalize) {
- this.postApply(textNodes, range);
- }
- }
- },
- undoToRange: function(range) {
- var textNodes = range.getNodes([wysihtml5.TEXT_NODE]), textNode, ancestorWithClass;
- if (textNodes.length) {
- range.splitBoundaries();
- textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
- } else {
- var doc = range.endContainer.ownerDocument,
- node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
- range.insertNode(node);
- range.selectNode(node);
- textNodes = [node];
- }
-
- for (var i = 0, len = textNodes.length; i < len; ++i) {
- textNode = textNodes[i];
- ancestorWithClass = this.getAncestorWithClass(textNode);
- if (ancestorWithClass) {
- this.undoToTextNode(textNode, range, ancestorWithClass);
- }
- }
-
- if (len == 1) {
- this.selectNode(range, textNodes[0]);
- } else {
- range.setStart(textNodes[0], 0);
- textNode = textNodes[textNodes.length - 1];
- range.setEnd(textNode, textNode.length);
- if (this.normalize) {
- this.postApply(textNodes, range);
- }
- }
- },
-
- selectNode: function(range, node) {
- var isElement = node.nodeType === wysihtml5.ELEMENT_NODE,
- canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : true,
- content = isElement ? node.innerHTML : node.data,
- isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE);
- if (isEmpty && isElement && canHaveHTML) {
- // Make sure that caret is visible in node by inserting a zero width no breaking space
- try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
- }
- range.selectNodeContents(node);
- if (isEmpty && isElement) {
- range.collapse(false);
- } else if (isEmpty) {
- range.setStartAfter(node);
- range.setEndAfter(node);
- }
- },
-
- getTextSelectedByRange: function(textNode, range) {
- var textRange = range.cloneRange();
- textRange.selectNodeContents(textNode);
- var intersectionRange = textRange.intersection(range);
- var text = intersectionRange ? intersectionRange.toString() : "";
- textRange.detach();
- return text;
- },
- isAppliedToRange: function(range) {
- var ancestors = [],
- ancestor,
- textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
- if (!textNodes.length) {
- ancestor = this.getAncestorWithClass(range.startContainer);
- return ancestor ? [ancestor] : false;
- }
-
- for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) {
- selectedText = this.getTextSelectedByRange(textNodes[i], range);
- ancestor = this.getAncestorWithClass(textNodes[i]);
- if (selectedText != "" && !ancestor) {
- return false;
- } else {
- ancestors.push(ancestor);
- }
- }
- return ancestors;
- },
- toggleRange: function(range) {
- if (this.isAppliedToRange(range)) {
- this.undoToRange(range);
- } else {
- this.applyToRange(range);
- }
- }
- };
- wysihtml5.selection.HTMLApplier = HTMLApplier;
-
- })(wysihtml5, rangy);/**
- * Rich Text Query/Formatting Commands
- *
- * @example
- * var commands = new wysihtml5.Commands(editor);
- */
- wysihtml5.Commands = Base.extend(
- /** @scope wysihtml5.Commands.prototype */ {
- constructor: function(editor) {
- this.editor = editor;
- this.composer = editor.composer;
- this.doc = this.composer.doc;
- },
-
- /**
- * Check whether the browser supports the given command
- *
- * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
- * @example
- * commands.supports("createLink");
- */
- support: function(command) {
- return wysihtml5.browser.supportsCommand(this.doc, command);
- },
-
- /**
- * Check whether the browser supports the given command
- *
- * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList")
- * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...)
- * @example
- * commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg");
- */
- exec: function(command, value) {
- var obj = wysihtml5.commands[command],
- args = wysihtml5.lang.array(arguments).get(),
- method = obj && obj.exec,
- result = null;
-
- this.editor.fire("beforecommand:composer");
-
- if (method) {
- args.unshift(this.composer);
- result = method.apply(obj, args);
- } else {
- try {
- // try/catch for buggy firefox
- result = this.doc.execCommand(command, false, value);
- } catch(e) {}
- }
-
- this.editor.fire("aftercommand:composer");
- return result;
- },
-
- /**
- * Check whether the current command is active
- * If the caret is within a bold text, then calling this with command "bold" should return true
- *
- * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
- * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src)
- * @return {Boolean} Whether the command is active
- * @example
- * var isCurrentSelectionBold = commands.state("bold");
- */
- state: function(command, commandValue) {
- var obj = wysihtml5.commands[command],
- args = wysihtml5.lang.array(arguments).get(),
- method = obj && obj.state;
- if (method) {
- args.unshift(this.composer);
- return method.apply(obj, args);
- } else {
- try {
- // try/catch for buggy firefox
- return this.doc.queryCommandState(command);
- } catch(e) {
- return false;
- }
- }
- },
-
- /**
- * Get the current command's value
- *
- * @param {String} command The command string which to check (eg. "formatBlock")
- * @return {String} The command value
- * @example
- * var currentBlockElement = commands.value("formatBlock");
- */
- value: function(command) {
- var obj = wysihtml5.commands[command],
- method = obj && obj.value;
- if (method) {
- return method.call(obj, this.composer, command);
- } else {
- try {
- // try/catch for buggy firefox
- return this.doc.queryCommandValue(command);
- } catch(e) {
- return null;
- }
- }
- }
- });
- (function(wysihtml5) {
- var undef;
-
- wysihtml5.commands.bold = {
- exec: function(composer, command) {
- return wysihtml5.commands.formatInline.exec(composer, command, "b");
- },
- state: function(composer, command, color) {
- // element.ownerDocument.queryCommandState("bold") results:
- // firefox: only <b>
- // chrome: <b>, <strong>, <h1>, <h2>, ...
- // ie: <b>, <strong>
- // opera: <b>, <strong>
- return wysihtml5.commands.formatInline.state(composer, command, "b");
- },
- value: function() {
- return undef;
- }
- };
- })(wysihtml5);
- (function(wysihtml5) {
- var undef,
- NODE_NAME = "A",
- dom = wysihtml5.dom;
-
- function _removeFormat(composer, anchors) {
- var length = anchors.length,
- i = 0,
- anchor,
- codeElement,
- textContent;
- for (; i<length; i++) {
- anchor = anchors[i];
- codeElement = dom.getParentElement(anchor, { nodeName: "code" });
- textContent = dom.getTextContent(anchor);
- // if <a> contains url-like text content, rename it to <code> to prevent re-autolinking
- // else replace <a> with its childNodes
- if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) {
- // <code> element is used to prevent later auto-linking of the content
- codeElement = dom.renameElement(anchor, "code");
- } else {
- dom.replaceWithChildNodes(anchor);
- }
- }
- }
- function _format(composer, attributes) {
- var doc = composer.doc,
- tempClass = "_wysihtml5-temp-" + (+new Date()),
- tempClassRegExp = /non-matching-class/g,
- i = 0,
- length,
- anchors,
- anchor,
- hasElementChild,
- isEmpty,
- elementToSetCaretAfter,
- textContent,
- whiteSpace,
- j;
- wysihtml5.commands.formatInline.exec(composer, undef, NODE_NAME, tempClass, tempClassRegExp);
- anchors = doc.querySelectorAll(NODE_NAME + "." + tempClass);
- length = anchors.length;
- for (; i<length; i++) {
- anchor = anchors[i];
- anchor.removeAttribute("class");
- for (j in attributes) {
- anchor.setAttribute(j, attributes[j]);
- }
- }
- elementToSetCaretAfter = anchor;
- if (length === 1) {
- textContent = dom.getTextContent(anchor);
- hasElementChild = !!anchor.querySelector("*");
- isEmpty = textContent === "" || textContent === wysihtml5.INVISIBLE_SPACE;
- if (!hasElementChild && isEmpty) {
- dom.setTextContent(anchor, attributes.text || anchor.href);
- whiteSpace = doc.createTextNode(" ");
- composer.selection.setAfter(anchor);
- composer.selection.insertNode(whiteSpace);
- elementToSetCaretAfter = whiteSpace;
- }
- }
- composer.selection.setAfter(elementToSetCaretAfter);
- }
-
- wysihtml5.commands.createLink = {
- /**
- * TODO: Use HTMLApplier or formatInline here
- *
- * Turns selection into a link
- * If selection is already a link, it removes the link and wraps it with a <code> element
- * The <code> element is needed to avoid auto linking
- *
- * @example
- * // either ...
- * wysihtml5.commands.createLink.exec(composer, "createLink", "http://www.google.de");
- * // ... or ...
- * wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" });
- */
- exec: function(composer, command, value) {
- var anchors = this.state(composer, command);
- if (anchors) {
- // Selection contains links
- composer.selection.executeAndRestore(function() {
- _removeFormat(composer, anchors);
- });
- } else {
- // Create links
- value = typeof(value) === "object" ? value : { href: value };
- _format(composer, value);
- }
- },
- state: function(composer, command) {
- return wysihtml5.commands.formatInline.state(composer, command, "A");
- },
- value: function() {
- return undef;
- }
- };
- })(wysihtml5);/**
- * document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags
- * which we don't want
- * Instead we set a css class
- */
- (function(wysihtml5) {
- var undef,
- REG_EXP = /wysiwyg-font-size-[a-z\-]+/g;
-
- wysihtml5.commands.fontSize = {
- exec: function(composer, command, size) {
- return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
- },
- state: function(composer, command, size) {
- return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
- },
- value: function() {
- return undef;
- }
- };
- })(wysihtml5);
- /**
- * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags
- * which we don't want
- * Instead we set a css class
- */
- (function(wysihtml5) {
- var undef,
- REG_EXP = /wysiwyg-color-[a-z]+/g;
-
- wysihtml5.commands.foreColor = {
- exec: function(composer, command, color) {
- return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
- },
- state: function(composer, command, color) {
- return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
- },
- value: function() {
- return undef;
- }
- };
- })(wysihtml5);(function(wysihtml5) {
- var undef,
- dom = wysihtml5.dom,
- DEFAULT_NODE_NAME = "DIV",
- // Following elements are grouped
- // when the caret is within a H1 and the H4 is invoked, the H1 should turn into H4
- // instead of creating a H4 within a H1 which would result in semantically invalid html
- BLOCK_ELEMENTS_GROUP = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "BLOCKQUOTE", DEFAULT_NODE_NAME];
-
- /**
- * Remove similiar classes (based on classRegExp)
- * and add the desired class name
- */
- function _addClass(element, className, classRegExp) {
- if (element.className) {
- _removeClass(element, classRegExp);
- element.className += " " + className;
- } else {
- element.className = className;
- }
- }
- function _removeClass(element, classRegExp) {
- element.className = element.className.replace(classRegExp, "");
- }
- /**
- * Check whether given node is a text node and whether it's empty
- */
- function _isBlankTextNode(node) {
- return node.nodeType === wysihtml5.TEXT_NODE && !wysihtml5.lang.string(node.data).trim();
- }
- /**
- * Returns previous sibling node that is not a blank text node
- */
- function _getPreviousSiblingThatIsNotBlank(node) {
- var previousSibling = node.previousSibling;
- while (previousSibling && _isBlankTextNode(previousSibling)) {
- previousSibling = previousSibling.previousSibling;
- }
- return previousSibling;
- }
- /**
- * Returns next sibling node that is not a blank text node
- */
- function _getNextSiblingThatIsNotBlank(node) {
- var nextSibling = node.nextSibling;
- while (nextSibling && _isBlankTextNode(nextSibling)) {
- nextSibling = nextSibling.nextSibling;
- }
- return nextSibling;
- }
- /**
- * Adds line breaks before and after the given node if the previous and next siblings
- * aren't already causing a visual line break (block element or <br>)
- */
- function _addLineBreakBeforeAndAfter(node) {
- var doc = node.ownerDocument,
- nextSibling = _getNextSiblingThatIsNotBlank(node),
- previousSibling = _getPreviousSiblingThatIsNotBlank(node);
- if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {
- node.parentNode.insertBefore(doc.createElement("br"), nextSibling);
- }
- if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {
- node.parentNode.insertBefore(doc.createElement("br"), node);
- }
- }
- /**
- * Removes line breaks before and after the given node
- */
- function _removeLineBreakBeforeAndAfter(node) {
- var nextSibling = _getNextSiblingThatIsNotBlank(node),
- previousSibling = _getPreviousSiblingThatIsNotBlank(node);
- if (nextSibling && _isLineBreak(nextSibling)) {
- nextSibling.parentNode.removeChild(nextSibling);
- }
- if (previousSibling && _isLineBreak(previousSibling)) {
- previousSibling.parentNode.removeChild(previousSibling);
- }
- }
- function _removeLastChildIfLineBreak(node) {
- var lastChild = node.lastChild;
- if (lastChild && _isLineBreak(lastChild)) {
- lastChild.parentNode.removeChild(lastChild);
- }
- }
- function _isLineBreak(node) {
- return node.nodeName === "BR";
- }
- /**
- * Checks whether the elment causes a visual line break
- * (<br> or block elements)
- */
- function _isLineBreakOrBlockElement(element) {
- if (_isLineBreak(element)) {
- return true;
- }
- if (dom.getStyle("display").from(element) === "block") {
- return true;
- }
- return false;
- }
- /**
- * Execute native query command
- * and if necessary modify the inserted node's className
- */
- function _execCommand(doc, command, nodeName, className) {
- if (className) {
- var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) {
- var target = event.target,
- displayStyle;
- if (target.nodeType !== wysihtml5.ELEMENT_NODE) {
- return;
- }
- displayStyle = dom.getStyle("display").from(target);
- if (displayStyle.substr(0, 6) !== "inline") {
- // Make sure that only block elements receive the given class
- target.className += " " + className;
- }
- });
- }
- doc.execCommand(command, false, nodeName);
- if (eventListener) {
- eventListener.stop();
- }
- }
- function _selectLineAndWrap(composer, element) {
- composer.selection.selectLine();
- composer.selection.surround(element);
- _removeLineBreakBeforeAndAfter(element);
- _removeLastChildIfLineBreak(element);
- composer.selection.selectNode(element);
- }
- function _hasClasses(element) {
- return !!wysihtml5.lang.string(element.className).trim();
- }
-
- wysihtml5.commands.formatBlock = {
- exec: function(composer, command, nodeName, className, classRegExp) {
- var doc = composer.doc,
- blockElement = this.state(composer, command, nodeName, className, classRegExp),
- selectedNode;
- nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
- if (blockElement) {
- composer.selection.executeAndRestoreSimple(function() {
- if (classRegExp) {
- _removeClass(blockElement, classRegExp);
- }
- var hasClasses = _hasClasses(blockElement);
- if (!hasClasses && blockElement.nodeName === (nodeName || DEFAULT_NODE_NAME)) {
- // Insert a line break afterwards and beforewards when there are siblings
- // that are not of type line break or block element
- _addLineBreakBeforeAndAfter(blockElement);
- dom.replaceWithChildNodes(blockElement);
- } else if (hasClasses) {
- // Make sure that styling is kept by renaming the element to <div> and copying over the class name
- dom.renameElement(blockElement, DEFAULT_NODE_NAME);
- }
- });
- return;
- }
- // Find similiar block element and rename it (<h2 class="foo"></h2> => <h1 class="foo"></h1>)
- if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) {
- selectedNode = composer.selection.getSelectedNode();
- blockElement = dom.getParentElement(selectedNode, {
- nodeName: BLOCK_ELEMENTS_GROUP
- });
- if (blockElement) {
- composer.selection.executeAndRestoreSimple(function() {
- // Rename current block element to new block element and add class
- if (nodeName) {
- blockElement = dom.renameElement(blockElement, nodeName);
- }
- if (className) {
- _addClass(blockElement, className, classRegExp);
- }
- });
- return;
- }
- }
- if (composer.commands.support(command)) {
- _execCommand(doc, command, nodeName || DEFAULT_NODE_NAME, className);
- return;
- }
- blockElement = doc.createElement(nodeName || DEFAULT_NODE_NAME);
- if (className) {
- blockElement.className = className;
- }
- _selectLineAndWrap(composer, blockElement);
- },
- state: function(composer, command, nodeName, className, classRegExp) {
- nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
- var selectedNode = composer.selection.getSelectedNode();
- return dom.getParentElement(selectedNode, {
- nodeName: nodeName,
- className: className,
- classRegExp: classRegExp
- });
- },
- value: function() {
- return undef;
- }
- };
- })(wysihtml5);/**
- * formatInline scenarios for tag "B" (| = caret, |foo| = selected text)
- *
- * #1 caret in unformatted text:
- * abcdefg|
- * output:
- * abcdefg<b>|</b>
- *
- * #2 unformatted text selected:
- * abc|deg|h
- * output:
- * abc<b>|deg|</b>h
- *
- * #3 unformatted text selected across boundaries:
- * ab|c <span>defg|h</span>
- * output:
- * ab<b>|c </b><span><b>defg</b>|h</span>
- *
- * #4 formatted text entirely selected
- * <b>|abc|</b>
- * output:
- * |abc|
- *
- * #5 formatted text partially selected
- * <b>ab|c|</b>
- * output:
- * <b>ab</b>|c|
- *
- * #6 formatted text selected across boundaries
- * <span>ab|c</span> <b>de|fgh</b>
- * output:
- * <span>ab|c</span> de|<b>fgh</b>
- */
- (function(wysihtml5) {
- var undef,
- // Treat <b> as <strong> and vice versa
- ALIAS_MAPPING = {
- "strong": "b",
- "em": "i",
- "b": "strong",
- "i": "em"
- },
- htmlApplier = {};
-
- function _getTagNames(tagName) {
- var alias = ALIAS_MAPPING[tagName];
- return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()];
- }
-
- function _getApplier(tagName, className, classRegExp) {
- var identifier = tagName + ":" + className;
- if (!htmlApplier[identifier]) {
- htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true);
- }
- return htmlApplier[identifier];
- }
-
- wysihtml5.commands.formatInline = {
- exec: function(composer, command, tagName, className, classRegExp) {
- var range = composer.selection.getRange();
- if (!range) {
- return false;
- }
- _getApplier(tagName, className, classRegExp).toggleRange(range);
- composer.selection.setSelection(range);
- },
- state: function(composer, command, tagName, className, classRegExp) {
- var doc = composer.doc,
- aliasTagName = ALIAS_MAPPING[tagName] || tagName,
- range;
- // Check whether the document contains a node with the desired tagName
- if (!wysihtml5.dom.hasElementWithTagName(doc, tagName) &&
- !wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) {
- return false;
- }
- // Check whether the document contains a node with the desired className
- if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) {
- return false;
- }
- range = composer.selection.getRange();
- if (!range) {
- return false;
- }
- return _getApplier(tagName, className, classRegExp).isAppliedToRange(range);
- },
- value: function() {
- return undef;
- }
- };
- })(wysihtml5);(function(wysihtml5) {
- var undef;
-
- wysihtml5.commands.insertHTML = {
- exec: function(composer, command, html) {
- if (composer.commands.support(command)) {
- composer.doc.execCommand(command, false, html);
- } else {
- composer.selection.insertHTML(html);
- }
- },
- state: function() {
- return false;
- },
- value: function() {
- return undef;
- }
- };
- })(wysihtml5);(function(wysihtml5) {
- var NODE_NAME = "IMG";
-
- wysihtml5.commands.insertImage = {
- /**
- * Inserts an <img>
- * If selection is already an image link, it removes it
- *
- * @example
- * // either ...
- * wysihtml5.commands.insertImage.exec(composer, "insertImage", "http://www.google.de/logo.jpg");
- * // ... or ...
- * wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "http://www.google.de/logo.jpg", title: "foo" });
- */
- exec: function(composer, command, value) {
- value = typeof(value) === "object" ? value : { src: value };
- var doc = composer.doc,
- image = this.state(composer),
- textNode,
- i,
- parent;
- if (image) {
- // Image already selected, set the caret before it and delete it
- composer.selection.setBefore(image);
- parent = image.parentNode;
- parent.removeChild(image);
- // and it's parent <a> too if it hasn't got any other relevant child nodes
- wysihtml5.dom.removeEmptyTextNodes(parent);
- if (parent.nodeName === "A" && !parent.firstChild) {
- composer.selection.setAfter(parent);
- parent.parentNode.removeChild(parent);
- }
- // firefox and ie sometimes don't remove the image handles, even though the image got removed
- wysihtml5.quirks.redraw(composer.element);
- return;
- }
- image = doc.createElement(NODE_NAME);
- for (i in value) {
- image[i] = value[i];
- }
- composer.selection.insertNode(image);
- if (wysihtml5.browser.hasProblemsSettingCaretAfterImg()) {
- textNode = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
- composer.selection.insertNode(textNode);
- composer.selection.setAfter(textNode);
- } else {
- composer.selection.setAfter(image);
- }
- },
- state: function(composer) {
- var doc = composer.doc,
- selectedNode,
- text,
- imagesInSelection;
- if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) {
- return false;
- }
- selectedNode = composer.selection.getSelectedNode();
- if (!selectedNode) {
- return false;
- }
- if (selectedNode.nodeName === NODE_NAME) {
- // This works perfectly in IE
- return selectedNode;
- }
- if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) {
- return false;
- }
- text = composer.selection.getText();
- text = wysihtml5.lang.string(text).trim();
- if (text) {
- return false;
- }
- imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) {
- return node.nodeName === "IMG";
- });
- if (imagesInSelection.length !== 1) {
- return false;
- }
- return imagesInSelection[0];
- },
- value: function(composer) {
- var image = this.state(composer);
- return image && image.src;
- }
- };
- })(wysihtml5);(function(wysihtml5) {
- var undef,
- LINE_BREAK = "<br>" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : "");
-
- wysihtml5.commands.insertLineBreak = {
- exec: function(composer, command) {
- if (composer.commands.support(command)) {
- composer.doc.execCommand(command, false, null);
- if (!wysihtml5.browser.autoScrollsToCaret()) {
- composer.selection.scrollIntoView();
- }
- } else {
- composer.commands.exec("insertHTML", LINE_BREAK);
- }
- },
- state: function() {
- return false;
- },
- value: function() {
- return undef;
- }
- };
- })(wysihtml5);(function(wysihtml5) {
- var undef;
-
- wysihtml5.commands.insertOrderedList = {
- exec: function(composer, command) {
- var doc = composer.doc,
- selectedNode = composer.selection.getSelectedNode(),
- list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }),
- otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }),
- tempClassName = "_wysihtml5-temp-" + new Date().getTime(),
- isEmpty,
- tempElement;
-
- if (composer.commands.support(command)) {
- doc.execCommand(command, false, null);
- return;
- }
-
- if (list) {
- // Unwrap list
- // <ol><li>foo</li><li>bar</li></ol>
- // becomes:
- // foo<br>bar<br>
- composer.selection.executeAndRestoreSimple(function() {
- wysihtml5.dom.resolveList(list);
- });
- } else if (otherList) {
- // Turn an unordered list into an ordered list
- // <ul><li>foo</li><li>bar</li></ul>
- // becomes:
- // <ol><li>foo</li><li>bar</li></ol>
- composer.selection.executeAndRestoreSimple(function() {
- wysihtml5.dom.renameElement(otherList, "ol");
- });
- } else {
- // Create list
- composer.commands.exec("formatBlock", "div", tempClassName);
- tempElement = doc.querySelector("." + tempClassName);
- isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE;
- composer.selection.executeAndRestoreSimple(function() {
- list = wysihtml5.dom.convertToList(tempElement, "ol");
- });
- if (isEmpty) {
- composer.selection.selectNode(list.querySelector("li"));
- }
- }
- },
-
- state: function(composer) {
- var selectedNode = composer.selection.getSelectedNode();
- return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" });
- },
- value: function() {
- return undef;
- }
- };
- })(wysihtml5);(function(wysihtml5) {
- var undef;
-
- wysihtml5.commands.insertUnorderedList = {
- exec: function(composer, command) {
- var doc = composer.doc,
- selectedNode = composer.selection.getSelectedNode(),
- list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }),
- otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }),
- tempClassName = "_wysihtml5-temp-" + new Date().getTime(),
- isEmpty,
- tempElement;
-
- if (composer.commands.support(command)) {
- doc.execCommand(command, false, null);
- return;
- }
-
- if (list) {
- // Unwrap list
- // <ul><li>foo</li><li>bar</li></ul>
- // becomes:
- // foo<br>bar<br>
- composer.selection.executeAndRestoreSimple(function() {
- wysihtml5.dom.resolveList(list);
- });
- } else if (otherList) {
- // Turn an ordered list into an unordered list
- // <ol><li>foo</li><li>bar</li></ol>
- // becomes:
- // <ul><li>foo</li><li>bar</li></ul>
- composer.selection.executeAndRestoreSimple(function() {
- wysihtml5.dom.renameElement(otherList, "ul");
- });
- } else {
- // Create list
- composer.commands.exec("formatBlock", "div", tempClassName);
- tempElement = doc.querySelector("." + tempClassName);
- isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE;
- composer.selection.executeAndRestoreSimple(function() {
- list = wysihtml5.dom.convertToList(tempElement, "ul");
- });
- if (isEmpty) {
- composer.selection.selectNode(list.querySelector("li"));
- }
- }
- },
-
- state: function(composer) {
- var selectedNode = composer.selection.getSelectedNode();
- return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" });
- },
- value: function() {
- return undef;
- }
- };
- })(wysihtml5);(function(wysihtml5) {
- var undef;
-
- wysihtml5.commands.italic = {
- exec: function(composer, command) {
- return wysihtml5.commands.formatInline.exec(composer, command, "i");
- },
- state: function(composer, command, color) {
- // element.ownerDocument.queryCommandState("italic") results:
- // firefox: only <i>
- // chrome: <i>, <em>, <blockquote>, ...
- // ie: <i>, <em>
- // opera: only <i>
- return wysihtml5.commands.formatInline.state(composer, command, "i");
- },
- value: function() {
- return undef;
- }
- };
- })(wysihtml5);(function(wysihtml5) {
- var undef,
- CLASS_NAME = "wysiwyg-text-align-center",
- REG_EXP = /wysiwyg-text-align-[a-z]+/g;
-
- wysihtml5.commands.justifyCenter = {
- exec: function(composer, command) {
- return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
- },
- state: function(composer, command) {
- return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
- },
- value: function() {
- return undef;
- }
- };
- })(wysihtml5);(function(wysihtml5) {
- var undef,
- CLASS_NAME = "wysiwyg-text-align-left",
- REG_EXP = /wysiwyg-text-align-[a-z]+/g;
-
- wysihtml5.commands.justifyLeft = {
- exec: function(composer, command) {
- return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
- },
- state: function(composer, command) {
- return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
- },
- value: function() {
- return undef;
- }
- };
- })(wysihtml5);(function(wysihtml5) {
- var undef,
- CLASS_NAME = "wysiwyg-text-align-right",
- REG_EXP = /wysiwyg-text-align-[a-z]+/g;
-
- wysihtml5.commands.justifyRight = {
- exec: function(composer, command) {
- return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
- },
- state: function(composer, command) {
- return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
- },
- value: function() {
- return undef;
- }
- };
- })(wysihtml5);(function(wysihtml5) {
- var undef;
- wysihtml5.commands.underline = {
- exec: function(composer, command) {
- return wysihtml5.commands.formatInline.exec(composer, command, "u");
- },
- state: function(composer, command) {
- return wysihtml5.commands.formatInline.state(composer, command, "u");
- },
- value: function() {
- return undef;
- }
- };
- })(wysihtml5);/**
- * Undo Manager for wysihtml5
- * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface
- */
- (function(wysihtml5) {
- var Z_KEY = 90,
- Y_KEY = 89,
- BACKSPACE_KEY = 8,
- DELETE_KEY = 46,
- MAX_HISTORY_ENTRIES = 40,
- UNDO_HTML = '<span id="_wysihtml5-undo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',
- REDO_HTML = '<span id="_wysihtml5-redo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',
- dom = wysihtml5.dom;
-
- function cleanTempElements(doc) {
- var tempElement;
- while (tempElement = doc.querySelector("._wysihtml5-temp")) {
- tempElement.parentNode.removeChild(tempElement);
- }
- }
-
- wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend(
- /** @scope wysihtml5.UndoManager.prototype */ {
- constructor: function(editor) {
- this.editor = editor;
- this.composer = editor.composer;
- this.element = this.composer.element;
- this.history = [this.composer.getValue()];
- this.position = 1;
-
- // Undo manager currently only supported in browsers who have the insertHTML command (not IE)
- if (this.composer.commands.support("insertHTML")) {
- this._observe();
- }
- },
-
- _observe: function() {
- var that = this,
- doc = this.composer.sandbox.getDocument(),
- lastKey;
-
- // Catch CTRL+Z and CTRL+Y
- dom.observe(this.element, "keydown", function(event) {
- if (event.altKey || (!event.ctrlKey && !event.metaKey)) {
- return;
- }
-
- var keyCode = event.keyCode,
- isUndo = keyCode === Z_KEY && !event.shiftKey,
- isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
-
- if (isUndo) {
- that.undo();
- event.preventDefault();
- } else if (isRedo) {
- that.redo();
- event.preventDefault();
- }
- });
-
- // Catch delete and backspace
- dom.observe(this.element, "keydown", function(event) {
- var keyCode = event.keyCode;
- if (keyCode === lastKey) {
- return;
- }
-
- lastKey = keyCode;
-
- if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {
- that.transact();
- }
- });
-
- // Now this is very hacky:
- // These days browsers don't offer a undo/redo event which we could hook into
- // to be notified when the user hits undo/redo in the contextmenu.
- // Therefore we simply insert two elements as soon as the contextmenu gets opened.
- // The last element being inserted will be immediately be removed again by a exexCommand("undo")
- // => When the second element appears in the dom tree then we know the user clicked "redo" in the context menu
- // => When the first element disappears from the dom tree then we know the user clicked "undo" in the context menu
- if (wysihtml5.browser.hasUndoInContextMenu()) {
- var interval, observed, cleanUp = function() {
- cleanTempElements(doc);
- clearInterval(interval);
- };
-
- dom.observe(this.element, "contextmenu", function() {
- cleanUp();
- that.composer.selection.executeAndRestoreSimple(function() {
- if (that.element.lastChild) {
- that.composer.selection.setAfter(that.element.lastChild);
- }
- // enable undo button in context menu
- doc.execCommand("insertHTML", false, UNDO_HTML);
- // enable redo button in context menu
- doc.execCommand("insertHTML", false, REDO_HTML);
- doc.execCommand("undo", false, null);
- });
- interval = setInterval(function() {
- if (doc.getElementById("_wysihtml5-redo")) {
- cleanUp();
- that.redo();
- } else if (!doc.getElementById("_wysihtml5-undo")) {
- cleanUp();
- that.undo();
- }
- }, 400);
- if (!observed) {
- observed = true;
- dom.observe(document, "mousedown", cleanUp);
- dom.observe(doc, ["mousedown", "paste", "cut", "copy"], cleanUp);
- }
- });
- }
-
- this.editor
- .observe("newword:composer", function() {
- that.transact();
- })
-
- .observe("beforecommand:composer", function() {
- that.transact();
- });
- },
-
- transact: function() {
- var previousHtml = this.history[this.position - 1],
- currentHtml = this.composer.getValue();
-
- if (currentHtml == previousHtml) {
- return;
- }
-
- var length = this.history.length = this.position;
- if (length > MAX_HISTORY_ENTRIES) {
- this.history.shift();
- this.position--;
- }
-
- this.position++;
- this.history.push(currentHtml);
- },
-
- undo: function() {
- this.transact();
-
- if (this.position <= 1) {
- return;
- }
-
- this.set(this.history[--this.position - 1]);
- this.editor.fire("undo:composer");
- },
-
- redo: function() {
- if (this.position >= this.history.length) {
- return;
- }
-
- this.set(this.history[++this.position - 1]);
- this.editor.fire("redo:composer");
- },
-
- set: function(html) {
- this.composer.setValue(html);
- this.editor.focus(true);
- }
- });
- })(wysihtml5);
- /**
- * TODO: the following methods still need unit test coverage
- */
- wysihtml5.views.View = Base.extend(
- /** @scope wysihtml5.views.View.prototype */ {
- constructor: function(parent, textareaElement, config) {
- this.parent = parent;
- this.element = textareaElement;
- this.config = config;
-
- this._observeViewChange();
- },
-
- _observeViewChange: function() {
- var that = this;
- this.parent.observe("beforeload", function() {
- that.parent.observe("change_view", function(view) {
- if (view === that.name) {
- that.parent.currentView = that;
- that.show();
- // Using tiny delay here to make sure that the placeholder is set before focusing
- setTimeout(function() { that.focus(); }, 0);
- } else {
- that.hide();
- }
- });
- });
- },
-
- focus: function() {
- if (this.element.ownerDocument.querySelector(":focus") === this.element) {
- return;
- }
-
- try { this.element.focus(); } catch(e) {}
- },
-
- hide: function() {
- this.element.style.display = "none";
- },
-
- show: function() {
- this.element.style.display = "";
- },
-
- disable: function() {
- this.element.setAttribute("disabled", "disabled");
- },
-
- enable: function() {
- this.element.removeAttribute("disabled");
- }
- });(function(wysihtml5) {
- var dom = wysihtml5.dom,
- browser = wysihtml5.browser;
-
- wysihtml5.views.Composer = wysihtml5.views.View.extend(
- /** @scope wysihtml5.views.Composer.prototype */ {
- name: "composer",
- // Needed for firefox in order to display a proper caret in an empty contentEditable
- CARET_HACK: "<br>",
- constructor: function(parent, textareaElement, config) {
- this.base(parent, textareaElement, config);
- this.textarea = this.parent.textarea;
- this._initSandbox();
- },
- clear: function() {
- this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : this.CARET_HACK;
- },
- getValue: function(parse) {
- var value = this.isEmpty() ? "" : wysihtml5.quirks.getCorrectInnerHTML(this.element);
-
- if (parse) {
- value = this.parent.parse(value);
- }
- // Replace all "zero width no breaking space" chars
- // which are used as hacks to enable some functionalities
- // Also remove all CARET hacks that somehow got left
- value = wysihtml5.lang.string(value).replace(wysihtml5.INVISIBLE_SPACE).by("");
- return value;
- },
- setValue: function(html, parse) {
- if (parse) {
- html = this.parent.parse(html);
- }
- this.element.innerHTML = html;
- },
- show: function() {
- this.iframe.style.display = this._displayStyle || "";
- // Firefox needs this, otherwise contentEditable becomes uneditable
- this.disable();
- this.enable();
- },
- hide: function() {
- this._displayStyle = dom.getStyle("display").from(this.iframe);
- if (this._displayStyle === "none") {
- this._displayStyle = null;
- }
- this.iframe.style.display = "none";
- },
- disable: function() {
- this.element.removeAttribute("contentEditable");
- this.base();
- },
- enable: function() {
- this.element.setAttribute("contentEditable", "true");
- this.base();
- },
- focus: function(setToEnd) {
- // IE 8 fires the focus event after .focus()
- // This is needed by our simulate_placeholder.js to work
- // therefore we clear it ourselves this time
- if (wysihtml5.browser.doesAsyncFocus() && this.hasPlaceholderSet()) {
- this.clear();
- }
-
- this.base();
-
- var lastChild = this.element.lastChild;
- if (setToEnd && lastChild) {
- if (lastChild.nodeName === "BR") {
- this.selection.setBefore(this.element.lastChild);
- } else {
- this.selection.setAfter(this.element.lastChild);
- }
- }
- },
- getTextContent: function() {
- return dom.getTextContent(this.element);
- },
- hasPlaceholderSet: function() {
- return this.getTextContent() == this.textarea.element.getAttribute("placeholder");
- },
- isEmpty: function() {
- var innerHTML = this.element.innerHTML,
- elementsWithVisualValue = "blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea";
- return innerHTML === "" ||
- innerHTML === this.CARET_HACK ||
- this.hasPlaceholderSet() ||
- (this.getTextContent() === "" && !this.element.querySelector(elementsWithVisualValue));
- },
- _initSandbox: function() {
- var that = this;
-
- this.sandbox = new dom.Sandbox(function() {
- that._create();
- }, {
- stylesheets: this.config.stylesheets
- });
- this.iframe = this.sandbox.getIframe();
- // Create hidden field which tells the server after submit, that the user used an wysiwyg editor
- var hiddenField = document.createElement("input");
- hiddenField.type = "hidden";
- hiddenField.name = "_wysihtml5_mode";
- hiddenField.value = 1;
- // Store reference to current wysihtml5 instance on the textarea element
- var textareaElement = this.textarea.element;
- dom.insert(this.iframe).after(textareaElement);
- dom.insert(hiddenField).after(textareaElement);
- },
- _create: function() {
- var that = this;
-
- this.doc = this.sandbox.getDocument();
- this.element = this.doc.body;
- this.textarea = this.parent.textarea;
- this.element.innerHTML = this.textarea.getValue(true);
- this.enable();
-
- // Make sure our selection handler is ready
- this.selection = new wysihtml5.Selection(this.parent);
-
- // Make sure commands dispatcher is ready
- this.commands = new wysihtml5.Commands(this.parent);
- dom.copyAttributes([
- "className", "spellcheck", "title", "lang", "dir", "accessKey"
- ]).from(this.textarea.element).to(this.element);
-
- dom.addClass(this.element, this.config.composerClassName);
- // Make the editor look like the original textarea, by syncing styles
- if (this.config.style) {
- this.style();
- }
- this.observe();
- var name = this.config.name;
- if (name) {
- dom.addClass(this.element, name);
- dom.addClass(this.iframe, name);
- }
- // Simulate html5 placeholder attribute on contentEditable element
- var placeholderText = typeof(this.config.placeholder) === "string"
- ? this.config.placeholder
- : this.textarea.element.getAttribute("placeholder");
- if (placeholderText) {
- dom.simulatePlaceholder(this.parent, this, placeholderText);
- }
-
- // Make sure that the browser avoids using inline styles whenever possible
- this.commands.exec("styleWithCSS", false);
- this._initAutoLinking();
- this._initObjectResizing();
- this._initUndoManager();
- // Simulate html5 autofocus on contentEditable element
- if (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) {
- setTimeout(function() { that.focus(); }, 100);
- }
- wysihtml5.quirks.insertLineBreakOnReturn(this);
- // IE sometimes leaves a single paragraph, which can't be removed by the user
- if (!browser.clearsContentEditableCorrectly()) {
- wysihtml5.quirks.ensureProperClearing(this);
- }
- if (!browser.clearsListsInContentEditableCorrectly()) {
- wysihtml5.quirks.ensureProperClearingOfLists(this);
- }
- // Set up a sync that makes sure that textarea and editor have the same content
- if (this.initSync && this.config.sync) {
- this.initSync();
- }
- // Okay hide the textarea, we are ready to go
- this.textarea.hide();
- // Fire global (before-)load event
- this.parent.fire("beforeload").fire("load");
- },
- _initAutoLinking: function() {
- var that = this,
- supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(),
- supportsAutoLinking = browser.doesAutoLinkingInContentEditable();
- if (supportsDisablingOfAutoLinking) {
- this.commands.exec("autoUrlDetect", false);
- }
- if (!this.config.autoLink) {
- return;
- }
- // Only do the auto linking by ourselves when the browser doesn't support auto linking
- // OR when he supports auto linking but we were able to turn it off (IE9+)
- if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) {
- this.parent.observe("newword:composer", function() {
- that.selection.executeAndRestore(function(startContainer, endContainer) {
- dom.autoLink(endContainer.parentNode);
- });
- });
- }
- // Assuming we have the following:
- // <a href="http://www.google.de">http://www.google.de</a>
- // If a user now changes the url in the innerHTML we want to make sure that
- // it's synchronized with the href attribute (as long as the innerHTML is still a url)
- var // Use a live NodeList to check whether there are any links in the document
- links = this.sandbox.getDocument().getElementsByTagName("a"),
- // The autoLink helper method reveals a reg exp to detect correct urls
- urlRegExp = dom.autoLink.URL_REG_EXP,
- getTextContent = function(element) {
- var textContent = wysihtml5.lang.string(dom.getTextContent(element)).trim();
- if (textContent.substr(0, 4) === "www.") {
- textContent = "http://" + textContent;
- }
- return textContent;
- };
- dom.observe(this.element, "keydown", function(event) {
- if (!links.length) {
- return;
- }
- var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument),
- link = dom.getParentElement(selectedNode, { nodeName: "A" }, 4),
- textContent;
- if (!link) {
- return;
- }
- textContent = getTextContent(link);
- // keydown is fired before the actual content is changed
- // therefore we set a timeout to change the href
- setTimeout(function() {
- var newTextContent = getTextContent(link);
- if (newTextContent === textContent) {
- return;
- }
- // Only set href when new href looks like a valid url
- if (newTextContent.match(urlRegExp)) {
- link.setAttribute("href", newTextContent);
- }
- }, 0);
- });
- },
- _initObjectResizing: function() {
- var properties = ["width", "height"],
- propertiesLength = properties.length,
- element = this.element;
-
- this.commands.exec("enableObjectResizing", this.config.allowObjectResizing);
-
- if (this.config.allowObjectResizing) {
- // IE sets inline styles after resizing objects
- // The following lines make sure that the width/height css properties
- // are copied over to the width/height attributes
- if (browser.supportsEvent("resizeend")) {
- dom.observe(element, "resizeend", function(event) {
- var target = event.target || event.srcElement,
- style = target.style,
- i = 0,
- property;
- for(; i<propertiesLength; i++) {
- property = properties[i];
- if (style[property]) {
- target.setAttribute(property, parseInt(style[property], 10));
- style[property] = "";
- }
- }
- // After resizing IE sometimes forgets to remove the old resize handles
- wysihtml5.quirks.redraw(element);
- });
- }
- } else {
- if (browser.supportsEvent("resizestart")) {
- dom.observe(element, "resizestart", function(event) { event.preventDefault(); });
- }
- }
- },
-
- _initUndoManager: function() {
- new wysihtml5.UndoManager(this.parent);
- }
- });
- })(wysihtml5);(function(wysihtml5) {
- var dom = wysihtml5.dom,
- doc = document,
- win = window,
- HOST_TEMPLATE = doc.createElement("div"),
- /**
- * Styles to copy from textarea to the composer element
- */
- TEXT_FORMATTING = [
- "background-color",
- "color", "cursor",
- "font-family", "font-size", "font-style", "font-variant", "font-weight",
- "line-height", "letter-spacing",
- "text-align", "text-decoration", "text-indent", "text-rendering",
- "word-break", "word-wrap", "word-spacing"
- ],
- /**
- * Styles to copy from textarea to the iframe
- */
- BOX_FORMATTING = [
- "background-color",
- "border-collapse",
- "border-bottom-color", "border-bottom-style", "border-bottom-width",
- "border-left-color", "border-left-style", "border-left-width",
- "border-right-color", "border-right-style", "border-right-width",
- "border-top-color", "border-top-style", "border-top-width",
- "clear", "display", "float",
- "margin-bottom", "margin-left", "margin-right", "margin-top",
- "outline-color", "outline-offset", "outline-width", "outline-style",
- "padding-left", "padding-right", "padding-top", "padding-bottom",
- "position", "top", "left", "right", "bottom", "z-index",
- "vertical-align", "text-align",
- "-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing",
- "-webkit-box-shadow", "-moz-box-shadow", "-ms-box-shadow","box-shadow",
- "-webkit-border-top-right-radius", "-moz-border-radius-topright", "border-top-right-radius",
- "-webkit-border-bottom-right-radius", "-moz-border-radius-bottomright", "border-bottom-right-radius",
- "-webkit-border-bottom-left-radius", "-moz-border-radius-bottomleft", "border-bottom-left-radius",
- "-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius",
- "width", "height"
- ],
- /**
- * Styles to sync while the window gets resized
- */
- RESIZE_STYLE = [
- "width", "height",
- "top", "left", "right", "bottom"
- ],
- ADDITIONAL_CSS_RULES = [
- "html { height: 100%; }",
- "body { min-height: 100%; padding: 0; margin: 0; margin-top: -1px; padding-top: 1px; }",
- "._wysihtml5-temp { display: none; }",
- wysihtml5.browser.isGecko ?
- "body.placeholder { color: graytext !important; }" :
- "body.placeholder { color: #a9a9a9 !important; }",
- "body[disabled] { background-color: #eee !important; color: #999 !important; cursor: default !important; }",
- // Ensure that user see's broken images and can delete them
- "img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }"
- ];
-
- /**
- * With "setActive" IE offers a smart way of focusing elements without scrolling them into view:
- * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx
- *
- * Other browsers need a more hacky way: (pssst don't tell my mama)
- * In order to prevent the element being scrolled into view when focusing it, we simply
- * move it out of the scrollable area, focus it, and reset it's position
- */
- var focusWithoutScrolling = function(element) {
- if (element.setActive) {
- // Following line could cause a js error when the textarea is invisible
- // See https://github.com/xing/wysihtml5/issues/9
- try { element.setActive(); } catch(e) {}
- } else {
- var elementStyle = element.style,
- originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop,
- originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft,
- originalStyles = {
- position: elementStyle.position,
- top: elementStyle.top,
- left: elementStyle.left,
- WebkitUserSelect: elementStyle.WebkitUserSelect
- };
-
- dom.setStyles({
- position: "absolute",
- top: "-99999px",
- left: "-99999px",
- // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother
- WebkitUserSelect: "none"
- }).on(element);
-
- element.focus();
-
- dom.setStyles(originalStyles).on(element);
-
- if (win.scrollTo) {
- // Some browser extensions unset this method to prevent annoyances
- // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100
- // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1
- win.scrollTo(originalScrollLeft, originalScrollTop);
- }
- }
- };
-
-
- wysihtml5.views.Composer.prototype.style = function() {
- var that = this,
- originalActiveElement = doc.querySelector(":focus"),
- textareaElement = this.textarea.element,
- hasPlaceholder = textareaElement.hasAttribute("placeholder"),
- originalPlaceholder = hasPlaceholder && textareaElement.getAttribute("placeholder");
- this.focusStylesHost = this.focusStylesHost || HOST_TEMPLATE.cloneNode(false);
- this.blurStylesHost = this.blurStylesHost || HOST_TEMPLATE.cloneNode(false);
-
- // Remove placeholder before copying (as the placeholder has an affect on the computed style)
- if (hasPlaceholder) {
- textareaElement.removeAttribute("placeholder");
- }
-
- if (textareaElement === originalActiveElement) {
- textareaElement.blur();
- }
-
- // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) ---------
- dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.iframe).andTo(this.blurStylesHost);
-
- // --------- editor styles ---------
- dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost);
-
- // --------- apply standard rules ---------
- dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument);
-
- // --------- :focus styles ---------
- focusWithoutScrolling(textareaElement);
- dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost);
- dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost);
-
- // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus
- // this is needed for when the change_view event is fired where the iframe is hidden and then
- // the blur event fires and re-displays it
- var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(["display"]);
-
- // --------- restore focus ---------
- if (originalActiveElement) {
- originalActiveElement.focus();
- } else {
- textareaElement.blur();
- }
-
- // --------- restore placeholder ---------
- if (hasPlaceholder) {
- textareaElement.setAttribute("placeholder", originalPlaceholder);
- }
-
- // When copying styles, we only get the computed style which is never returned in percent unit
- // Therefore we've to recalculate style onresize
- if (!wysihtml5.browser.hasCurrentStyleProperty()) {
- var winObserver = dom.observe(win, "resize", function() {
- // Remove event listener if composer doesn't exist anymore
- if (!dom.contains(document.documentElement, that.iframe)) {
- winObserver.stop();
- return;
- }
- var originalTextareaDisplayStyle = dom.getStyle("display").from(textareaElement),
- originalComposerDisplayStyle = dom.getStyle("display").from(that.iframe);
- textareaElement.style.display = "";
- that.iframe.style.display = "none";
- dom.copyStyles(RESIZE_STYLE)
- .from(textareaElement)
- .to(that.iframe)
- .andTo(that.focusStylesHost)
- .andTo(that.blurStylesHost);
- that.iframe.style.display = originalComposerDisplayStyle;
- textareaElement.style.display = originalTextareaDisplayStyle;
- });
- }
-
- // --------- Sync focus/blur styles ---------
- this.parent.observe("focus:composer", function() {
- dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.iframe);
- dom.copyStyles(TEXT_FORMATTING) .from(that.focusStylesHost).to(that.element);
- });
- this.parent.observe("blur:composer", function() {
- dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.iframe);
- dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element);
- });
-
- return this;
- };
- })(wysihtml5);/**
- * Taking care of events
- * - Simulating 'change' event on contentEditable element
- * - Handling drag & drop logic
- * - Catch paste events
- * - Dispatch proprietary newword:composer event
- * - Keyboard shortcuts
- */
- (function(wysihtml5) {
- var dom = wysihtml5.dom,
- browser = wysihtml5.browser,
- /**
- * Map keyCodes to query commands
- */
- shortcuts = {
- "66": "bold", // B
- "73": "italic", // I
- "85": "underline" // U
- };
-
- wysihtml5.views.Composer.prototype.observe = function() {
- var that = this,
- state = this.getValue(),
- iframe = this.sandbox.getIframe(),
- element = this.element,
- focusBlurElement = browser.supportsEventsInIframeCorrectly() ? element : this.sandbox.getWindow(),
- // Firefox < 3.5 doesn't support the drop event, instead it supports a so called "dragdrop" event which behaves almost the same
- pasteEvents = browser.supportsEvent("drop") ? ["drop", "paste"] : ["dragdrop", "paste"];
- // --------- destroy:composer event ---------
- dom.observe(iframe, "DOMNodeRemoved", function() {
- clearInterval(domNodeRemovedInterval);
- that.parent.fire("destroy:composer");
- });
- // DOMNodeRemoved event is not supported in IE 8
- var domNodeRemovedInterval = setInterval(function() {
- if (!dom.contains(document.documentElement, iframe)) {
- clearInterval(domNodeRemovedInterval);
- that.parent.fire("destroy:composer");
- }
- }, 250);
- // --------- Focus & blur logic ---------
- dom.observe(focusBlurElement, "focus", function() {
- that.parent.fire("focus").fire("focus:composer");
- // Delay storing of state until all focus handler are fired
- // especially the one which resets the placeholder
- setTimeout(function() { state = that.getValue(); }, 0);
- });
- dom.observe(focusBlurElement, "blur", function() {
- if (state !== that.getValue()) {
- that.parent.fire("change").fire("change:composer");
- }
- that.parent.fire("blur").fire("blur:composer");
- });
-
- if (wysihtml5.browser.isIos()) {
- // When on iPad/iPhone/IPod after clicking outside of editor, the editor loses focus
- // but the UI still acts as if the editor has focus (blinking caret and onscreen keyboard visible)
- // We prevent that by focusing a temporary input element which immediately loses focus
- dom.observe(element, "blur", function() {
- var input = element.ownerDocument.createElement("input"),
- originalScrollTop = document.documentElement.scrollTop || document.body.scrollTop,
- originalScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
- try {
- that.selection.insertNode(input);
- } catch(e) {
- element.appendChild(input);
- }
- input.focus();
- input.parentNode.removeChild(input);
-
- window.scrollTo(originalScrollLeft, originalScrollTop);
- });
- }
- // --------- Drag & Drop logic ---------
- dom.observe(element, "dragenter", function() {
- that.parent.fire("unset_placeholder");
- });
- if (browser.firesOnDropOnlyWhenOnDragOverIsCancelled()) {
- dom.observe(element, ["dragover", "dragenter"], function(event) {
- event.preventDefault();
- });
- }
- dom.observe(element, pasteEvents, function(event) {
- var dataTransfer = event.dataTransfer,
- data;
- if (dataTransfer && browser.supportsDataTransfer()) {
- data = dataTransfer.getData("text/html") || dataTransfer.getData("text/plain");
- }
- if (data) {
- element.focus();
- that.commands.exec("insertHTML", data);
- that.parent.fire("paste").fire("paste:composer");
- event.stopPropagation();
- event.preventDefault();
- } else {
- setTimeout(function() {
- that.parent.fire("paste").fire("paste:composer");
- }, 0);
- }
- });
- // --------- neword event ---------
- dom.observe(element, "keyup", function(event) {
- var keyCode = event.keyCode;
- if (keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) {
- that.parent.fire("newword:composer");
- }
- });
- this.parent.observe("paste:composer", function() {
- setTimeout(function() { that.parent.fire("newword:composer"); }, 0);
- });
- // --------- Make sure that images are selected when clicking on them ---------
- if (!browser.canSelectImagesInContentEditable()) {
- dom.observe(element, "mousedown", function(event) {
- var target = event.target;
- if (target.nodeName === "IMG") {
- that.selection.selectNode(target);
- event.preventDefault();
- }
- });
- }
-
- // --------- Shortcut logic ---------
- dom.observe(element, "keydown", function(event) {
- var keyCode = event.keyCode,
- command = shortcuts[keyCode];
- if ((event.ctrlKey || event.metaKey) && !event.altKey && command) {
- that.commands.exec(command);
- event.preventDefault();
- }
- });
- // --------- Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor ---------
- dom.observe(element, "keydown", function(event) {
- var target = that.selection.getSelectedNode(true),
- keyCode = event.keyCode,
- parent;
- if (target && target.nodeName === "IMG" && (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY)) { // 8 => backspace, 46 => delete
- parent = target.parentNode;
- // delete the <img>
- parent.removeChild(target);
- // and it's parent <a> too if it hasn't got any other child nodes
- if (parent.nodeName === "A" && !parent.firstChild) {
- parent.parentNode.removeChild(parent);
- }
- setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0);
- event.preventDefault();
- }
- });
- // --------- Show url in tooltip when hovering links or images ---------
- var titlePrefixes = {
- IMG: "Image: ",
- A: "Link: "
- };
-
- dom.observe(element, "mouseover", function(event) {
- var target = event.target,
- nodeName = target.nodeName,
- title;
- if (nodeName !== "A" && nodeName !== "IMG") {
- return;
- }
- var hasTitle = target.hasAttribute("title");
- if(!hasTitle){
- title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src"));
- target.setAttribute("title", title);
- }
- });
- };
- })(wysihtml5);/**
- * Class that takes care that the value of the composer and the textarea is always in sync
- */
- (function(wysihtml5) {
- var INTERVAL = 400;
-
- wysihtml5.views.Synchronizer = Base.extend(
- /** @scope wysihtml5.views.Synchronizer.prototype */ {
- constructor: function(editor, textarea, composer) {
- this.editor = editor;
- this.textarea = textarea;
- this.composer = composer;
- this._observe();
- },
- /**
- * Sync html from composer to textarea
- * Takes care of placeholders
- * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea
- */
- fromComposerToTextarea: function(shouldParseHtml) {
- this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue()).trim(), shouldParseHtml);
- },
- /**
- * Sync value of textarea to composer
- * Takes care of placeholders
- * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer
- */
- fromTextareaToComposer: function(shouldParseHtml) {
- var textareaValue = this.textarea.getValue();
- if (textareaValue) {
- this.composer.setValue(textareaValue, shouldParseHtml);
- } else {
- this.composer.clear();
- this.editor.fire("set_placeholder");
- }
- },
- /**
- * Invoke syncing based on view state
- * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea
- */
- sync: function(shouldParseHtml) {
- if (this.editor.currentView.name === "textarea") {
- this.fromTextareaToComposer(shouldParseHtml);
- } else {
- this.fromComposerToTextarea(shouldParseHtml);
- }
- },
- /**
- * Initializes interval-based syncing
- * also makes sure that on-submit the composer's content is synced with the textarea
- * immediately when the form gets submitted
- */
- _observe: function() {
- var interval,
- that = this,
- form = this.textarea.element.form,
- startInterval = function() {
- interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);
- },
- stopInterval = function() {
- clearInterval(interval);
- interval = null;
- };
- startInterval();
- if (form) {
- // If the textarea is in a form make sure that after onreset and onsubmit the composer
- // has the correct state
- wysihtml5.dom.observe(form, "submit", function() {
- that.sync(true);
- });
- wysihtml5.dom.observe(form, "reset", function() {
- setTimeout(function() { that.fromTextareaToComposer(); }, 0);
- });
- }
- this.editor.observe("change_view", function(view) {
- if (view === "composer" && !interval) {
- that.fromTextareaToComposer(true);
- startInterval();
- } else if (view === "textarea") {
- that.fromComposerToTextarea(true);
- stopInterval();
- }
- });
- this.editor.observe("destroy:composer", stopInterval);
- }
- });
- })(wysihtml5);
- wysihtml5.views.Textarea = wysihtml5.views.View.extend(
- /** @scope wysihtml5.views.Textarea.prototype */ {
- name: "textarea",
-
- constructor: function(parent, textareaElement, config) {
- this.base(parent, textareaElement, config);
-
- this._observe();
- },
-
- clear: function() {
- this.element.value = "";
- },
-
- getValue: function(parse) {
- var value = this.isEmpty() ? "" : this.element.value;
- if (parse) {
- value = this.parent.parse(value);
- }
- return value;
- },
-
- setValue: function(html, parse) {
- if (parse) {
- html = this.parent.parse(html);
- }
- this.element.value = html;
- },
-
- hasPlaceholderSet: function() {
- var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element),
- placeholderText = this.element.getAttribute("placeholder") || null,
- value = this.element.value,
- isEmpty = !value;
- return (supportsPlaceholder && isEmpty) || (value === placeholderText);
- },
-
- isEmpty: function() {
- return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet();
- },
-
- _observe: function() {
- var element = this.element,
- parent = this.parent,
- eventMapping = {
- focusin: "focus",
- focusout: "blur"
- },
- /**
- * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events
- * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai
- */
- events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"];
-
- parent.observe("beforeload", function() {
- wysihtml5.dom.observe(element, events, function(event) {
- var eventName = eventMapping[event.type] || event.type;
- parent.fire(eventName).fire(eventName + ":textarea");
- });
-
- wysihtml5.dom.observe(element, ["paste", "drop"], function() {
- setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);
- });
- });
- }
- });/**
- * Toolbar Dialog
- *
- * @param {Element} link The toolbar link which causes the dialog to show up
- * @param {Element} container The dialog container
- *
- * @example
- * <!-- Toolbar link -->
- * <a data-wysihtml5-command="insertImage">insert an image</a>
- *
- * <!-- Dialog -->
- * <div data-wysihtml5-dialog="insertImage" style="display: none;">
- * <label>
- * URL: <input data-wysihtml5-dialog-field="src" value="http://">
- * </label>
- * <label>
- * Alternative text: <input data-wysihtml5-dialog-field="alt" value="">
- * </label>
- * </div>
- *
- * <script>
- * var dialog = new wysihtml5.toolbar.Dialog(
- * document.querySelector("[data-wysihtml5-command='insertImage']"),
- * document.querySelector("[data-wysihtml5-dialog='insertImage']")
- * );
- * dialog.observe("save", function(attributes) {
- * // do something
- * });
- * </script>
- */
- (function(wysihtml5) {
- var dom = wysihtml5.dom,
- CLASS_NAME_OPENED = "wysihtml5-command-dialog-opened",
- SELECTOR_FORM_ELEMENTS = "input, select, textarea",
- SELECTOR_FIELDS = "[data-wysihtml5-dialog-field]",
- ATTRIBUTE_FIELDS = "data-wysihtml5-dialog-field";
-
-
- wysihtml5.toolbar.Dialog = wysihtml5.lang.Dispatcher.extend(
- /** @scope wysihtml5.toolbar.Dialog.prototype */ {
- constructor: function(link, container) {
- this.link = link;
- this.container = container;
- },
- _observe: function() {
- if (this._observed) {
- return;
- }
-
- var that = this,
- callbackWrapper = function(event) {
- var attributes = that._serialize();
- if (attributes == that.elementToChange) {
- that.fire("edit", attributes);
- } else {
- that.fire("save", attributes);
- }
- that.hide();
- event.preventDefault();
- event.stopPropagation();
- };
- dom.observe(that.link, "click", function(event) {
- if (dom.hasClass(that.link, CLASS_NAME_OPENED)) {
- setTimeout(function() { that.hide(); }, 0);
- }
- });
- dom.observe(this.container, "keydown", function(event) {
- var keyCode = event.keyCode;
- if (keyCode === wysihtml5.ENTER_KEY) {
- callbackWrapper(event);
- }
- if (keyCode === wysihtml5.ESCAPE_KEY) {
- that.hide();
- }
- });
- dom.delegate(this.container, "[data-wysihtml5-dialog-action=save]", "click", callbackWrapper);
- dom.delegate(this.container, "[data-wysihtml5-dialog-action=cancel]", "click", function(event) {
- that.fire("cancel");
- that.hide();
- event.preventDefault();
- event.stopPropagation();
- });
- var formElements = this.container.querySelectorAll(SELECTOR_FORM_ELEMENTS),
- i = 0,
- length = formElements.length,
- _clearInterval = function() { clearInterval(that.interval); };
- for (; i<length; i++) {
- dom.observe(formElements[i], "change", _clearInterval);
- }
- this._observed = true;
- },
- /**
- * Grabs all fields in the dialog and puts them in key=>value style in an object which
- * then gets returned
- */
- _serialize: function() {
- var data = this.elementToChange || {},
- fields = this.container.querySelectorAll(SELECTOR_FIELDS),
- length = fields.length,
- i = 0;
- for (; i<length; i++) {
- data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value;
- }
- return data;
- },
- /**
- * Takes the attributes of the "elementToChange"
- * and inserts them in their corresponding dialog input fields
- *
- * Assume the "elementToChange" looks like this:
- * <a href="http://www.google.com" target="_blank">foo</a>
- *
- * and we have the following dialog:
- * <input type="text" data-wysihtml5-dialog-field="href" value="">
- * <input type="text" data-wysihtml5-dialog-field="target" value="">
- *
- * after calling _interpolate() the dialog will look like this
- * <input type="text" data-wysihtml5-dialog-field="href" value="http://www.google.com">
- * <input type="text" data-wysihtml5-dialog-field="target" value="_blank">
- *
- * Basically it adopted the attribute values into the corresponding input fields
- *
- */
- _interpolate: function(avoidHiddenFields) {
- var field,
- fieldName,
- newValue,
- focusedElement = document.querySelector(":focus"),
- fields = this.container.querySelectorAll(SELECTOR_FIELDS),
- length = fields.length,
- i = 0;
- for (; i<length; i++) {
- field = fields[i];
-
- // Never change elements where the user is currently typing in
- if (field === focusedElement) {
- continue;
- }
-
- // Don't update hidden fields
- // See https://github.com/xing/wysihtml5/pull/14
- if (avoidHiddenFields && field.type === "hidden") {
- continue;
- }
-
- fieldName = field.getAttribute(ATTRIBUTE_FIELDS);
- newValue = this.elementToChange ? (this.elementToChange[fieldName] || "") : field.defaultValue;
- field.value = newValue;
- }
- },
- /**
- * Show the dialog element
- */
- show: function(elementToChange) {
- var that = this,
- firstField = this.container.querySelector(SELECTOR_FORM_ELEMENTS);
- this.elementToChange = elementToChange;
- this._observe();
- this._interpolate();
- if (elementToChange) {
- this.interval = setInterval(function() { that._interpolate(true); }, 500);
- }
- dom.addClass(this.link, CLASS_NAME_OPENED);
- this.container.style.display = "";
- this.fire("show");
- if (firstField && !elementToChange) {
- try {
- firstField.focus();
- } catch(e) {}
- }
- },
- /**
- * Hide the dialog element
- */
- hide: function() {
- clearInterval(this.interval);
- this.elementToChange = null;
- dom.removeClass(this.link, CLASS_NAME_OPENED);
- this.container.style.display = "none";
- this.fire("hide");
- }
- });
- })(wysihtml5);
- /**
- * Converts speech-to-text and inserts this into the editor
- * As of now (2011/03/25) this only is supported in Chrome >= 11
- *
- * Note that it sends the recorded audio to the google speech recognition api:
- * http://stackoverflow.com/questions/4361826/does-chrome-have-buil-in-speech-recognition-for-input-type-text-x-webkit-speec
- *
- * Current HTML5 draft can be found here
- * http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html
- *
- * "Accessing Google Speech API Chrome 11"
- * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
- */
- (function(wysihtml5) {
- var dom = wysihtml5.dom;
-
- var linkStyles = {
- position: "relative"
- };
-
- var wrapperStyles = {
- left: 0,
- margin: 0,
- opacity: 0,
- overflow: "hidden",
- padding: 0,
- position: "absolute",
- top: 0,
- zIndex: 1
- };
-
- var inputStyles = {
- cursor: "inherit",
- fontSize: "50px",
- height: "50px",
- marginTop: "-25px",
- outline: 0,
- padding: 0,
- position: "absolute",
- right: "-4px",
- top: "50%"
- };
-
- var inputAttributes = {
- "x-webkit-speech": "",
- "speech": ""
- };
-
- wysihtml5.toolbar.Speech = function(parent, link) {
- var input = document.createElement("input");
- if (!wysihtml5.browser.supportsSpeechApiOn(input)) {
- link.style.display = "none";
- return;
- }
-
- var wrapper = document.createElement("div");
-
- wysihtml5.lang.object(wrapperStyles).merge({
- width: link.offsetWidth + "px",
- height: link.offsetHeight + "px"
- });
-
- dom.insert(input).into(wrapper);
- dom.insert(wrapper).into(link);
-
- dom.setStyles(inputStyles).on(input);
- dom.setAttributes(inputAttributes).on(input)
-
- dom.setStyles(wrapperStyles).on(wrapper);
- dom.setStyles(linkStyles).on(link);
-
- var eventName = "onwebkitspeechchange" in input ? "webkitspeechchange" : "speechchange";
- dom.observe(input, eventName, function() {
- parent.execCommand("insertText", input.value);
- input.value = "";
- });
-
- dom.observe(input, "click", function(event) {
- if (dom.hasClass(link, "wysihtml5-command-disabled")) {
- event.preventDefault();
- }
-
- event.stopPropagation();
- });
- };
- })(wysihtml5);/**
- * Toolbar
- *
- * @param {Object} parent Reference to instance of Editor instance
- * @param {Element} container Reference to the toolbar container element
- *
- * @example
- * <div id="toolbar">
- * <a data-wysihtml5-command="createLink">insert link</a>
- * <a data-wysihtml5-command="formatBlock" data-wysihtml5-command-value="h1">insert h1</a>
- * </div>
- *
- * <script>
- * var toolbar = new wysihtml5.toolbar.Toolbar(editor, document.getElementById("toolbar"));
- * </script>
- */
- (function(wysihtml5) {
- var CLASS_NAME_COMMAND_DISABLED = "wysihtml5-command-disabled",
- CLASS_NAME_COMMANDS_DISABLED = "wysihtml5-commands-disabled",
- CLASS_NAME_COMMAND_ACTIVE = "wysihtml5-command-active",
- CLASS_NAME_ACTION_ACTIVE = "wysihtml5-action-active",
- dom = wysihtml5.dom;
-
- wysihtml5.toolbar.Toolbar = Base.extend(
- /** @scope wysihtml5.toolbar.Toolbar.prototype */ {
- constructor: function(editor, container) {
- this.editor = editor;
- this.container = typeof(container) === "string" ? document.getElementById(container) : container;
- this.composer = editor.composer;
- this._getLinks("command");
- this._getLinks("action");
- this._observe();
- this.show();
-
- var speechInputLinks = this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"),
- length = speechInputLinks.length,
- i = 0;
- for (; i<length; i++) {
- new wysihtml5.toolbar.Speech(this, speechInputLinks[i]);
- }
- },
- _getLinks: function(type) {
- var links = this[type + "Links"] = wysihtml5.lang.array(this.container.querySelectorAll("[data-wysihtml5-" + type + "]")).get(),
- length = links.length,
- i = 0,
- mapping = this[type + "Mapping"] = {},
- link,
- group,
- name,
- value,
- dialog;
- for (; i<length; i++) {
- link = links[i];
- name = link.getAttribute("data-wysihtml5-" + type);
- value = link.getAttribute("data-wysihtml5-" + type + "-value");
- group = this.container.querySelector("[data-wysihtml5-" + type + "-group='" + name + "']");
- dialog = this._getDialog(link, name);
-
- mapping[name + ":" + value] = {
- link: link,
- group: group,
- name: name,
- value: value,
- dialog: dialog,
- state: false
- };
- }
- },
- _getDialog: function(link, command) {
- var that = this,
- dialogElement = this.container.querySelector("[data-wysihtml5-dialog='" + command + "']"),
- dialog,
- caretBookmark;
-
- if (dialogElement) {
- dialog = new wysihtml5.toolbar.Dialog(link, dialogElement);
- dialog.observe("show", function() {
- caretBookmark = that.composer.selection.getBookmark();
- that.editor.fire("show:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
- });
- dialog.observe("save", function(attributes) {
- if (caretBookmark) {
- that.composer.selection.setBookmark(caretBookmark);
- }
- that._execCommand(command, attributes);
-
- that.editor.fire("save:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
- });
- dialog.observe("cancel", function() {
- that.editor.focus(false);
- that.editor.fire("cancel:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
- });
- }
- return dialog;
- },
- /**
- * @example
- * var toolbar = new wysihtml5.Toolbar();
- * // Insert a <blockquote> element or wrap current selection in <blockquote>
- * toolbar.execCommand("formatBlock", "blockquote");
- */
- execCommand: function(command, commandValue) {
- if (this.commandsDisabled) {
- return;
- }
- var commandObj = this.commandMapping[command + ":" + commandValue];
- // Show dialog when available
- if (commandObj && commandObj.dialog && !commandObj.state) {
- commandObj.dialog.show();
- } else {
- this._execCommand(command, commandValue);
- }
- },
- _execCommand: function(command, commandValue) {
- // Make sure that composer is focussed (false => don't move caret to the end)
- this.editor.focus(false);
- this.composer.commands.exec(command, commandValue);
- this._updateLinkStates();
- },
- execAction: function(action) {
- var editor = this.editor;
- switch(action) {
- case "change_view":
- if (editor.currentView === editor.textarea) {
- editor.fire("change_view", "composer");
- } else {
- editor.fire("change_view", "textarea");
- }
- break;
- }
- },
- _observe: function() {
- var that = this,
- editor = this.editor,
- container = this.container,
- links = this.commandLinks.concat(this.actionLinks),
- length = links.length,
- i = 0;
-
- for (; i<length; i++) {
- // 'javascript:;' and unselectable=on Needed for IE, but done in all browsers to make sure that all get the same css applied
- // (you know, a:link { ... } doesn't match anchors with missing href attribute)
- dom.setAttributes({
- href: "javascript:;",
- unselectable: "on"
- }).on(links[i]);
- }
- // Needed for opera
- dom.delegate(container, "[data-wysihtml5-command]", "mousedown", function(event) { event.preventDefault(); });
-
- dom.delegate(container, "[data-wysihtml5-command]", "click", function(event) {
- var link = this,
- command = link.getAttribute("data-wysihtml5-command"),
- commandValue = link.getAttribute("data-wysihtml5-command-value");
- that.execCommand(command, commandValue);
- event.preventDefault();
- });
- dom.delegate(container, "[data-wysihtml5-action]", "click", function(event) {
- var action = this.getAttribute("data-wysihtml5-action");
- that.execAction(action);
- event.preventDefault();
- });
- editor.observe("focus:composer", function() {
- that.bookmark = null;
- clearInterval(that.interval);
- that.interval = setInterval(function() { that._updateLinkStates(); }, 500);
- });
- editor.observe("blur:composer", function() {
- clearInterval(that.interval);
- });
- editor.observe("destroy:composer", function() {
- clearInterval(that.interval);
- });
- editor.observe("change_view", function(currentView) {
- // Set timeout needed in order to let the blur event fire first
- setTimeout(function() {
- that.commandsDisabled = (currentView !== "composer");
- that._updateLinkStates();
- if (that.commandsDisabled) {
- dom.addClass(container, CLASS_NAME_COMMANDS_DISABLED);
- } else {
- dom.removeClass(container, CLASS_NAME_COMMANDS_DISABLED);
- }
- }, 0);
- });
- },
- _updateLinkStates: function() {
- var element = this.composer.element,
- commandMapping = this.commandMapping,
- actionMapping = this.actionMapping,
- i,
- state,
- action,
- command;
- // every millisecond counts... this is executed quite often
- for (i in commandMapping) {
- command = commandMapping[i];
- if (this.commandsDisabled) {
- state = false;
- dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
- if (command.group) {
- dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
- }
- if (command.dialog) {
- command.dialog.hide();
- }
- } else {
- state = this.composer.commands.state(command.name, command.value);
- if (wysihtml5.lang.object(state).isArray()) {
- // Grab first and only object/element in state array, otherwise convert state into boolean
- // to avoid showing a dialog for multiple selected elements which may have different attributes
- // eg. when two links with different href are selected, the state will be an array consisting of both link elements
- // but the dialog interface can only update one
- state = state.length === 1 ? state[0] : true;
- }
- dom.removeClass(command.link, CLASS_NAME_COMMAND_DISABLED);
- if (command.group) {
- dom.removeClass(command.group, CLASS_NAME_COMMAND_DISABLED);
- }
- }
- if (command.state === state) {
- continue;
- }
- command.state = state;
- if (state) {
- dom.addClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
- if (command.group) {
- dom.addClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
- }
- if (command.dialog) {
- if (typeof(state) === "object") {
- command.dialog.show(state);
- } else {
- command.dialog.hide();
- }
- }
- } else {
- dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
- if (command.group) {
- dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
- }
- if (command.dialog) {
- command.dialog.hide();
- }
- }
- }
-
- for (i in actionMapping) {
- action = actionMapping[i];
-
- if (action.name === "change_view") {
- action.state = this.editor.currentView === this.editor.textarea;
- if (action.state) {
- dom.addClass(action.link, CLASS_NAME_ACTION_ACTIVE);
- } else {
- dom.removeClass(action.link, CLASS_NAME_ACTION_ACTIVE);
- }
- }
- }
- },
- show: function() {
- this.container.style.display = "";
- },
- hide: function() {
- this.container.style.display = "none";
- }
- });
-
- })(wysihtml5);
- /**
- * WYSIHTML5 Editor
- *
- * @param {Element} textareaElement Reference to the textarea which should be turned into a rich text interface
- * @param {Object} [config] See defaultConfig object below for explanation of each individual config option
- *
- * @events
- * load
- * beforeload (for internal use only)
- * focus
- * focus:composer
- * focus:textarea
- * blur
- * blur:composer
- * blur:textarea
- * change
- * change:composer
- * change:textarea
- * paste
- * paste:composer
- * paste:textarea
- * newword:composer
- * destroy:composer
- * undo:composer
- * redo:composer
- * beforecommand:composer
- * aftercommand:composer
- * change_view
- */
- (function(wysihtml5) {
- var undef;
-
- var defaultConfig = {
- // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body
- name: undef,
- // Whether the editor should look like the textarea (by adopting styles)
- style: true,
- // Id of the toolbar element, pass falsey value if you don't want any toolbar logic
- toolbar: undef,
- // Whether urls, entered by the user should automatically become clickable-links
- autoLink: true,
- // Object which includes parser rules to apply when html gets inserted via copy & paste
- // See parser_rules/*.js for examples
- parserRules: { tags: { br: {}, span: {}, div: {}, p: {} }, classes: {} },
- // Parser method to use when the user inserts content via copy & paste
- parser: wysihtml5.dom.parse,
- // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option
- composerClassName: "wysihtml5-editor",
- // Class name to add to the body when the wysihtml5 editor is supported
- bodyClassName: "wysihtml5-supported",
- // Array (or single string) of stylesheet urls to be loaded in the editor's iframe
- stylesheets: [],
- // Placeholder text to use, defaults to the placeholder attribute on the textarea element
- placeholderText: undef,
- // Whether the composer should allow the user to manually resize images, tables etc.
- allowObjectResizing: true,
- // Whether the rich text editor should be rendered on touch devices (wysihtml5 >= 0.3.0 comes with basic support for iOS 5)
- supportTouchDevices: true
- };
-
- wysihtml5.Editor = wysihtml5.lang.Dispatcher.extend(
- /** @scope wysihtml5.Editor.prototype */ {
- constructor: function(textareaElement, config) {
- this.textareaElement = typeof(textareaElement) === "string" ? document.getElementById(textareaElement) : textareaElement;
- this.config = wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get();
- this.textarea = new wysihtml5.views.Textarea(this, this.textareaElement, this.config);
- this.currentView = this.textarea;
- this._isCompatible = wysihtml5.browser.supported();
-
- // Sort out unsupported/unwanted browsers here
- if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) {
- var that = this;
- setTimeout(function() { that.fire("beforeload").fire("load"); }, 0);
- return;
- }
-
- // Add class name to body, to indicate that the editor is supported
- wysihtml5.dom.addClass(document.body, this.config.bodyClassName);
-
- this.composer = new wysihtml5.views.Composer(this, this.textareaElement, this.config);
- this.currentView = this.composer;
-
- if (typeof(this.config.parser) === "function") {
- this._initParser();
- }
-
- this.observe("beforeload", function() {
- this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer);
- if (this.config.toolbar) {
- this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar);
- }
- });
-
- try {
- console.log("Heya! This page is using wysihtml5 for rich text editing. Check out https://github.com/xing/wysihtml5");
- } catch(e) {}
- },
-
- isCompatible: function() {
- return this._isCompatible;
- },
- clear: function() {
- this.currentView.clear();
- return this;
- },
- getValue: function(parse) {
- return this.currentView.getValue(parse);
- },
- setValue: function(html, parse) {
- if (!html) {
- return this.clear();
- }
- this.currentView.setValue(html, parse);
- return this;
- },
- focus: function(setToEnd) {
- this.currentView.focus(setToEnd);
- return this;
- },
- /**
- * Deactivate editor (make it readonly)
- */
- disable: function() {
- this.currentView.disable();
- return this;
- },
-
- /**
- * Activate editor
- */
- enable: function() {
- this.currentView.enable();
- return this;
- },
-
- isEmpty: function() {
- return this.currentView.isEmpty();
- },
-
- hasPlaceholderSet: function() {
- return this.currentView.hasPlaceholderSet();
- },
-
- parse: function(htmlOrElement) {
- var returnValue = this.config.parser(htmlOrElement, this.config.parserRules, this.composer.sandbox.getDocument(), true);
- if (typeof(htmlOrElement) === "object") {
- wysihtml5.quirks.redraw(htmlOrElement);
- }
- return returnValue;
- },
-
- /**
- * Prepare html parser logic
- * - Observes for paste and drop
- */
- _initParser: function() {
- this.observe("paste:composer", function() {
- var keepScrollPosition = true,
- that = this;
- that.composer.selection.executeAndRestore(function() {
- wysihtml5.quirks.cleanPastedHTML(that.composer.element);
- that.parse(that.composer.element);
- }, keepScrollPosition);
- });
-
- this.observe("paste:textarea", function() {
- var value = this.textarea.getValue(),
- newValue;
- newValue = this.parse(value);
- this.textarea.setValue(newValue);
- });
- }
- });
- })(wysihtml5);
|