summernote.js 159 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380
  1. /**
  2. * Super simple wysiwyg editor on Bootstrap v0.5.10
  3. * http://hackerwins.github.io/summernote/
  4. *
  5. * summernote.js
  6. * Copyright 2013-2014 Alan Hong. and other contributors
  7. * summernote may be freely distributed under the MIT license./
  8. *
  9. * Date: 2014-10-03T06:12Z
  10. */
  11. (function (factory) {
  12. /* global define */
  13. if (typeof define === 'function' && define.amd) {
  14. // AMD. Register as an anonymous module.
  15. define(['jquery'], factory);
  16. } else {
  17. // Browser globals: jQuery
  18. factory(window.jQuery);
  19. }
  20. }(function ($) {
  21. if ('function' !== typeof Array.prototype.reduce) {
  22. /**
  23. * Array.prototype.reduce fallback
  24. *
  25. * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
  26. */
  27. Array.prototype.reduce = function (callback, optInitialValue) {
  28. var idx, value, length = this.length >>> 0, isValueSet = false;
  29. if (1 < arguments.length) {
  30. value = optInitialValue;
  31. isValueSet = true;
  32. }
  33. for (idx = 0; length > idx; ++idx) {
  34. if (this.hasOwnProperty(idx)) {
  35. if (isValueSet) {
  36. value = callback(value, this[idx], idx, this);
  37. } else {
  38. value = this[idx];
  39. isValueSet = true;
  40. }
  41. }
  42. }
  43. if (!isValueSet) {
  44. throw new TypeError('Reduce of empty array with no initial value');
  45. }
  46. return value;
  47. };
  48. }
  49. if ('function' !== typeof Array.prototype.filter) {
  50. Array.prototype.filter = function (fun/*, thisArg*/) {
  51. if (this === void 0 || this === null) {
  52. throw new TypeError();
  53. }
  54. var t = Object(this);
  55. var len = t.length >>> 0;
  56. if (typeof fun !== 'function') {
  57. throw new TypeError();
  58. }
  59. var res = [];
  60. var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
  61. for (var i = 0; i < len; i++) {
  62. if (i in t) {
  63. var val = t[i];
  64. if (fun.call(thisArg, val, i, t)) {
  65. res.push(val);
  66. }
  67. }
  68. }
  69. return res;
  70. };
  71. }
  72. var isSupportAmd = typeof define === 'function' && define.amd;
  73. /**
  74. * returns whether font is installed or not.
  75. * @param {String} fontName
  76. * @return {Boolean}
  77. */
  78. var isFontInstalled = function (fontName) {
  79. var testFontName = fontName === 'Comic Sans MS' ? 'Courier New' : 'Comic Sans MS';
  80. var $tester = $('<div>').css({
  81. position: 'absolute',
  82. left: '-9999px',
  83. top: '-9999px',
  84. fontSize: '200px'
  85. }).text('mmmmmmmmmwwwwwww').appendTo(document.body);
  86. var originalWidth = $tester.css('fontFamily', testFontName).width();
  87. var width = $tester.css('fontFamily', fontName + ',' + testFontName).width();
  88. $tester.remove();
  89. return originalWidth !== width;
  90. };
  91. /**
  92. * Object which check platform and agent
  93. */
  94. var agent = {
  95. isMac: navigator.appVersion.indexOf('Mac') > -1,
  96. isMSIE: navigator.userAgent.indexOf('MSIE') > -1 || navigator.userAgent.indexOf('Trident') > -1,
  97. isFF: navigator.userAgent.indexOf('Firefox') > -1,
  98. jqueryVersion: parseFloat($.fn.jquery),
  99. isSupportAmd: isSupportAmd,
  100. hasCodeMirror: isSupportAmd ? require.specified('CodeMirror') : !!window.CodeMirror,
  101. isFontInstalled: isFontInstalled,
  102. isW3CRangeSupport: !!document.createRange
  103. };
  104. /**
  105. * func utils (for high-order func's arg)
  106. */
  107. var func = (function () {
  108. var eq = function (itemA) {
  109. return function (itemB) {
  110. return itemA === itemB;
  111. };
  112. };
  113. var eq2 = function (itemA, itemB) {
  114. return itemA === itemB;
  115. };
  116. var peq2 = function (propName) {
  117. return function (itemA, itemB) {
  118. return itemA[propName] === itemB[propName];
  119. };
  120. };
  121. var ok = function () {
  122. return true;
  123. };
  124. var fail = function () {
  125. return false;
  126. };
  127. var not = function (f) {
  128. return function () {
  129. return !f.apply(f, arguments);
  130. };
  131. };
  132. var and = function (fA, fB) {
  133. return function (item) {
  134. return fA(item) && fB(item);
  135. };
  136. };
  137. var self = function (a) {
  138. return a;
  139. };
  140. var idCounter = 0;
  141. /**
  142. * generate a globally-unique id
  143. *
  144. * @param {String} [prefix]
  145. */
  146. var uniqueId = function (prefix) {
  147. var id = ++idCounter + '';
  148. return prefix ? prefix + id : id;
  149. };
  150. /**
  151. * returns bnd (bounds) from rect
  152. *
  153. * - IE Compatability Issue: http://goo.gl/sRLOAo
  154. * - Scroll Issue: http://goo.gl/sNjUc
  155. *
  156. * @param {Rect} rect
  157. * @return {Object} bounds
  158. * @return {Number} bounds.top
  159. * @return {Number} bounds.left
  160. * @return {Number} bounds.width
  161. * @return {Number} bounds.height
  162. */
  163. var rect2bnd = function (rect) {
  164. var $document = $(document);
  165. return {
  166. top: rect.top + $document.scrollTop(),
  167. left: rect.left + $document.scrollLeft(),
  168. width: rect.right - rect.left,
  169. height: rect.bottom - rect.top
  170. };
  171. };
  172. /**
  173. * returns a copy of the object where the keys have become the values and the values the keys.
  174. * @param {Object} obj
  175. * @return {Object}
  176. */
  177. var invertObject = function (obj) {
  178. var inverted = {};
  179. for (var key in obj) {
  180. if (obj.hasOwnProperty(key)) {
  181. inverted[obj[key]] = key;
  182. }
  183. }
  184. return inverted;
  185. };
  186. return {
  187. eq: eq,
  188. eq2: eq2,
  189. peq2: peq2,
  190. ok: ok,
  191. fail: fail,
  192. self: self,
  193. not: not,
  194. and: and,
  195. uniqueId: uniqueId,
  196. rect2bnd: rect2bnd,
  197. invertObject: invertObject
  198. };
  199. })();
  200. /**
  201. * list utils
  202. */
  203. var list = (function () {
  204. /**
  205. * returns the first item of an array.
  206. *
  207. * @param {Array} array
  208. */
  209. var head = function (array) {
  210. return array[0];
  211. };
  212. /**
  213. * returns the last item of an array.
  214. *
  215. * @param {Array} array
  216. */
  217. var last = function (array) {
  218. return array[array.length - 1];
  219. };
  220. /**
  221. * returns everything but the last entry of the array.
  222. *
  223. * @param {Array} array
  224. */
  225. var initial = function (array) {
  226. return array.slice(0, array.length - 1);
  227. };
  228. /**
  229. * returns the rest of the items in an array.
  230. *
  231. * @param {Array} array
  232. */
  233. var tail = function (array) {
  234. return array.slice(1);
  235. };
  236. /**
  237. * returns item of array
  238. */
  239. var find = function (array, pred) {
  240. for (var idx = 0, len = array.length; idx < len; idx ++) {
  241. var item = array[idx];
  242. if (pred(item)) {
  243. return item;
  244. }
  245. }
  246. };
  247. /**
  248. * returns true if all of the values in the array pass the predicate truth test.
  249. */
  250. var all = function (array, pred) {
  251. for (var idx = 0, len = array.length; idx < len; idx ++) {
  252. if (!pred(array[idx])) {
  253. return false;
  254. }
  255. }
  256. return true;
  257. };
  258. /**
  259. * returns true if the value is present in the list.
  260. */
  261. var contains = function (array, item) {
  262. return $.inArray(item, array) !== -1;
  263. };
  264. /**
  265. * get sum from a list
  266. *
  267. * @param {Array} array - array
  268. * @param {Function} fn - iterator
  269. */
  270. var sum = function (array, fn) {
  271. fn = fn || func.self;
  272. return array.reduce(function (memo, v) {
  273. return memo + fn(v);
  274. }, 0);
  275. };
  276. /**
  277. * returns a copy of the collection with array type.
  278. * @param {Collection} collection - collection eg) node.childNodes, ...
  279. */
  280. var from = function (collection) {
  281. var result = [], idx = -1, length = collection.length;
  282. while (++idx < length) {
  283. result[idx] = collection[idx];
  284. }
  285. return result;
  286. };
  287. /**
  288. * cluster elements by predicate function.
  289. *
  290. * @param {Array} array - array
  291. * @param {Function} fn - predicate function for cluster rule
  292. * @param {Array[]}
  293. */
  294. var clusterBy = function (array, fn) {
  295. if (!array.length) { return []; }
  296. var aTail = tail(array);
  297. return aTail.reduce(function (memo, v) {
  298. var aLast = last(memo);
  299. if (fn(last(aLast), v)) {
  300. aLast[aLast.length] = v;
  301. } else {
  302. memo[memo.length] = [v];
  303. }
  304. return memo;
  305. }, [[head(array)]]);
  306. };
  307. /**
  308. * returns a copy of the array with all falsy values removed
  309. *
  310. * @param {Array} array - array
  311. * @param {Function} fn - predicate function for cluster rule
  312. */
  313. var compact = function (array) {
  314. var aResult = [];
  315. for (var idx = 0, len = array.length; idx < len; idx ++) {
  316. if (array[idx]) { aResult.push(array[idx]); }
  317. }
  318. return aResult;
  319. };
  320. /**
  321. * produces a duplicate-free version of the array
  322. *
  323. * @param {Array} array
  324. */
  325. var unique = function (array) {
  326. var results = [];
  327. for (var idx = 0, len = array.length; idx < len; idx ++) {
  328. if (!contains(results, array[idx])) {
  329. results.push(array[idx]);
  330. }
  331. }
  332. return results;
  333. };
  334. return { head: head, last: last, initial: initial, tail: tail,
  335. find: find, contains: contains,
  336. all: all, sum: sum, from: from,
  337. clusterBy: clusterBy, compact: compact, unique: unique };
  338. })();
  339. var NBSP_CHAR = String.fromCharCode(160);
  340. var ZERO_WIDTH_NBSP_CHAR = '\ufeff';
  341. /**
  342. * Dom functions
  343. */
  344. var dom = (function () {
  345. /**
  346. * returns whether node is `note-editable` or not.
  347. *
  348. * @param {Node} node
  349. * @return {Boolean}
  350. */
  351. var isEditable = function (node) {
  352. return node && $(node).hasClass('note-editable');
  353. };
  354. /**
  355. * returns whether node is `note-control-sizing` or not.
  356. *
  357. * @param {Node} node
  358. * @return {Boolean}
  359. */
  360. var isControlSizing = function (node) {
  361. return node && $(node).hasClass('note-control-sizing');
  362. };
  363. /**
  364. * build layoutInfo from $editor(.note-editor)
  365. *
  366. * @param {jQuery} $editor
  367. * @return {Object}
  368. */
  369. var buildLayoutInfo = function ($editor) {
  370. var makeFinder;
  371. // air mode
  372. if ($editor.hasClass('note-air-editor')) {
  373. var id = list.last($editor.attr('id').split('-'));
  374. makeFinder = function (sIdPrefix) {
  375. return function () { return $(sIdPrefix + id); };
  376. };
  377. return {
  378. editor: function () { return $editor; },
  379. editable: function () { return $editor; },
  380. popover: makeFinder('#note-popover-'),
  381. handle: makeFinder('#note-handle-'),
  382. dialog: makeFinder('#note-dialog-')
  383. };
  384. // frame mode
  385. } else {
  386. makeFinder = function (sClassName) {
  387. return function () { return $editor.find(sClassName); };
  388. };
  389. return {
  390. editor: function () { return $editor; },
  391. dropzone: makeFinder('.note-dropzone'),
  392. toolbar: makeFinder('.note-toolbar'),
  393. editable: makeFinder('.note-editable'),
  394. codable: makeFinder('.note-codable'),
  395. statusbar: makeFinder('.note-statusbar'),
  396. popover: makeFinder('.note-popover'),
  397. handle: makeFinder('.note-handle'),
  398. dialog: makeFinder('.note-dialog')
  399. };
  400. }
  401. };
  402. /**
  403. * returns predicate which judge whether nodeName is same
  404. *
  405. * @param {String} nodeName
  406. * @return {String}
  407. */
  408. var makePredByNodeName = function (nodeName) {
  409. nodeName = nodeName.toUpperCase();
  410. return function (node) {
  411. return node && node.nodeName.toUpperCase() === nodeName;
  412. };
  413. };
  414. var isText = function (node) {
  415. return node && node.nodeType === 3;
  416. };
  417. /**
  418. * ex) br, col, embed, hr, img, input, ...
  419. * @see http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements
  420. */
  421. var isVoid = function (node) {
  422. return node && /^BR|^IMG|^HR/.test(node.nodeName.toUpperCase());
  423. };
  424. var isPara = function (node) {
  425. if (isEditable(node)) {
  426. return false;
  427. }
  428. // Chrome(v31.0), FF(v25.0.1) use DIV for paragraph
  429. return node && /^DIV|^P|^LI|^H[1-7]/.test(node.nodeName.toUpperCase());
  430. };
  431. var isLi = makePredByNodeName('LI');
  432. var isPurePara = function (node) {
  433. return isPara(node) && !isLi(node);
  434. };
  435. var isInline = function (node) {
  436. return !isBodyContainer(node) && !isList(node) && !isPara(node);
  437. };
  438. var isList = function (node) {
  439. return node && /^UL|^OL/.test(node.nodeName.toUpperCase());
  440. };
  441. var isCell = function (node) {
  442. return node && /^TD|^TH/.test(node.nodeName.toUpperCase());
  443. };
  444. var isBlockquote = makePredByNodeName('BLOCKQUOTE');
  445. var isBodyContainer = function (node) {
  446. return isCell(node) || isBlockquote(node) || isEditable(node);
  447. };
  448. var isAnchor = makePredByNodeName('A');
  449. var isParaInline = function (node) {
  450. return isInline(node) && !!ancestor(node, isPara);
  451. };
  452. var isBodyInline = function (node) {
  453. return isInline(node) && !ancestor(node, isPara);
  454. };
  455. var isBody = makePredByNodeName('BODY');
  456. /**
  457. * blank HTML for cursor position
  458. */
  459. var blankHTML = agent.isMSIE ? '&nbsp;' : '<br>';
  460. /**
  461. * returns #text's text size or element's childNodes size
  462. *
  463. * @param {Node} node
  464. */
  465. var nodeLength = function (node) {
  466. if (isText(node)) {
  467. return node.nodeValue.length;
  468. }
  469. return node.childNodes.length;
  470. };
  471. /**
  472. * returns whether node is empty or not.
  473. *
  474. * @param {Node} node
  475. * @return {Boolean}
  476. */
  477. var isEmpty = function (node) {
  478. var len = nodeLength(node);
  479. if (len === 0) {
  480. return true;
  481. } else if (!dom.isText(node) && len === 1 && node.innerHTML === blankHTML) {
  482. // ex) <p><br></p>, <span><br></span>
  483. return true;
  484. }
  485. return false;
  486. };
  487. /**
  488. * padding blankHTML if node is empty (for cursor position)
  489. */
  490. var paddingBlankHTML = function (node) {
  491. if (!isVoid(node) && !nodeLength(node)) {
  492. node.innerHTML = blankHTML;
  493. }
  494. };
  495. /**
  496. * find nearest ancestor predicate hit
  497. *
  498. * @param {Node} node
  499. * @param {Function} pred - predicate function
  500. */
  501. var ancestor = function (node, pred) {
  502. while (node) {
  503. if (pred(node)) { return node; }
  504. if (isEditable(node)) { break; }
  505. node = node.parentNode;
  506. }
  507. return null;
  508. };
  509. /**
  510. * returns new array of ancestor nodes (until predicate hit).
  511. *
  512. * @param {Node} node
  513. * @param {Function} [optional] pred - predicate function
  514. */
  515. var listAncestor = function (node, pred) {
  516. pred = pred || func.fail;
  517. var ancestors = [];
  518. ancestor(node, function (el) {
  519. if (!isEditable(el)) {
  520. ancestors.push(el);
  521. }
  522. return pred(el);
  523. });
  524. return ancestors;
  525. };
  526. /**
  527. * find farthest ancestor predicate hit
  528. */
  529. var lastAncestor = function (node, pred) {
  530. var ancestors = listAncestor(node);
  531. return list.last(ancestors.filter(pred));
  532. };
  533. /**
  534. * returns common ancestor node between two nodes.
  535. *
  536. * @param {Node} nodeA
  537. * @param {Node} nodeB
  538. */
  539. var commonAncestor = function (nodeA, nodeB) {
  540. var ancestors = listAncestor(nodeA);
  541. for (var n = nodeB; n; n = n.parentNode) {
  542. if ($.inArray(n, ancestors) > -1) { return n; }
  543. }
  544. return null; // difference document area
  545. };
  546. /**
  547. * listing all previous siblings (until predicate hit).
  548. *
  549. * @param {Node} node
  550. * @param {Function} [optional] pred - predicate function
  551. */
  552. var listPrev = function (node, pred) {
  553. pred = pred || func.fail;
  554. var nodes = [];
  555. while (node) {
  556. if (pred(node)) { break; }
  557. nodes.push(node);
  558. node = node.previousSibling;
  559. }
  560. return nodes;
  561. };
  562. /**
  563. * listing next siblings (until predicate hit).
  564. *
  565. * @param {Node} node
  566. * @param {Function} [pred] - predicate function
  567. */
  568. var listNext = function (node, pred) {
  569. pred = pred || func.fail;
  570. var nodes = [];
  571. while (node) {
  572. if (pred(node)) { break; }
  573. nodes.push(node);
  574. node = node.nextSibling;
  575. }
  576. return nodes;
  577. };
  578. /**
  579. * listing descendant nodes
  580. *
  581. * @param {Node} node
  582. * @param {Function} [pred] - predicate function
  583. */
  584. var listDescendant = function (node, pred) {
  585. var descendents = [];
  586. pred = pred || func.ok;
  587. // start DFS(depth first search) with node
  588. (function fnWalk(current) {
  589. if (node !== current && pred(current)) {
  590. descendents.push(current);
  591. }
  592. for (var idx = 0, len = current.childNodes.length; idx < len; idx++) {
  593. fnWalk(current.childNodes[idx]);
  594. }
  595. })(node);
  596. return descendents;
  597. };
  598. /**
  599. * wrap node with new tag.
  600. *
  601. * @param {Node} node
  602. * @param {Node} tagName of wrapper
  603. * @return {Node} - wrapper
  604. */
  605. var wrap = function (node, wrapperName) {
  606. var parent = node.parentNode;
  607. var wrapper = $('<' + wrapperName + '>')[0];
  608. parent.insertBefore(wrapper, node);
  609. wrapper.appendChild(node);
  610. return wrapper;
  611. };
  612. /**
  613. * insert node after preceding
  614. *
  615. * @param {Node} node
  616. * @param {Node} preceding - predicate function
  617. */
  618. var insertAfter = function (node, preceding) {
  619. var next = preceding.nextSibling, parent = preceding.parentNode;
  620. if (next) {
  621. parent.insertBefore(node, next);
  622. } else {
  623. parent.appendChild(node);
  624. }
  625. return node;
  626. };
  627. /**
  628. * append elements.
  629. *
  630. * @param {Node} node
  631. * @param {Collection} aChild
  632. */
  633. var appendChildNodes = function (node, aChild) {
  634. $.each(aChild, function (idx, child) {
  635. node.appendChild(child);
  636. });
  637. return node;
  638. };
  639. /**
  640. * returns whether boundaryPoint is left edge or not.
  641. *
  642. * @param {BoundaryPoint} point
  643. * @return {Boolean}
  644. */
  645. var isLeftEdgePoint = function (point) {
  646. return point.offset === 0;
  647. };
  648. /**
  649. * returns whether boundaryPoint is right edge or not.
  650. *
  651. * @param {BoundaryPoint} point
  652. * @return {Boolean}
  653. */
  654. var isRightEdgePoint = function (point) {
  655. return point.offset === nodeLength(point.node);
  656. };
  657. /**
  658. * returns whether boundaryPoint is edge or not.
  659. *
  660. * @param {BoundaryPoint} point
  661. * @return {Boolean}
  662. */
  663. var isEdgePoint = function (point) {
  664. return isLeftEdgePoint(point) || isRightEdgePoint(point);
  665. };
  666. /**
  667. * returns wheter node is left edge of ancestor or not.
  668. *
  669. * @param {Node} node
  670. * @param {Node} ancestor
  671. * @return {Boolean}
  672. */
  673. var isLeftEdgeOf = function (node, ancestor) {
  674. while (node && node !== ancestor) {
  675. if (position(node) !== 0) {
  676. return false;
  677. }
  678. node = node.parentNode;
  679. }
  680. return true;
  681. };
  682. /**
  683. * returns whether node is right edge of ancestor or not.
  684. *
  685. * @param {Node} node
  686. * @param {Node} ancestor
  687. * @return {Boolean}
  688. */
  689. var isRightEdgeOf = function (node, ancestor) {
  690. while (node && node !== ancestor) {
  691. if (position(node) !== nodeLength(node.parentNode) - 1) {
  692. return false;
  693. }
  694. node = node.parentNode;
  695. }
  696. return true;
  697. };
  698. /**
  699. * returns offset from parent.
  700. *
  701. * @param {Node} node
  702. */
  703. var position = function (node) {
  704. var offset = 0;
  705. while ((node = node.previousSibling)) {
  706. offset += 1;
  707. }
  708. return offset;
  709. };
  710. var hasChildren = function (node) {
  711. return !!(node && node.childNodes && node.childNodes.length);
  712. };
  713. /**
  714. * returns previous boundaryPoint
  715. *
  716. * @param {BoundaryPoint} point
  717. * @param {Boolean} isSkipInnerOffset
  718. * @return {BoundaryPoint}
  719. */
  720. var prevPoint = function (point, isSkipInnerOffset) {
  721. var node, offset;
  722. if (point.offset === 0) {
  723. if (isEditable(point.node)) {
  724. return null;
  725. }
  726. node = point.node.parentNode;
  727. offset = position(point.node);
  728. } else if (hasChildren(point.node)) {
  729. node = point.node.childNodes[point.offset - 1];
  730. offset = nodeLength(node);
  731. } else {
  732. node = point.node;
  733. offset = isSkipInnerOffset ? 0 : point.offset - 1;
  734. }
  735. return {
  736. node: node,
  737. offset: offset
  738. };
  739. };
  740. /**
  741. * returns next boundaryPoint
  742. *
  743. * @param {BoundaryPoint} point
  744. * @param {Boolean} isSkipInnerOffset
  745. * @return {BoundaryPoint}
  746. */
  747. var nextPoint = function (point, isSkipInnerOffset) {
  748. var node, offset;
  749. if (nodeLength(point.node) === point.offset) {
  750. if (isEditable(point.node)) {
  751. return null;
  752. }
  753. node = point.node.parentNode;
  754. offset = position(point.node) + 1;
  755. } else if (hasChildren(point.node)) {
  756. node = point.node.childNodes[point.offset];
  757. offset = 0;
  758. } else {
  759. node = point.node;
  760. offset = isSkipInnerOffset ? nodeLength(point.node) : point.offset + 1;
  761. }
  762. return {
  763. node: node,
  764. offset: offset
  765. };
  766. };
  767. /**
  768. * returns whether pointA and pointB is same or not.
  769. *
  770. * @param {BoundaryPoint} pointA
  771. * @param {BoundaryPoint} pointB
  772. * @return {Boolean}
  773. */
  774. var isSamePoint = function (pointA, pointB) {
  775. return pointA.node === pointB.node && pointA.offset === pointB.offset;
  776. };
  777. /**
  778. * returns whether point is visible (can set cursor) or not.
  779. *
  780. * @param {BoundaryPoint} point
  781. * @return {Boolean}
  782. */
  783. var isVisiblePoint = function (point) {
  784. if (isText(point.node) || !hasChildren(point.node) || isEmpty(point.node)) {
  785. return true;
  786. }
  787. var leftNode = point.node.childNodes[point.offset - 1];
  788. var rightNode = point.node.childNodes[point.offset];
  789. if ((!leftNode || isVoid(leftNode)) && (!rightNode || isVoid(rightNode))) {
  790. return true;
  791. }
  792. return false;
  793. };
  794. /**
  795. * @param {BoundaryPoint} point
  796. * @param {Function} pred
  797. * @return {BoundaryPoint}
  798. */
  799. var prevPointUntil = function (point, pred) {
  800. while (point) {
  801. if (pred(point)) {
  802. return point;
  803. }
  804. point = prevPoint(point);
  805. }
  806. return null;
  807. };
  808. /**
  809. * @param {BoundaryPoint} point
  810. * @param {Function} pred
  811. * @return {BoundaryPoint}
  812. */
  813. var nextPointUntil = function (point, pred) {
  814. while (point) {
  815. if (pred(point)) {
  816. return point;
  817. }
  818. point = nextPoint(point);
  819. }
  820. return null;
  821. };
  822. /**
  823. * @param {BoundaryPoint} startPoint
  824. * @param {BoundaryPoint} endPoint
  825. * @param {Function} handler
  826. * @param {Boolean} isSkipInnerOffset
  827. */
  828. var walkPoint = function (startPoint, endPoint, handler, isSkipInnerOffset) {
  829. var point = startPoint;
  830. while (point) {
  831. handler(point);
  832. if (isSamePoint(point, endPoint)) {
  833. break;
  834. }
  835. var isSkipOffset = isSkipInnerOffset &&
  836. startPoint.node !== point.node &&
  837. endPoint.node !== point.node;
  838. point = nextPoint(point, isSkipOffset);
  839. }
  840. };
  841. /**
  842. * return offsetPath(array of offset) from ancestor
  843. *
  844. * @param {Node} ancestor - ancestor node
  845. * @param {Node} node
  846. */
  847. var makeOffsetPath = function (ancestor, node) {
  848. var ancestors = listAncestor(node, func.eq(ancestor));
  849. return $.map(ancestors, position).reverse();
  850. };
  851. /**
  852. * return element from offsetPath(array of offset)
  853. *
  854. * @param {Node} ancestor - ancestor node
  855. * @param {array} aOffset - offsetPath
  856. */
  857. var fromOffsetPath = function (ancestor, aOffset) {
  858. var current = ancestor;
  859. for (var i = 0, len = aOffset.length; i < len; i++) {
  860. if (current.childNodes.length <= aOffset[i]) {
  861. current = current.childNodes[current.childNodes.length - 1];
  862. } else {
  863. current = current.childNodes[aOffset[i]];
  864. }
  865. }
  866. return current;
  867. };
  868. /**
  869. * split element or #text
  870. *
  871. * @param {BoundaryPoint} point
  872. * @param {Boolean} [isSkipPaddingBlankHTML]
  873. * @return {Node} right node of boundaryPoint
  874. */
  875. var splitNode = function (point, isSkipPaddingBlankHTML) {
  876. // split #text
  877. if (isText(point.node)) {
  878. // edge case
  879. if (isLeftEdgePoint(point)) {
  880. return point.node;
  881. } else if (isRightEdgePoint(point)) {
  882. return point.node.nextSibling;
  883. }
  884. return point.node.splitText(point.offset);
  885. }
  886. // split element
  887. var childNode = point.node.childNodes[point.offset];
  888. var clone = insertAfter(point.node.cloneNode(false), point.node);
  889. appendChildNodes(clone, listNext(childNode));
  890. if (!isSkipPaddingBlankHTML) {
  891. paddingBlankHTML(point.node);
  892. paddingBlankHTML(clone);
  893. }
  894. return clone;
  895. };
  896. /**
  897. * split tree by point
  898. *
  899. * @param {Node} root - split root
  900. * @param {BoundaryPoint} point
  901. * @param {Boolean} [isSkipPaddingBlankHTML]
  902. * @return {Node} right node of boundaryPoint
  903. */
  904. var splitTree = function (root, point, isSkipPaddingBlankHTML) {
  905. // ex) [#text, <span>, <p>]
  906. var ancestors = listAncestor(point.node, func.eq(root));
  907. if (!ancestors.length) {
  908. return null;
  909. } else if (ancestors.length === 1) {
  910. return splitNode(point, isSkipPaddingBlankHTML);
  911. }
  912. return ancestors.reduce(function (node, parent) {
  913. var clone = insertAfter(parent.cloneNode(false), parent);
  914. if (node === point.node) {
  915. node = splitNode(point, isSkipPaddingBlankHTML);
  916. }
  917. appendChildNodes(clone, listNext(node));
  918. if (!isSkipPaddingBlankHTML) {
  919. paddingBlankHTML(parent);
  920. paddingBlankHTML(clone);
  921. }
  922. return clone;
  923. });
  924. };
  925. var create = function (nodeName) {
  926. return document.createElement(nodeName);
  927. };
  928. var createText = function (text) {
  929. return document.createTextNode(text);
  930. };
  931. /**
  932. * remove node, (isRemoveChild: remove child or not)
  933. * @param {Node} node
  934. * @param {Boolean} isRemoveChild
  935. */
  936. var remove = function (node, isRemoveChild) {
  937. if (!node || !node.parentNode) { return; }
  938. if (node.removeNode) { return node.removeNode(isRemoveChild); }
  939. var parent = node.parentNode;
  940. if (!isRemoveChild) {
  941. var nodes = [];
  942. var i, len;
  943. for (i = 0, len = node.childNodes.length; i < len; i++) {
  944. nodes.push(node.childNodes[i]);
  945. }
  946. for (i = 0, len = nodes.length; i < len; i++) {
  947. parent.insertBefore(nodes[i], node);
  948. }
  949. }
  950. parent.removeChild(node);
  951. };
  952. /**
  953. * @param {Node} node
  954. * @param {Function} pred
  955. */
  956. var removeWhile = function (node, pred) {
  957. while (node) {
  958. if (isEditable(node) || !pred(node)) {
  959. break;
  960. }
  961. var parent = node.parentNode;
  962. remove(node);
  963. node = parent;
  964. }
  965. };
  966. /**
  967. * replace node with provided nodeName
  968. *
  969. * @param {Node} node
  970. * @param {String} nodeName
  971. * @return {Node} - new node
  972. */
  973. var replace = function (node, nodeName) {
  974. if (node.nodeName.toUpperCase() === nodeName.toUpperCase()) {
  975. return node;
  976. }
  977. var newNode = create(nodeName);
  978. if (node.style.cssText) {
  979. newNode.style.cssText = node.style.cssText;
  980. }
  981. appendChildNodes(newNode, list.from(node.childNodes));
  982. insertAfter(newNode, node);
  983. remove(node);
  984. return newNode;
  985. };
  986. var isTextarea = makePredByNodeName('TEXTAREA');
  987. /**
  988. * get the HTML contents of node
  989. *
  990. * @param {jQuery} $node
  991. * @param {Boolean} [isNewlineOnBlock]
  992. */
  993. var html = function ($node, isNewlineOnBlock) {
  994. var markup = isTextarea($node[0]) ? $node.val() : $node.html();
  995. if (isNewlineOnBlock) {
  996. var regexTag = /<(\/?)(\b(?!!)[^>\s]*)(.*?)(\s*\/?>)/g;
  997. markup = markup.replace(regexTag, function (match, endSlash, name) {
  998. name = name.toUpperCase();
  999. var isEndOfInlineContainer = /^DIV|^TD|^TH|^P|^LI|^H[1-7]/.test(name) &&
  1000. !!endSlash;
  1001. var isBlockNode = /^BLOCKQUOTE|^TABLE|^TBODY|^TR|^HR|^UL|^OL/.test(name);
  1002. return match + ((isEndOfInlineContainer || isBlockNode) ? '\n' : '');
  1003. });
  1004. markup = $.trim(markup);
  1005. }
  1006. return markup;
  1007. };
  1008. var value = function ($textarea) {
  1009. var val = $textarea.val();
  1010. // strip line breaks
  1011. return val.replace(/[\n\r]/g, '');
  1012. };
  1013. return {
  1014. NBSP_CHAR: NBSP_CHAR,
  1015. ZERO_WIDTH_NBSP_CHAR: ZERO_WIDTH_NBSP_CHAR,
  1016. blank: blankHTML,
  1017. emptyPara: '<p>' + blankHTML + '</p>',
  1018. isEditable: isEditable,
  1019. isControlSizing: isControlSizing,
  1020. buildLayoutInfo: buildLayoutInfo,
  1021. isText: isText,
  1022. isPara: isPara,
  1023. isPurePara: isPurePara,
  1024. isInline: isInline,
  1025. isBodyInline: isBodyInline,
  1026. isBody: isBody,
  1027. isParaInline: isParaInline,
  1028. isList: isList,
  1029. isTable: makePredByNodeName('TABLE'),
  1030. isCell: isCell,
  1031. isBlockquote: isBlockquote,
  1032. isBodyContainer: isBodyContainer,
  1033. isAnchor: isAnchor,
  1034. isDiv: makePredByNodeName('DIV'),
  1035. isLi: isLi,
  1036. isSpan: makePredByNodeName('SPAN'),
  1037. isB: makePredByNodeName('B'),
  1038. isU: makePredByNodeName('U'),
  1039. isS: makePredByNodeName('S'),
  1040. isI: makePredByNodeName('I'),
  1041. isImg: makePredByNodeName('IMG'),
  1042. isTextarea: isTextarea,
  1043. isEmpty: isEmpty,
  1044. isEmptyAnchor: func.and(isAnchor, isEmpty),
  1045. nodeLength: nodeLength,
  1046. isLeftEdgePoint: isLeftEdgePoint,
  1047. isRightEdgePoint: isRightEdgePoint,
  1048. isEdgePoint: isEdgePoint,
  1049. isLeftEdgeOf: isLeftEdgeOf,
  1050. isRightEdgeOf: isRightEdgeOf,
  1051. prevPoint: prevPoint,
  1052. nextPoint: nextPoint,
  1053. isSamePoint: isSamePoint,
  1054. isVisiblePoint: isVisiblePoint,
  1055. prevPointUntil: prevPointUntil,
  1056. nextPointUntil: nextPointUntil,
  1057. walkPoint: walkPoint,
  1058. ancestor: ancestor,
  1059. listAncestor: listAncestor,
  1060. lastAncestor: lastAncestor,
  1061. listNext: listNext,
  1062. listPrev: listPrev,
  1063. listDescendant: listDescendant,
  1064. commonAncestor: commonAncestor,
  1065. wrap: wrap,
  1066. insertAfter: insertAfter,
  1067. appendChildNodes: appendChildNodes,
  1068. position: position,
  1069. hasChildren: hasChildren,
  1070. makeOffsetPath: makeOffsetPath,
  1071. fromOffsetPath: fromOffsetPath,
  1072. splitTree: splitTree,
  1073. create: create,
  1074. createText: createText,
  1075. remove: remove,
  1076. removeWhile: removeWhile,
  1077. replace: replace,
  1078. html: html,
  1079. value: value
  1080. };
  1081. })();
  1082. var settings = {
  1083. // version
  1084. version: '0.5.10',
  1085. /**
  1086. * options
  1087. */
  1088. options: {
  1089. width: null, // set editor width
  1090. height: null, // set editor height, ex) 300
  1091. minHeight: null, // set minimum height of editor
  1092. maxHeight: null, // set maximum height of editor
  1093. focus: false, // set focus to editable area after initializing summernote
  1094. tabsize: 4, // size of tab ex) 2 or 4
  1095. styleWithSpan: true, // style with span (Chrome and FF only)
  1096. disableLinkTarget: false, // hide link Target Checkbox
  1097. disableDragAndDrop: false, // disable drag and drop event
  1098. disableResizeEditor: false, // disable resizing editor
  1099. codemirror: { // codemirror options
  1100. mode: 'text/html',
  1101. htmlMode: true,
  1102. lineNumbers: true
  1103. },
  1104. // language
  1105. lang: 'en-US', // language 'en-US', 'ko-KR', ...
  1106. direction: null, // text direction, ex) 'rtl'
  1107. // toolbar
  1108. toolbar: [
  1109. ['style', ['style']],
  1110. ['font', ['bold', 'italic', 'underline', 'superscript', 'subscript', 'strikethrough', 'clear']],
  1111. ['fontname', ['fontname']],
  1112. // ['fontsize', ['fontsize']], // Still buggy
  1113. ['color', ['color']],
  1114. ['para', ['ul', 'ol', 'paragraph']],
  1115. ['height', ['height']],
  1116. ['table', ['table']],
  1117. ['insert', ['link', 'picture', 'video', 'hr']],
  1118. ['view', ['fullscreen', 'codeview']],
  1119. ['help', ['help']]
  1120. ],
  1121. // air mode: inline editor
  1122. airMode: false,
  1123. // airPopover: [
  1124. // ['style', ['style']],
  1125. // ['font', ['bold', 'italic', 'underline', 'clear']],
  1126. // ['fontname', ['fontname']],
  1127. // ['fontsize', ['fontsize']], // Still buggy
  1128. // ['color', ['color']],
  1129. // ['para', ['ul', 'ol', 'paragraph']],
  1130. // ['height', ['height']],
  1131. // ['table', ['table']],
  1132. // ['insert', ['link', 'picture', 'video']],
  1133. // ['help', ['help']]
  1134. // ],
  1135. airPopover: [
  1136. ['color', ['color']],
  1137. ['font', ['bold', 'underline', 'clear']],
  1138. ['para', ['ul', 'paragraph']],
  1139. ['table', ['table']],
  1140. ['insert', ['link', 'picture']]
  1141. ],
  1142. // style tag
  1143. styleTags: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
  1144. // default fontName
  1145. defaultFontName: 'Helvetica Neue',
  1146. // fontName
  1147. fontNames: [
  1148. 'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New',
  1149. 'Helvetica Neue', 'Impact', 'Lucida Grande',
  1150. 'Tahoma', 'Times New Roman', 'Verdana'
  1151. ],
  1152. // pallete colors(n x n)
  1153. colors: [
  1154. ['#000000', '#424242', '#636363', '#9C9C94', '#CEC6CE', '#EFEFEF', '#F7F7F7', '#FFFFFF'],
  1155. ['#FF0000', '#FF9C00', '#FFFF00', '#00FF00', '#00FFFF', '#0000FF', '#9C00FF', '#FF00FF'],
  1156. ['#F7C6CE', '#FFE7CE', '#FFEFC6', '#D6EFD6', '#CEDEE7', '#CEE7F7', '#D6D6E7', '#E7D6DE'],
  1157. ['#E79C9C', '#FFC69C', '#FFE79C', '#B5D6A5', '#A5C6CE', '#9CC6EF', '#B5A5D6', '#D6A5BD'],
  1158. ['#E76363', '#F7AD6B', '#FFD663', '#94BD7B', '#73A5AD', '#6BADDE', '#8C7BC6', '#C67BA5'],
  1159. ['#CE0000', '#E79439', '#EFC631', '#6BA54A', '#4A7B8C', '#3984C6', '#634AA5', '#A54A7B'],
  1160. ['#9C0000', '#B56308', '#BD9400', '#397B21', '#104A5A', '#085294', '#311873', '#731842'],
  1161. ['#630000', '#7B3900', '#846300', '#295218', '#083139', '#003163', '#21104A', '#4A1031']
  1162. ],
  1163. // fontSize
  1164. fontSizes: ['8', '9', '10', '11', '12', '14', '18', '24', '36'],
  1165. // lineHeight
  1166. lineHeights: ['1.0', '1.2', '1.4', '1.5', '1.6', '1.8', '2.0', '3.0'],
  1167. // insertTable max size
  1168. insertTableMaxSize: {
  1169. col: 10,
  1170. row: 10
  1171. },
  1172. // callbacks
  1173. oninit: null, // initialize
  1174. onfocus: null, // editable has focus
  1175. onblur: null, // editable out of focus
  1176. onenter: null, // enter key pressed
  1177. onkeyup: null, // keyup
  1178. onkeydown: null, // keydown
  1179. onImageUpload: null, // imageUpload
  1180. onImageUploadError: null, // imageUploadError
  1181. onToolbarClick: null,
  1182. /**
  1183. * manipulate link address when user create link
  1184. * @param {String} sLinkUrl
  1185. * @return {String}
  1186. */
  1187. onCreateLink: function (sLinkUrl) {
  1188. if (sLinkUrl.indexOf('@') !== -1 && sLinkUrl.indexOf(':') === -1) {
  1189. sLinkUrl = 'mailto:' + sLinkUrl;
  1190. } else if (sLinkUrl.indexOf('://') === -1) {
  1191. sLinkUrl = 'http://' + sLinkUrl;
  1192. }
  1193. return sLinkUrl;
  1194. },
  1195. keyMap: {
  1196. pc: {
  1197. 'ENTER': 'insertParagraph',
  1198. 'CTRL+Z': 'undo',
  1199. 'CTRL+Y': 'redo',
  1200. 'TAB': 'tab',
  1201. 'SHIFT+TAB': 'untab',
  1202. 'CTRL+B': 'bold',
  1203. 'CTRL+I': 'italic',
  1204. 'CTRL+U': 'underline',
  1205. 'CTRL+SHIFT+S': 'strikethrough',
  1206. 'CTRL+BACKSLASH': 'removeFormat',
  1207. 'CTRL+SHIFT+L': 'justifyLeft',
  1208. 'CTRL+SHIFT+E': 'justifyCenter',
  1209. 'CTRL+SHIFT+R': 'justifyRight',
  1210. 'CTRL+SHIFT+J': 'justifyFull',
  1211. 'CTRL+SHIFT+NUM7': 'insertUnorderedList',
  1212. 'CTRL+SHIFT+NUM8': 'insertOrderedList',
  1213. 'CTRL+LEFTBRACKET': 'outdent',
  1214. 'CTRL+RIGHTBRACKET': 'indent',
  1215. 'CTRL+NUM0': 'formatPara',
  1216. 'CTRL+NUM1': 'formatH1',
  1217. 'CTRL+NUM2': 'formatH2',
  1218. 'CTRL+NUM3': 'formatH3',
  1219. 'CTRL+NUM4': 'formatH4',
  1220. 'CTRL+NUM5': 'formatH5',
  1221. 'CTRL+NUM6': 'formatH6',
  1222. 'CTRL+ENTER': 'insertHorizontalRule',
  1223. 'CTRL+K': 'showLinkDialog'
  1224. },
  1225. mac: {
  1226. 'ENTER': 'insertParagraph',
  1227. 'CMD+Z': 'undo',
  1228. 'CMD+SHIFT+Z': 'redo',
  1229. 'TAB': 'tab',
  1230. 'SHIFT+TAB': 'untab',
  1231. 'CMD+B': 'bold',
  1232. 'CMD+I': 'italic',
  1233. 'CMD+U': 'underline',
  1234. 'CMD+SHIFT+S': 'strikethrough',
  1235. 'CMD+BACKSLASH': 'removeFormat',
  1236. 'CMD+SHIFT+L': 'justifyLeft',
  1237. 'CMD+SHIFT+E': 'justifyCenter',
  1238. 'CMD+SHIFT+R': 'justifyRight',
  1239. 'CMD+SHIFT+J': 'justifyFull',
  1240. 'CMD+SHIFT+NUM7': 'insertUnorderedList',
  1241. 'CMD+SHIFT+NUM8': 'insertOrderedList',
  1242. 'CMD+LEFTBRACKET': 'outdent',
  1243. 'CMD+RIGHTBRACKET': 'indent',
  1244. 'CMD+NUM0': 'formatPara',
  1245. 'CMD+NUM1': 'formatH1',
  1246. 'CMD+NUM2': 'formatH2',
  1247. 'CMD+NUM3': 'formatH3',
  1248. 'CMD+NUM4': 'formatH4',
  1249. 'CMD+NUM5': 'formatH5',
  1250. 'CMD+NUM6': 'formatH6',
  1251. 'CMD+ENTER': 'insertHorizontalRule',
  1252. 'CMD+K': 'showLinkDialog'
  1253. }
  1254. }
  1255. },
  1256. // default language: en-US
  1257. lang: {
  1258. 'en-US': {
  1259. font: {
  1260. bold: 'Bold',
  1261. italic: 'Italic',
  1262. underline: 'Underline',
  1263. strikethrough: 'Strikethrough',
  1264. subscript: 'Subscript',
  1265. superscript: 'Superscript',
  1266. clear: 'Remove Font Style',
  1267. height: 'Line Height',
  1268. name: 'Font Family',
  1269. size: 'Font Size'
  1270. },
  1271. image: {
  1272. image: 'Picture',
  1273. insert: 'Insert Image',
  1274. resizeFull: 'Resize Full',
  1275. resizeHalf: 'Resize Half',
  1276. resizeQuarter: 'Resize Quarter',
  1277. floatLeft: 'Float Left',
  1278. floatRight: 'Float Right',
  1279. floatNone: 'Float None',
  1280. shapeRounded: 'Shape: Rounded',
  1281. shapeCircle: 'Shape: Circle',
  1282. shapeThumbnail: 'Shape: Thumbnail',
  1283. shapeNone: 'Shape: None',
  1284. dragImageHere: 'Drag an image here',
  1285. selectFromFiles: 'Select from files',
  1286. url: 'Image URL',
  1287. remove: 'Remove Image'
  1288. },
  1289. link: {
  1290. link: 'Link',
  1291. insert: 'Insert Link',
  1292. unlink: 'Unlink',
  1293. edit: 'Edit',
  1294. textToDisplay: 'Text to display',
  1295. url: 'To what URL should this link go?',
  1296. openInNewWindow: 'Open in new window'
  1297. },
  1298. video: {
  1299. video: 'Video',
  1300. videoLink: 'Video Link',
  1301. insert: 'Insert Video',
  1302. url: 'Video URL?',
  1303. providers: '(YouTube, Vimeo, Vine, Instagram, DailyMotion or Youku)'
  1304. },
  1305. table: {
  1306. table: 'Table'
  1307. },
  1308. hr: {
  1309. insert: 'Insert Horizontal Rule'
  1310. },
  1311. style: {
  1312. style: 'Style',
  1313. normal: 'Normal',
  1314. blockquote: 'Quote',
  1315. pre: 'Code',
  1316. h1: 'Header 1',
  1317. h2: 'Header 2',
  1318. h3: 'Header 3',
  1319. h4: 'Header 4',
  1320. h5: 'Header 5',
  1321. h6: 'Header 6'
  1322. },
  1323. lists: {
  1324. unordered: 'Unordered list',
  1325. ordered: 'Ordered list'
  1326. },
  1327. options: {
  1328. help: 'Help',
  1329. fullscreen: 'Full Screen',
  1330. codeview: 'Code View'
  1331. },
  1332. paragraph: {
  1333. paragraph: 'Paragraph',
  1334. outdent: 'Outdent',
  1335. indent: 'Indent',
  1336. left: 'Align left',
  1337. center: 'Align center',
  1338. right: 'Align right',
  1339. justify: 'Justify full'
  1340. },
  1341. color: {
  1342. recent: 'Recent Color',
  1343. more: 'More Color',
  1344. background: 'Background Color',
  1345. foreground: 'Foreground Color',
  1346. transparent: 'Transparent',
  1347. setTransparent: 'Set transparent',
  1348. reset: 'Reset',
  1349. resetToDefault: 'Reset to default'
  1350. },
  1351. shortcut: {
  1352. shortcuts: 'Keyboard shortcuts',
  1353. close: 'Close',
  1354. textFormatting: 'Text formatting',
  1355. action: 'Action',
  1356. paragraphFormatting: 'Paragraph formatting',
  1357. documentStyle: 'Document Style'
  1358. },
  1359. history: {
  1360. undo: 'Undo',
  1361. redo: 'Redo'
  1362. }
  1363. }
  1364. }
  1365. };
  1366. /**
  1367. * Async functions which returns `Promise`
  1368. */
  1369. var async = (function () {
  1370. /**
  1371. * read contents of file as representing URL
  1372. *
  1373. * @param {File} file
  1374. * @return {Promise} - then: sDataUrl
  1375. */
  1376. var readFileAsDataURL = function (file) {
  1377. return $.Deferred(function (deferred) {
  1378. $.extend(new FileReader(), {
  1379. onload: function (e) {
  1380. var sDataURL = e.target.result;
  1381. deferred.resolve(sDataURL);
  1382. },
  1383. onerror: function () {
  1384. deferred.reject(this);
  1385. }
  1386. }).readAsDataURL(file);
  1387. }).promise();
  1388. };
  1389. /**
  1390. * create `<image>` from url string
  1391. *
  1392. * @param {String} sUrl
  1393. * @return {Promise} - then: $image
  1394. */
  1395. var createImage = function (sUrl, filename) {
  1396. return $.Deferred(function (deferred) {
  1397. $('<img>').one('load', function () {
  1398. deferred.resolve($(this));
  1399. }).one('error abort', function () {
  1400. deferred.reject($(this));
  1401. }).css({
  1402. display: 'none'
  1403. }).appendTo(document.body)
  1404. .attr('src', sUrl)
  1405. .attr('data-filename', filename);
  1406. }).promise();
  1407. };
  1408. return {
  1409. readFileAsDataURL: readFileAsDataURL,
  1410. createImage: createImage
  1411. };
  1412. })();
  1413. /**
  1414. * Object for keycodes.
  1415. */
  1416. var key = {
  1417. isEdit: function (keyCode) {
  1418. return list.contains([8, 9, 13, 32], keyCode);
  1419. },
  1420. nameFromCode: {
  1421. '8': 'BACKSPACE',
  1422. '9': 'TAB',
  1423. '13': 'ENTER',
  1424. '32': 'SPACE',
  1425. // Number: 0-9
  1426. '48': 'NUM0',
  1427. '49': 'NUM1',
  1428. '50': 'NUM2',
  1429. '51': 'NUM3',
  1430. '52': 'NUM4',
  1431. '53': 'NUM5',
  1432. '54': 'NUM6',
  1433. '55': 'NUM7',
  1434. '56': 'NUM8',
  1435. // Alphabet: a-z
  1436. '66': 'B',
  1437. '69': 'E',
  1438. '73': 'I',
  1439. '74': 'J',
  1440. '75': 'K',
  1441. '76': 'L',
  1442. '82': 'R',
  1443. '83': 'S',
  1444. '85': 'U',
  1445. '89': 'Y',
  1446. '90': 'Z',
  1447. '191': 'SLASH',
  1448. '219': 'LEFTBRACKET',
  1449. '220': 'BACKSLASH',
  1450. '221': 'RIGHTBRACKET'
  1451. }
  1452. };
  1453. /**
  1454. * Style
  1455. * @class
  1456. */
  1457. var Style = function () {
  1458. /**
  1459. * passing an array of style properties to .css()
  1460. * will result in an object of property-value pairs.
  1461. * (compability with version < 1.9)
  1462. *
  1463. * @param {jQuery} $obj
  1464. * @param {Array} propertyNames - An array of one or more CSS properties.
  1465. * @returns {Object}
  1466. */
  1467. var jQueryCSS = function ($obj, propertyNames) {
  1468. if (agent.jqueryVersion < 1.9) {
  1469. var result = {};
  1470. $.each(propertyNames, function (idx, propertyName) {
  1471. result[propertyName] = $obj.css(propertyName);
  1472. });
  1473. return result;
  1474. }
  1475. return $obj.css.call($obj, propertyNames);
  1476. };
  1477. /**
  1478. * paragraph level style
  1479. *
  1480. * @param {WrappedRange} rng
  1481. * @param {Object} styleInfo
  1482. */
  1483. this.stylePara = function (rng, styleInfo) {
  1484. $.each(rng.nodes(dom.isPara, {
  1485. includeAncestor: true
  1486. }), function (idx, para) {
  1487. $(para).css(styleInfo);
  1488. });
  1489. };
  1490. /**
  1491. * get current style on cursor
  1492. *
  1493. * @param {WrappedRange} rng
  1494. * @param {Node} target - target element on event
  1495. * @return {Object} - object contains style properties.
  1496. */
  1497. this.current = function (rng, target) {
  1498. var $cont = $(dom.isText(rng.sc) ? rng.sc.parentNode : rng.sc);
  1499. var properties = ['font-family', 'font-size', 'text-align', 'list-style-type', 'line-height'];
  1500. var styleInfo = jQueryCSS($cont, properties) || {};
  1501. styleInfo['font-size'] = parseInt(styleInfo['font-size'], 10);
  1502. // document.queryCommandState for toggle state
  1503. styleInfo['font-bold'] = document.queryCommandState('bold') ? 'bold' : 'normal';
  1504. styleInfo['font-italic'] = document.queryCommandState('italic') ? 'italic' : 'normal';
  1505. styleInfo['font-underline'] = document.queryCommandState('underline') ? 'underline' : 'normal';
  1506. styleInfo['font-strikethrough'] = document.queryCommandState('strikeThrough') ? 'strikethrough' : 'normal';
  1507. styleInfo['font-superscript'] = document.queryCommandState('superscript') ? 'superscript' : 'normal';
  1508. styleInfo['font-subscript'] = document.queryCommandState('subscript') ? 'subscript' : 'normal';
  1509. // list-style-type to list-style(unordered, ordered)
  1510. if (!rng.isOnList()) {
  1511. styleInfo['list-style'] = 'none';
  1512. } else {
  1513. var aOrderedType = ['circle', 'disc', 'disc-leading-zero', 'square'];
  1514. var isUnordered = $.inArray(styleInfo['list-style-type'], aOrderedType) > -1;
  1515. styleInfo['list-style'] = isUnordered ? 'unordered' : 'ordered';
  1516. }
  1517. var para = dom.ancestor(rng.sc, dom.isPara);
  1518. if (para && para.style['line-height']) {
  1519. styleInfo['line-height'] = para.style.lineHeight;
  1520. } else {
  1521. var lineHeight = parseInt(styleInfo['line-height'], 10) / parseInt(styleInfo['font-size'], 10);
  1522. styleInfo['line-height'] = lineHeight.toFixed(1);
  1523. }
  1524. styleInfo.image = dom.isImg(target) && target;
  1525. styleInfo.anchor = rng.isOnAnchor() && dom.ancestor(rng.sc, dom.isAnchor);
  1526. styleInfo.ancestors = dom.listAncestor(rng.sc, dom.isEditable);
  1527. styleInfo.range = rng;
  1528. return styleInfo;
  1529. };
  1530. };
  1531. /**
  1532. * Data structure
  1533. * - {BoundaryPoint}: a point of dom tree
  1534. * - {BoundaryPoints}: two boundaryPoints corresponding to the start and the end of the Range
  1535. *
  1536. * @see http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Position
  1537. */
  1538. var range = (function () {
  1539. /**
  1540. * return boundaryPoint from TextRange, inspired by Andy Na's HuskyRange.js
  1541. *
  1542. * @param {TextRange} textRange
  1543. * @param {Boolean} isStart
  1544. * @return {BoundaryPoint}
  1545. *
  1546. * @see http://msdn.microsoft.com/en-us/library/ie/ms535872(v=vs.85).aspx
  1547. */
  1548. var textRangeToPoint = function (textRange, isStart) {
  1549. var container = textRange.parentElement(), offset;
  1550. var tester = document.body.createTextRange(), prevContainer;
  1551. var childNodes = list.from(container.childNodes);
  1552. for (offset = 0; offset < childNodes.length; offset++) {
  1553. if (dom.isText(childNodes[offset])) {
  1554. continue;
  1555. }
  1556. tester.moveToElementText(childNodes[offset]);
  1557. if (tester.compareEndPoints('StartToStart', textRange) >= 0) {
  1558. break;
  1559. }
  1560. prevContainer = childNodes[offset];
  1561. }
  1562. if (offset !== 0 && dom.isText(childNodes[offset - 1])) {
  1563. var textRangeStart = document.body.createTextRange(), curTextNode = null;
  1564. textRangeStart.moveToElementText(prevContainer || container);
  1565. textRangeStart.collapse(!prevContainer);
  1566. curTextNode = prevContainer ? prevContainer.nextSibling : container.firstChild;
  1567. var pointTester = textRange.duplicate();
  1568. pointTester.setEndPoint('StartToStart', textRangeStart);
  1569. var textCount = pointTester.text.replace(/[\r\n]/g, '').length;
  1570. while (textCount > curTextNode.nodeValue.length && curTextNode.nextSibling) {
  1571. textCount -= curTextNode.nodeValue.length;
  1572. curTextNode = curTextNode.nextSibling;
  1573. }
  1574. /* jshint ignore:start */
  1575. var dummy = curTextNode.nodeValue; // enforce IE to re-reference curTextNode, hack
  1576. /* jshint ignore:end */
  1577. if (isStart && curTextNode.nextSibling && dom.isText(curTextNode.nextSibling) &&
  1578. textCount === curTextNode.nodeValue.length) {
  1579. textCount -= curTextNode.nodeValue.length;
  1580. curTextNode = curTextNode.nextSibling;
  1581. }
  1582. container = curTextNode;
  1583. offset = textCount;
  1584. }
  1585. return {
  1586. cont: container,
  1587. offset: offset
  1588. };
  1589. };
  1590. /**
  1591. * return TextRange from boundary point (inspired by google closure-library)
  1592. * @param {BoundaryPoint} point
  1593. * @return {TextRange}
  1594. */
  1595. var pointToTextRange = function (point) {
  1596. var textRangeInfo = function (container, offset) {
  1597. var node, isCollapseToStart;
  1598. if (dom.isText(container)) {
  1599. var prevTextNodes = dom.listPrev(container, func.not(dom.isText));
  1600. var prevContainer = list.last(prevTextNodes).previousSibling;
  1601. node = prevContainer || container.parentNode;
  1602. offset += list.sum(list.tail(prevTextNodes), dom.nodeLength);
  1603. isCollapseToStart = !prevContainer;
  1604. } else {
  1605. node = container.childNodes[offset] || container;
  1606. if (dom.isText(node)) {
  1607. return textRangeInfo(node, 0);
  1608. }
  1609. offset = 0;
  1610. isCollapseToStart = false;
  1611. }
  1612. return {
  1613. node: node,
  1614. collapseToStart: isCollapseToStart,
  1615. offset: offset
  1616. };
  1617. };
  1618. var textRange = document.body.createTextRange();
  1619. var info = textRangeInfo(point.node, point.offset);
  1620. textRange.moveToElementText(info.node);
  1621. textRange.collapse(info.collapseToStart);
  1622. textRange.moveStart('character', info.offset);
  1623. return textRange;
  1624. };
  1625. /**
  1626. * Wrapped Range
  1627. *
  1628. * @param {Node} sc - start container
  1629. * @param {Number} so - start offset
  1630. * @param {Node} ec - end container
  1631. * @param {Number} eo - end offset
  1632. */
  1633. var WrappedRange = function (sc, so, ec, eo) {
  1634. this.sc = sc;
  1635. this.so = so;
  1636. this.ec = ec;
  1637. this.eo = eo;
  1638. // nativeRange: get nativeRange from sc, so, ec, eo
  1639. var nativeRange = function () {
  1640. if (agent.isW3CRangeSupport) {
  1641. var w3cRange = document.createRange();
  1642. w3cRange.setStart(sc, so);
  1643. w3cRange.setEnd(ec, eo);
  1644. return w3cRange;
  1645. } else {
  1646. var textRange = pointToTextRange({
  1647. node: sc,
  1648. offset: so
  1649. });
  1650. textRange.setEndPoint('EndToEnd', pointToTextRange({
  1651. node: ec,
  1652. offset: eo
  1653. }));
  1654. return textRange;
  1655. }
  1656. };
  1657. this.getPoints = function () {
  1658. return {
  1659. sc: sc,
  1660. so: so,
  1661. ec: ec,
  1662. eo: eo
  1663. };
  1664. };
  1665. this.getStartPoint = function () {
  1666. return {
  1667. node: sc,
  1668. offset: so
  1669. };
  1670. };
  1671. this.getEndPoint = function () {
  1672. return {
  1673. node: ec,
  1674. offset: eo
  1675. };
  1676. };
  1677. /**
  1678. * select update visible range
  1679. */
  1680. this.select = function () {
  1681. var nativeRng = nativeRange();
  1682. if (agent.isW3CRangeSupport) {
  1683. var selection = document.getSelection();
  1684. if (selection.rangeCount > 0) {
  1685. selection.removeAllRanges();
  1686. }
  1687. selection.addRange(nativeRng);
  1688. } else {
  1689. nativeRng.select();
  1690. }
  1691. };
  1692. /**
  1693. * @return {WrappedRange}
  1694. */
  1695. this.normalize = function () {
  1696. var getVisiblePoint = function (point) {
  1697. if (!dom.isVisiblePoint(point)) {
  1698. if (dom.isLeftEdgePoint(point)) {
  1699. point = dom.nextPointUntil(point, dom.isVisiblePoint);
  1700. } else if (dom.isRightEdgePoint(point)) {
  1701. point = dom.prevPointUntil(point, dom.isVisiblePoint);
  1702. }
  1703. }
  1704. return point;
  1705. };
  1706. var startPoint = getVisiblePoint(this.getStartPoint());
  1707. var endPoint = getVisiblePoint(this.getStartPoint());
  1708. return new WrappedRange(
  1709. startPoint.node,
  1710. startPoint.offset,
  1711. endPoint.node,
  1712. endPoint.offset
  1713. );
  1714. };
  1715. /**
  1716. * returns matched nodes on range
  1717. *
  1718. * @param {Function} [pred] - predicate function
  1719. * @param {Object} [options]
  1720. * @param {Boolean} [options.includeAncestor]
  1721. * @param {Boolean} [options.fullyContains]
  1722. * @return {Node[]}
  1723. */
  1724. this.nodes = function (pred, options) {
  1725. pred = pred || func.ok;
  1726. var includeAncestor = options && options.includeAncestor;
  1727. var fullyContains = options && options.fullyContains;
  1728. // TODO compare points and sort
  1729. var startPoint = this.getStartPoint();
  1730. var endPoint = this.getEndPoint();
  1731. var nodes = [];
  1732. var leftEdgeNodes = [];
  1733. dom.walkPoint(startPoint, endPoint, function (point) {
  1734. if (dom.isEditable(point.node)) {
  1735. return;
  1736. }
  1737. var node;
  1738. if (fullyContains) {
  1739. if (dom.isLeftEdgePoint(point)) {
  1740. leftEdgeNodes.push(point.node);
  1741. }
  1742. if (dom.isRightEdgePoint(point) && list.contains(leftEdgeNodes, point.node)) {
  1743. node = point.node;
  1744. }
  1745. } else if (includeAncestor) {
  1746. node = dom.ancestor(point.node, pred);
  1747. } else {
  1748. node = point.node;
  1749. }
  1750. if (node && pred(node)) {
  1751. nodes.push(node);
  1752. }
  1753. }, true);
  1754. return list.unique(nodes);
  1755. };
  1756. /**
  1757. * returns commonAncestor of range
  1758. * @return {Element} - commonAncestor
  1759. */
  1760. this.commonAncestor = function () {
  1761. return dom.commonAncestor(sc, ec);
  1762. };
  1763. /**
  1764. * returns expanded range by pred
  1765. *
  1766. * @param {Function} pred - predicate function
  1767. * @return {WrappedRange}
  1768. */
  1769. this.expand = function (pred) {
  1770. var startAncestor = dom.ancestor(sc, pred);
  1771. var endAncestor = dom.ancestor(ec, pred);
  1772. if (!startAncestor && !endAncestor) {
  1773. return new WrappedRange(sc, so, ec, eo);
  1774. }
  1775. var boundaryPoints = this.getPoints();
  1776. if (startAncestor) {
  1777. boundaryPoints.sc = startAncestor;
  1778. boundaryPoints.so = 0;
  1779. }
  1780. if (endAncestor) {
  1781. boundaryPoints.ec = endAncestor;
  1782. boundaryPoints.eo = dom.nodeLength(endAncestor);
  1783. }
  1784. return new WrappedRange(
  1785. boundaryPoints.sc,
  1786. boundaryPoints.so,
  1787. boundaryPoints.ec,
  1788. boundaryPoints.eo
  1789. );
  1790. };
  1791. /**
  1792. * @param {Boolean} isCollapseToStart
  1793. * @return {WrappedRange}
  1794. */
  1795. this.collapse = function (isCollapseToStart) {
  1796. if (isCollapseToStart) {
  1797. return new WrappedRange(sc, so, sc, so);
  1798. } else {
  1799. return new WrappedRange(ec, eo, ec, eo);
  1800. }
  1801. };
  1802. /**
  1803. * splitText on range
  1804. */
  1805. this.splitText = function () {
  1806. var isSameContainer = sc === ec;
  1807. var boundaryPoints = this.getPoints();
  1808. if (dom.isText(ec) && !dom.isEdgePoint(this.getEndPoint())) {
  1809. ec.splitText(eo);
  1810. }
  1811. if (dom.isText(sc) && !dom.isEdgePoint(this.getStartPoint())) {
  1812. boundaryPoints.sc = sc.splitText(so);
  1813. boundaryPoints.so = 0;
  1814. if (isSameContainer) {
  1815. boundaryPoints.ec = boundaryPoints.sc;
  1816. boundaryPoints.eo = eo - so;
  1817. }
  1818. }
  1819. return new WrappedRange(
  1820. boundaryPoints.sc,
  1821. boundaryPoints.so,
  1822. boundaryPoints.ec,
  1823. boundaryPoints.eo
  1824. );
  1825. };
  1826. /**
  1827. * delete contents on range
  1828. * @return {WrappedRange}
  1829. */
  1830. this.deleteContents = function () {
  1831. if (this.isCollapsed()) {
  1832. return this;
  1833. }
  1834. var rng = this.splitText();
  1835. var nodes = rng.nodes(null, {
  1836. fullyContains: true
  1837. });
  1838. var point = dom.prevPointUntil(rng.getStartPoint(), function (point) {
  1839. return !list.contains(nodes, point.node);
  1840. });
  1841. var emptyParents = [];
  1842. $.each(nodes, function (idx, node) {
  1843. // find empty parents
  1844. var parent = node.parentNode;
  1845. if (point.node !== parent && dom.nodeLength(parent) === 1) {
  1846. emptyParents.push(parent);
  1847. }
  1848. dom.remove(node, false);
  1849. });
  1850. // remove empty parents
  1851. $.each(emptyParents, function (idx, node) {
  1852. dom.remove(node, false);
  1853. });
  1854. return new WrappedRange(
  1855. point.node,
  1856. point.offset,
  1857. point.node,
  1858. point.offset
  1859. );
  1860. };
  1861. /**
  1862. * makeIsOn: return isOn(pred) function
  1863. */
  1864. var makeIsOn = function (pred) {
  1865. return function () {
  1866. var ancestor = dom.ancestor(sc, pred);
  1867. return !!ancestor && (ancestor === dom.ancestor(ec, pred));
  1868. };
  1869. };
  1870. // isOnEditable: judge whether range is on editable or not
  1871. this.isOnEditable = makeIsOn(dom.isEditable);
  1872. // isOnList: judge whether range is on list node or not
  1873. this.isOnList = makeIsOn(dom.isList);
  1874. // isOnAnchor: judge whether range is on anchor node or not
  1875. this.isOnAnchor = makeIsOn(dom.isAnchor);
  1876. // isOnAnchor: judge whether range is on cell node or not
  1877. this.isOnCell = makeIsOn(dom.isCell);
  1878. /**
  1879. * @param {Function} pred
  1880. * @return {Boolean}
  1881. */
  1882. this.isLeftEdgeOf = function (pred) {
  1883. if (!dom.isLeftEdgePoint(this.getStartPoint())) {
  1884. return false;
  1885. }
  1886. var node = dom.ancestor(this.sc, pred);
  1887. return node && dom.isLeftEdgeOf(this.sc, node);
  1888. };
  1889. /**
  1890. * returns whether range was collapsed or not
  1891. */
  1892. this.isCollapsed = function () {
  1893. return sc === ec && so === eo;
  1894. };
  1895. /**
  1896. * wrap inline nodes which children of body with paragraph
  1897. *
  1898. * @return {WrappedRange}
  1899. */
  1900. this.wrapBodyInlineWithPara = function () {
  1901. if (dom.isBodyContainer(sc) && dom.isEmpty(sc)) {
  1902. sc.innerHTML = dom.emptyPara;
  1903. return new WrappedRange(sc.firstChild, 0);
  1904. } else if (!dom.isInline(sc) || dom.isParaInline(sc)) {
  1905. return this;
  1906. }
  1907. // find inline top ancestor
  1908. var ancestors = dom.listAncestor(sc, func.not(dom.isInline));
  1909. var topAncestor = list.last(ancestors);
  1910. if (!dom.isInline(topAncestor)) {
  1911. topAncestor = ancestors[ancestors.length - 2] || sc.childNodes[so];
  1912. }
  1913. // siblings not in paragraph
  1914. var inlineSiblings = dom.listPrev(topAncestor, dom.isParaInline).reverse();
  1915. inlineSiblings = inlineSiblings.concat(dom.listNext(topAncestor.nextSibling, dom.isParaInline));
  1916. // wrap with paragraph
  1917. if (inlineSiblings.length) {
  1918. var para = dom.wrap(list.head(inlineSiblings), 'p');
  1919. dom.appendChildNodes(para, list.tail(inlineSiblings));
  1920. }
  1921. return this;
  1922. };
  1923. /**
  1924. * insert node at current cursor
  1925. *
  1926. * @param {Node} node
  1927. * @param {Boolean} [isInline]
  1928. * @return {Node}
  1929. */
  1930. this.insertNode = function (node, isInline) {
  1931. var rng = this.wrapBodyInlineWithPara();
  1932. var point = rng.getStartPoint();
  1933. var splitRoot, container, pivot;
  1934. if (isInline) {
  1935. container = dom.isPara(point.node) ? point.node : point.node.parentNode;
  1936. if (dom.isPara(point.node)) {
  1937. pivot = point.node.childNodes[point.offset];
  1938. } else {
  1939. pivot = dom.splitTree(point.node, point);
  1940. }
  1941. } else {
  1942. // splitRoot will be childNode of container
  1943. var ancestors = dom.listAncestor(point.node, dom.isBodyContainer);
  1944. var topAncestor = list.last(ancestors) || point.node;
  1945. if (dom.isBodyContainer(topAncestor)) {
  1946. splitRoot = ancestors[ancestors.length - 2];
  1947. container = topAncestor;
  1948. } else {
  1949. splitRoot = topAncestor;
  1950. container = splitRoot.parentNode;
  1951. }
  1952. pivot = splitRoot && dom.splitTree(splitRoot, point);
  1953. }
  1954. if (pivot) {
  1955. pivot.parentNode.insertBefore(node, pivot);
  1956. } else {
  1957. container.appendChild(node);
  1958. }
  1959. return node;
  1960. };
  1961. this.toString = function () {
  1962. var nativeRng = nativeRange();
  1963. return agent.isW3CRangeSupport ? nativeRng.toString() : nativeRng.text;
  1964. };
  1965. /**
  1966. * create offsetPath bookmark
  1967. * @param {Node} editable
  1968. */
  1969. this.bookmark = function (editable) {
  1970. return {
  1971. s: {
  1972. path: dom.makeOffsetPath(editable, sc),
  1973. offset: so
  1974. },
  1975. e: {
  1976. path: dom.makeOffsetPath(editable, ec),
  1977. offset: eo
  1978. }
  1979. };
  1980. };
  1981. /**
  1982. * getClientRects
  1983. * @return {Rect[]}
  1984. */
  1985. this.getClientRects = function () {
  1986. var nativeRng = nativeRange();
  1987. return nativeRng.getClientRects();
  1988. };
  1989. };
  1990. return {
  1991. /**
  1992. * create Range Object From arguments or Browser Selection
  1993. *
  1994. * @param {Node} sc - start container
  1995. * @param {Number} so - start offset
  1996. * @param {Node} ec - end container
  1997. * @param {Number} eo - end offset
  1998. */
  1999. create : function (sc, so, ec, eo) {
  2000. if (!arguments.length) { // from Browser Selection
  2001. if (agent.isW3CRangeSupport) {
  2002. var selection = document.getSelection();
  2003. if (selection.rangeCount === 0) {
  2004. return null;
  2005. } else if (dom.isBody(selection.anchorNode)) {
  2006. // Firefox: returns entire body as range on initialization. We won't never need it.
  2007. return null;
  2008. }
  2009. var nativeRng = selection.getRangeAt(0);
  2010. sc = nativeRng.startContainer;
  2011. so = nativeRng.startOffset;
  2012. ec = nativeRng.endContainer;
  2013. eo = nativeRng.endOffset;
  2014. } else { // IE8: TextRange
  2015. var textRange = document.selection.createRange();
  2016. var textRangeEnd = textRange.duplicate();
  2017. textRangeEnd.collapse(false);
  2018. var textRangeStart = textRange;
  2019. textRangeStart.collapse(true);
  2020. var startPoint = textRangeToPoint(textRangeStart, true),
  2021. endPoint = textRangeToPoint(textRangeEnd, false);
  2022. // same visible point case: range was collapsed.
  2023. if (dom.isText(startPoint.node) && dom.isLeftEdgePoint(startPoint) &&
  2024. dom.isTextNode(endPoint.node) && dom.isRightEdgePoint(endPoint) &&
  2025. endPoint.node.nextSibling === startPoint.node) {
  2026. startPoint = endPoint;
  2027. }
  2028. sc = startPoint.cont;
  2029. so = startPoint.offset;
  2030. ec = endPoint.cont;
  2031. eo = endPoint.offset;
  2032. }
  2033. } else if (arguments.length === 2) { //collapsed
  2034. ec = sc;
  2035. eo = so;
  2036. }
  2037. return new WrappedRange(sc, so, ec, eo);
  2038. },
  2039. /**
  2040. * create WrappedRange from node
  2041. *
  2042. * @param {Node} node
  2043. * @return {WrappedRange}
  2044. */
  2045. createFromNode: function (node) {
  2046. return this.create(node, 0, node, 1);
  2047. },
  2048. /**
  2049. * create WrappedRange from Bookmark
  2050. *
  2051. * @param {Node} editable
  2052. * @param {Obkect} bookmark
  2053. * @return {WrappedRange}
  2054. */
  2055. createFromBookmark : function (editable, bookmark) {
  2056. var sc = dom.fromOffsetPath(editable, bookmark.s.path);
  2057. var so = bookmark.s.offset;
  2058. var ec = dom.fromOffsetPath(editable, bookmark.e.path);
  2059. var eo = bookmark.e.offset;
  2060. return new WrappedRange(sc, so, ec, eo);
  2061. }
  2062. };
  2063. })();
  2064. var Typing = function () {
  2065. /**
  2066. * @param {jQuery} $editable
  2067. * @param {WrappedRange} rng
  2068. * @param {Number} tabsize
  2069. */
  2070. this.insertTab = function ($editable, rng, tabsize) {
  2071. var tab = dom.createText(new Array(tabsize + 1).join(dom.NBSP_CHAR));
  2072. rng = rng.deleteContents();
  2073. rng.insertNode(tab, true);
  2074. rng = range.create(tab, tabsize);
  2075. rng.select();
  2076. };
  2077. /**
  2078. * insert paragraph
  2079. */
  2080. this.insertParagraph = function () {
  2081. var rng = range.create();
  2082. // deleteContents on range.
  2083. rng = rng.deleteContents();
  2084. // Wrap range if it needs to be wrapped by paragraph
  2085. rng = rng.wrapBodyInlineWithPara();
  2086. // finding paragraph
  2087. var splitRoot = dom.ancestor(rng.sc, dom.isPara);
  2088. var nextPara;
  2089. // on paragraph: split paragraph
  2090. if (splitRoot) {
  2091. nextPara = dom.splitTree(splitRoot, rng.getStartPoint());
  2092. var emptyAnchors = dom.listDescendant(splitRoot, dom.isEmptyAnchor);
  2093. emptyAnchors = emptyAnchors.concat(dom.listDescendant(nextPara, dom.isEmptyAnchor));
  2094. $.each(emptyAnchors, function (idx, anchor) {
  2095. dom.remove(anchor);
  2096. });
  2097. // no paragraph: insert empty paragraph
  2098. } else {
  2099. var next = rng.sc.childNodes[rng.so];
  2100. nextPara = $(dom.emptyPara)[0];
  2101. if (next) {
  2102. rng.sc.insertBefore(nextPara, next);
  2103. } else {
  2104. rng.sc.appendChild(nextPara);
  2105. }
  2106. }
  2107. range.create(nextPara, 0).normalize().select();
  2108. };
  2109. };
  2110. /**
  2111. * Table
  2112. * @class
  2113. */
  2114. var Table = function () {
  2115. /**
  2116. * handle tab key
  2117. *
  2118. * @param {WrappedRange} rng
  2119. * @param {Boolean} isShift
  2120. */
  2121. this.tab = function (rng, isShift) {
  2122. var cell = dom.ancestor(rng.commonAncestor(), dom.isCell);
  2123. var table = dom.ancestor(cell, dom.isTable);
  2124. var cells = dom.listDescendant(table, dom.isCell);
  2125. var nextCell = list[isShift ? 'prev' : 'next'](cells, cell);
  2126. if (nextCell) {
  2127. range.create(nextCell, 0).select();
  2128. }
  2129. };
  2130. /**
  2131. * create empty table element
  2132. *
  2133. * @param {Number} rowCount
  2134. * @param {Number} colCount
  2135. * @return {Node}
  2136. */
  2137. this.createTable = function (colCount, rowCount) {
  2138. var tds = [], tdHTML;
  2139. for (var idxCol = 0; idxCol < colCount; idxCol++) {
  2140. tds.push('<td>' + dom.blank + '</td>');
  2141. }
  2142. tdHTML = tds.join('');
  2143. var trs = [], trHTML;
  2144. for (var idxRow = 0; idxRow < rowCount; idxRow++) {
  2145. trs.push('<tr>' + tdHTML + '</tr>');
  2146. }
  2147. trHTML = trs.join('');
  2148. return $('<table class="table table-bordered">' + trHTML + '</table>')[0];
  2149. };
  2150. };
  2151. var Bullet = function () {
  2152. /**
  2153. * toggle ordered list
  2154. * @type command
  2155. */
  2156. this.insertOrderedList = function () {
  2157. this.toggleList('OL');
  2158. };
  2159. /**
  2160. * toggle unordered list
  2161. * @type command
  2162. */
  2163. this.insertUnorderedList = function () {
  2164. this.toggleList('UL');
  2165. };
  2166. /**
  2167. * indent
  2168. * @type command
  2169. */
  2170. this.indent = function () {
  2171. var self = this;
  2172. var rng = range.create().wrapBodyInlineWithPara();
  2173. var paras = rng.nodes(dom.isPara, { includeAncestor: true });
  2174. var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
  2175. $.each(clustereds, function (idx, paras) {
  2176. var head = list.head(paras);
  2177. if (dom.isLi(head)) {
  2178. self.wrapList(paras, head.parentNode.nodeName);
  2179. } else {
  2180. $.each(paras, function (idx, para) {
  2181. $(para).css('marginLeft', function (idx, val) {
  2182. return (parseInt(val, 10) || 0) + 25;
  2183. });
  2184. });
  2185. }
  2186. });
  2187. rng.select();
  2188. };
  2189. /**
  2190. * outdent
  2191. * @type command
  2192. */
  2193. this.outdent = function () {
  2194. var self = this;
  2195. var rng = range.create().wrapBodyInlineWithPara();
  2196. var paras = rng.nodes(dom.isPara, { includeAncestor: true });
  2197. var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
  2198. $.each(clustereds, function (idx, paras) {
  2199. var head = list.head(paras);
  2200. if (dom.isLi(head)) {
  2201. self.releaseList([paras]);
  2202. } else {
  2203. $.each(paras, function (idx, para) {
  2204. $(para).css('marginLeft', function (idx, val) {
  2205. val = (parseInt(val, 10) || 0);
  2206. return val > 25 ? val - 25 : '';
  2207. });
  2208. });
  2209. }
  2210. });
  2211. rng.select();
  2212. };
  2213. /**
  2214. * toggle list
  2215. * @param {String} listName - OL or UL
  2216. */
  2217. this.toggleList = function (listName) {
  2218. var self = this;
  2219. var rng = range.create().wrapBodyInlineWithPara();
  2220. var paras = rng.nodes(dom.isPara, { includeAncestor: true });
  2221. var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
  2222. // paragraph to list
  2223. if (list.find(paras, dom.isPurePara)) {
  2224. $.each(clustereds, function (idx, paras) {
  2225. self.wrapList(paras, listName);
  2226. });
  2227. // list to paragraph or change list style
  2228. } else {
  2229. var diffLists = rng.nodes(dom.isList, {
  2230. includeAncestor: true
  2231. }).filter(function (listNode) {
  2232. return !$.nodeName(listNode, listName);
  2233. });
  2234. if (diffLists.length) {
  2235. $.each(diffLists, function (idx, listNode) {
  2236. dom.replace(listNode, listName);
  2237. });
  2238. } else {
  2239. this.releaseList(clustereds, true);
  2240. }
  2241. }
  2242. rng.select();
  2243. };
  2244. /**
  2245. * @param {Node[]} paras
  2246. * @param {String} listName
  2247. */
  2248. this.wrapList = function (paras, listName) {
  2249. var head = list.head(paras);
  2250. var last = list.last(paras);
  2251. var prevList = dom.isList(head.previousSibling) && head.previousSibling;
  2252. var nextList = dom.isList(last.nextSibling) && last.nextSibling;
  2253. var listNode = prevList || dom.insertAfter(dom.create(listName || 'UL'), last);
  2254. // P to LI
  2255. paras = $.map(paras, function (para) {
  2256. return dom.isPurePara(para) ? dom.replace(para, 'LI') : para;
  2257. });
  2258. // append to list(<ul>, <ol>)
  2259. dom.appendChildNodes(listNode, paras);
  2260. if (nextList) {
  2261. dom.appendChildNodes(listNode, list.from(nextList.childNodes));
  2262. dom.remove(nextList);
  2263. }
  2264. };
  2265. /**
  2266. * @param {Array[]} clustereds
  2267. * @param {Boolean} isEscapseToBody
  2268. * @return {Node[]}
  2269. */
  2270. this.releaseList = function (clustereds, isEscapseToBody) {
  2271. var releasedParas = [];
  2272. $.each(clustereds, function (idx, paras) {
  2273. var head = list.head(paras);
  2274. var last = list.last(paras);
  2275. var headList = isEscapseToBody ? dom.lastAncestor(head, dom.isList) :
  2276. head.parentNode;
  2277. var lastList = headList.childNodes.length > 1 ? dom.splitTree(headList, {
  2278. node: last.parentNode,
  2279. offset: dom.position(last) + 1
  2280. }, true) : null;
  2281. var middleList = dom.splitTree(headList, {
  2282. node: head.parentNode,
  2283. offset: dom.position(head)
  2284. }, true);
  2285. paras = isEscapseToBody ? dom.listDescendant(middleList, dom.isLi) :
  2286. list.from(middleList.childNodes).filter(dom.isLi);
  2287. // LI to P
  2288. if (isEscapseToBody || !dom.isList(headList.parentNode)) {
  2289. paras = $.map(paras, function (para) {
  2290. return dom.replace(para, 'P');
  2291. });
  2292. }
  2293. $.each(list.from(paras).reverse(), function (idx, para) {
  2294. dom.insertAfter(para, headList);
  2295. });
  2296. // remove empty lists
  2297. var rootLists = list.compact([headList, middleList, lastList]);
  2298. $.each(rootLists, function (idx, rootList) {
  2299. var listNodes = [rootList].concat(dom.listDescendant(rootList, dom.isList));
  2300. $.each(listNodes.reverse(), function (idx, listNode) {
  2301. if (!dom.nodeLength(listNode)) {
  2302. dom.remove(listNode, true);
  2303. }
  2304. });
  2305. });
  2306. releasedParas = releasedParas.concat(paras);
  2307. });
  2308. return releasedParas;
  2309. };
  2310. };
  2311. /**
  2312. * Editor
  2313. * @class
  2314. */
  2315. var Editor = function () {
  2316. var style = new Style();
  2317. var table = new Table();
  2318. var typing = new Typing();
  2319. var bullet = new Bullet();
  2320. /**
  2321. * save current range
  2322. *
  2323. * @param {jQuery} $editable
  2324. */
  2325. this.saveRange = function ($editable, thenCollapse) {
  2326. $editable.focus();
  2327. $editable.data('range', range.create());
  2328. if (thenCollapse) {
  2329. range.create().collapse().select();
  2330. }
  2331. };
  2332. /**
  2333. * restore lately range
  2334. *
  2335. * @param {jQuery} $editable
  2336. */
  2337. this.restoreRange = function ($editable) {
  2338. var rng = $editable.data('range');
  2339. if (rng) {
  2340. rng.select();
  2341. $editable.focus();
  2342. }
  2343. };
  2344. /**
  2345. * current style
  2346. * @param {Node} target
  2347. */
  2348. this.currentStyle = function (target) {
  2349. var rng = range.create();
  2350. return rng ? rng.isOnEditable() && style.current(rng, target) : false;
  2351. };
  2352. var triggerOnChange = this.triggerOnChange = function ($editable) {
  2353. var onChange = $editable.data('callbacks').onChange;
  2354. if (onChange) {
  2355. onChange($editable.html(), $editable);
  2356. }
  2357. };
  2358. /**
  2359. * undo
  2360. * @param {jQuery} $editable
  2361. */
  2362. this.undo = function ($editable) {
  2363. $editable.data('NoteHistory').undo();
  2364. triggerOnChange($editable);
  2365. };
  2366. /**
  2367. * redo
  2368. * @param {jQuery} $editable
  2369. */
  2370. this.redo = function ($editable) {
  2371. $editable.data('NoteHistory').redo();
  2372. triggerOnChange($editable);
  2373. };
  2374. /**
  2375. * after command
  2376. * @param {jQuery} $editable
  2377. */
  2378. var afterCommand = this.afterCommand = function ($editable) {
  2379. $editable.data('NoteHistory').recordUndo();
  2380. triggerOnChange($editable);
  2381. };
  2382. /* jshint ignore:start */
  2383. // native commands(with execCommand), generate function for execCommand
  2384. var commands = ['bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript',
  2385. 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull',
  2386. 'formatBlock', 'removeFormat',
  2387. 'backColor', 'foreColor', 'insertHorizontalRule', 'fontName'];
  2388. for (var idx = 0, len = commands.length; idx < len; idx ++) {
  2389. this[commands[idx]] = (function (sCmd) {
  2390. return function ($editable, value) {
  2391. document.execCommand(sCmd, false, value);
  2392. afterCommand($editable);
  2393. };
  2394. })(commands[idx]);
  2395. }
  2396. /* jshint ignore:end */
  2397. /**
  2398. * handle tab key
  2399. *
  2400. * @param {jQuery} $editable
  2401. * @param {Object} options
  2402. */
  2403. this.tab = function ($editable, options) {
  2404. var rng = range.create();
  2405. if (rng.isCollapsed() && rng.isOnCell()) {
  2406. table.tab(rng);
  2407. } else {
  2408. typing.insertTab($editable, rng, options.tabsize);
  2409. afterCommand($editable);
  2410. }
  2411. };
  2412. /**
  2413. * handle shift+tab key
  2414. */
  2415. this.untab = function () {
  2416. var rng = range.create();
  2417. if (rng.isCollapsed() && rng.isOnCell()) {
  2418. table.tab(rng, true);
  2419. }
  2420. };
  2421. /**
  2422. * insert paragraph
  2423. *
  2424. * @param {Node} $editable
  2425. */
  2426. this.insertParagraph = function ($editable) {
  2427. typing.insertParagraph($editable);
  2428. afterCommand($editable);
  2429. };
  2430. /**
  2431. * @param {jQuery} $editable
  2432. */
  2433. this.insertOrderedList = function ($editable) {
  2434. bullet.insertOrderedList($editable);
  2435. afterCommand($editable);
  2436. };
  2437. /**
  2438. * @param {jQuery} $editable
  2439. */
  2440. this.insertUnorderedList = function ($editable) {
  2441. bullet.insertUnorderedList($editable);
  2442. afterCommand($editable);
  2443. };
  2444. /**
  2445. * @param {jQuery} $editable
  2446. */
  2447. this.indent = function ($editable) {
  2448. bullet.indent($editable);
  2449. afterCommand($editable);
  2450. };
  2451. /**
  2452. * @param {jQuery} $editable
  2453. */
  2454. this.outdent = function ($editable) {
  2455. bullet.outdent($editable);
  2456. afterCommand($editable);
  2457. };
  2458. /**
  2459. * insert image
  2460. *
  2461. * @param {jQuery} $editable
  2462. * @param {String} sUrl
  2463. */
  2464. this.insertImage = function ($editable, sUrl, filename) {
  2465. async.createImage(sUrl, filename).then(function ($image) {
  2466. $image.css({
  2467. display: '',
  2468. width: Math.min($editable.width(), $image.width())
  2469. });
  2470. range.create().insertNode($image[0]);
  2471. afterCommand($editable);
  2472. }).fail(function () {
  2473. var callbacks = $editable.data('callbacks');
  2474. if (callbacks.onImageUploadError) {
  2475. callbacks.onImageUploadError();
  2476. }
  2477. });
  2478. };
  2479. /**
  2480. * insert video
  2481. * @param {jQuery} $editable
  2482. * @param {String} sUrl
  2483. */
  2484. this.insertVideo = function ($editable, sUrl) {
  2485. // video url patterns(youtube, instagram, vimeo, dailymotion, youku)
  2486. var ytRegExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
  2487. var ytMatch = sUrl.match(ytRegExp);
  2488. var igRegExp = /\/\/instagram.com\/p\/(.[a-zA-Z0-9]*)/;
  2489. var igMatch = sUrl.match(igRegExp);
  2490. var vRegExp = /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/;
  2491. var vMatch = sUrl.match(vRegExp);
  2492. var vimRegExp = /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/;
  2493. var vimMatch = sUrl.match(vimRegExp);
  2494. var dmRegExp = /.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/;
  2495. var dmMatch = sUrl.match(dmRegExp);
  2496. var youkuRegExp = /\/\/v\.youku\.com\/v_show\/id_(\w+)\.html/;
  2497. var youkuMatch = sUrl.match(youkuRegExp);
  2498. var $video;
  2499. if (ytMatch && ytMatch[2].length === 11) {
  2500. var youtubeId = ytMatch[2];
  2501. $video = $('<iframe>')
  2502. .attr('src', '//www.youtube.com/embed/' + youtubeId)
  2503. .attr('width', '640').attr('height', '360');
  2504. } else if (igMatch && igMatch[0].length) {
  2505. $video = $('<iframe>')
  2506. .attr('src', igMatch[0] + '/embed/')
  2507. .attr('width', '612').attr('height', '710')
  2508. .attr('scrolling', 'no')
  2509. .attr('allowtransparency', 'true');
  2510. } else if (vMatch && vMatch[0].length) {
  2511. $video = $('<iframe>')
  2512. .attr('src', vMatch[0] + '/embed/simple')
  2513. .attr('width', '600').attr('height', '600')
  2514. .attr('class', 'vine-embed');
  2515. } else if (vimMatch && vimMatch[3].length) {
  2516. $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
  2517. .attr('src', '//player.vimeo.com/video/' + vimMatch[3])
  2518. .attr('width', '640').attr('height', '360');
  2519. } else if (dmMatch && dmMatch[2].length) {
  2520. $video = $('<iframe>')
  2521. .attr('src', '//www.dailymotion.com/embed/video/' + dmMatch[2])
  2522. .attr('width', '640').attr('height', '360');
  2523. } else if (youkuMatch && youkuMatch[1].length) {
  2524. $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
  2525. .attr('height', '498')
  2526. .attr('width', '510')
  2527. .attr('src', '//player.youku.com/embed/' + youkuMatch[1]);
  2528. } else {
  2529. // this is not a known video link. Now what, Cat? Now what?
  2530. }
  2531. if ($video) {
  2532. $video.attr('frameborder', 0);
  2533. range.create().insertNode($video[0]);
  2534. afterCommand($editable);
  2535. }
  2536. };
  2537. /**
  2538. * formatBlock
  2539. *
  2540. * @param {jQuery} $editable
  2541. * @param {String} tagName
  2542. */
  2543. this.formatBlock = function ($editable, tagName) {
  2544. tagName = agent.isMSIE ? '<' + tagName + '>' : tagName;
  2545. document.execCommand('FormatBlock', false, tagName);
  2546. afterCommand($editable);
  2547. };
  2548. this.formatPara = function ($editable) {
  2549. this.formatBlock($editable, 'P');
  2550. afterCommand($editable);
  2551. };
  2552. /* jshint ignore:start */
  2553. for (var idx = 1; idx <= 6; idx ++) {
  2554. this['formatH' + idx] = function (idx) {
  2555. return function ($editable) {
  2556. this.formatBlock($editable, 'H' + idx);
  2557. };
  2558. }(idx);
  2559. };
  2560. /* jshint ignore:end */
  2561. /**
  2562. * fontsize
  2563. * FIXME: Still buggy
  2564. *
  2565. * @param {jQuery} $editable
  2566. * @param {String} value - px
  2567. */
  2568. this.fontSize = function ($editable, value) {
  2569. document.execCommand('fontSize', false, 3);
  2570. if (agent.isFF) {
  2571. // firefox: <font size="3"> to <span style='font-size={value}px;'>, buggy
  2572. $editable.find('font[size=3]').removeAttr('size').css('font-size', value + 'px');
  2573. } else {
  2574. // chrome: <span style="font-size: medium"> to <span style='font-size={value}px;'>
  2575. $editable.find('span').filter(function () {
  2576. return this.style.fontSize === 'medium';
  2577. }).css('font-size', value + 'px');
  2578. }
  2579. afterCommand($editable);
  2580. };
  2581. /**
  2582. * lineHeight
  2583. * @param {jQuery} $editable
  2584. * @param {String} value
  2585. */
  2586. this.lineHeight = function ($editable, value) {
  2587. style.stylePara(range.create(), {
  2588. lineHeight: value
  2589. });
  2590. afterCommand($editable);
  2591. };
  2592. /**
  2593. * unlink
  2594. *
  2595. * @type command
  2596. *
  2597. * @param {jQuery} $editable
  2598. */
  2599. this.unlink = function ($editable) {
  2600. var rng = range.create();
  2601. if (rng.isOnAnchor()) {
  2602. var anchor = dom.ancestor(rng.sc, dom.isAnchor);
  2603. rng = range.createFromNode(anchor);
  2604. rng.select();
  2605. document.execCommand('unlink');
  2606. afterCommand($editable);
  2607. }
  2608. };
  2609. /**
  2610. * create link
  2611. *
  2612. * @type command
  2613. *
  2614. * @param {jQuery} $editable
  2615. * @param {Object} linkInfo
  2616. * @param {Object} options
  2617. */
  2618. this.createLink = function ($editable, linkInfo, options) {
  2619. var linkUrl = linkInfo.url;
  2620. var linkText = linkInfo.text;
  2621. var isNewWindow = linkInfo.newWindow;
  2622. var rng = linkInfo.range;
  2623. if (options.onCreateLink) {
  2624. linkUrl = options.onCreateLink(linkUrl);
  2625. }
  2626. rng = rng.deleteContents();
  2627. // Create a new link when there is no anchor on range.
  2628. var anchor = rng.insertNode($('<A>' + linkText + '</A>')[0], true);
  2629. $(anchor).attr({
  2630. href: linkUrl,
  2631. target: isNewWindow ? '_blank' : ''
  2632. });
  2633. range.createFromNode(anchor).select();
  2634. afterCommand($editable);
  2635. };
  2636. /**
  2637. * returns link info
  2638. *
  2639. * @return {Object}
  2640. */
  2641. this.getLinkInfo = function ($editable) {
  2642. $editable.focus();
  2643. var rng = range.create().expand(dom.isAnchor);
  2644. // Get the first anchor on range(for edit).
  2645. var $anchor = $(list.head(rng.nodes(dom.isAnchor)));
  2646. return {
  2647. range: rng,
  2648. text: rng.toString(),
  2649. isNewWindow: $anchor.length ? $anchor.attr('target') === '_blank' : true,
  2650. url: $anchor.length ? $anchor.attr('href') : ''
  2651. };
  2652. };
  2653. /**
  2654. * get video info
  2655. *
  2656. * @param {jQuery} $editable
  2657. * @return {Object}
  2658. */
  2659. this.getVideoInfo = function ($editable) {
  2660. $editable.focus();
  2661. var rng = range.create();
  2662. if (rng.isOnAnchor()) {
  2663. var anchor = dom.ancestor(rng.sc, dom.isAnchor);
  2664. rng = range.createFromNode(anchor);
  2665. }
  2666. return {
  2667. text: rng.toString()
  2668. };
  2669. };
  2670. this.color = function ($editable, sObjColor) {
  2671. var oColor = JSON.parse(sObjColor);
  2672. var foreColor = oColor.foreColor, backColor = oColor.backColor;
  2673. if (foreColor) { document.execCommand('foreColor', false, foreColor); }
  2674. if (backColor) { document.execCommand('backColor', false, backColor); }
  2675. afterCommand($editable);
  2676. };
  2677. this.insertTable = function ($editable, sDim) {
  2678. var dimension = sDim.split('x');
  2679. var rng = range.create();
  2680. rng = rng.deleteContents();
  2681. rng.insertNode(table.createTable(dimension[0], dimension[1]));
  2682. afterCommand($editable);
  2683. };
  2684. /**
  2685. * @param {jQuery} $editable
  2686. * @param {String} value
  2687. * @param {jQuery} $target
  2688. */
  2689. this.floatMe = function ($editable, value, $target) {
  2690. $target.css('float', value);
  2691. afterCommand($editable);
  2692. };
  2693. this.imageShape = function ($editable, value, $target) {
  2694. $target.removeClass('img-rounded img-circle img-thumbnail');
  2695. if (value) {
  2696. $target.addClass(value);
  2697. }
  2698. };
  2699. /**
  2700. * resize overlay element
  2701. * @param {jQuery} $editable
  2702. * @param {String} value
  2703. * @param {jQuery} $target - target element
  2704. */
  2705. this.resize = function ($editable, value, $target) {
  2706. $target.css({
  2707. width: value * 100 + '%',
  2708. height: ''
  2709. });
  2710. afterCommand($editable);
  2711. };
  2712. /**
  2713. * @param {Position} pos
  2714. * @param {jQuery} $target - target element
  2715. * @param {Boolean} [bKeepRatio] - keep ratio
  2716. */
  2717. this.resizeTo = function (pos, $target, bKeepRatio) {
  2718. var imageSize;
  2719. if (bKeepRatio) {
  2720. var newRatio = pos.y / pos.x;
  2721. var ratio = $target.data('ratio');
  2722. imageSize = {
  2723. width: ratio > newRatio ? pos.x : pos.y / ratio,
  2724. height: ratio > newRatio ? pos.x * ratio : pos.y
  2725. };
  2726. } else {
  2727. imageSize = {
  2728. width: pos.x,
  2729. height: pos.y
  2730. };
  2731. }
  2732. $target.css(imageSize);
  2733. };
  2734. /**
  2735. * remove media object
  2736. *
  2737. * @param {jQuery} $editable
  2738. * @param {String} value - dummy argument (for keep interface)
  2739. * @param {jQuery} $target - target element
  2740. */
  2741. this.removeMedia = function ($editable, value, $target) {
  2742. $target.detach();
  2743. afterCommand($editable);
  2744. };
  2745. };
  2746. /**
  2747. * History
  2748. * @class
  2749. */
  2750. var History = function ($editable) {
  2751. var stack = [], stackOffset = -1;
  2752. var editable = $editable[0];
  2753. var makeSnapshot = function () {
  2754. var rng = range.create();
  2755. var emptyBookmark = {s: {path: [0], offset: 0}, e: {path: [0], offset: 0}};
  2756. return {
  2757. contents: $editable.html(),
  2758. bookmark: (rng ? rng.bookmark(editable) : emptyBookmark)
  2759. };
  2760. };
  2761. var applySnapshot = function (snapshot) {
  2762. if (snapshot.contents !== null) {
  2763. $editable.html(snapshot.contents);
  2764. }
  2765. if (snapshot.bookmark !== null) {
  2766. range.createFromBookmark(editable, snapshot.bookmark).select();
  2767. }
  2768. };
  2769. this.undo = function () {
  2770. if (0 < stackOffset) {
  2771. stackOffset--;
  2772. applySnapshot(stack[stackOffset]);
  2773. }
  2774. };
  2775. this.redo = function () {
  2776. if (stack.length - 1 > stackOffset) {
  2777. stackOffset++;
  2778. applySnapshot(stack[stackOffset]);
  2779. }
  2780. };
  2781. this.recordUndo = function () {
  2782. stackOffset++;
  2783. // Wash out stack after stackOffset
  2784. if (stack.length > stackOffset) {
  2785. stack = stack.slice(0, stackOffset);
  2786. }
  2787. // Create new snapshot and push it to the end
  2788. stack.push(makeSnapshot());
  2789. };
  2790. // Create first undo stack
  2791. this.recordUndo();
  2792. };
  2793. /**
  2794. * Button
  2795. */
  2796. var Button = function () {
  2797. /**
  2798. * update button status
  2799. *
  2800. * @param {jQuery} $container
  2801. * @param {Object} styleInfo
  2802. */
  2803. this.update = function ($container, styleInfo) {
  2804. /**
  2805. * handle dropdown's check mark (for fontname, fontsize, lineHeight).
  2806. * @param {jQuery} $btn
  2807. * @param {Number} value
  2808. */
  2809. var checkDropdownMenu = function ($btn, value) {
  2810. $btn.find('.dropdown-menu li a').each(function () {
  2811. // always compare string to avoid creating another func.
  2812. var isChecked = ($(this).data('value') + '') === (value + '');
  2813. this.className = isChecked ? 'checked' : '';
  2814. });
  2815. };
  2816. /**
  2817. * update button state(active or not).
  2818. *
  2819. * @param {String} selector
  2820. * @param {Function} pred
  2821. */
  2822. var btnState = function (selector, pred) {
  2823. var $btn = $container.find(selector);
  2824. $btn.toggleClass('active', pred());
  2825. };
  2826. // fontname
  2827. var $fontname = $container.find('.note-fontname');
  2828. if ($fontname.length) {
  2829. var selectedFont = styleInfo['font-family'];
  2830. if (!!selectedFont) {
  2831. selectedFont = list.head(selectedFont.split(','));
  2832. selectedFont = selectedFont.replace(/\'/g, '');
  2833. $fontname.find('.note-current-fontname').text(selectedFont);
  2834. checkDropdownMenu($fontname, selectedFont);
  2835. }
  2836. }
  2837. // fontsize
  2838. var $fontsize = $container.find('.note-fontsize');
  2839. $fontsize.find('.note-current-fontsize').text(styleInfo['font-size']);
  2840. checkDropdownMenu($fontsize, parseFloat(styleInfo['font-size']));
  2841. // lineheight
  2842. var $lineHeight = $container.find('.note-height');
  2843. checkDropdownMenu($lineHeight, parseFloat(styleInfo['line-height']));
  2844. btnState('button[data-event="bold"]', function () {
  2845. return styleInfo['font-bold'] === 'bold';
  2846. });
  2847. btnState('button[data-event="italic"]', function () {
  2848. return styleInfo['font-italic'] === 'italic';
  2849. });
  2850. btnState('button[data-event="underline"]', function () {
  2851. return styleInfo['font-underline'] === 'underline';
  2852. });
  2853. btnState('button[data-event="strikethrough"]', function () {
  2854. return styleInfo['font-strikethrough'] === 'strikethrough';
  2855. });
  2856. btnState('button[data-event="superscript"]', function () {
  2857. return styleInfo['font-superscript'] === 'superscript';
  2858. });
  2859. btnState('button[data-event="subscript"]', function () {
  2860. return styleInfo['font-subscript'] === 'subscript';
  2861. });
  2862. btnState('button[data-event="justifyLeft"]', function () {
  2863. return styleInfo['text-align'] === 'left' || styleInfo['text-align'] === 'start';
  2864. });
  2865. btnState('button[data-event="justifyCenter"]', function () {
  2866. return styleInfo['text-align'] === 'center';
  2867. });
  2868. btnState('button[data-event="justifyRight"]', function () {
  2869. return styleInfo['text-align'] === 'right';
  2870. });
  2871. btnState('button[data-event="justifyFull"]', function () {
  2872. return styleInfo['text-align'] === 'justify';
  2873. });
  2874. btnState('button[data-event="insertUnorderedList"]', function () {
  2875. return styleInfo['list-style'] === 'unordered';
  2876. });
  2877. btnState('button[data-event="insertOrderedList"]', function () {
  2878. return styleInfo['list-style'] === 'ordered';
  2879. });
  2880. };
  2881. /**
  2882. * update recent color
  2883. *
  2884. * @param {Node} button
  2885. * @param {String} eventName
  2886. * @param {value} value
  2887. */
  2888. this.updateRecentColor = function (button, eventName, value) {
  2889. var $color = $(button).closest('.note-color');
  2890. var $recentColor = $color.find('.note-recent-color');
  2891. var colorInfo = JSON.parse($recentColor.attr('data-value'));
  2892. colorInfo[eventName] = value;
  2893. $recentColor.attr('data-value', JSON.stringify(colorInfo));
  2894. var sKey = eventName === 'backColor' ? 'background-color' : 'color';
  2895. $recentColor.find('i').css(sKey, value);
  2896. };
  2897. };
  2898. /**
  2899. * Toolbar
  2900. */
  2901. var Toolbar = function () {
  2902. var button = new Button();
  2903. this.update = function ($toolbar, styleInfo) {
  2904. button.update($toolbar, styleInfo);
  2905. };
  2906. /**
  2907. * @param {Node} button
  2908. * @param {String} eventName
  2909. * @param {String} value
  2910. */
  2911. this.updateRecentColor = function (buttonNode, eventName, value) {
  2912. button.updateRecentColor(buttonNode, eventName, value);
  2913. };
  2914. /**
  2915. * activate buttons exclude codeview
  2916. * @param {jQuery} $toolbar
  2917. */
  2918. this.activate = function ($toolbar) {
  2919. $toolbar.find('button')
  2920. .not('button[data-event="codeview"]')
  2921. .removeClass('disabled');
  2922. };
  2923. /**
  2924. * deactivate buttons exclude codeview
  2925. * @param {jQuery} $toolbar
  2926. */
  2927. this.deactivate = function ($toolbar) {
  2928. $toolbar.find('button')
  2929. .not('button[data-event="codeview"]')
  2930. .addClass('disabled');
  2931. };
  2932. this.updateFullscreen = function ($container, bFullscreen) {
  2933. var $btn = $container.find('button[data-event="fullscreen"]');
  2934. $btn.toggleClass('active', bFullscreen);
  2935. };
  2936. this.updateCodeview = function ($container, isCodeview) {
  2937. var $btn = $container.find('button[data-event="codeview"]');
  2938. $btn.toggleClass('active', isCodeview);
  2939. };
  2940. };
  2941. /**
  2942. * Popover (http://getbootstrap.com/javascript/#popovers)
  2943. */
  2944. var Popover = function () {
  2945. var button = new Button();
  2946. /**
  2947. * returns position from placeholder
  2948. * @param {Node} placeholder
  2949. * @param {Boolean} isAirMode
  2950. */
  2951. var posFromPlaceholder = function (placeholder, isAirMode) {
  2952. var $placeholder = $(placeholder);
  2953. var pos = isAirMode ? $placeholder.offset() : $placeholder.position();
  2954. var height = $placeholder.outerHeight(true); // include margin
  2955. // popover below placeholder.
  2956. return {
  2957. left: pos.left,
  2958. top: pos.top + height
  2959. };
  2960. };
  2961. /**
  2962. * show popover
  2963. * @param {jQuery} popover
  2964. * @param {Position} pos
  2965. */
  2966. var showPopover = function ($popover, pos) {
  2967. $popover.css({
  2968. display: 'block',
  2969. left: pos.left,
  2970. top: pos.top
  2971. });
  2972. };
  2973. var PX_POPOVER_ARROW_OFFSET_X = 20;
  2974. /**
  2975. * update current state
  2976. * @param {jQuery} $popover - popover container
  2977. * @param {Object} styleInfo - style object
  2978. * @param {Boolean} isAirMode
  2979. */
  2980. this.update = function ($popover, styleInfo, isAirMode) {
  2981. button.update($popover, styleInfo);
  2982. var $linkPopover = $popover.find('.note-link-popover');
  2983. if (styleInfo.anchor) {
  2984. var $anchor = $linkPopover.find('a');
  2985. var href = $(styleInfo.anchor).attr('href');
  2986. $anchor.attr('href', href).html(href);
  2987. showPopover($linkPopover, posFromPlaceholder(styleInfo.anchor, isAirMode));
  2988. } else {
  2989. $linkPopover.hide();
  2990. }
  2991. var $imagePopover = $popover.find('.note-image-popover');
  2992. if (styleInfo.image) {
  2993. showPopover($imagePopover, posFromPlaceholder(styleInfo.image, isAirMode));
  2994. } else {
  2995. $imagePopover.hide();
  2996. }
  2997. var $airPopover = $popover.find('.note-air-popover');
  2998. if (isAirMode && !styleInfo.range.isCollapsed()) {
  2999. var bnd = func.rect2bnd(list.last(styleInfo.range.getClientRects()));
  3000. showPopover($airPopover, {
  3001. left: Math.max(bnd.left + bnd.width / 2 - PX_POPOVER_ARROW_OFFSET_X, 0),
  3002. top: bnd.top + bnd.height
  3003. });
  3004. } else {
  3005. $airPopover.hide();
  3006. }
  3007. };
  3008. /**
  3009. * @param {Node} button
  3010. * @param {String} eventName
  3011. * @param {String} value
  3012. */
  3013. this.updateRecentColor = function (button, eventName, value) {
  3014. button.updateRecentColor(button, eventName, value);
  3015. };
  3016. /**
  3017. * hide all popovers
  3018. * @param {jQuery} $popover - popover contaienr
  3019. */
  3020. this.hide = function ($popover) {
  3021. $popover.children().hide();
  3022. };
  3023. };
  3024. /**
  3025. * Handle
  3026. */
  3027. var Handle = function () {
  3028. /**
  3029. * update handle
  3030. * @param {jQuery} $handle
  3031. * @param {Object} styleInfo
  3032. * @param {Boolean} isAirMode
  3033. */
  3034. this.update = function ($handle, styleInfo, isAirMode) {
  3035. var $selection = $handle.find('.note-control-selection');
  3036. if (styleInfo.image) {
  3037. var $image = $(styleInfo.image);
  3038. var pos = isAirMode ? $image.offset() : $image.position();
  3039. // include margin
  3040. var imageSize = {
  3041. w: $image.outerWidth(true),
  3042. h: $image.outerHeight(true)
  3043. };
  3044. $selection.css({
  3045. display: 'block',
  3046. left: pos.left,
  3047. top: pos.top,
  3048. width: imageSize.w,
  3049. height: imageSize.h
  3050. }).data('target', styleInfo.image); // save current image element.
  3051. var sizingText = imageSize.w + 'x' + imageSize.h;
  3052. $selection.find('.note-control-selection-info').text(sizingText);
  3053. } else {
  3054. $selection.hide();
  3055. }
  3056. };
  3057. this.hide = function ($handle) {
  3058. $handle.children().hide();
  3059. };
  3060. };
  3061. /**
  3062. * Dialog
  3063. *
  3064. * @class
  3065. */
  3066. var Dialog = function () {
  3067. /**
  3068. * toggle button status
  3069. *
  3070. * @param {jQuery} $btn
  3071. * @param {Boolean} isEnable
  3072. */
  3073. var toggleBtn = function ($btn, isEnable) {
  3074. $btn.toggleClass('disabled', !isEnable);
  3075. $btn.attr('disabled', !isEnable);
  3076. };
  3077. /**
  3078. * show image dialog
  3079. *
  3080. * @param {jQuery} $editable
  3081. * @param {jQuery} $dialog
  3082. * @return {Promise}
  3083. */
  3084. this.showImageDialog = function ($editable, $dialog) {
  3085. return $.Deferred(function (deferred) {
  3086. var $imageDialog = $dialog.find('.note-image-dialog');
  3087. var $imageInput = $dialog.find('.note-image-input'),
  3088. $imageUrl = $dialog.find('.note-image-url'),
  3089. $imageBtn = $dialog.find('.note-image-btn');
  3090. $imageDialog.one('shown.bs.modal', function () {
  3091. // Cloning imageInput to clear element.
  3092. $imageInput.replaceWith($imageInput.clone()
  3093. .on('change', function () {
  3094. deferred.resolve(this.files);
  3095. $imageDialog.modal('hide');
  3096. })
  3097. .val('')
  3098. );
  3099. $imageBtn.click(function (event) {
  3100. event.preventDefault();
  3101. deferred.resolve($imageUrl.val());
  3102. $imageDialog.modal('hide');
  3103. });
  3104. $imageUrl.on('keyup paste', function (event) {
  3105. var url;
  3106. if (event.type === 'paste') {
  3107. url = event.originalEvent.clipboardData.getData('text');
  3108. } else {
  3109. url = $imageUrl.val();
  3110. }
  3111. toggleBtn($imageBtn, url);
  3112. }).val('').trigger('focus');
  3113. }).one('hidden.bs.modal', function () {
  3114. $imageInput.off('change');
  3115. $imageUrl.off('keyup paste');
  3116. $imageBtn.off('click');
  3117. if (deferred.state() === 'pending') {
  3118. deferred.reject();
  3119. }
  3120. }).modal('show');
  3121. });
  3122. };
  3123. /**
  3124. * Show video dialog and set event handlers on dialog controls.
  3125. *
  3126. * @param {jQuery} $dialog
  3127. * @param {Object} videoInfo
  3128. * @return {Promise}
  3129. */
  3130. this.showVideoDialog = function ($editable, $dialog, videoInfo) {
  3131. return $.Deferred(function (deferred) {
  3132. var $videoDialog = $dialog.find('.note-video-dialog');
  3133. var $videoUrl = $videoDialog.find('.note-video-url'),
  3134. $videoBtn = $videoDialog.find('.note-video-btn');
  3135. $videoDialog.one('shown.bs.modal', function () {
  3136. $videoUrl.val(videoInfo.text).keyup(function () {
  3137. toggleBtn($videoBtn, $videoUrl.val());
  3138. }).trigger('keyup').trigger('focus');
  3139. $videoBtn.click(function (event) {
  3140. event.preventDefault();
  3141. deferred.resolve($videoUrl.val());
  3142. $videoDialog.modal('hide');
  3143. });
  3144. }).one('hidden.bs.modal', function () {
  3145. // dettach events
  3146. $videoUrl.off('keyup');
  3147. $videoBtn.off('click');
  3148. if (deferred.state() === 'pending') {
  3149. deferred.reject();
  3150. }
  3151. }).modal('show');
  3152. });
  3153. };
  3154. /**
  3155. * Show link dialog and set event handlers on dialog controls.
  3156. *
  3157. * @param {jQuery} $dialog
  3158. * @param {Object} linkInfo
  3159. * @return {Promise}
  3160. */
  3161. this.showLinkDialog = function ($editable, $dialog, linkInfo) {
  3162. return $.Deferred(function (deferred) {
  3163. var $linkDialog = $dialog.find('.note-link-dialog');
  3164. var $linkText = $linkDialog.find('.note-link-text'),
  3165. $linkUrl = $linkDialog.find('.note-link-url'),
  3166. $linkBtn = $linkDialog.find('.note-link-btn'),
  3167. $openInNewWindow = $linkDialog.find('input[type=checkbox]');
  3168. $linkDialog.one('shown.bs.modal', function () {
  3169. $linkText.val(linkInfo.text);
  3170. $linkText.keyup(function () {
  3171. // if linktext was modified by keyup,
  3172. // stop cloning text from linkUrl
  3173. linkInfo.text = $linkText.val();
  3174. });
  3175. // if no url was given, copy text to url
  3176. if (!linkInfo.url) {
  3177. linkInfo.url = linkInfo.text;
  3178. toggleBtn($linkBtn, linkInfo.text);
  3179. }
  3180. $linkUrl.keyup(function () {
  3181. toggleBtn($linkBtn, $linkUrl.val());
  3182. // display same link on `Text to display` input
  3183. // when create a new link
  3184. if (!linkInfo.text) {
  3185. $linkText.val($linkUrl.val());
  3186. }
  3187. }).val(linkInfo.url).trigger('focus').trigger('select');
  3188. $openInNewWindow.prop('checked', linkInfo.newWindow);
  3189. $linkBtn.one('click', function (event) {
  3190. event.preventDefault();
  3191. deferred.resolve({
  3192. range: linkInfo.range,
  3193. url: $linkUrl.val(),
  3194. text: $linkText.val(),
  3195. newWindow: $openInNewWindow.is(':checked')
  3196. });
  3197. $linkDialog.modal('hide');
  3198. });
  3199. }).one('hidden.bs.modal', function () {
  3200. // dettach events
  3201. $linkText.off('keyup');
  3202. $linkUrl.off('keyup');
  3203. $linkBtn.off('click');
  3204. if (deferred.state() === 'pending') {
  3205. deferred.reject();
  3206. }
  3207. }).modal('show');
  3208. }).promise();
  3209. };
  3210. /**
  3211. * show help dialog
  3212. *
  3213. * @param {jQuery} $dialog
  3214. */
  3215. this.showHelpDialog = function ($editable, $dialog) {
  3216. return $.Deferred(function (deferred) {
  3217. var $helpDialog = $dialog.find('.note-help-dialog');
  3218. $helpDialog.one('hidden.bs.modal', function () {
  3219. deferred.resolve();
  3220. }).modal('show');
  3221. }).promise();
  3222. };
  3223. };
  3224. var CodeMirror;
  3225. if (agent.hasCodeMirror) {
  3226. if (agent.isSupportAmd) {
  3227. require(['CodeMirror'], function (cm) {
  3228. CodeMirror = cm;
  3229. });
  3230. } else {
  3231. CodeMirror = window.CodeMirror;
  3232. }
  3233. }
  3234. /**
  3235. * EventHandler
  3236. */
  3237. var EventHandler = function () {
  3238. var $window = $(window);
  3239. var $document = $(document);
  3240. var $scrollbar = $('html, body');
  3241. var editor = new Editor();
  3242. var toolbar = new Toolbar(), popover = new Popover();
  3243. var handle = new Handle(), dialog = new Dialog();
  3244. /**
  3245. * returns makeLayoutInfo from editor's descendant node.
  3246. *
  3247. * @param {Node} descendant
  3248. * @returns {Object}
  3249. */
  3250. var makeLayoutInfo = function (descendant) {
  3251. var $target = $(descendant).closest('.note-editor, .note-air-editor, .note-air-layout');
  3252. if (!$target.length) { return null; }
  3253. var $editor;
  3254. if ($target.is('.note-editor, .note-air-editor')) {
  3255. $editor = $target;
  3256. } else {
  3257. $editor = $('#note-editor-' + list.last($target.attr('id').split('-')));
  3258. }
  3259. return dom.buildLayoutInfo($editor);
  3260. };
  3261. /**
  3262. * insert Images from file array.
  3263. *
  3264. * @param {jQuery} $editable
  3265. * @param {File[]} files
  3266. */
  3267. var insertImages = function ($editable, files) {
  3268. var callbacks = $editable.data('callbacks');
  3269. // If onImageUpload options setted
  3270. if (callbacks.onImageUpload) {
  3271. callbacks.onImageUpload(files, editor, $editable);
  3272. // else insert Image as dataURL
  3273. } else {
  3274. $.each(files, function (idx, file) {
  3275. var filename = file.name;
  3276. async.readFileAsDataURL(file).then(function (sDataURL) {
  3277. editor.insertImage($editable, sDataURL, filename);
  3278. }).fail(function () {
  3279. if (callbacks.onImageUploadError) {
  3280. callbacks.onImageUploadError();
  3281. }
  3282. });
  3283. });
  3284. }
  3285. };
  3286. var commands = {
  3287. /**
  3288. * @param {Object} layoutInfo
  3289. */
  3290. showLinkDialog: function (layoutInfo) {
  3291. var $editor = layoutInfo.editor(),
  3292. $dialog = layoutInfo.dialog(),
  3293. $editable = layoutInfo.editable(),
  3294. linkInfo = editor.getLinkInfo($editable);
  3295. var options = $editor.data('options');
  3296. editor.saveRange($editable);
  3297. dialog.showLinkDialog($editable, $dialog, linkInfo).then(function (linkInfo) {
  3298. editor.restoreRange($editable);
  3299. editor.createLink($editable, linkInfo, options);
  3300. // hide popover after creating link
  3301. popover.hide(layoutInfo.popover());
  3302. }).fail(function () {
  3303. editor.restoreRange($editable);
  3304. });
  3305. },
  3306. /**
  3307. * @param {Object} layoutInfo
  3308. */
  3309. showImageDialog: function (layoutInfo) {
  3310. var $dialog = layoutInfo.dialog(),
  3311. $editable = layoutInfo.editable();
  3312. editor.saveRange($editable);
  3313. dialog.showImageDialog($editable, $dialog).then(function (data) {
  3314. editor.restoreRange($editable);
  3315. if (typeof data === 'string') {
  3316. // image url
  3317. editor.insertImage($editable, data);
  3318. } else {
  3319. // array of files
  3320. insertImages($editable, data);
  3321. }
  3322. }).fail(function () {
  3323. editor.restoreRange($editable);
  3324. });
  3325. },
  3326. /**
  3327. * @param {Object} layoutInfo
  3328. */
  3329. showVideoDialog: function (layoutInfo) {
  3330. var $dialog = layoutInfo.dialog(),
  3331. $editable = layoutInfo.editable(),
  3332. videoInfo = editor.getVideoInfo($editable);
  3333. editor.saveRange($editable);
  3334. dialog.showVideoDialog($editable, $dialog, videoInfo).then(function (sUrl) {
  3335. editor.restoreRange($editable);
  3336. editor.insertVideo($editable, sUrl);
  3337. }).fail(function () {
  3338. editor.restoreRange($editable);
  3339. });
  3340. },
  3341. /**
  3342. * @param {Object} layoutInfo
  3343. */
  3344. showHelpDialog: function (layoutInfo) {
  3345. var $dialog = layoutInfo.dialog(),
  3346. $editable = layoutInfo.editable();
  3347. editor.saveRange($editable, true);
  3348. dialog.showHelpDialog($editable, $dialog).then(function () {
  3349. editor.restoreRange($editable);
  3350. });
  3351. },
  3352. fullscreen: function (layoutInfo) {
  3353. var $editor = layoutInfo.editor(),
  3354. $toolbar = layoutInfo.toolbar(),
  3355. $editable = layoutInfo.editable(),
  3356. $codable = layoutInfo.codable();
  3357. var options = $editor.data('options');
  3358. var resize = function (size) {
  3359. $editor.css('width', size.w);
  3360. $editable.css('height', size.h);
  3361. $codable.css('height', size.h);
  3362. if ($codable.data('cmeditor')) {
  3363. $codable.data('cmeditor').setsize(null, size.h);
  3364. }
  3365. };
  3366. $editor.toggleClass('fullscreen');
  3367. var isFullscreen = $editor.hasClass('fullscreen');
  3368. if (isFullscreen) {
  3369. $editable.data('orgheight', $editable.css('height'));
  3370. $window.on('resize', function () {
  3371. resize({
  3372. w: $window.width(),
  3373. h: $window.height() - $toolbar.outerHeight()
  3374. });
  3375. }).trigger('resize');
  3376. $scrollbar.css('overflow', 'hidden');
  3377. } else {
  3378. $window.off('resize');
  3379. resize({
  3380. w: options.width || '',
  3381. h: $editable.data('orgheight')
  3382. });
  3383. $scrollbar.css('overflow', 'visible');
  3384. }
  3385. toolbar.updateFullscreen($toolbar, isFullscreen);
  3386. },
  3387. codeview: function (layoutInfo) {
  3388. var $editor = layoutInfo.editor(),
  3389. $toolbar = layoutInfo.toolbar(),
  3390. $editable = layoutInfo.editable(),
  3391. $codable = layoutInfo.codable(),
  3392. $popover = layoutInfo.popover();
  3393. var options = $editor.data('options');
  3394. var cmEditor, server;
  3395. $editor.toggleClass('codeview');
  3396. var isCodeview = $editor.hasClass('codeview');
  3397. if (isCodeview) {
  3398. $codable.val(dom.html($editable, true));
  3399. $codable.height($editable.height());
  3400. toolbar.deactivate($toolbar);
  3401. popover.hide($popover);
  3402. $codable.focus();
  3403. // activate CodeMirror as codable
  3404. if (agent.hasCodeMirror) {
  3405. cmEditor = CodeMirror.fromTextArea($codable[0], options.codemirror);
  3406. // CodeMirror TernServer
  3407. if (options.codemirror.tern) {
  3408. server = new CodeMirror.TernServer(options.codemirror.tern);
  3409. cmEditor.ternServer = server;
  3410. cmEditor.on('cursorActivity', function (cm) {
  3411. server.updateArgHints(cm);
  3412. });
  3413. }
  3414. // CodeMirror hasn't Padding.
  3415. cmEditor.setSize(null, $editable.outerHeight());
  3416. $codable.data('cmEditor', cmEditor);
  3417. }
  3418. } else {
  3419. // deactivate CodeMirror as codable
  3420. if (agent.hasCodeMirror) {
  3421. cmEditor = $codable.data('cmEditor');
  3422. $codable.val(cmEditor.getValue());
  3423. cmEditor.toTextArea();
  3424. }
  3425. $editable.html(dom.value($codable) || dom.emptyPara);
  3426. $editable.height(options.height ? $codable.height() : 'auto');
  3427. toolbar.activate($toolbar);
  3428. $editable.focus();
  3429. }
  3430. toolbar.updateCodeview(layoutInfo.toolbar(), isCodeview);
  3431. }
  3432. };
  3433. var hMousedown = function (event) {
  3434. //preventDefault Selection for FF, IE8+
  3435. if (dom.isImg(event.target)) {
  3436. event.preventDefault();
  3437. }
  3438. };
  3439. var hToolbarAndPopoverUpdate = function (event) {
  3440. // delay for range after mouseup
  3441. setTimeout(function () {
  3442. var layoutInfo = makeLayoutInfo(event.currentTarget || event.target);
  3443. var styleInfo = editor.currentStyle(event.target);
  3444. if (!styleInfo) { return; }
  3445. var isAirMode = layoutInfo.editor().data('options').airMode;
  3446. if (!isAirMode) {
  3447. toolbar.update(layoutInfo.toolbar(), styleInfo);
  3448. }
  3449. popover.update(layoutInfo.popover(), styleInfo, isAirMode);
  3450. handle.update(layoutInfo.handle(), styleInfo, isAirMode);
  3451. }, 0);
  3452. };
  3453. var hScroll = function (event) {
  3454. var layoutInfo = makeLayoutInfo(event.currentTarget || event.target);
  3455. //hide popover and handle when scrolled
  3456. popover.hide(layoutInfo.popover());
  3457. handle.hide(layoutInfo.handle());
  3458. };
  3459. /**
  3460. * paste clipboard image
  3461. *
  3462. * @param {Event} event
  3463. */
  3464. var hPasteClipboardImage = function (event) {
  3465. var clipboardData = event.originalEvent.clipboardData;
  3466. if (!clipboardData || !clipboardData.items || !clipboardData.items.length) {
  3467. return;
  3468. }
  3469. var layoutInfo = makeLayoutInfo(event.currentTarget || event.target),
  3470. $editable = layoutInfo.editable();
  3471. var item = list.head(clipboardData.items);
  3472. var isClipboardImage = item.kind === 'file' && item.type.indexOf('image/') !== -1;
  3473. if (isClipboardImage) {
  3474. insertImages($editable, [item.getAsFile()]);
  3475. }
  3476. editor.afterCommand($editable);
  3477. };
  3478. /**
  3479. * `mousedown` event handler on $handle
  3480. * - controlSizing: resize image
  3481. *
  3482. * @param {MouseEvent} event
  3483. */
  3484. var hHandleMousedown = function (event) {
  3485. if (dom.isControlSizing(event.target)) {
  3486. event.preventDefault();
  3487. event.stopPropagation();
  3488. var layoutInfo = makeLayoutInfo(event.target),
  3489. $handle = layoutInfo.handle(), $popover = layoutInfo.popover(),
  3490. $editable = layoutInfo.editable(),
  3491. $editor = layoutInfo.editor();
  3492. var target = $handle.find('.note-control-selection').data('target'),
  3493. $target = $(target), posStart = $target.offset(),
  3494. scrollTop = $document.scrollTop();
  3495. var isAirMode = $editor.data('options').airMode;
  3496. $document.on('mousemove', function (event) {
  3497. editor.resizeTo({
  3498. x: event.clientX - posStart.left,
  3499. y: event.clientY - (posStart.top - scrollTop)
  3500. }, $target, !event.shiftKey);
  3501. handle.update($handle, {image: target}, isAirMode);
  3502. popover.update($popover, {image: target}, isAirMode);
  3503. }).one('mouseup', function () {
  3504. $document.off('mousemove');
  3505. });
  3506. if (!$target.data('ratio')) { // original ratio.
  3507. $target.data('ratio', $target.height() / $target.width());
  3508. }
  3509. editor.afterCommand($editable);
  3510. }
  3511. };
  3512. var hToolbarAndPopoverMousedown = function (event) {
  3513. // prevent default event when insertTable (FF, Webkit)
  3514. var $btn = $(event.target).closest('[data-event]');
  3515. if ($btn.length) {
  3516. event.preventDefault();
  3517. }
  3518. };
  3519. var hToolbarAndPopoverClick = function (event) {
  3520. var $btn = $(event.target).closest('[data-event]');
  3521. if ($btn.length) {
  3522. var eventName = $btn.attr('data-event'),
  3523. value = $btn.attr('data-value'),
  3524. hide = $btn.attr('data-hide');
  3525. var layoutInfo = makeLayoutInfo(event.target);
  3526. event.preventDefault();
  3527. // before command: detect control selection element($target)
  3528. var $target;
  3529. if ($.inArray(eventName, ['resize', 'floatMe', 'removeMedia', 'imageShape']) !== -1) {
  3530. var $selection = layoutInfo.handle().find('.note-control-selection');
  3531. $target = $($selection.data('target'));
  3532. }
  3533. // If requested, hide the popover when the button is clicked.
  3534. // Useful for things like showHelpDialog.
  3535. if (hide) {
  3536. $btn.parents('.popover').hide();
  3537. }
  3538. if (editor[eventName]) { // on command
  3539. var $editable = layoutInfo.editable();
  3540. $editable.trigger('focus');
  3541. editor[eventName]($editable, value, $target);
  3542. } else if (commands[eventName]) {
  3543. commands[eventName].call(this, layoutInfo);
  3544. }
  3545. // after command
  3546. if ($.inArray(eventName, ['backColor', 'foreColor']) !== -1) {
  3547. var options = layoutInfo.editor().data('options', options);
  3548. var module = options.airMode ? popover : toolbar;
  3549. module.updateRecentColor(list.head($btn), eventName, value);
  3550. }
  3551. hToolbarAndPopoverUpdate(event);
  3552. }
  3553. };
  3554. var EDITABLE_PADDING = 24;
  3555. /**
  3556. * `mousedown` event handler on statusbar
  3557. *
  3558. * @param {MouseEvent} event
  3559. */
  3560. var hStatusbarMousedown = function (event) {
  3561. event.preventDefault();
  3562. event.stopPropagation();
  3563. var $editable = makeLayoutInfo(event.target).editable();
  3564. var nEditableTop = $editable.offset().top - $document.scrollTop();
  3565. var layoutInfo = makeLayoutInfo(event.currentTarget || event.target);
  3566. var options = layoutInfo.editor().data('options');
  3567. $document.on('mousemove', function (event) {
  3568. var nHeight = event.clientY - (nEditableTop + EDITABLE_PADDING);
  3569. nHeight = (options.minHeight > 0) ? Math.max(nHeight, options.minHeight) : nHeight;
  3570. nHeight = (options.maxHeight > 0) ? Math.min(nHeight, options.maxHeight) : nHeight;
  3571. $editable.height(nHeight);
  3572. }).one('mouseup', function () {
  3573. $document.off('mousemove');
  3574. });
  3575. };
  3576. var PX_PER_EM = 18;
  3577. var hDimensionPickerMove = function (event, options) {
  3578. var $picker = $(event.target.parentNode); // target is mousecatcher
  3579. var $dimensionDisplay = $picker.next();
  3580. var $catcher = $picker.find('.note-dimension-picker-mousecatcher');
  3581. var $highlighted = $picker.find('.note-dimension-picker-highlighted');
  3582. var $unhighlighted = $picker.find('.note-dimension-picker-unhighlighted');
  3583. var posOffset;
  3584. // HTML5 with jQuery - e.offsetX is undefined in Firefox
  3585. if (event.offsetX === undefined) {
  3586. var posCatcher = $(event.target).offset();
  3587. posOffset = {
  3588. x: event.pageX - posCatcher.left,
  3589. y: event.pageY - posCatcher.top
  3590. };
  3591. } else {
  3592. posOffset = {
  3593. x: event.offsetX,
  3594. y: event.offsetY
  3595. };
  3596. }
  3597. var dim = {
  3598. c: Math.ceil(posOffset.x / PX_PER_EM) || 1,
  3599. r: Math.ceil(posOffset.y / PX_PER_EM) || 1
  3600. };
  3601. $highlighted.css({ width: dim.c + 'em', height: dim.r + 'em' });
  3602. $catcher.attr('data-value', dim.c + 'x' + dim.r);
  3603. if (3 < dim.c && dim.c < options.insertTableMaxSize.col) {
  3604. $unhighlighted.css({ width: dim.c + 1 + 'em'});
  3605. }
  3606. if (3 < dim.r && dim.r < options.insertTableMaxSize.row) {
  3607. $unhighlighted.css({ height: dim.r + 1 + 'em'});
  3608. }
  3609. $dimensionDisplay.html(dim.c + ' x ' + dim.r);
  3610. };
  3611. /**
  3612. * Drag and Drop Events
  3613. *
  3614. * @param {Object} layoutInfo - layout Informations
  3615. * @param {Boolean} disableDragAndDrop
  3616. */
  3617. var handleDragAndDropEvent = function (layoutInfo, disableDragAndDrop) {
  3618. if (disableDragAndDrop) {
  3619. // prevent default drop event
  3620. $document.on('drop', function (e) {
  3621. e.preventDefault();
  3622. });
  3623. } else {
  3624. attachDragAndDropEvent(layoutInfo);
  3625. }
  3626. };
  3627. /**
  3628. * attach Drag and Drop Events
  3629. *
  3630. * @param {Object} layoutInfo - layout Informations
  3631. */
  3632. var attachDragAndDropEvent = function (layoutInfo) {
  3633. var collection = $(),
  3634. $dropzone = layoutInfo.dropzone,
  3635. $dropzoneMessage = layoutInfo.dropzone.find('.note-dropzone-message');
  3636. // show dropzone on dragenter when dragging a object to document.
  3637. $document.on('dragenter', function (e) {
  3638. var isCodeview = layoutInfo.editor.hasClass('codeview');
  3639. if (!isCodeview && !collection.length) {
  3640. layoutInfo.editor.addClass('dragover');
  3641. $dropzone.width(layoutInfo.editor.width());
  3642. $dropzone.height(layoutInfo.editor.height());
  3643. $dropzoneMessage.text('Drag Image Here');
  3644. }
  3645. collection = collection.add(e.target);
  3646. }).on('dragleave', function (e) {
  3647. collection = collection.not(e.target);
  3648. if (!collection.length) {
  3649. layoutInfo.editor.removeClass('dragover');
  3650. }
  3651. }).on('drop', function () {
  3652. collection = $();
  3653. layoutInfo.editor.removeClass('dragover');
  3654. });
  3655. // change dropzone's message on hover.
  3656. $dropzone.on('dragenter', function () {
  3657. $dropzone.addClass('hover');
  3658. $dropzoneMessage.text('Drop Image');
  3659. }).on('dragleave', function () {
  3660. $dropzone.removeClass('hover');
  3661. $dropzoneMessage.text('Drag Image Here');
  3662. });
  3663. // attach dropImage
  3664. $dropzone.on('drop', function (event) {
  3665. event.preventDefault();
  3666. var dataTransfer = event.originalEvent.dataTransfer;
  3667. if (dataTransfer && dataTransfer.files) {
  3668. var layoutInfo = makeLayoutInfo(event.currentTarget || event.target);
  3669. layoutInfo.editable().focus();
  3670. insertImages(layoutInfo.editable(), dataTransfer.files);
  3671. }
  3672. }).on('dragover', false); // prevent default dragover event
  3673. };
  3674. /**
  3675. * bind KeyMap on keydown
  3676. *
  3677. * @param {Object} layoutInfo
  3678. * @param {Object} keyMap
  3679. */
  3680. this.bindKeyMap = function (layoutInfo, keyMap) {
  3681. var $editor = layoutInfo.editor;
  3682. var $editable = layoutInfo.editable;
  3683. layoutInfo = makeLayoutInfo($editable);
  3684. $editable.on('keydown', function (event) {
  3685. var aKey = [];
  3686. // modifier
  3687. if (event.metaKey) { aKey.push('CMD'); }
  3688. if (event.ctrlKey && !event.altKey) { aKey.push('CTRL'); }
  3689. if (event.shiftKey) { aKey.push('SHIFT'); }
  3690. // keycode
  3691. var keyName = key.nameFromCode[event.keyCode];
  3692. if (keyName) { aKey.push(keyName); }
  3693. var eventName = keyMap[aKey.join('+')];
  3694. if (eventName) {
  3695. event.preventDefault();
  3696. if (editor[eventName]) {
  3697. editor[eventName]($editable, $editor.data('options'));
  3698. } else if (commands[eventName]) {
  3699. commands[eventName].call(this, layoutInfo);
  3700. }
  3701. } else if (key.isEdit(event.keyCode)) {
  3702. editor.afterCommand($editable);
  3703. }
  3704. });
  3705. };
  3706. /**
  3707. * attach eventhandler
  3708. *
  3709. * @param {Object} layoutInfo - layout Informations
  3710. * @param {Object} options - user options include custom event handlers
  3711. * @param {Function} options.enter - enter key handler
  3712. */
  3713. this.attach = function (layoutInfo, options) {
  3714. // handlers for editable
  3715. this.bindKeyMap(layoutInfo, options.keyMap[agent.isMac ? 'mac' : 'pc']);
  3716. layoutInfo.editable.on('mousedown', hMousedown);
  3717. layoutInfo.editable.on('keyup mouseup', hToolbarAndPopoverUpdate);
  3718. layoutInfo.editable.on('scroll', hScroll);
  3719. layoutInfo.editable.on('paste', hPasteClipboardImage);
  3720. // handler for handle and popover
  3721. layoutInfo.handle.on('mousedown', hHandleMousedown);
  3722. layoutInfo.popover.on('click', hToolbarAndPopoverClick);
  3723. layoutInfo.popover.on('mousedown', hToolbarAndPopoverMousedown);
  3724. // handlers for frame mode (toolbar, statusbar)
  3725. if (!options.airMode) {
  3726. // handler for drag and drop
  3727. handleDragAndDropEvent(layoutInfo, options.disableDragAndDrop);
  3728. // handler for toolbar
  3729. layoutInfo.toolbar.on('click', hToolbarAndPopoverClick);
  3730. layoutInfo.toolbar.on('mousedown', hToolbarAndPopoverMousedown);
  3731. // handler for statusbar
  3732. if (!options.disableResizeEditor) {
  3733. layoutInfo.statusbar.on('mousedown', hStatusbarMousedown);
  3734. }
  3735. }
  3736. // handler for table dimension
  3737. var $catcherContainer = options.airMode ? layoutInfo.popover :
  3738. layoutInfo.toolbar;
  3739. var $catcher = $catcherContainer.find('.note-dimension-picker-mousecatcher');
  3740. $catcher.css({
  3741. width: options.insertTableMaxSize.col + 'em',
  3742. height: options.insertTableMaxSize.row + 'em'
  3743. }).on('mousemove', function (event) {
  3744. hDimensionPickerMove(event, options);
  3745. });
  3746. // save options on editor
  3747. layoutInfo.editor.data('options', options);
  3748. // ret styleWithCSS for backColor / foreColor clearing with 'inherit'.
  3749. if (options.styleWithSpan && !agent.isMSIE) {
  3750. // protect FF Error: NS_ERROR_FAILURE: Failure
  3751. setTimeout(function () {
  3752. document.execCommand('styleWithCSS', 0, true);
  3753. }, 0);
  3754. }
  3755. // History
  3756. var history = new History(layoutInfo.editable);
  3757. layoutInfo.editable.data('NoteHistory', history);
  3758. // basic event callbacks (lowercase)
  3759. // enter, focus, blur, keyup, keydown
  3760. if (options.onenter) {
  3761. layoutInfo.editable.keypress(function (event) {
  3762. if (event.keyCode === key.ENTER) { options.onenter(event); }
  3763. });
  3764. }
  3765. if (options.onfocus) { layoutInfo.editable.focus(options.onfocus); }
  3766. if (options.onblur) { layoutInfo.editable.blur(options.onblur); }
  3767. if (options.onkeyup) { layoutInfo.editable.keyup(options.onkeyup); }
  3768. if (options.onkeydown) { layoutInfo.editable.keydown(options.onkeydown); }
  3769. if (options.onpaste) { layoutInfo.editable.on('paste', options.onpaste); }
  3770. // callbacks for advanced features (camel)
  3771. if (options.onToolbarClick) { layoutInfo.toolbar.click(options.onToolbarClick); }
  3772. if (options.onChange) {
  3773. var hChange = function () {
  3774. editor.triggerOnChange(layoutInfo.editable);
  3775. };
  3776. if (agent.isMSIE) {
  3777. var sDomEvents = 'DOMCharacterDataModified DOMSubtreeModified DOMNodeInserted';
  3778. layoutInfo.editable.on(sDomEvents, hChange);
  3779. } else {
  3780. layoutInfo.editable.on('input', hChange);
  3781. }
  3782. }
  3783. // All editor status will be saved on editable with jquery's data
  3784. // for support multiple editor with singleton object.
  3785. layoutInfo.editable.data('callbacks', {
  3786. onChange: options.onChange,
  3787. onAutoSave: options.onAutoSave,
  3788. onImageUpload: options.onImageUpload,
  3789. onImageUploadError: options.onImageUploadError,
  3790. onFileUpload: options.onFileUpload,
  3791. onFileUploadError: options.onFileUpload
  3792. });
  3793. };
  3794. this.dettach = function (layoutInfo, options) {
  3795. layoutInfo.editable.off();
  3796. layoutInfo.popover.off();
  3797. layoutInfo.handle.off();
  3798. layoutInfo.dialog.off();
  3799. if (!options.airMode) {
  3800. layoutInfo.dropzone.off();
  3801. layoutInfo.toolbar.off();
  3802. layoutInfo.statusbar.off();
  3803. }
  3804. };
  3805. };
  3806. /**
  3807. * renderer
  3808. *
  3809. * rendering toolbar and editable
  3810. */
  3811. var Renderer = function () {
  3812. /**
  3813. * bootstrap button template
  3814. *
  3815. * @param {String} label
  3816. * @param {Object} [options]
  3817. * @param {String} [options.event]
  3818. * @param {String} [options.value]
  3819. * @param {String} [options.title]
  3820. * @param {String} [options.dropdown]
  3821. * @param {String} [options.hide]
  3822. */
  3823. var tplButton = function (label, options) {
  3824. var event = options.event;
  3825. var value = options.value;
  3826. var title = options.title;
  3827. var className = options.className;
  3828. var dropdown = options.dropdown;
  3829. var hide = options.hide;
  3830. return '<button type="button"' +
  3831. ' class="btn btn-default btn-sm btn-small' +
  3832. (className ? ' ' + className : '') +
  3833. (dropdown ? ' dropdown-toggle' : '') +
  3834. '"' +
  3835. (dropdown ? ' data-toggle="dropdown"' : '') +
  3836. (title ? ' title="' + title + '"' : '') +
  3837. (event ? ' data-event="' + event + '"' : '') +
  3838. (value ? ' data-value=\'' + value + '\'' : '') +
  3839. (hide ? ' data-hide=\'' + hide + '\'' : '') +
  3840. ' tabindex="-1">' +
  3841. label +
  3842. (dropdown ? ' <span class="caret"></span>' : '') +
  3843. '</button>' +
  3844. (dropdown || '');
  3845. };
  3846. /**
  3847. * bootstrap icon button template
  3848. *
  3849. * @param {String} iconClassName
  3850. * @param {Object} [options]
  3851. * @param {String} [options.event]
  3852. * @param {String} [options.value]
  3853. * @param {String} [options.title]
  3854. * @param {String} [options.dropdown]
  3855. */
  3856. var tplIconButton = function (iconClassName, options) {
  3857. var label = '<i class="' + iconClassName + '"></i>';
  3858. return tplButton(label, options);
  3859. };
  3860. /**
  3861. * bootstrap popover template
  3862. *
  3863. * @param {String} className
  3864. * @param {String} content
  3865. */
  3866. var tplPopover = function (className, content) {
  3867. return '<div class="' + className + ' popover bottom in" style="display: none;">' +
  3868. '<div class="arrow"></div>' +
  3869. '<div class="popover-content">' +
  3870. content +
  3871. '</div>' +
  3872. '</div>';
  3873. };
  3874. /**
  3875. * bootstrap dialog template
  3876. *
  3877. * @param {String} className
  3878. * @param {String} [title]
  3879. * @param {String} body
  3880. * @param {String} [footer]
  3881. */
  3882. var tplDialog = function (className, title, body, footer) {
  3883. return '<div class="' + className + ' modal" aria-hidden="false">' +
  3884. '<div class="modal-dialog">' +
  3885. '<div class="modal-content">' +
  3886. (title ?
  3887. '<div class="modal-header">' +
  3888. '<button type="button" class="close" aria-hidden="true" tabindex="-1">&times;</button>' +
  3889. '<h4 class="modal-title">' + title + '</h4>' +
  3890. '</div>' : ''
  3891. ) +
  3892. '<form class="note-modal-form">' +
  3893. '<div class="modal-body">' +
  3894. '<div class="row-fluid">' + body + '</div>' +
  3895. '</div>' +
  3896. (footer ?
  3897. '<div class="modal-footer">' + footer + '</div>' : ''
  3898. ) +
  3899. '</form>' +
  3900. '</div>' +
  3901. '</div>' +
  3902. '</div>';
  3903. };
  3904. var tplButtonInfo = {
  3905. picture: function (lang) {
  3906. return tplIconButton('fa fa-picture-o icon-picture', {
  3907. event: 'showImageDialog',
  3908. title: lang.image.image,
  3909. hide: true
  3910. });
  3911. },
  3912. link: function (lang) {
  3913. return tplIconButton('fa fa-link icon-link', {
  3914. event: 'showLinkDialog',
  3915. title: lang.link.link,
  3916. hide: true
  3917. });
  3918. },
  3919. video: function (lang) {
  3920. return tplIconButton('fa fa-youtube-play icon-play', {
  3921. event: 'showVideoDialog',
  3922. title: lang.video.video,
  3923. hide: true
  3924. });
  3925. },
  3926. table: function (lang) {
  3927. var dropdown = '<ul class="note-table dropdown-menu">' +
  3928. '<div class="note-dimension-picker">' +
  3929. '<div class="note-dimension-picker-mousecatcher" data-event="insertTable" data-value="1x1"></div>' +
  3930. '<div class="note-dimension-picker-highlighted"></div>' +
  3931. '<div class="note-dimension-picker-unhighlighted"></div>' +
  3932. '</div>' +
  3933. '<div class="note-dimension-display"> 1 x 1 </div>' +
  3934. '</ul>';
  3935. return tplIconButton('fa fa-table icon-table', {
  3936. title: lang.table.table,
  3937. dropdown: dropdown
  3938. });
  3939. },
  3940. style: function (lang, options) {
  3941. var items = options.styleTags.reduce(function (memo, v) {
  3942. var label = lang.style[v === 'p' ? 'normal' : v];
  3943. return memo + '<li><a data-event="formatBlock" href="#" data-value="' + v + '">' +
  3944. (
  3945. (v === 'p' || v === 'pre') ? label :
  3946. '<' + v + '>' + label + '</' + v + '>'
  3947. ) +
  3948. '</a></li>';
  3949. }, '');
  3950. return tplIconButton('fa fa-magic icon-magic', {
  3951. title: lang.style.style,
  3952. dropdown: '<ul class="dropdown-menu">' + items + '</ul>'
  3953. });
  3954. },
  3955. fontname: function (lang, options) {
  3956. var items = options.fontNames.reduce(function (memo, v) {
  3957. if (!agent.isFontInstalled(v)) { return memo; }
  3958. return memo + '<li><a data-event="fontName" href="#" data-value="' + v + '">' +
  3959. '<i class="fa fa-check icon-ok"></i> ' + v +
  3960. '</a></li>';
  3961. }, '');
  3962. var label = '<span class="note-current-fontname">' +
  3963. options.defaultFontName +
  3964. '</span>';
  3965. return tplButton(label, {
  3966. title: lang.font.name,
  3967. dropdown: '<ul class="dropdown-menu">' + items + '</ul>'
  3968. });
  3969. },
  3970. fontsize: function (lang, options) {
  3971. var items = options.fontSizes.reduce(function (memo, v) {
  3972. return memo + '<li><a data-event="fontSize" href="#" data-value="' + v + '">' +
  3973. '<i class="fa fa-check icon-ok"></i> ' + v +
  3974. '</a></li>';
  3975. }, '');
  3976. var label = '<span class="note-current-fontsize">11</span>';
  3977. return tplButton(label, {
  3978. title: lang.font.size,
  3979. dropdown: '<ul class="dropdown-menu">' + items + '</ul>'
  3980. });
  3981. },
  3982. color: function (lang) {
  3983. var colorButtonLabel = '<i class="fa fa-font icon-font" style="color:black;background-color:yellow;"></i>';
  3984. var colorButton = tplButton(colorButtonLabel, {
  3985. className: 'note-recent-color',
  3986. title: lang.color.recent,
  3987. event: 'color',
  3988. value: '{"backColor":"yellow"}'
  3989. });
  3990. var dropdown = '<ul class="dropdown-menu">' +
  3991. '<li>' +
  3992. '<div class="btn-group">' +
  3993. '<div class="note-palette-title">' + lang.color.background + '</div>' +
  3994. '<div class="note-color-reset" data-event="backColor"' +
  3995. ' data-value="inherit" title="' + lang.color.transparent + '">' +
  3996. lang.color.setTransparent +
  3997. '</div>' +
  3998. '<div class="note-color-palette" data-target-event="backColor"></div>' +
  3999. '</div>' +
  4000. '<div class="btn-group">' +
  4001. '<div class="note-palette-title">' + lang.color.foreground + '</div>' +
  4002. '<div class="note-color-reset" data-event="foreColor" data-value="inherit" title="' + lang.color.reset + '">' +
  4003. lang.color.resetToDefault +
  4004. '</div>' +
  4005. '<div class="note-color-palette" data-target-event="foreColor"></div>' +
  4006. '</div>' +
  4007. '</li>' +
  4008. '</ul>';
  4009. var moreButton = tplButton('', {
  4010. title: lang.color.more,
  4011. dropdown: dropdown
  4012. });
  4013. return colorButton + moreButton;
  4014. },
  4015. bold: function (lang) {
  4016. return tplIconButton('fa fa-bold icon-bold', {
  4017. event: 'bold',
  4018. title: lang.font.bold
  4019. });
  4020. },
  4021. italic: function (lang) {
  4022. return tplIconButton('fa fa-italic icon-italic', {
  4023. event: 'italic',
  4024. title: lang.font.italic
  4025. });
  4026. },
  4027. underline: function (lang) {
  4028. return tplIconButton('fa fa-underline icon-underline', {
  4029. event: 'underline',
  4030. title: lang.font.underline
  4031. });
  4032. },
  4033. strikethrough: function (lang) {
  4034. return tplIconButton('fa fa-strikethrough icon-strikethrough', {
  4035. event: 'strikethrough',
  4036. title: lang.font.strikethrough
  4037. });
  4038. },
  4039. superscript: function (lang) {
  4040. return tplIconButton('fa fa-superscript icon-superscript', {
  4041. event: 'superscript',
  4042. title: lang.font.superscript
  4043. });
  4044. },
  4045. subscript: function (lang) {
  4046. return tplIconButton('fa fa-subscript icon-subscript', {
  4047. event: 'subscript',
  4048. title: lang.font.subscript
  4049. });
  4050. },
  4051. clear: function (lang) {
  4052. return tplIconButton('fa fa-eraser icon-eraser', {
  4053. event: 'removeFormat',
  4054. title: lang.font.clear
  4055. });
  4056. },
  4057. ul: function (lang) {
  4058. return tplIconButton('fa fa-list-ul icon-list-ul', {
  4059. event: 'insertUnorderedList',
  4060. title: lang.lists.unordered
  4061. });
  4062. },
  4063. ol: function (lang) {
  4064. return tplIconButton('fa fa-list-ol icon-list-ol', {
  4065. event: 'insertOrderedList',
  4066. title: lang.lists.ordered
  4067. });
  4068. },
  4069. paragraph: function (lang) {
  4070. var leftButton = tplIconButton('fa fa-align-left icon-align-left', {
  4071. title: lang.paragraph.left,
  4072. event: 'justifyLeft'
  4073. });
  4074. var centerButton = tplIconButton('fa fa-align-center icon-align-center', {
  4075. title: lang.paragraph.center,
  4076. event: 'justifyCenter'
  4077. });
  4078. var rightButton = tplIconButton('fa fa-align-right icon-align-right', {
  4079. title: lang.paragraph.right,
  4080. event: 'justifyRight'
  4081. });
  4082. var justifyButton = tplIconButton('fa fa-align-justify icon-align-justify', {
  4083. title: lang.paragraph.justify,
  4084. event: 'justifyFull'
  4085. });
  4086. var outdentButton = tplIconButton('fa fa-outdent icon-indent-left', {
  4087. title: lang.paragraph.outdent,
  4088. event: 'outdent'
  4089. });
  4090. var indentButton = tplIconButton('fa fa-indent icon-indent-right', {
  4091. title: lang.paragraph.indent,
  4092. event: 'indent'
  4093. });
  4094. var dropdown = '<div class="dropdown-menu">' +
  4095. '<div class="note-align btn-group">' +
  4096. leftButton + centerButton + rightButton + justifyButton +
  4097. '</div>' +
  4098. '<div class="note-list btn-group">' +
  4099. indentButton + outdentButton +
  4100. '</div>' +
  4101. '</div>';
  4102. return tplIconButton('fa fa-align-left icon-align-left', {
  4103. title: lang.paragraph.paragraph,
  4104. dropdown: dropdown
  4105. });
  4106. },
  4107. height: function (lang, options) {
  4108. var items = options.lineHeights.reduce(function (memo, v) {
  4109. return memo + '<li><a data-event="lineHeight" href="#" data-value="' + parseFloat(v) + '">' +
  4110. '<i class="fa fa-check icon-ok"></i> ' + v +
  4111. '</a></li>';
  4112. }, '');
  4113. return tplIconButton('fa fa-text-height icon-text-height', {
  4114. title: lang.font.height,
  4115. dropdown: '<ul class="dropdown-menu">' + items + '</ul>'
  4116. });
  4117. },
  4118. help: function (lang) {
  4119. return tplIconButton('fa fa-question icon-question', {
  4120. event: 'showHelpDialog',
  4121. title: lang.options.help,
  4122. hide: true
  4123. });
  4124. },
  4125. fullscreen: function (lang) {
  4126. return tplIconButton('fa fa-arrows-alt icon-fullscreen', {
  4127. event: 'fullscreen',
  4128. title: lang.options.fullscreen
  4129. });
  4130. },
  4131. codeview: function (lang) {
  4132. return tplIconButton('fa fa-code icon-code', {
  4133. event: 'codeview',
  4134. title: lang.options.codeview
  4135. });
  4136. },
  4137. undo: function (lang) {
  4138. return tplIconButton('fa fa-undo icon-undo', {
  4139. event: 'undo',
  4140. title: lang.history.undo
  4141. });
  4142. },
  4143. redo: function (lang) {
  4144. return tplIconButton('fa fa-repeat icon-repeat', {
  4145. event: 'redo',
  4146. title: lang.history.redo
  4147. });
  4148. },
  4149. hr: function (lang) {
  4150. return tplIconButton('fa fa-minus icon-hr', {
  4151. event: 'insertHorizontalRule',
  4152. title: lang.hr.insert
  4153. });
  4154. }
  4155. };
  4156. var tplPopovers = function (lang, options) {
  4157. var tplLinkPopover = function () {
  4158. var linkButton = tplIconButton('fa fa-edit icon-edit', {
  4159. title: lang.link.edit,
  4160. event: 'showLinkDialog',
  4161. hide: true
  4162. });
  4163. var unlinkButton = tplIconButton('fa fa-unlink icon-unlink', {
  4164. title: lang.link.unlink,
  4165. event: 'unlink'
  4166. });
  4167. var content = '<a href="http://www.google.com" target="_blank">www.google.com</a>&nbsp;&nbsp;' +
  4168. '<div class="note-insert btn-group">' +
  4169. linkButton + unlinkButton +
  4170. '</div>';
  4171. return tplPopover('note-link-popover', content);
  4172. };
  4173. var tplImagePopover = function () {
  4174. var fullButton = tplButton('<span class="note-fontsize-10">100%</span>', {
  4175. title: lang.image.resizeFull,
  4176. event: 'resize',
  4177. value: '1'
  4178. });
  4179. var halfButton = tplButton('<span class="note-fontsize-10">50%</span>', {
  4180. title: lang.image.resizeHalf,
  4181. event: 'resize',
  4182. value: '0.5'
  4183. });
  4184. var quarterButton = tplButton('<span class="note-fontsize-10">25%</span>', {
  4185. title: lang.image.resizeQuarter,
  4186. event: 'resize',
  4187. value: '0.25'
  4188. });
  4189. var leftButton = tplIconButton('fa fa-align-left icon-align-left', {
  4190. title: lang.image.floatLeft,
  4191. event: 'floatMe',
  4192. value: 'left'
  4193. });
  4194. var rightButton = tplIconButton('fa fa-align-right icon-align-right', {
  4195. title: lang.image.floatRight,
  4196. event: 'floatMe',
  4197. value: 'right'
  4198. });
  4199. var justifyButton = tplIconButton('fa fa-align-justify icon-align-justify', {
  4200. title: lang.image.floatNone,
  4201. event: 'floatMe',
  4202. value: 'none'
  4203. });
  4204. var roundedButton = tplIconButton('fa fa-square icon-unchecked', {
  4205. title: lang.image.shapeRounded,
  4206. event: 'imageShape',
  4207. value: 'img-rounded'
  4208. });
  4209. var circleButton = tplIconButton('fa fa-circle-o icon-circle-blank', {
  4210. title: lang.image.shapeCircle,
  4211. event: 'imageShape',
  4212. value: 'img-circle'
  4213. });
  4214. var thumbnailButton = tplIconButton('fa fa-picture-o icon-picture', {
  4215. title: lang.image.shapeThumbnail,
  4216. event: 'imageShape',
  4217. value: 'img-thumbnail'
  4218. });
  4219. var noneButton = tplIconButton('fa fa-times icon-times', {
  4220. title: lang.image.shapeNone,
  4221. event: 'imageShape',
  4222. value: ''
  4223. });
  4224. var removeButton = tplIconButton('fa fa-trash-o icon-trash', {
  4225. title: lang.image.remove,
  4226. event: 'removeMedia',
  4227. value: 'none'
  4228. });
  4229. var content = '<div class="btn-group">' + fullButton + halfButton + quarterButton + '</div>' +
  4230. '<div class="btn-group">' + leftButton + rightButton + justifyButton + '</div>' +
  4231. '<div class="btn-group">' + roundedButton + circleButton + thumbnailButton + noneButton + '</div>' +
  4232. '<div class="btn-group">' + removeButton + '</div>';
  4233. return tplPopover('note-image-popover', content);
  4234. };
  4235. var tplAirPopover = function () {
  4236. var content = '';
  4237. for (var idx = 0, len = options.airPopover.length; idx < len; idx ++) {
  4238. var group = options.airPopover[idx];
  4239. content += '<div class="note-' + group[0] + ' btn-group">';
  4240. for (var i = 0, lenGroup = group[1].length; i < lenGroup; i++) {
  4241. content += tplButtonInfo[group[1][i]](lang, options);
  4242. }
  4243. content += '</div>';
  4244. }
  4245. return tplPopover('note-air-popover', content);
  4246. };
  4247. return '<div class="note-popover">' +
  4248. tplLinkPopover() +
  4249. tplImagePopover() +
  4250. (options.airMode ? tplAirPopover() : '') +
  4251. '</div>';
  4252. };
  4253. var tplHandles = function () {
  4254. return '<div class="note-handle">' +
  4255. '<div class="note-control-selection">' +
  4256. '<div class="note-control-selection-bg"></div>' +
  4257. '<div class="note-control-holder note-control-nw"></div>' +
  4258. '<div class="note-control-holder note-control-ne"></div>' +
  4259. '<div class="note-control-holder note-control-sw"></div>' +
  4260. '<div class="note-control-sizing note-control-se"></div>' +
  4261. '<div class="note-control-selection-info"></div>' +
  4262. '</div>' +
  4263. '</div>';
  4264. };
  4265. /**
  4266. * shortcut table template
  4267. * @param {String} title
  4268. * @param {String} body
  4269. */
  4270. var tplShortcut = function (title, body) {
  4271. return '<table class="note-shortcut">' +
  4272. '<thead>' +
  4273. '<tr><th></th><th>' + title + '</th></tr>' +
  4274. '</thead>' +
  4275. '<tbody>' + body + '</tbody>' +
  4276. '</table>';
  4277. };
  4278. var tplShortcutText = function (lang) {
  4279. var body = '<tr><td>⌘ + B</td><td>' + lang.font.bold + '</td></tr>' +
  4280. '<tr><td>⌘ + I</td><td>' + lang.font.italic + '</td></tr>' +
  4281. '<tr><td>⌘ + U</td><td>' + lang.font.underline + '</td></tr>' +
  4282. '<tr><td>⌘ + ⇧ + S</td><td>' + lang.font.strikethrough + '</td></tr>' +
  4283. '<tr><td>⌘ + \\</td><td>' + lang.font.clear + '</td></tr>';
  4284. return tplShortcut(lang.shortcut.textFormatting, body);
  4285. };
  4286. var tplShortcutAction = function (lang) {
  4287. var body = '<tr><td>⌘ + Z</td><td>' + lang.history.undo + '</td></tr>' +
  4288. '<tr><td>⌘ + ⇧ + Z</td><td>' + lang.history.redo + '</td></tr>' +
  4289. '<tr><td>⌘ + ]</td><td>' + lang.paragraph.indent + '</td></tr>' +
  4290. '<tr><td>⌘ + [</td><td>' + lang.paragraph.outdent + '</td></tr>' +
  4291. '<tr><td>⌘ + ENTER</td><td>' + lang.hr.insert + '</td></tr>';
  4292. return tplShortcut(lang.shortcut.action, body);
  4293. };
  4294. var tplShortcutPara = function (lang) {
  4295. var body = '<tr><td>⌘ + ⇧ + L</td><td>' + lang.paragraph.left + '</td></tr>' +
  4296. '<tr><td>⌘ + ⇧ + E</td><td>' + lang.paragraph.center + '</td></tr>' +
  4297. '<tr><td>⌘ + ⇧ + R</td><td>' + lang.paragraph.right + '</td></tr>' +
  4298. '<tr><td>⌘ + ⇧ + J</td><td>' + lang.paragraph.justify + '</td></tr>' +
  4299. '<tr><td>⌘ + ⇧ + NUM7</td><td>' + lang.lists.ordered + '</td></tr>' +
  4300. '<tr><td>⌘ + ⇧ + NUM8</td><td>' + lang.lists.unordered + '</td></tr>';
  4301. return tplShortcut(lang.shortcut.paragraphFormatting, body);
  4302. };
  4303. var tplShortcutStyle = function (lang) {
  4304. var body = '<tr><td>⌘ + NUM0</td><td>' + lang.style.normal + '</td></tr>' +
  4305. '<tr><td>⌘ + NUM1</td><td>' + lang.style.h1 + '</td></tr>' +
  4306. '<tr><td>⌘ + NUM2</td><td>' + lang.style.h2 + '</td></tr>' +
  4307. '<tr><td>⌘ + NUM3</td><td>' + lang.style.h3 + '</td></tr>' +
  4308. '<tr><td>⌘ + NUM4</td><td>' + lang.style.h4 + '</td></tr>' +
  4309. '<tr><td>⌘ + NUM5</td><td>' + lang.style.h5 + '</td></tr>' +
  4310. '<tr><td>⌘ + NUM6</td><td>' + lang.style.h6 + '</td></tr>';
  4311. return tplShortcut(lang.shortcut.documentStyle, body);
  4312. };
  4313. var tplExtraShortcuts = function (lang, options) {
  4314. var extraKeys = options.extraKeys;
  4315. var body = '';
  4316. for (var key in extraKeys) {
  4317. if (extraKeys.hasOwnProperty(key)) {
  4318. body += '<tr><td>' + key + '</td><td>' + extraKeys[key] + '</td></tr>';
  4319. }
  4320. }
  4321. return tplShortcut(lang.shortcut.extraKeys, body);
  4322. };
  4323. var tplShortcutTable = function (lang, options) {
  4324. var template = '<table class="note-shortcut-layout">' +
  4325. '<tbody>' +
  4326. '<tr><td>' + tplShortcutAction(lang, options) + '</td><td>' + tplShortcutText(lang, options) + '</td></tr>' +
  4327. '<tr><td>' + tplShortcutStyle(lang, options) + '</td><td>' + tplShortcutPara(lang, options) + '</td></tr>';
  4328. if (options.extraKeys) {
  4329. template += '<tr><td colspan="2">' + tplExtraShortcuts(lang, options) + '</td></tr>';
  4330. }
  4331. template += '</tbody></table>';
  4332. return template;
  4333. };
  4334. var replaceMacKeys = function (sHtml) {
  4335. return sHtml.replace(/⌘/g, 'Ctrl').replace(/⇧/g, 'Shift');
  4336. };
  4337. var tplDialogs = function (lang, options) {
  4338. var tplImageDialog = function () {
  4339. var body =
  4340. '<div class="note-group-select-from-files">' +
  4341. '<h5>' + lang.image.selectFromFiles + '</h5>' +
  4342. '<input class="note-image-input" type="file" name="files" accept="image/*" />' +
  4343. '</div>' +
  4344. '<h5>' + lang.image.url + '</h5>' +
  4345. '<input class="note-image-url form-control span12" type="text" />';
  4346. var footer = '<button href="#" class="btn btn-primary note-image-btn disabled" disabled>' + lang.image.insert + '</button>';
  4347. return tplDialog('note-image-dialog', lang.image.insert, body, footer);
  4348. };
  4349. var tplLinkDialog = function () {
  4350. var body = '<div class="form-group">' +
  4351. '<label>' + lang.link.textToDisplay + '</label>' +
  4352. '<input class="note-link-text form-control span12" type="text" />' +
  4353. '</div>' +
  4354. '<div class="form-group">' +
  4355. '<label>' + lang.link.url + '</label>' +
  4356. '<input class="note-link-url form-control span12" type="text" />' +
  4357. '</div>' +
  4358. (!options.disableLinkTarget ?
  4359. '<div class="checkbox">' +
  4360. '<label>' + '<input type="checkbox" checked> ' +
  4361. lang.link.openInNewWindow +
  4362. '</label>' +
  4363. '</div>' : ''
  4364. );
  4365. var footer = '<button href="#" class="btn btn-primary note-link-btn disabled" disabled>' + lang.link.insert + '</button>';
  4366. return tplDialog('note-link-dialog', lang.link.insert, body, footer);
  4367. };
  4368. var tplVideoDialog = function () {
  4369. var body = '<div class="form-group">' +
  4370. '<label>' + lang.video.url + '</label>&nbsp;<small class="text-muted">' + lang.video.providers + '</small>' +
  4371. '<input class="note-video-url form-control span12" type="text" />' +
  4372. '</div>';
  4373. var footer = '<button href="#" class="btn btn-primary note-video-btn disabled" disabled>' + lang.video.insert + '</button>';
  4374. return tplDialog('note-video-dialog', lang.video.insert, body, footer);
  4375. };
  4376. var tplHelpDialog = function () {
  4377. var body = '<a class="modal-close pull-right" aria-hidden="true" tabindex="-1">' + lang.shortcut.close + '</a>' +
  4378. '<div class="title">' + lang.shortcut.shortcuts + '</div>' +
  4379. (agent.isMac ? tplShortcutTable(lang, options) : replaceMacKeys(tplShortcutTable(lang, options))) +
  4380. '<p class="text-center">' +
  4381. '<a href="//hackerwins.github.io/summernote/" target="_blank">Summernote 0.5.10</a> · ' +
  4382. '<a href="//github.com/HackerWins/summernote" target="_blank">Project</a> · ' +
  4383. '<a href="//github.com/HackerWins/summernote/issues" target="_blank">Issues</a>' +
  4384. '</p>';
  4385. return tplDialog('note-help-dialog', '', body, '');
  4386. };
  4387. return '<div class="note-dialog">' +
  4388. tplImageDialog() +
  4389. tplLinkDialog() +
  4390. tplVideoDialog() +
  4391. tplHelpDialog() +
  4392. '</div>';
  4393. };
  4394. var tplStatusbar = function () {
  4395. return '<div class="note-resizebar">' +
  4396. '<div class="note-icon-bar"></div>' +
  4397. '<div class="note-icon-bar"></div>' +
  4398. '<div class="note-icon-bar"></div>' +
  4399. '</div>';
  4400. };
  4401. var representShortcut = function (str) {
  4402. if (agent.isMac) {
  4403. str = str.replace('CMD', '⌘').replace('SHIFT', '⇧');
  4404. }
  4405. return str.replace('BACKSLASH', '\\')
  4406. .replace('SLASH', '/')
  4407. .replace('LEFTBRACKET', '[')
  4408. .replace('RIGHTBRACKET', ']');
  4409. };
  4410. /**
  4411. * createTooltip
  4412. *
  4413. * @param {jQuery} $container
  4414. * @param {Object} keyMap
  4415. * @param {String} [sPlacement]
  4416. */
  4417. var createTooltip = function ($container, keyMap, sPlacement) {
  4418. var invertedKeyMap = func.invertObject(keyMap);
  4419. var $buttons = $container.find('button');
  4420. $buttons.each(function (i, elBtn) {
  4421. var $btn = $(elBtn);
  4422. var sShortcut = invertedKeyMap[$btn.data('event')];
  4423. if (sShortcut) {
  4424. $btn.attr('title', function (i, v) {
  4425. return v + ' (' + representShortcut(sShortcut) + ')';
  4426. });
  4427. }
  4428. // bootstrap tooltip on btn-group bug
  4429. // https://github.com/twbs/bootstrap/issues/5687
  4430. }).tooltip({
  4431. container: 'body',
  4432. trigger: 'hover',
  4433. placement: sPlacement || 'top'
  4434. }).on('click', function () {
  4435. $(this).tooltip('hide');
  4436. });
  4437. };
  4438. // createPalette
  4439. var createPalette = function ($container, options) {
  4440. var colorInfo = options.colors;
  4441. $container.find('.note-color-palette').each(function () {
  4442. var $palette = $(this), eventName = $palette.attr('data-target-event');
  4443. var paletteContents = [];
  4444. for (var row = 0, lenRow = colorInfo.length; row < lenRow; row++) {
  4445. var colors = colorInfo[row];
  4446. var buttons = [];
  4447. for (var col = 0, lenCol = colors.length; col < lenCol; col++) {
  4448. var color = colors[col];
  4449. buttons.push(['<button type="button" class="note-color-btn" style="background-color:', color,
  4450. ';" data-event="', eventName,
  4451. '" data-value="', color,
  4452. '" title="', color,
  4453. '" data-toggle="button" tabindex="-1"></button>'].join(''));
  4454. }
  4455. paletteContents.push('<div class="note-color-row">' + buttons.join('') + '</div>');
  4456. }
  4457. $palette.html(paletteContents.join(''));
  4458. });
  4459. };
  4460. /**
  4461. * create summernote layout (air mode)
  4462. *
  4463. * @param {jQuery} $holder
  4464. * @param {Object} options
  4465. */
  4466. this.createLayoutByAirMode = function ($holder, options) {
  4467. var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc'];
  4468. var langInfo = $.extend($.summernote.lang['en-US'], $.summernote.lang[options.lang]);
  4469. var id = func.uniqueId();
  4470. $holder.addClass('note-air-editor note-editable');
  4471. $holder.attr({
  4472. 'id': 'note-editor-' + id,
  4473. 'contentEditable': true
  4474. });
  4475. var body = document.body;
  4476. // create Popover
  4477. var $popover = $(tplPopovers(langInfo, options));
  4478. $popover.addClass('note-air-layout');
  4479. $popover.attr('id', 'note-popover-' + id);
  4480. $popover.appendTo(body);
  4481. createTooltip($popover, keyMap);
  4482. createPalette($popover, options);
  4483. // create Handle
  4484. var $handle = $(tplHandles());
  4485. $handle.addClass('note-air-layout');
  4486. $handle.attr('id', 'note-handle-' + id);
  4487. $handle.appendTo(body);
  4488. // create Dialog
  4489. var $dialog = $(tplDialogs(langInfo, options));
  4490. $dialog.addClass('note-air-layout');
  4491. $dialog.attr('id', 'note-dialog-' + id);
  4492. $dialog.find('button.close, a.modal-close').click(function () {
  4493. $(this).closest('.modal').modal('hide');
  4494. });
  4495. $dialog.appendTo(body);
  4496. };
  4497. /**
  4498. * create summernote layout (normal mode)
  4499. *
  4500. * @param {jQuery} $holder
  4501. * @param {Object} options
  4502. */
  4503. this.createLayoutByFrame = function ($holder, options) {
  4504. //01. create Editor
  4505. var $editor = $('<div class="note-editor"></div>');
  4506. if (options.width) {
  4507. $editor.width(options.width);
  4508. }
  4509. //02. statusbar (resizebar)
  4510. if (options.height > 0) {
  4511. $('<div class="note-statusbar">' + (options.disableResizeEditor ? '' : tplStatusbar()) + '</div>').prependTo($editor);
  4512. }
  4513. //03. create Editable
  4514. var isContentEditable = !$holder.is(':disabled');
  4515. var $editable = $('<div class="note-editable" contentEditable="' + isContentEditable + '"></div>')
  4516. .prependTo($editor);
  4517. if (options.height) {
  4518. $editable.height(options.height);
  4519. }
  4520. if (options.direction) {
  4521. $editable.attr('dir', options.direction);
  4522. }
  4523. $editable.html(dom.html($holder) || dom.emptyPara);
  4524. //031. create codable
  4525. $('<textarea class="note-codable"></textarea>').prependTo($editor);
  4526. var langInfo = $.extend($.summernote.lang['en-US'], $.summernote.lang[options.lang]);
  4527. //04. create Toolbar
  4528. var toolbarHTML = '';
  4529. for (var idx = 0, len = options.toolbar.length; idx < len; idx ++) {
  4530. var groupName = options.toolbar[idx][0];
  4531. var groupButtons = options.toolbar[idx][1];
  4532. toolbarHTML += '<div class="note-' + groupName + ' btn-group">';
  4533. for (var i = 0, btnLength = groupButtons.length; i < btnLength; i++) {
  4534. // continue creating toolbar even if a button doesn't exist
  4535. if (!$.isFunction(tplButtonInfo[groupButtons[i]])) { continue; }
  4536. toolbarHTML += tplButtonInfo[groupButtons[i]](langInfo, options);
  4537. }
  4538. toolbarHTML += '</div>';
  4539. }
  4540. toolbarHTML = '<div class="note-toolbar btn-toolbar">' + toolbarHTML + '</div>';
  4541. var $toolbar = $(toolbarHTML).prependTo($editor);
  4542. var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc'];
  4543. createPalette($toolbar, options);
  4544. createTooltip($toolbar, keyMap, 'bottom');
  4545. //05. create Popover
  4546. var $popover = $(tplPopovers(langInfo, options)).prependTo($editor);
  4547. createPalette($popover, options);
  4548. createTooltip($popover, keyMap);
  4549. //06. handle(control selection, ...)
  4550. $(tplHandles()).prependTo($editor);
  4551. //07. create Dialog
  4552. var $dialog = $(tplDialogs(langInfo, options)).prependTo($editor);
  4553. $dialog.find('button.close, a.modal-close').click(function () {
  4554. $(this).closest('.modal').modal('hide');
  4555. });
  4556. //08. create Dropzone
  4557. $('<div class="note-dropzone"><div class="note-dropzone-message"></div></div>').prependTo($editor);
  4558. //09. Editor/Holder switch
  4559. $editor.insertAfter($holder);
  4560. $holder.hide();
  4561. };
  4562. this.noteEditorFromHolder = function ($holder) {
  4563. if ($holder.hasClass('note-air-editor')) {
  4564. return $holder;
  4565. } else if ($holder.next().hasClass('note-editor')) {
  4566. return $holder.next();
  4567. } else {
  4568. return $();
  4569. }
  4570. };
  4571. /**
  4572. * create summernote layout
  4573. *
  4574. * @param {jQuery} $holder
  4575. * @param {Object} options
  4576. */
  4577. this.createLayout = function ($holder, options) {
  4578. if (this.noteEditorFromHolder($holder).length) {
  4579. return;
  4580. }
  4581. if (options.airMode) {
  4582. this.createLayoutByAirMode($holder, options);
  4583. } else {
  4584. this.createLayoutByFrame($holder, options);
  4585. }
  4586. };
  4587. /**
  4588. * returns layoutInfo from holder
  4589. *
  4590. * @param {jQuery} $holder - placeholder
  4591. * @returns {Object}
  4592. */
  4593. this.layoutInfoFromHolder = function ($holder) {
  4594. var $editor = this.noteEditorFromHolder($holder);
  4595. if (!$editor.length) { return; }
  4596. var layoutInfo = dom.buildLayoutInfo($editor);
  4597. // cache all properties.
  4598. for (var key in layoutInfo) {
  4599. if (layoutInfo.hasOwnProperty(key)) {
  4600. layoutInfo[key] = layoutInfo[key].call();
  4601. }
  4602. }
  4603. return layoutInfo;
  4604. };
  4605. /**
  4606. * removeLayout
  4607. *
  4608. * @param {jQuery} $holder - placeholder
  4609. * @param {Object} layoutInfo
  4610. * @param {Object} options
  4611. *
  4612. */
  4613. this.removeLayout = function ($holder, layoutInfo, options) {
  4614. if (options.airMode) {
  4615. $holder.removeClass('note-air-editor note-editable')
  4616. .removeAttr('id contentEditable');
  4617. layoutInfo.popover.remove();
  4618. layoutInfo.handle.remove();
  4619. layoutInfo.dialog.remove();
  4620. } else {
  4621. $holder.html(layoutInfo.editable.html());
  4622. layoutInfo.editor.remove();
  4623. $holder.show();
  4624. }
  4625. };
  4626. };
  4627. // jQuery namespace for summernote
  4628. $.summernote = $.summernote || {};
  4629. // extends default `settings`
  4630. $.extend($.summernote, settings);
  4631. var renderer = new Renderer();
  4632. var eventHandler = new EventHandler();
  4633. /**
  4634. * extend jquery fn
  4635. */
  4636. $.fn.extend({
  4637. /**
  4638. * initialize summernote
  4639. * - create editor layout and attach Mouse and keyboard events.
  4640. *
  4641. * @param {Object} options
  4642. * @returns {this}
  4643. */
  4644. summernote: function (options) {
  4645. // extend default options
  4646. options = $.extend({}, $.summernote.options, options);
  4647. this.each(function (idx, elHolder) {
  4648. var $holder = $(elHolder);
  4649. // createLayout with options
  4650. renderer.createLayout($holder, options);
  4651. var info = renderer.layoutInfoFromHolder($holder);
  4652. eventHandler.attach(info, options);
  4653. // Textarea: auto filling the code before form submit.
  4654. if (dom.isTextarea($holder[0])) {
  4655. $holder.closest('form').submit(function () {
  4656. $holder.val($holder.code());
  4657. });
  4658. }
  4659. });
  4660. // focus on first editable element
  4661. if (this.first().length && options.focus) {
  4662. var info = renderer.layoutInfoFromHolder(this.first());
  4663. info.editable.focus();
  4664. }
  4665. // callback on init
  4666. if (this.length && options.oninit) {
  4667. options.oninit();
  4668. }
  4669. return this;
  4670. },
  4671. //
  4672. /**
  4673. * get the HTML contents of note or set the HTML contents of note.
  4674. *
  4675. * @param {String} [sHTML] - HTML contents(optional, set)
  4676. * @returns {this|String} - context(set) or HTML contents of note(get).
  4677. */
  4678. code: function (sHTML) {
  4679. // get the HTML contents of note
  4680. if (sHTML === undefined) {
  4681. var $holder = this.first();
  4682. if (!$holder.length) { return; }
  4683. var info = renderer.layoutInfoFromHolder($holder);
  4684. if (!!(info && info.editable)) {
  4685. var isCodeview = info.editor.hasClass('codeview');
  4686. if (isCodeview && agent.hasCodeMirror) {
  4687. info.codable.data('cmEditor').save();
  4688. }
  4689. return isCodeview ? info.codable.val() : info.editable.html();
  4690. }
  4691. return dom.isTextarea($holder[0]) ? $holder.val() : $holder.html();
  4692. }
  4693. // set the HTML contents of note
  4694. this.each(function (i, elHolder) {
  4695. var info = renderer.layoutInfoFromHolder($(elHolder));
  4696. if (info && info.editable) { info.editable.html(sHTML); }
  4697. });
  4698. return this;
  4699. },
  4700. /**
  4701. * destroy Editor Layout and dettach Key and Mouse Event
  4702. * @returns {this}
  4703. */
  4704. destroy: function () {
  4705. this.each(function (idx, elHolder) {
  4706. var $holder = $(elHolder);
  4707. var info = renderer.layoutInfoFromHolder($holder);
  4708. if (!info || !info.editable) { return; }
  4709. var options = info.editor.data('options');
  4710. eventHandler.dettach(info, options);
  4711. renderer.removeLayout($holder, info, options);
  4712. });
  4713. return this;
  4714. }
  4715. });
  4716. }));