jquery.sceditor.js 72 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741
  1. /**
  2. * SCEditor v1.3.4
  3. * http://www.samclarke.com/2011/07/sceditor/
  4. *
  5. * Copyright (C) 2011-2012, Sam Clarke (samclarke.com)
  6. *
  7. * SCEditor is licensed under the MIT license:
  8. * http://www.opensource.org/licenses/mit-license.php
  9. */
  10. // ==ClosureCompiler==
  11. // @output_file_name jquery.sceditor.min.js
  12. // @compilation_level SIMPLE_OPTIMIZATIONS
  13. // ==/ClosureCompiler==
  14. /*jshint smarttabs: true, scripturl: true, jquery: true, devel:true, eqnull:true, curly: false */
  15. /*global XMLSerializer: true*/
  16. (function ($) {
  17. 'use strict';
  18. $.sceditor = function (el, options) {
  19. var base = this;
  20. /**
  21. * The textarea element being replaced
  22. * @private
  23. */
  24. var $textarea = $(el);
  25. var textarea = el;
  26. /**
  27. * The div which contains the editor and toolbar
  28. * @private
  29. */
  30. var editorContainer = null;
  31. /**
  32. * The editors toolbar
  33. * @private
  34. */
  35. var $toolbar = null;
  36. /**
  37. * The editors iframe which should be in design mode
  38. * @private
  39. */
  40. var $wysiwygEditor = null;
  41. var wysiwygEditor = null;
  42. /**
  43. * The editors textarea for viewing source
  44. * @private
  45. */
  46. var $textEditor = null;
  47. var textEditor = null;
  48. /**
  49. * The current dropdown
  50. * @private
  51. */
  52. var $dropdown = null;
  53. var dropdownIgnoreLastClick = false;
  54. /**
  55. * Array of all the commands key press functions
  56. * @private
  57. */
  58. var keyPressFuncs = [];
  59. /**
  60. * Store the last cursor position. Needed for IE because it forgets
  61. * @private
  62. */
  63. var lastRange = null;
  64. /**
  65. * The editors locale
  66. * @private
  67. */
  68. var locale = null;
  69. /**
  70. * Stores a cache of preloaded images
  71. * @private
  72. */
  73. var preLoadCache = [];
  74. var rangeHelper = null;
  75. var init,
  76. replaceEmoticons,
  77. handleCommand,
  78. saveRange,
  79. handlePasteEvt,
  80. handlePasteData,
  81. handleKeyPress,
  82. handleKeyUp,
  83. handleMouseDown,
  84. initEditor,
  85. initToolBar,
  86. initKeyPressFuncs,
  87. initResize,
  88. documentClickHandler,
  89. formSubmitHandler,
  90. preLoadEmoticons,
  91. getWysiwygDoc,
  92. handleWindowResize,
  93. setHeight,
  94. setWidth,
  95. initLocale;
  96. /**
  97. * All the commands supported by the editor
  98. */
  99. base.commands = $.extend({}, (options.commands || $.sceditor.commands));
  100. /**
  101. * Initializer. Creates the editor iframe and textarea
  102. * @private
  103. */
  104. init = function () {
  105. $textarea.data("sceditor", base);
  106. base.options = $.extend({}, $.sceditor.defaultOptions, options);
  107. // Load locale
  108. if(base.options.locale !== null && base.options.locale !== "en")
  109. initLocale();
  110. if(base.options.height !== null)
  111. $textarea.height(base.options.height);
  112. if(base.options.width !== null)
  113. $textarea.width(base.options.width);
  114. // if either width or height are % based, add the resize handler to update the editor
  115. // when the window is resized
  116. if((base.options.height !== null && base.options.height.toString().indexOf("%") > -1) ||
  117. (base.options.width !== null && base.options.width.toString().indexOf("%") > -1))
  118. $(window).resize(handleWindowResize);
  119. editorContainer = $('<div class="sceditor-container" />')
  120. .width($textarea.outerWidth())
  121. .height($textarea.outerHeight());
  122. $textarea.after(editorContainer);
  123. // create the editor
  124. initToolBar();
  125. initEditor();
  126. initKeyPressFuncs();
  127. if(base.options.resizeEnabled)
  128. initResize();
  129. $(document).click(documentClickHandler);
  130. (textarea.form ? $(textarea.form) : $textarea.parents("form"))
  131. .attr('novalidate','novalidate')
  132. .submit(formSubmitHandler);
  133. // prefix emoticon root to emoticon urls
  134. if(base.options.emoticonsRoot && base.options.emoticons)
  135. {
  136. $.each(base.options.emoticons, function (idx, emoticons) {
  137. $.each(emoticons, function (key, url) {
  138. base.options.emoticons[idx][key] = base.options.emoticonsRoot + url;
  139. });
  140. });
  141. }
  142. // load any textarea value into the editor
  143. var val = $textarea.hide().val();
  144. // Pass the value though the getTextHandler if it is set so that
  145. // BBCode, ect. can be converted
  146. if(base.options.getTextHandler && base.options.supportedWysiwyg)
  147. {
  148. val = base.options.getTextHandler(val);
  149. base.setWysiwygEditorValue(val);
  150. }
  151. else
  152. {
  153. base.toggleTextMode();
  154. base.setTextareaValue(val);
  155. }
  156. if(base.options.toolbar.indexOf('emoticon') !== -1)
  157. preLoadEmoticons();
  158. };
  159. /**
  160. * Creates the editor iframe and textarea
  161. * @private
  162. */
  163. initEditor = function () {
  164. var contentEditable = $('<div contenteditable="true">')[0].contentEditable,
  165. contentEditableSupported = typeof contentEditable !== 'undefined' && contentEditable !== 'inherit',
  166. $doc, $body;
  167. $textEditor = $('<textarea></textarea>').hide();
  168. $wysiwygEditor = $('<iframe frameborder="0"></iframe>');
  169. if(window.location.protocol === "https:")
  170. $wysiwygEditor.attr("src", "javascript:false");
  171. // add the editor to the HTML and store the editors element
  172. editorContainer.append($wysiwygEditor).append($textEditor);
  173. wysiwygEditor = $wysiwygEditor[0];
  174. textEditor = $textEditor[0];
  175. setWidth($textarea.width());
  176. setHeight($textarea.height());
  177. // turn on design mode if contenteditable not supported
  178. if(!contentEditableSupported)
  179. getWysiwygDoc().designMode = 'On';
  180. getWysiwygDoc().open();
  181. getWysiwygDoc().write(
  182. '<html><head><!--[if gte IE 9]><style>* {min-height: auto !important}</style><![endif]-->' +
  183. '<meta http-equiv="Content-Type" content="text/html;charset=' + base.options.charset + '" />' +
  184. '<link rel="stylesheet" type="text/css" href="' + base.options.style + '" />' +
  185. '</head><body contenteditable="true"></body></html>'
  186. );
  187. getWysiwygDoc().close();
  188. // turn on design mode if contenteditable not supported
  189. if(!contentEditableSupported)
  190. getWysiwygDoc().designMode = 'On';
  191. $doc = $(getWysiwygDoc());
  192. $body = $doc.find("body");
  193. // set the key press event
  194. $body.keypress(handleKeyPress);
  195. $body.keyup(handleKeyUp);
  196. $doc.keypress(handleKeyPress);
  197. $doc.keyup(handleKeyUp);
  198. $doc.mousedown(handleMouseDown);
  199. $doc.bind("beforedeactivate keyup", saveRange);
  200. $doc.focus(function() {
  201. lastRange = null;
  202. });
  203. if(base.options.enablePasteFiltering)
  204. $body.bind("paste", handlePasteEvt);
  205. rangeHelper = new $.sceditor.rangeHelper(wysiwygEditor.contentWindow);
  206. };
  207. /**
  208. * Creates the toolbar and appends it to the container
  209. * @private
  210. */
  211. initToolBar = function () {
  212. var buttonClick = function (e) {
  213. handleCommand($(this), base.commands[$(this).data("sceditor-command")]);
  214. e.preventDefault();
  215. };
  216. $toolbar = $('<div class="sceditor-toolbar" />');
  217. var rows = base.options.toolbar.split("||");
  218. for (var r=0; r < rows.length; r++) {
  219. var row = $('<div class="sceditor-row" />');
  220. var groups = rows[r].split("|"),
  221. group, buttons, accessibilityName, button, i;
  222. for (i=0; i < groups.length; i++) {
  223. group = $('<div class="sceditor-group" />');
  224. buttons = groups[i].split(",");
  225. for (var x=0; x < buttons.length; x++) {
  226. // the button must be a valid command otherwise ignore it
  227. if(!base.commands.hasOwnProperty(buttons[x]))
  228. continue;
  229. accessibilityName = base.commands[buttons[x]].tooltip ? base._(base.commands[buttons[x]].tooltip) : buttons[x];
  230. button = $('<a class="sceditor-button sceditor-button-' + buttons[x] +
  231. ' " unselectable="on"><div unselectable="on">' + accessibilityName + '</div></a>');
  232. if(base.commands[buttons[x]].hasOwnProperty("tooltip"))
  233. button.attr('title', base._(base.commands[buttons[x]].tooltip));
  234. if(base.commands[buttons[x]].exec)
  235. button.data('sceditor-wysiwygmode', true);
  236. else
  237. button.addClass('disabled');
  238. if(base.commands[buttons[x]].txtExec)
  239. button.data('sceditor-txtmode', true);
  240. // add the click handler for the button
  241. button.data("sceditor-command", buttons[x]);
  242. button.click(buttonClick);
  243. group.append(button);
  244. }
  245. row.append(group);
  246. }
  247. $toolbar.append(row);
  248. }
  249. // append the toolbar to the toolbarContainer option if given
  250. if(base.options.toolbarContainer === null)
  251. editorContainer.append($toolbar);
  252. else
  253. $(base.options.toolbarContainer).append($toolbar);
  254. };
  255. /**
  256. * Creates an array of all the key press functions
  257. * like emoticons, ect.
  258. * @private
  259. */
  260. initKeyPressFuncs = function () {
  261. $.each(base.commands, function (command, values) {
  262. if(typeof values.keyPress !== "undefined")
  263. keyPressFuncs.push(values.keyPress);
  264. });
  265. };
  266. setWidth = function (width) {
  267. editorContainer.width(width);
  268. // fix the height and width of the textarea/iframe
  269. $wysiwygEditor.width(width);
  270. $wysiwygEditor.width(width + (width - $wysiwygEditor.outerWidth(true)));
  271. $textEditor.width(width);
  272. $textEditor.width(width + (width - $textEditor.outerWidth(true)));
  273. };
  274. setHeight = function (height) {
  275. editorContainer.height(height);
  276. height = height - (base.options.toolbarContainer === null?$toolbar.outerHeight():0);
  277. // fix the height and width of the textarea/iframe
  278. $wysiwygEditor.height(height);
  279. $wysiwygEditor.height(height + (height - $wysiwygEditor.outerHeight(true)));
  280. $textEditor.height(height);
  281. $textEditor.height(height + (height - $textEditor.outerHeight(true)));
  282. };
  283. /**
  284. * Creates the resizer.
  285. * @private
  286. */
  287. initResize = function () {
  288. var $grip = $('<div class="sceditor-grip" />'),
  289. // cover is used to cover the editor iframe so document still gets mouse move events
  290. $cover = $('<div class="sceditor-resize-cover" />'),
  291. startX = 0,
  292. startY = 0,
  293. startWidth = 0,
  294. startHeight = 0,
  295. origWidth = editorContainer.width(),
  296. origHeight = editorContainer.height(),
  297. dragging = false,
  298. minHeight, maxHeight, minWidth, maxWidth, mouseMoveFunc;
  299. minHeight = (base.options.resizeMinHeight == null ?
  300. origHeight / 2 :
  301. base.options.resizeMinHeight);
  302. maxHeight = (base.options.resizeMaxHeight == null ?
  303. origHeight * 2 :
  304. base.options.resizeMaxHeight);
  305. minWidth = (base.options.resizeMinWidth == null ?
  306. origWidth / 2 :
  307. base.options.resizeMinWidth);
  308. maxWidth = (base.options.resizeMaxWidth == null ?
  309. origWidth * 2 :
  310. base.options.resizeMaxWidth);
  311. mouseMoveFunc = function (e) {
  312. var newHeight = startHeight + (e.pageY - startY),
  313. newWidth = startWidth + (e.pageX - startX);
  314. if (newWidth >= minWidth && (maxWidth < 0 || newWidth <= maxWidth))
  315. setWidth(newWidth);
  316. if (newHeight >= minHeight && (maxHeight < 0 || newHeight <= maxHeight))
  317. setHeight(newHeight);
  318. e.preventDefault();
  319. };
  320. editorContainer.append($grip);
  321. editorContainer.append($cover.hide());
  322. $grip.mousedown(function (e) {
  323. startX = e.pageX;
  324. startY = e.pageY;
  325. startWidth = editorContainer.width();
  326. startHeight = editorContainer.height();
  327. dragging = true;
  328. $cover.show();
  329. $(document).bind('mousemove', mouseMoveFunc);
  330. e.preventDefault();
  331. });
  332. $(document).mouseup(function (e) {
  333. if(!dragging)
  334. return;
  335. dragging = false;
  336. $cover.hide();
  337. $(document).unbind('mousemove', mouseMoveFunc);
  338. e.preventDefault();
  339. });
  340. };
  341. formSubmitHandler = function(e) {
  342. base.updateTextareaValue();
  343. $(this).removeAttr('novalidate');
  344. if(this.checkValidity && !this.checkValidity())
  345. e.preventDefault();
  346. $(this).attr('novalidate','novalidate');
  347. };
  348. /**
  349. * Destroys the editor, removing all elements and
  350. * event handlers.
  351. */
  352. base.destory = function () {
  353. $(document).unbind('click', documentClickHandler);
  354. $textarea.removeAttr('novalidate').unbind('submit', formSubmitHandler);
  355. $(window).unbind('resize', handleWindowResize);
  356. editorContainer.remove();
  357. editorContainer = null;
  358. $textarea.removeData("sceditor").show();
  359. };
  360. /**
  361. * Preloads the emoticon images
  362. * Idea from: http://engineeredweb.com/blog/09/12/preloading-images-jquery-and-javascript
  363. * @private
  364. */
  365. preLoadEmoticons = function () {
  366. var emoticons = $.extend({}, base.options.emoticons.more, base.options.emoticons.dropdown, base.options.emoticons.hidden),
  367. emoticon;
  368. $.each(emoticons, function (key, url) {
  369. if (key == '')
  370. emoticon = document.createElement('br');
  371. else
  372. {
  373. emoticon = document.createElement('img');
  374. emoticon.src = url;
  375. }
  376. preLoadCache.push(emoticon);
  377. });
  378. };
  379. /**
  380. * Creates a menu item drop down
  381. * @param HTMLElement menuItem The button to align the drop down with
  382. * @param string dropDownName Used for styling the dropown, will be a class sceditor-dropDownName
  383. * @param string content The HTML content of the dropdown
  384. * @param bool ieUnselectable If to add the unselectable attribute to all the contents elements. Stops
  385. * IE from deselecting the text in the editor
  386. */
  387. base.createDropDown = function (menuItem, dropDownName, content, ieUnselectable) {
  388. base.closeDropDown();
  389. // IE needs unselectable attr to stop it from unselecting the text in the editor.
  390. // The editor can cope if IE does unselect the text it's just not nice.
  391. if(ieUnselectable !== false) {
  392. content = $(content);
  393. content.find(':not(input,textarea)').filter(function() { return this.nodeType===1; }).attr('unselectable', 'on');
  394. }
  395. var o_css = {
  396. top: menuItem.offset().top,
  397. left: menuItem.offset().left
  398. };
  399. $.extend(o_css, base.options.dropDownCss);
  400. $dropdown = $('<div class="sceditor-dropdown sceditor-' + dropDownName + '" />').css(o_css).append(content);
  401. //editorContainer.after($dropdown);
  402. $dropdown.appendTo($('body'));
  403. dropdownIgnoreLastClick = true;
  404. // stop clicks within the dropdown from being handled
  405. $dropdown.click(function (e) {
  406. e.stopPropagation();
  407. });
  408. };
  409. /**
  410. * Handles any document click and closes the dropdown if open
  411. * @private
  412. */
  413. documentClickHandler = function (e) {
  414. // ignore right clicks
  415. if(!dropdownIgnoreLastClick && e.which !== 3)
  416. base.closeDropDown();
  417. dropdownIgnoreLastClick = false;
  418. };
  419. handlePasteEvt = function(e) {
  420. var elm = getWysiwygDoc().body,
  421. checkCount = 0,
  422. pastearea = elm.ownerDocument.createElement('div'),
  423. prePasteContent = elm.ownerDocument.createDocumentFragment();
  424. rangeHelper.saveRange();
  425. document.body.appendChild(pastearea);
  426. if (e && e.clipboardData && e.clipboardData.getData)
  427. {
  428. var html, handled=true;
  429. if ((html = e.clipboardData.getData('text/html')) || (html = e.clipboardData.getData('text/plain')))
  430. pastearea.innerHTML = html;
  431. else
  432. handled = false;
  433. if(handled)
  434. {
  435. handlePasteData(elm, pastearea);
  436. if (e.preventDefault)
  437. {
  438. e.stopPropagation();
  439. e.preventDefault();
  440. }
  441. return false;
  442. }
  443. }
  444. while(elm.firstChild)
  445. prePasteContent.appendChild(elm.firstChild);
  446. function handlePaste(elm, pastearea) {
  447. if (elm.childNodes.length > 0)
  448. {
  449. while(elm.firstChild)
  450. pastearea.appendChild(elm.firstChild);
  451. while(prePasteContent.firstChild)
  452. elm.appendChild(prePasteContent.firstChild);
  453. handlePasteData(elm, pastearea);
  454. }
  455. else
  456. {
  457. // Allow max 25 checks before giving up.
  458. // Needed inscase empty input is posted or
  459. // something gose wrong.
  460. if(checkCount > 25)
  461. {
  462. while(prePasteContent.firstChild)
  463. elm.appendChild(prePasteContent.firstChild);
  464. return;
  465. }
  466. ++checkCount;
  467. setTimeout(function () {
  468. handlePaste(elm, pastearea);
  469. }, 20);
  470. }
  471. }
  472. handlePaste(elm, pastearea);
  473. base.focus();
  474. return true;
  475. };
  476. /**
  477. * @param {Element} elm
  478. * @param {Element} pastearea
  479. */
  480. handlePasteData = function(elm, pastearea) {
  481. // fix any invalid nesting
  482. $.sceditor.dom.fixNesting(pastearea);
  483. var pasteddata = pastearea.innerHTML;
  484. if(base.options.getHtmlHandler)
  485. pasteddata = base.options.getHtmlHandler(pasteddata, $(pastearea));
  486. pastearea.parentNode.removeChild(pastearea);
  487. if(base.options.getTextHandler)
  488. pasteddata = base.options.getTextHandler(pasteddata, true);
  489. rangeHelper.restoreRange();
  490. rangeHelper.insertHTML(pasteddata);
  491. };
  492. /**
  493. * Closes the current drop down
  494. *
  495. * @param bool focus If to focus the editor on close
  496. */
  497. base.closeDropDown = function (focus) {
  498. if($dropdown !== null) {
  499. $dropdown.remove();
  500. $dropdown = null;
  501. }
  502. if(focus === true)
  503. base.focus();
  504. };
  505. /**
  506. * Gets the WYSIWYG editors document
  507. */
  508. getWysiwygDoc = function () {
  509. if (wysiwygEditor.contentDocument)
  510. return wysiwygEditor.contentDocument;
  511. if (wysiwygEditor.contentWindow && wysiwygEditor.contentWindow.document)
  512. return wysiwygEditor.contentWindow.document;
  513. if (wysiwygEditor.document)
  514. return wysiwygEditor.document;
  515. return null;
  516. };
  517. /**
  518. * Inserts HTML into WYSIWYG editor. If endHtml is defined and some text is selected the
  519. * selected text will be put inbetween html and endHtml. If endHtml isn't defined and some
  520. * text is selected it will be replaced by the HTML
  521. *
  522. * The HTML can have only one root node, if it has more than one only the first will be used.
  523. * e.g. with: <b>test</b><i>test2</i> only <b>test</b> will be inserted. To fix this you could
  524. * do: <span><b>test</b><i>test2</i></span>
  525. *
  526. * @param string html The HTML to insert
  527. * @param string endHtml If specified instead of the inserted HTML replacing the selected text the selected text
  528. * will be placed between html and endHtml. If there is no selected text html and endHtml will
  529. * be concated together.
  530. */
  531. base.wysiwygEditorInsertHtml = function (html, endHtml, overrideCodeBlocking) {
  532. base.focus();
  533. // don't apply to code elements
  534. if(!overrideCodeBlocking && ($(rangeHelper.parentNode()).is('code') ||
  535. $(rangeHelper.parentNode()).parents('code').length !== 0))
  536. return;
  537. rangeHelper.insertHTML(html, endHtml);
  538. };
  539. /**
  540. * Like wysiwygEditorInsertHtml except it converts any HTML to text
  541. * @private
  542. */
  543. base.wysiwygEditorInsertText = function (text) {
  544. text = text.replace(/&/g, "&amp;")
  545. .replace(/</g, "&lt;")
  546. .replace(/>/g, "&gt;")
  547. .replace(/ /g, "&nbsp;")
  548. .replace(/\r\n|\r/g, "\n")
  549. .replace(/\n/g, "<br />");
  550. base.wysiwygEditorInsertHtml(text);
  551. };
  552. /**
  553. * Like wysiwygEditorInsertHtml but works on the
  554. * text editor instead
  555. *
  556. * @param {String} text
  557. * @param {String} endText
  558. */
  559. base.textEditorInsertText = function (text, endText) {
  560. var range, start, end, txtLen;
  561. textEditor.focus();
  562. if(textEditor.selectionStart != null)
  563. {
  564. start = textEditor.selectionStart;
  565. end = textEditor.selectionEnd;
  566. txtLen = text.length;
  567. if(endText)
  568. text += textEditor.value.substring(start, end) + endText;
  569. textEditor.value = textEditor.value.substring(0, start) + text + textEditor.value.substring(end, textEditor.value.length);
  570. if(endText)
  571. textEditor.selectionStart = (start + text.length) - endText.length;
  572. else
  573. textEditor.selectionStart = start + text.length;
  574. textEditor.selectionEnd = textEditor.selectionStart;
  575. }
  576. else if(document.selection.createRange)
  577. {
  578. range = document.selection.createRange();
  579. if(endText)
  580. text += range.text + endText;
  581. range.text = text;
  582. }
  583. else
  584. textEditor.value += text + endText;
  585. textEditor.focus();
  586. };
  587. /**
  588. * Gets the current rangeHelper instance
  589. */
  590. base.getRangeHelper = function () {
  591. return rangeHelper;
  592. };
  593. /**
  594. * Gets the WYSIWYG editors HTML which is between the body tags
  595. */
  596. base.getWysiwygEditorValue = function (filter) {
  597. if (!base.options.supportedWysiwyg)
  598. return;
  599. var $body = $wysiwygEditor.contents().find("body"),
  600. html;
  601. // fix any invalid nesting
  602. $.sceditor.dom.fixNesting($body.get(0));
  603. html = $body.html();
  604. if(filter !== false && base.options.getHtmlHandler)
  605. html = base.options.getHtmlHandler(html, $body);
  606. return html;
  607. };
  608. /**
  609. * Gets the text editor value
  610. * @param bool filter If to run the returned string through the filter or if to return the raw value. Defaults to filter.
  611. */
  612. base.getTextareaValue = function (filter) {
  613. var val = $textEditor.val();
  614. if(filter !== false && base.options.getTextHandler)
  615. val = base.options.getTextHandler(val);
  616. return val;
  617. };
  618. /**
  619. * Sets the WYSIWYG HTML editor value. Should only be the HTML contained within the body tags
  620. * @param bool filter If to run the returned string through the filter or if to return the raw value. Defaults to filter.
  621. */
  622. base.setWysiwygEditorValue = function (value) {
  623. getWysiwygDoc().body.innerHTML = replaceEmoticons(value);
  624. };
  625. /**
  626. * Sets the text editor value
  627. */
  628. base.setTextareaValue = function (value) {
  629. $textEditor.val(value);
  630. };
  631. /**
  632. * Updates the forms textarea value
  633. */
  634. base.updateTextareaValue = function () {
  635. if(base.inSourceMode())
  636. $textarea.val(base.getTextareaValue(false));
  637. else
  638. $textarea.val(base.getWysiwygEditorValue());
  639. };
  640. /**
  641. * Replaces any emoticon codes in the passed HTML with their emoticon images
  642. * @private
  643. */
  644. replaceEmoticons = function (html) {
  645. if(base.options.toolbar.indexOf('emoticon') === -1)
  646. return html;
  647. var emoticons = $.extend({}, base.options.emoticons.more, base.options.emoticons.dropdown, base.options.emoticons.hidden);
  648. $.each(emoticons, function (key, url) {
  649. if (key == '')
  650. return;
  651. // escape the key before using it as a regex
  652. // and append the regex to only find emoticons outside
  653. // of HTML tags
  654. var reg = $.sceditor.regexEscape(key) + "(?=([^\\<\\>]*?<(?!/code)|[^\\<\\>]*?$))",
  655. group = '';
  656. // Make sure the emoticon is surrounded by whitespace or is at the start/end of a string or html tag
  657. if(base.options.emoticonsCompat)
  658. {
  659. reg = "((>|^|\\s|\xA0|\u2002|\u2003|\u2009|&nbsp;))" + reg + "(?=(\\s|$|<|\xA0|\u2002|\u2003|\u2009|&nbsp;))";
  660. group = '$1';
  661. }
  662. html = html.replace(
  663. new RegExp(reg, 'gm'),
  664. group + '<img src="' + url + '" data-sceditor-emoticon="' + key + '" alt="' + key + '" />'
  665. );
  666. });
  667. return html;
  668. };
  669. /**
  670. * If the editor is in source code mode
  671. * @return boolean
  672. */
  673. base.inSourceMode = function () {
  674. return $textEditor.is(':visible');
  675. };
  676. /**
  677. * Switches between the WYSIWYG and plain text modes
  678. */
  679. base.toggleTextMode = function () {
  680. if(base.inSourceMode() && base.options.supportedWysiwyg)
  681. base.setWysiwygEditorValue(base.getTextareaValue());
  682. else
  683. base.setTextareaValue(base.getWysiwygEditorValue());
  684. // enable all the buttons
  685. $toolbar.find('.sceditor-button').removeClass('disabled');
  686. lastRange = null;
  687. $textEditor.toggle();
  688. $wysiwygEditor.toggle();
  689. // diable any buttons that are not allowed for this mode
  690. $toolbar.find('.sceditor-button').each(function () {
  691. var button = $(this);
  692. if(base.inSourceMode() && !button.data('sceditor-txtmode'))
  693. button.addClass('disabled');
  694. else if (!base.inSourceMode() && !button.data('sceditor-wysiwygmode'))
  695. button.addClass('disabled');
  696. });
  697. };
  698. /**
  699. * Handles the passed command
  700. * @private
  701. */
  702. handleCommand = function (caller, command) {
  703. // check if in text mode and handle text commands
  704. if(base.inSourceMode())
  705. {
  706. if(command.txtExec)
  707. {
  708. if($.isArray(command.txtExec))
  709. base.textEditorInsertText.apply(base, command.txtExec);
  710. else
  711. command.txtExec.call(base, caller);
  712. }
  713. return;
  714. }
  715. if(!command.hasOwnProperty("exec"))
  716. return;
  717. if($.isFunction(command.exec))
  718. command.exec.call(base, caller);
  719. else
  720. base.execCommand (command.exec, command.hasOwnProperty("execParam") ? command.execParam : null);
  721. };
  722. /**
  723. * Fucuses the editors input area
  724. */
  725. base.focus = function () {
  726. if(!base.inSourceMode())
  727. {
  728. wysiwygEditor.contentWindow.focus();
  729. // Needed for IE < 9
  730. if(lastRange !== null) {
  731. rangeHelper.selectRange(lastRange);
  732. // remove the stored range after being set.
  733. // If the editor loses focus it should be
  734. // saved again.
  735. lastRange = null;
  736. }
  737. }
  738. else
  739. textEditor.focus();
  740. };
  741. /**
  742. * Saves the current range. Needed for IE because it forgets
  743. * where the cursor was and what was selected
  744. * @private
  745. */
  746. saveRange = function () {
  747. /* this is only needed for IE */
  748. if(!$.browser.msie)
  749. return;
  750. lastRange = rangeHelper.selectedRange();
  751. };
  752. /**
  753. * Executes a command on the WYSIWYG editor
  754. *
  755. * @param string|function command
  756. * @param mixed param
  757. */
  758. base.execCommand = function (command, param) {
  759. var executed = false;
  760. base.focus();
  761. // don't apply any comannds to code elements
  762. if($(rangeHelper.parentNode()).is('code') ||
  763. $(rangeHelper.parentNode()).parents('code').length !== 0)
  764. return;
  765. if(getWysiwygDoc())
  766. {
  767. try
  768. {
  769. executed = getWysiwygDoc().execCommand(command, false, param);
  770. }
  771. catch (e){}
  772. }
  773. // show error if execution failed and an error message exists
  774. if(!executed && typeof base.commands[command] !== "undefined" &&
  775. typeof base.commands[command].errorMessage !== "undefined")
  776. alert(base._(base.commands[command].errorMessage));
  777. };
  778. /**
  779. * Handles any key press in the WYSIWYG editor
  780. *
  781. * @private
  782. */
  783. handleKeyPress = function (e) {
  784. base.closeDropDown();
  785. var selectedContainer = rangeHelper.parentNode(),
  786. $selectedContainer = $(selectedContainer);
  787. // "Fix" (ok it's a hack) for blocklevel elements being duplicated in some browsers when
  788. // enter is pressed instead of inserting a newline
  789. if(e.which === 13)
  790. {
  791. if($selectedContainer.is('code, blockquote') || $selectedContainer.parents('code, blockquote').length !== 0)
  792. {
  793. lastRange = null;
  794. base.wysiwygEditorInsertHtml('<br />', null, true);
  795. return false;
  796. }
  797. }
  798. // make sure there is always a newline after code or quote tags
  799. var d = getWysiwygDoc();
  800. $.sceditor.dom.rTraverse(d.body, function(node) {
  801. if((node.nodeType === 3 && node.nodeValue !== "") ||
  802. node.nodeName.toLowerCase() === 'br') {
  803. // this is the last text or br node, if its in a code or quote tag
  804. // then add a newline after it
  805. if($(node).parents('code, blockquote').length > 0)
  806. $(d.body).append(d.createElement('br'));
  807. return false;
  808. }
  809. }, true);
  810. // don't apply to code elements
  811. if($selectedContainer.is('code') || $selectedContainer.parents('code').length !== 0)
  812. return;
  813. var i = keyPressFuncs.length;
  814. while(i--)
  815. keyPressFuncs[i].call(base, e, wysiwygEditor, $textEditor);
  816. };
  817. handleKeyUp = function (e) {
  818. };
  819. /**
  820. * Handles any mousedown press in the WYSIWYG editor
  821. * @private
  822. */
  823. handleMouseDown = function (e) {
  824. base.closeDropDown();
  825. lastRange = null;
  826. };
  827. /**
  828. * Handles the window resize event. Needed to resize then editor
  829. * when the window size changes in fluid deisgns.
  830. */
  831. handleWindowResize = function () {
  832. if(base.options.height !== null && base.options.height.toString().indexOf("%") > -1)
  833. setHeight(editorContainer.parent().height() *
  834. (parseFloat(base.options.height) / 100));
  835. if(base.options.width !== null && base.options.width.toString().indexOf("%") > -1)
  836. setWidth(editorContainer.parent().width() *
  837. (parseFloat(base.options.width) / 100));
  838. };
  839. /**
  840. * Translates the string into the locale language.
  841. *
  842. * Replaces any {0}, {1}, {2}, ect. with the params provided.
  843. * @public
  844. * @return string
  845. */
  846. base._ = function() {
  847. var args = arguments;
  848. if(locale !== null && locale[args[0]])
  849. args[0] = locale[args[0]];
  850. return args[0].replace(/\{(\d+)\}/g, function(str, p1) {
  851. return typeof args[p1-0+1] !== 'undefined'?
  852. args[p1-0+1] :
  853. '{' + p1 + '}';
  854. });
  855. };
  856. /**
  857. * Init the locale variable with the specified locale if possible
  858. * @private
  859. * @return void
  860. */
  861. initLocale = function () {
  862. if($.sceditor.locale[base.options.locale])
  863. locale = $.sceditor.locale[base.options.locale];
  864. else
  865. {
  866. var lang = base.options.locale.split("-");
  867. if($.sceditor.locale[lang[0]])
  868. locale = $.sceditor.locale[lang[0]];
  869. }
  870. if(locale !== null && locale.dateFormat)
  871. base.options.dateFormat = locale.dateFormat;
  872. };
  873. // run the initializer
  874. init();
  875. };
  876. // ----------------------------------------------------------
  877. // A short snippet for detecting versions of IE in JavaScript
  878. // without resorting to user-agent sniffing
  879. // ----------------------------------------------------------
  880. // If you're not in IE (or IE version is less than 5) then:
  881. // ie === undefined
  882. // If you're in IE (>=5) then you can determine which version:
  883. // ie === 7; // IE7
  884. // Thus, to detect IE:
  885. // if (ie) {}
  886. // And to detect the version:
  887. // ie === 6 // IE6
  888. // ie > 7 // IE8, IE9 ...
  889. // ie < 9 // Anything less than IE9
  890. // ----------------------------------------------------------
  891. // UPDATE: Now using Live NodeList idea from @jdalton
  892. // Source: https://gist.github.com/527683
  893. $.sceditor.ie = (function(){
  894. var undef,
  895. v = 3,
  896. div = document.createElement('div'),
  897. all = div.getElementsByTagName('i');
  898. while (
  899. div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
  900. all[0]
  901. );
  902. return v > 4 ? v : undef;
  903. }());
  904. /**
  905. * Escapes a string so it's safe to use in regex
  906. * @param string str The strong to escape
  907. * @return string
  908. */
  909. $.sceditor.regexEscape = function (str) {
  910. return str.replace(/[\$\?\[\]\.\*\(\)\|\\]/g, "\\$&")
  911. .replace("<", "&lt;")
  912. .replace(">", "&gt;");
  913. };
  914. $.sceditor.locale = {};
  915. $.sceditor.commands = {
  916. // START_COMMAND: Bold
  917. bold: {
  918. exec: "bold",
  919. tooltip: "Bold"
  920. },
  921. // END_COMMAND
  922. // START_COMMAND: Italic
  923. italic: {
  924. exec: "italic",
  925. tooltip: "Italic"
  926. },
  927. // END_COMMAND
  928. // START_COMMAND: Underline
  929. underline: {
  930. exec: "underline",
  931. tooltip: "Underline"
  932. },
  933. // END_COMMAND
  934. // START_COMMAND: Strikethrough
  935. strike: {
  936. exec: "strikethrough",
  937. tooltip: "Strikethrough"
  938. },
  939. // END_COMMAND
  940. // START_COMMAND: Subscript
  941. subscript: {
  942. exec: "subscript",
  943. tooltip: "Subscript"
  944. },
  945. // END_COMMAND
  946. // START_COMMAND: Superscript
  947. superscript: {
  948. exec: "superscript",
  949. tooltip: "Superscript"
  950. },
  951. // END_COMMAND
  952. // START_COMMAND: Left
  953. left: {
  954. exec: "justifyleft",
  955. tooltip: "Align left"
  956. },
  957. // END_COMMAND
  958. // START_COMMAND: Centre
  959. center: {
  960. exec: "justifycenter",
  961. tooltip: "Center"
  962. },
  963. // END_COMMAND
  964. // START_COMMAND: Right
  965. right: {
  966. exec: "justifyright",
  967. tooltip: "Align right"
  968. },
  969. // END_COMMAND
  970. // START_COMMAND: Justify
  971. justify: {
  972. exec: "justifyfull",
  973. tooltip: "Justify"
  974. },
  975. // END_COMMAND
  976. // START_COMMAND: Font
  977. font: {
  978. exec: function (caller) {
  979. var editor = this,
  980. fonts = editor.options.fonts.split(","),
  981. content = $("<div />"),
  982. clickFunc = function (e) {
  983. editor.execCommand("fontname", $(this).data('sceditor-font'));
  984. editor.closeDropDown(true);
  985. e.preventDefault();
  986. };
  987. for (var i=0; i < fonts.length; i++) {
  988. content.append(
  989. $('<a class="sceditor-font-option" href="#"><font face="' + fonts[i] + '">' + fonts[i] + '</font></a>')
  990. .data('sceditor-font', fonts[i])
  991. .click(clickFunc));
  992. }
  993. editor.createDropDown(caller, "font-picker", content);
  994. },
  995. tooltip: "Font Name"
  996. },
  997. // END_COMMAND
  998. // START_COMMAND: Size
  999. size: {
  1000. exec: function (caller) {
  1001. var sizes = [0, 8, 10, 12, 14, 18, 24, 36];
  1002. var editor = this,
  1003. content = $("<div />"),
  1004. clickFunc = function (e) {
  1005. editor.execCommand("fontsize", $(this).data('sceditor-fontsize'));
  1006. editor.closeDropDown(true);
  1007. e.preventDefault();
  1008. };
  1009. for (var i=1; i<= 7; i++) {
  1010. content.append(
  1011. $('<a class="sceditor-fontsize-option" style="line-height:' + sizes[i] + 'pt" href="#"><font size="' + i + '">' + i + '</font></a>')
  1012. .data('sceditor-fontsize', i)
  1013. .click(clickFunc));
  1014. }
  1015. editor.createDropDown(caller, "fontsize-picker", content);
  1016. },
  1017. tooltip: "Font Size"
  1018. },
  1019. // END_COMMAND
  1020. // START_COMMAND: Colour
  1021. color: {
  1022. exec: function (caller) {
  1023. var editor = this,
  1024. genColor = {r: 255, g: 255, b: 255},
  1025. content = $("<div />"),
  1026. colorColumns = this.options.colors?this.options.colors.split("|"):new Array(21),
  1027. // IE is slow at string concation so use an array
  1028. html = [],
  1029. htmlIndex = 0;
  1030. for (var i=0; i < colorColumns.length; ++i) {
  1031. var colors = (typeof colorColumns[i] !== "undefined")?colorColumns[i].split(","):new Array(21);
  1032. html[htmlIndex++] = '<div class="sceditor-color-column">';
  1033. for (var x=0; x < colors.length; ++x) {
  1034. // use pre defined colour if can otherwise use the generated color
  1035. var color = (typeof colors[x] !== "undefined")?colors[x]:"#" + genColor.r.toString(16) + genColor.g.toString(16) + genColor.b.toString(16);
  1036. html[htmlIndex++] = '<a href="#" class="sceditor-color-option" style="background-color: '+color+'" data-color="'+color+'"></a>';
  1037. // calculate the next generated color
  1038. if(x%5===0)
  1039. genColor = {r: genColor.r, g: genColor.g-51, b: 255};
  1040. else
  1041. genColor = {r: genColor.r, g: genColor.g, b: genColor.b-51};
  1042. }
  1043. html[htmlIndex++] = '</div>';
  1044. // calculate the next generated color
  1045. if(i%5===0)
  1046. genColor = {r: genColor.r-51, g: 255, b: 255};
  1047. else
  1048. genColor = {r: genColor.r, g: 255, b: 255};
  1049. }
  1050. content.append(html.join(''))
  1051. .find('a')
  1052. .click(function (e) {
  1053. editor.execCommand("forecolor", $(this).attr('data-color'));
  1054. editor.closeDropDown(true);
  1055. e.preventDefault();
  1056. });
  1057. editor.createDropDown(caller, "color-picker", content);
  1058. },
  1059. tooltip: "Font Color"
  1060. },
  1061. // END_COMMAND
  1062. // START_COMMAND: Remove Format
  1063. removeformat: {
  1064. exec: "removeformat",
  1065. tooltip: "Remove Formatting"
  1066. },
  1067. // END_COMMAND
  1068. // START_COMMAND: Cut
  1069. cut: {
  1070. exec: "cut",
  1071. tooltip: "Cut",
  1072. errorMessage: "Your browser does not allow the cut command. Please use the keyboard shortcut Ctrl/Cmd-X"
  1073. },
  1074. // END_COMMAND
  1075. // START_COMMAND: Copy
  1076. copy: {
  1077. exec: "copy",
  1078. tooltip: "Copy",
  1079. errorMessage: "Your browser does not allow the copy command. Please use the keyboard shortcut Ctrl/Cmd-C"
  1080. },
  1081. // END_COMMAND
  1082. // START_COMMAND: Paste
  1083. paste: {
  1084. exec: "paste",
  1085. tooltip: "Paste",
  1086. errorMessage: "Your browser does not allow the paste command. Please use the keyboard shortcut Ctrl/Cmd-V"
  1087. },
  1088. // END_COMMAND
  1089. // START_COMMAND: Paste Text
  1090. pastetext: {
  1091. exec: function (caller) {
  1092. var editor = this,
  1093. content = $(this._('<form><div><label for="txt">{0}</label> <textarea cols="20" rows="7" id="txt">' +
  1094. '</textarea></div></form>',
  1095. this._("Paste your text inside the following box:")
  1096. ))
  1097. .submit(function () {return false;});
  1098. content.append($(this._('<div><input type="button" class="button" value="{0}" /></div>',
  1099. this._("Insert")
  1100. )).click(function (e) {
  1101. editor.wysiwygEditorInsertText($(this).parent("form").find("#txt").val());
  1102. editor.closeDropDown(true);
  1103. e.preventDefault();
  1104. }));
  1105. editor.createDropDown(caller, "pastetext", content);
  1106. },
  1107. tooltip: "Paste Text"
  1108. },
  1109. // END_COMMAND
  1110. // START_COMMAND: Bullet List
  1111. bulletlist: {
  1112. exec: "insertunorderedlist",
  1113. tooltip: "Bullet list"
  1114. },
  1115. // END_COMMAND
  1116. // START_COMMAND: Ordered List
  1117. orderedlist: {
  1118. exec: "insertorderedlist",
  1119. tooltip: "Numbered list"
  1120. },
  1121. // END_COMMAND
  1122. // START_COMMAND: Table
  1123. table: {
  1124. exec: function (caller) {
  1125. var editor = this,
  1126. content = $(this._(
  1127. '<form><div><label for="rows">{0}</label><input type="text" id="rows" value="2" /></div>' +
  1128. '<div><label for="cols">{1}</label><input type="text" id="cols" value="2" /></div></form>',
  1129. this._("Rows:"),
  1130. this._("Cols:")
  1131. ))
  1132. .submit(function () {return false;});
  1133. content.append($(this._('<div><input type="button" class="button" value="{0}" /></div>',
  1134. this._("Insert")
  1135. )).click(function (e) {
  1136. var rows = $(this).parent("form").find("#rows").val() - 0,
  1137. cols = $(this).parent("form").find("#cols").val() - 0,
  1138. html = '<table>';
  1139. if(rows < 1 || cols < 1)
  1140. return;
  1141. for (var row=0; row < rows; row++) {
  1142. html += '<tr>';
  1143. for (var col=0; col < cols; col++) {
  1144. if($.browser.msie)
  1145. html += '<td></td>';
  1146. else
  1147. html += '<td><br class="sceditor-ignore" /></td>';
  1148. }
  1149. html += '</tr>';
  1150. }
  1151. html += '</table>';
  1152. editor.wysiwygEditorInsertHtml(html);
  1153. editor.closeDropDown(true);
  1154. e.preventDefault();
  1155. }));
  1156. editor.createDropDown(caller, "inserttable", content);
  1157. },
  1158. tooltip: "Insert a table"
  1159. },
  1160. // END_COMMAND
  1161. // START_COMMAND: Horizontal Rule
  1162. horizontalrule: {
  1163. exec: "inserthorizontalrule",
  1164. tooltip: "Insert a horizontal rule"
  1165. },
  1166. // END_COMMAND
  1167. // START_COMMAND: Code
  1168. code: {
  1169. exec: function () {
  1170. this.wysiwygEditorInsertHtml('<code>', '<br /></code>');
  1171. },
  1172. tooltip: "Code"
  1173. },
  1174. // END_COMMAND
  1175. // START_COMMAND: Image
  1176. image: {
  1177. exec: function (caller) {
  1178. var editor = this,
  1179. content = $(this._('<form><div><label for="link">{0}</label> <input type="text" id="image" value="http://" /></div>' +
  1180. '<div><label for="width">{1}</label> <input type="text" id="width" size="2" /></div>' +
  1181. '<div><label for="height">{2}</label> <input type="text" id="height" size="2" /></div></form>',
  1182. this._("URL:"),
  1183. this._("Width (optional):"),
  1184. this._("Height (optional):")
  1185. ))
  1186. .submit(function () {return false;});
  1187. content.append($(this._('<div><input type="button" class="button" value="Insert" /></div>',
  1188. this._("Insert")
  1189. )).click(function (e) {
  1190. var $form = $(this).parent("form"),
  1191. val = $form.find("#image").val(),
  1192. attrs = '',
  1193. width,
  1194. height;
  1195. if((width = $form.find("#width").val()))
  1196. attrs += ' width="' + width + '"';
  1197. if((height = $form.find("#height").val()))
  1198. attrs += ' height="' + height + '"';
  1199. if(val && val !== "http://")
  1200. editor.wysiwygEditorInsertHtml('<img' + attrs + ' src="' + val + '" />');
  1201. editor.closeDropDown(true);
  1202. e.preventDefault();
  1203. }));
  1204. editor.createDropDown(caller, "insertimage", content);
  1205. },
  1206. tooltip: "Insert an image"
  1207. },
  1208. // END_COMMAND
  1209. // START_COMMAND: E-mail
  1210. email: {
  1211. exec: function (caller) {
  1212. var editor = this,
  1213. content = $(this._('<form><div><label for="email">{0}</label> <input type="text" id="email" value="" /></div></form>',
  1214. this._("E-mail:")
  1215. ))
  1216. .submit(function () {return false;});
  1217. content.append($('<div><input type="button" class="button" value="Insert" /></div>').click(function (e) {
  1218. var val = $(this).parent("form").find("#email").val();
  1219. if(val)
  1220. {
  1221. // needed for IE to reset the last range
  1222. editor.focus();
  1223. if(!editor.getRangeHelper().selectedHtml())
  1224. editor.wysiwygEditorInsertHtml('<a href="' + 'mailto:' + val + '">' + val + '</a>');
  1225. else
  1226. editor.execCommand("createlink", 'mailto:' + val);
  1227. }
  1228. editor.closeDropDown(true);
  1229. e.preventDefault();
  1230. }));
  1231. editor.createDropDown(caller, "insertemail", content);
  1232. },
  1233. tooltip: "Insert an email"
  1234. },
  1235. // END_COMMAND
  1236. // START_COMMAND: Link
  1237. link: {
  1238. exec: function (caller) {
  1239. var editor = this,
  1240. content = $(this._('<form><div><label for="link">{0}</label> <input type="text" id="link" value="http://" /></div>' +
  1241. '<div><label for="des">{1}</label> <input type="text" id="des" value="" /></div></form>',
  1242. this._("URL:"),
  1243. this._("Description (optional):")
  1244. ))
  1245. .submit(function () {return false;});
  1246. content.append($(
  1247. this._('<div><input type="button" class="button" value="{0}" /></div>',
  1248. this._("Insert")
  1249. )).click(function (e) {
  1250. var val = $(this).parent("form").find("#link").val(),
  1251. description = $(this).parent("form").find("#des").val();
  1252. if(val !== "" && val !== "http://") {
  1253. // needed for IE to reset the last range
  1254. editor.focus();
  1255. if(!editor.getRangeHelper().selectedHtml() || description)
  1256. {
  1257. if(!description)
  1258. description = val;
  1259. editor.wysiwygEditorInsertHtml('<a href="' + val + '">' + description + '</a>');
  1260. }
  1261. else
  1262. editor.execCommand("createlink", val);
  1263. }
  1264. editor.closeDropDown(true);
  1265. e.preventDefault();
  1266. }));
  1267. editor.createDropDown(caller, "insertlink", content);
  1268. },
  1269. tooltip: "Insert a link"
  1270. },
  1271. // END_COMMAND
  1272. // START_COMMAND: Unlink
  1273. unlink: {
  1274. exec: "unlink",
  1275. tooltip: "Unlink"
  1276. },
  1277. // END_COMMAND
  1278. // START_COMMAND: Quote
  1279. quote: {
  1280. exec: function (caller, html, author) {
  1281. var before = '<blockquote>',
  1282. end = '</blockquote>';
  1283. // if there is HTML passed set end to null so any selected
  1284. // text is replaced
  1285. if(html)
  1286. {
  1287. author = (author ? '<cite>' + author + '</cite>' : '');
  1288. before = before + author + html + end + '<br />';
  1289. end = null;
  1290. }
  1291. // if not add a newline to the end of the inserted quote
  1292. else if(this.getRangeHelper().selectedHtml() === "")
  1293. end = '<br />' + end;
  1294. this.wysiwygEditorInsertHtml(before, end);
  1295. },
  1296. tooltip: "Insert a Quote"
  1297. },
  1298. // END_COMMAND
  1299. // START_COMMAND: Emoticons
  1300. emoticon: {
  1301. exec: function (caller) {
  1302. var appendEmoticon,
  1303. editor = this,
  1304. content = $('<div />'),
  1305. line = $('<div />');
  1306. appendEmoticon = function (code, emoticon) {
  1307. line.append($('<img />')
  1308. .attr({
  1309. src: emoticon,
  1310. alt: code
  1311. })
  1312. .click(function (e) {
  1313. var start = '', end = '';
  1314. if(editor.options.emoticonsCompat)
  1315. {
  1316. start = '<span> ';
  1317. end = ' </span>';
  1318. }
  1319. editor.wysiwygEditorInsertHtml(start + '<img src="' + $(this).attr("src") +
  1320. '" data-sceditor-emoticon="' + $(this).attr('alt') + '" />' + end);
  1321. editor.closeDropDown(true);
  1322. e.preventDefault();
  1323. })
  1324. );
  1325. if(line.children().length > 3) {
  1326. content.append(line);
  1327. line = $('<div />');
  1328. }
  1329. };
  1330. $.each(editor.options.emoticons.dropdown, appendEmoticon);
  1331. if(line.children().length > 0)
  1332. content.append(line);
  1333. if(typeof editor.options.emoticons.more !== "undefined") {
  1334. var more = $(this._('<a class="sceditor-more">{0}</a>', this._("More"))).click(function () {
  1335. var emoticons = $.extend({}, editor.options.emoticons.dropdown, editor.options.emoticons.more);
  1336. content = $('<div />');
  1337. line = $('<div />');
  1338. $.each(emoticons, appendEmoticon);
  1339. if(line.children().length > 0)
  1340. content.append(line);
  1341. editor.createDropDown(caller, "insertemoticon", content);
  1342. });
  1343. content.append(more);
  1344. }
  1345. editor.createDropDown(caller, "insertemoticon", content);
  1346. },
  1347. keyPress: function (e, wysiwygEditor)
  1348. {
  1349. // make sure emoticons command is in the toolbar before running
  1350. if(this.options.toolbar.indexOf('emoticon') === -1)
  1351. return;
  1352. var editor = this,
  1353. pos = 0,
  1354. curChar = String.fromCharCode(e.which);
  1355. if(!editor.EmoticonsCache) {
  1356. editor.EmoticonsCache = [];
  1357. $.each($.extend({}, editor.options.emoticons.more, editor.options.emoticons.dropdown, editor.options.emoticons.hidden), function(key, url) {
  1358. editor.EmoticonsCache[pos++] = [
  1359. key,
  1360. '<img src="' + url + '" data-sceditor-emoticon="' + key + '" alt="' + key + '" />'
  1361. ];
  1362. });
  1363. editor.EmoticonsCache.sort(function(a, b){
  1364. return a[0].length - b[0].length;
  1365. });
  1366. }
  1367. if(!editor.longestEmoticonCode)
  1368. editor.longestEmoticonCode = editor.EmoticonsCache[editor.EmoticonsCache.length - 1][0].length;
  1369. if(editor.getRangeHelper().raplaceKeyword(editor.EmoticonsCache, true, true, editor.longestEmoticonCode, editor.options.emoticonsCompat, curChar))
  1370. {
  1371. if(/^\s$/.test(curChar) && editor.options.emoticonsCompat)
  1372. return true;
  1373. e.preventDefault();
  1374. e.stopPropagation();
  1375. return false;
  1376. }
  1377. },
  1378. tooltip: "Insert an emoticon"
  1379. },
  1380. // END_COMMAND
  1381. // START_COMMAND: YouTube
  1382. youtube: {
  1383. exec: function (caller) {
  1384. var editor = this;
  1385. var content = $(
  1386. this._('<form><div><label for="link">{0}</label> <input type="text" id="link" value="http://" /></div></form>',
  1387. this._("Video URL:")
  1388. ))
  1389. .submit(function () {return false;});
  1390. content.append(
  1391. $(this._('<div><input type="button" class="button" value="{0}" /></div>',
  1392. this._("Insert")
  1393. ))
  1394. .click(function (e) {
  1395. var val = $(this).parent("form").find("#link").val();
  1396. if(val !== "" && val !== "http://") {
  1397. // See http://www.abovetopsecret.com/forum/thread270269/pg1
  1398. val = val.replace(/^[^v]+v.(.{11}).*/,"$1");
  1399. editor.wysiwygEditorInsertHtml('<iframe width="560" height="315" src="http://www.youtube.com/embed/' + val +
  1400. '?wmode=opaque" data-youtube-id="' + val + '" frameborder="0" allowfullscreen></iframe>');
  1401. }
  1402. editor.closeDropDown(true);
  1403. e.preventDefault();
  1404. }));
  1405. editor.createDropDown(caller, "insertlink", content);
  1406. },
  1407. tooltip: "Insert a YouTube video"
  1408. },
  1409. // END_COMMAND
  1410. // START_COMMAND: Date
  1411. date: {
  1412. exec: function () {
  1413. var now = new Date(),
  1414. year = now.getYear(),
  1415. month = now.getMonth()+1,
  1416. day = now.getDate();
  1417. if(year < 2000)
  1418. year = 1900 + year;
  1419. if(month < 10)
  1420. month = "0" + month;
  1421. if(day < 10)
  1422. day = "0" + day;
  1423. this.wysiwygEditorInsertHtml('<span>' +
  1424. this.options.dateFormat.replace(/year/i, year).replace(/month/i, month).replace(/day/i, day) +
  1425. '</span>');
  1426. },
  1427. tooltip: "Insert current date"
  1428. },
  1429. // END_COMMAND
  1430. // START_COMMAND: Time
  1431. time: {
  1432. exec: function () {
  1433. var now = new Date(),
  1434. hours = now.getHours(),
  1435. mins = now.getMinutes(),
  1436. secs = now.getSeconds();
  1437. if(hours < 10)
  1438. hours = "0" + hours;
  1439. if(mins < 10)
  1440. mins = "0" + mins;
  1441. if(secs < 10)
  1442. secs = "0" + secs;
  1443. this.wysiwygEditorInsertHtml('<span>' + hours + ':' + mins + ':' + secs + '</span>');
  1444. },
  1445. tooltip: "Insert current time"
  1446. },
  1447. // END_COMMAND
  1448. // START_COMMAND: Print
  1449. print: {
  1450. exec: "print",
  1451. tooltip: "Print"
  1452. },
  1453. // END_COMMAND
  1454. // START_COMMAND: Source
  1455. source: {
  1456. exec: function () {
  1457. this.toggleTextMode();
  1458. },
  1459. txtExec: function () {
  1460. this.toggleTextMode();
  1461. },
  1462. tooltip: "View source"
  1463. },
  1464. // END_COMMAND
  1465. // this is here so that commands above can be removed
  1466. // without having to remove the , after the last one.
  1467. // Needed for IE.
  1468. ignore: {}
  1469. };
  1470. /**
  1471. * Range helper class
  1472. */
  1473. $.sceditor.rangeHelper = function(w, d) {
  1474. var win, doc,
  1475. isW3C = true,
  1476. startMarker = "sceditor-start-marker",
  1477. endMarker = "sceditor-end-marker",
  1478. base = this,
  1479. init, _createMarker, _getOuterText, _selectOuterText;
  1480. /**
  1481. * @constructor
  1482. * @param Window window
  1483. * @param Document document
  1484. * @private
  1485. */
  1486. init = function (window, document) {
  1487. doc = document || window.contentDocument || window.document;
  1488. win = window;
  1489. isW3C = !!window.getSelection;
  1490. }(w, d);
  1491. /**
  1492. * Inserts HTML.
  1493. *
  1494. * If endHTML is specified the selected contents will be put between
  1495. * html and endHTML.
  1496. * @param string html
  1497. * @param string endHTML
  1498. */
  1499. base.insertHTML = function(html, endHTML) {
  1500. var node, endNode, div;
  1501. if(endHTML)
  1502. html += base.selectedHtml() + endHTML;
  1503. if(isW3C)
  1504. {
  1505. div = doc.createElement('div');
  1506. node = doc.createDocumentFragment();
  1507. div.innerHTML = html;
  1508. while(div.firstChild)
  1509. node.appendChild(div.firstChild);
  1510. base.insertNode(node);
  1511. }
  1512. else
  1513. base.selectedRange().pasteHTML(html);
  1514. };
  1515. /**
  1516. * Inserts a DOM node.
  1517. *
  1518. * If endNode is specified the selected contents will be put between
  1519. * node and endNode.
  1520. * @param Node node
  1521. * @param Node endNode
  1522. */
  1523. base.insertNode = function(node, endNode) {
  1524. if(isW3C)
  1525. {
  1526. var toInsert = doc.createDocumentFragment(),
  1527. range = base.selectedRange(),
  1528. selection, selectAfter;
  1529. toInsert.appendChild(node);
  1530. if(endNode)
  1531. {
  1532. toInsert.appendChild(range.extractContents());
  1533. toInsert.appendChild(endNode);
  1534. }
  1535. selectAfter = toInsert.lastChild;
  1536. range.deleteContents();
  1537. range.insertNode(toInsert);
  1538. selection = doc.createRange();
  1539. selection.setStartAfter(selectAfter);
  1540. base.selectRange(selection);
  1541. }
  1542. else
  1543. base.insertHTML(node.outerHTML, endNode?endNode.outerHTML:null);
  1544. };
  1545. /**
  1546. * Clones the selected Range
  1547. * @return Range|TextRange
  1548. */
  1549. base.cloneSelected = function() {
  1550. if(!isW3C)
  1551. return base.selectedRange().duplicate();
  1552. return base.selectedRange().cloneRange();
  1553. };
  1554. /**
  1555. * Gets the selected Range
  1556. * @return Range|TextRange
  1557. */
  1558. base.selectedRange = function() {
  1559. var sel;
  1560. if(win.getSelection)
  1561. sel = win.getSelection();
  1562. else
  1563. sel = doc.selection;
  1564. if(sel.getRangeAt && sel.rangeCount <= 0)
  1565. sel.addRange(doc.createRange());
  1566. if(!isW3C)
  1567. return sel.createRange();
  1568. return sel.getRangeAt(0);
  1569. };
  1570. /**
  1571. * Gets the selected HTML
  1572. * @return string
  1573. */
  1574. base.selectedHtml = function() {
  1575. var range = base.selectedRange();
  1576. if(!range)
  1577. return '';
  1578. // IE9+ and all other browsers
  1579. if (window.XMLSerializer)
  1580. return new XMLSerializer().serializeToString(range.cloneContents());
  1581. // IE < 9
  1582. if(!isW3C)
  1583. {
  1584. if(range.text !== '' && range.htmlText)
  1585. return range.htmlText;
  1586. }
  1587. return '';
  1588. };
  1589. base.parentNode = function() {
  1590. var range = base.selectedRange();
  1591. if(isW3C)
  1592. return range.commonAncestorContainer;
  1593. else
  1594. return range.parentElement();
  1595. };
  1596. /**
  1597. * Inserts a node at either the start or end of the current selection
  1598. * @param Bool start
  1599. * @param Node node
  1600. */
  1601. base.insertNodeAt = function(start, node) {
  1602. var range = base.cloneSelected();
  1603. range.collapse(start);
  1604. if(range.insertNode)
  1605. range.insertNode(node);
  1606. else
  1607. range.pasteHTML(node.outerHTML);
  1608. };
  1609. /**
  1610. * Creates a marker node
  1611. * @param String id
  1612. * @return Node
  1613. */
  1614. _createMarker = function(id) {
  1615. base.removeMarker(id);
  1616. var marker = doc.createElement("span");
  1617. marker.id = id;
  1618. marker.style.lineHeight = "0";
  1619. marker.style.display = "none";
  1620. marker.className = "sceditor-selection";
  1621. return marker;
  1622. };
  1623. /**
  1624. * Inserts start/end markers for the current selection
  1625. */
  1626. base.insertMarkers = function() {
  1627. base.insertNodeAt(true, _createMarker(startMarker));
  1628. base.insertNodeAt(false, _createMarker(endMarker));
  1629. };
  1630. /**
  1631. * Gets the marker with the specified ID
  1632. * @param String id
  1633. * @return Node
  1634. */
  1635. base.getMarker = function(id) {
  1636. return doc.getElementById(id);
  1637. };
  1638. /**
  1639. * Removes the marker with the specified ID
  1640. * @param String id
  1641. */
  1642. base.removeMarker = function(id) {
  1643. var marker = base.getMarker(id);
  1644. if(marker)
  1645. marker.parentNode.removeChild(marker);
  1646. };
  1647. /**
  1648. * Removes the start/end markers
  1649. */
  1650. base.removeMarkers = function() {
  1651. base.removeMarker(startMarker);
  1652. base.removeMarker(endMarker);
  1653. };
  1654. /**
  1655. * Saves the current range location
  1656. */
  1657. base.saveRange = function() {
  1658. base.insertMarkers();
  1659. };
  1660. /**
  1661. * Selected the specified range
  1662. * @param Range|TextRange range
  1663. */
  1664. base.selectRange = function(range) {
  1665. if(!isW3C)
  1666. range.select();
  1667. else
  1668. {
  1669. win.getSelection().removeAllRanges();
  1670. win.getSelection().addRange(range);
  1671. }
  1672. };
  1673. /**
  1674. * Restores the last saved range if possible
  1675. */
  1676. base.restoreRange = function() {
  1677. var range = base.selectedRange(),
  1678. start = base.getMarker(startMarker),
  1679. end = base.getMarker(endMarker);
  1680. if(!start || !end)
  1681. return false;
  1682. if(!isW3C)
  1683. {
  1684. range = doc.body.createTextRange();
  1685. var marker = doc.body.createTextRange();
  1686. marker.moveToElementText(start);
  1687. range.setEndPoint('StartToStart', marker);
  1688. range.moveStart('character', 0);
  1689. marker.moveToElementText(end);
  1690. range.setEndPoint('EndToStart', marker);
  1691. range.moveEnd('character', 0);
  1692. base.selectRange(range);
  1693. }
  1694. else
  1695. {
  1696. range = doc.createRange();
  1697. range.setStartBefore(start);
  1698. range.setEndAfter(end);
  1699. base.selectRange(range);
  1700. }
  1701. base.removeMarkers();
  1702. };
  1703. /**
  1704. * Selects the text left and right of the current selection
  1705. * @param int left
  1706. * @param int right
  1707. * @private
  1708. */
  1709. _selectOuterText = function(left, right) {
  1710. var range = base.cloneSelected();
  1711. range.collapse(false);
  1712. if(!isW3C)
  1713. {
  1714. range.moveStart('character', 0-left);
  1715. range.moveEnd('character', right);
  1716. }
  1717. else
  1718. {
  1719. range.setStart(range.startContainer, range.startOffset-left);
  1720. range.setEnd(range.endContainer, range.endOffset+right);
  1721. //range.deleteContents();
  1722. }
  1723. base.selectRange(range);
  1724. };
  1725. /**
  1726. * Gets the text left or right of the current selection
  1727. * @param bool before
  1728. * @param int length
  1729. * @private
  1730. */
  1731. _getOuterText = function(before, length) {
  1732. var ret = "",
  1733. range = base.cloneSelected(),
  1734. node;
  1735. range.collapse(false);
  1736. if(before)
  1737. {
  1738. if(!isW3C)
  1739. {
  1740. range.moveStart('character', 0-length);
  1741. ret = range.text;
  1742. }
  1743. else
  1744. {
  1745. ret = range.startContainer.textContent.substr(0, range.startOffset);
  1746. ret = ret.substr(Math.max(0, ret.length - length));
  1747. }
  1748. }
  1749. else
  1750. {
  1751. if(!isW3C)
  1752. {
  1753. range.moveEnd('character', length);
  1754. ret = range.text;
  1755. }
  1756. else
  1757. ret = range.startContainer.textContent.substr(range.startOffset, length);
  1758. }
  1759. return ret;
  1760. };
  1761. /**
  1762. * Replaces keys with values based on the current range
  1763. * @param Array rep
  1764. * @param Bool includePrev If to include text before or just text after
  1765. * @param Bool repSorted If the keys array is pre sorted
  1766. * @param Int longestKey Length of the longest key
  1767. * @param Bool requireWhiteSpace If the key must be surrounded by whitespace
  1768. */
  1769. base.raplaceKeyword = function(rep, includeAfter, repSorted, longestKey, requireWhiteSpace, curChar) {
  1770. if(!repSorted)
  1771. rep.sort(function(a, b){
  1772. return a.length - b.length;
  1773. });
  1774. var maxKeyLen = longestKey || rep[rep.length-1][0].length,
  1775. before, after, str, i, start, left, pat, lookStart;
  1776. before = after = str = "";
  1777. if(requireWhiteSpace)
  1778. {
  1779. // forcing spaces around doesn't work with textRanges as they will select text
  1780. // on the other side of an image causing space-img-key to be returned as
  1781. // space-key which would be valid when it's not.
  1782. if(!isW3C)
  1783. return false;
  1784. ++maxKeyLen;
  1785. }
  1786. before = _getOuterText(true, maxKeyLen);
  1787. if(includeAfter)
  1788. after = _getOuterText(false, maxKeyLen);
  1789. str = before + (curChar!=null?curChar:"") + after;
  1790. i = rep.length;
  1791. while(i--)
  1792. {
  1793. //pat = new RegExp("(?:^|\\s)" + $.sceditor.regexEscape(rep[i][0]) + "(?=\\s|$)");
  1794. pat = new RegExp("(?:[\\s\xA0\u2002\u2003\u2009])" + $.sceditor.regexEscape(rep[i][0]) + "(?=[\\s\xA0\u2002\u2003\u2009])");
  1795. lookStart = before.length - 1 - rep[i][0].length;
  1796. if(requireWhiteSpace)
  1797. --lookStart;
  1798. lookStart = Math.max(0, lookStart);
  1799. if((!requireWhiteSpace && (start = str.indexOf(rep[i][0], lookStart)) > -1) ||
  1800. (requireWhiteSpace && (start = str.substr(lookStart).search(pat)) > -1))
  1801. {
  1802. if(requireWhiteSpace)
  1803. start += lookStart + 1;
  1804. // make sure the substr is between before and after not entierly in one
  1805. // or the other
  1806. if(start > before.length || start+rep[i][0].length + (requireWhiteSpace?1:0) < before.length)
  1807. continue;
  1808. left = before.length - start;
  1809. _selectOuterText(left, rep[i][0].length-left-(curChar!=null&&/^\S/.test(curChar)?1:0));
  1810. base.insertHTML(rep[i][1]);
  1811. return true;
  1812. }
  1813. }
  1814. return false;
  1815. };
  1816. };
  1817. /**
  1818. * Static DOM helper class
  1819. */
  1820. $.sceditor.dom = {
  1821. /**
  1822. * Loop all child nodes of the passed node
  1823. *
  1824. * The function should accept 1 parameter being the node.
  1825. * If the function returns false the loop will be exited.
  1826. *
  1827. * @param HTMLElement node
  1828. * @param function func Function that is called for every node, should accept 1 param for the node
  1829. * @param bool innermostFirst If the innermost node should be passed to the function before it's parents
  1830. * @param bool siblingsOnly If to only traverse the nodes siblings
  1831. * @param bool reverse If to traverse the nodes in reverse
  1832. */
  1833. traverse: function(node, func, innermostFirst, siblingsOnly, reverse) {
  1834. if(node)
  1835. {
  1836. node = reverse ? node.lastChild : node.firstChild;
  1837. while(node != null)
  1838. {
  1839. if(!innermostFirst && func(node) === false)
  1840. return false;
  1841. // traverse all children
  1842. if(!siblingsOnly && this.traverse(node, func, innermostFirst, siblingsOnly, reverse) === false)
  1843. return false;
  1844. if(innermostFirst && func(node) === false)
  1845. return false;
  1846. // move to next child
  1847. node = reverse ? node.previousSibling : node.nextSibling;
  1848. }
  1849. }
  1850. },
  1851. rTraverse: function(node, func, innermostFirst, siblingsOnly) {
  1852. this.traverse(node, func, innermostFirst, siblingsOnly, true);
  1853. },
  1854. /**
  1855. * Checks if an element is inline
  1856. *
  1857. * @param bool includeInlineBlock If passed inline-block will count as an inline element instead of block
  1858. * @return bool
  1859. */
  1860. isInline: function(elm, includeInlineBlock) {
  1861. if(elm == null || elm.nodeType !== 1)
  1862. return true;
  1863. var d = (window.getComputedStyle ? window.getComputedStyle(elm) : elm.currentStyle).display;
  1864. if(includeInlineBlock)
  1865. return d !== "block";
  1866. return d === "inline";
  1867. },
  1868. /**
  1869. * Gets the next node. If the node has no siblings
  1870. * it gets the parents next sibling, and so on untill
  1871. * another element is found. If none are found
  1872. * it returns null.
  1873. *
  1874. * @param HTMLElement node
  1875. * @return HTMLElement
  1876. */
  1877. /*getNext: function(node) {
  1878. if(!node)
  1879. return null;
  1880. var n = node.nextSibling;
  1881. if(n)
  1882. return n;
  1883. return getNext(node.parentNode);
  1884. },*/
  1885. copyCSS: function(from, to) {
  1886. to.style.cssText = from.style.cssText;
  1887. },
  1888. /**
  1889. * Fixes block level elements in inline elements.
  1890. *
  1891. * @param HTMLElement The node to fix
  1892. */
  1893. fixNesting: function(node) {
  1894. var base = this,
  1895. getLastInlineParent = function(node) {
  1896. while(base.isInline(node.parentNode))
  1897. node = node.parentNode;
  1898. return node;
  1899. };
  1900. base.traverse(node, function(node) {
  1901. // if node is an element, and is blocklevel and the parent isn't block level
  1902. // then it needs fixing
  1903. if(node.nodeType === 1 && !base.isInline(node) && base.isInline(node.parentNode))
  1904. {
  1905. var parent = getLastInlineParent(node),
  1906. rParent = parent.parentNode,
  1907. before = base.extractContents(parent, node),
  1908. middle = node;
  1909. // copy current styling so when moved out of the parent
  1910. // it still has the same styling
  1911. base.copyCSS(middle, middle);
  1912. rParent.insertBefore(before, parent);
  1913. rParent.insertBefore(middle, parent);
  1914. }
  1915. });
  1916. },
  1917. /**
  1918. * Finds the common parent of two nodes
  1919. *
  1920. * @param HTMLElement node1
  1921. * @param HTMLElement node2
  1922. * @return HTMLElement
  1923. */
  1924. findCommonAncestor: function(node1, node2) {
  1925. // not as fast as making two arrays of parents and comparing
  1926. // but is a lot smaller and as it's currently only used with
  1927. // fixing invalid nesting it doesn't need to be very fast
  1928. return $(node1).parents().has($(node2)).first();
  1929. },
  1930. /**
  1931. * Removes unused whitespace from the root and it's children
  1932. *
  1933. * @param HTMLElement root
  1934. * @return void
  1935. */
  1936. removeWhiteSpace: function(root) {
  1937. // 00A0 is non-breaking space which should not be striped
  1938. var regex = /[^\S|\u00A0]+/g;
  1939. this.traverse(root, function(node) {
  1940. if(node.nodeType === 3 && $(node).parents('code, pre').length === 0)
  1941. {
  1942. if(!/\S|\u00A0/.test(node.nodeValue))
  1943. node.nodeValue = " ";
  1944. else if(regex.test(node.nodeValue))
  1945. node.nodeValue = node.nodeValue.replace(regex, " ");
  1946. }
  1947. });
  1948. },
  1949. /**
  1950. * Extracts all the nodes between the start and end nodes
  1951. *
  1952. * @param HTMLElement startNode The node to start extracting at
  1953. * @param HTMLElement endNode The node to stop extracting at
  1954. * @return DocumentFragment
  1955. */
  1956. extractContents: function(startNode, endNode) {
  1957. var base = this,
  1958. $commonAncestor = base.findCommonAncestor(startNode, endNode),
  1959. commonAncestor = $commonAncestor===null?null:$commonAncestor.get(0),
  1960. startReached = false,
  1961. endReached = false;
  1962. return (function extract(root) {
  1963. var df = startNode.ownerDocument.createDocumentFragment();
  1964. base.traverse(root, function(node) {
  1965. // if end has been reached exit loop
  1966. if(endReached || (node === endNode && startReached))
  1967. {
  1968. endReached = true;
  1969. return false;
  1970. }
  1971. if(node === startNode)
  1972. startReached = true;
  1973. var c, n;
  1974. if(startReached)
  1975. {
  1976. // if the start has been reached and this elm contains
  1977. // the end node then clone it
  1978. if(jQuery.contains(node, endNode) && node.nodeType === 1)
  1979. {
  1980. c = extract(node);
  1981. n = node.cloneNode(false);
  1982. n.appendChild(c);
  1983. df.appendChild(n);
  1984. }
  1985. // otherwise just move it
  1986. else
  1987. df.appendChild(node);
  1988. }
  1989. // if this node contains the start node then add it
  1990. else if(jQuery.contains(node, startNode) && node.nodeType === 1)
  1991. {
  1992. c = extract(node);
  1993. n = node.cloneNode(false);
  1994. n.appendChild(c);
  1995. df.appendChild(n);
  1996. }
  1997. });
  1998. return df;
  1999. }(commonAncestor));
  2000. }
  2001. };
  2002. /**
  2003. * Checks if a command with the specified name exists
  2004. *
  2005. * @param {String} name
  2006. * @return Bool
  2007. */
  2008. $.sceditor.commandExists = function(name) {
  2009. return typeof $.sceditor.commands[name] !== "undefined";
  2010. };
  2011. /**
  2012. * Adds/updates a command.
  2013. *
  2014. * Only name and exec are required. Exec is only required if
  2015. * the command dose not already exist.
  2016. *
  2017. * @param {String} name The commands name
  2018. * @param {String|Function} exec The commands exec function or string for the native execCommand
  2019. * @param {String} tooltip The commands tooltip text
  2020. * @param {Function} keypress Function that gets called every time a key is pressed
  2021. * @param {Function|Array} txtExec Called when the command is executed in source mode or array containing prepend and optional append
  2022. * @return Bool
  2023. */
  2024. $.sceditor.setCommand = function(name, exec, tooltip, keypress, txtExec) {
  2025. if(!name || !($.sceditor.commandExists(name) || exec))
  2026. return false;
  2027. if(!$.sceditor.commandExists(name))
  2028. $.sceditor.commands[name] = {};
  2029. $.sceditor.commands[name].exec = exec;
  2030. if(tooltip)
  2031. $.sceditor.commands[name].tooltip = tooltip;
  2032. if(keypress)
  2033. $.sceditor.commands[name].keyPress = keypress;
  2034. if(txtExec)
  2035. $.sceditor.commands[name].txtExec = txtExec;
  2036. return true;
  2037. };
  2038. $.sceditor.defaultOptions = {
  2039. // Toolbar buttons order and groups. Should be comma seperated and have a bar | to seperate groups
  2040. toolbar: "bold,italic,underline,strike,subscript,superscript|left,center,right,justify|" +
  2041. "font,size,color,removeformat|cut,copy,paste,pastetext|bulletlist,orderedlist|" +
  2042. "table|code,quote|horizontalrule,image,email,link,unlink|emoticon,youtube,date,time|" +
  2043. "print,source",
  2044. // Stylesheet to include in the WYSIWYG editor. Will style the WYSIWYG elements
  2045. style: "jquery.sceditor.default.css",
  2046. // Comma seperated list of fonts for the font selector
  2047. fonts: "Arial,Arial Black,Comic Sans MS,Courier New,Georgia,Impact,Sans-serif,Serif,Times New Roman,Trebuchet MS,Verdana",
  2048. // Colors should be comma seperated and have a bar | to signal a new column. If null the colors will be auto generated.
  2049. colors: null,
  2050. locale: "en",
  2051. charset: "utf-8",
  2052. // compatibility mode for if you have emoticons such as :/ This mode requires
  2053. // emoticons to be surrounded by whitespace or end of line chars. This mode
  2054. // has limited As You Type emoticon converstion support (end of line chars)
  2055. // are not accepted as whitespace so only emoticons surrounded by whitespace
  2056. // will work
  2057. emoticonsCompat: false,
  2058. emoticonsRoot: '',
  2059. emoticons: {
  2060. dropdown: {
  2061. ":)": "emoticons/smile.png",
  2062. ":angel:": "emoticons/angel.png",
  2063. ":angry:": "emoticons/angry.png",
  2064. "8-)": "emoticons/cool.png",
  2065. ":'(": "emoticons/cwy.png",
  2066. ":ermm:": "emoticons/ermm.png",
  2067. ":D": "emoticons/grin.png",
  2068. "<3": "emoticons/heart.png",
  2069. ":(": "emoticons/sad.png",
  2070. ":O": "emoticons/shocked.png",
  2071. ":P": "emoticons/tongue.png",
  2072. ";)": "emoticons/wink.png"
  2073. },
  2074. more: {
  2075. ":alien:": "emoticons/alien.png",
  2076. ":blink:": "emoticons/blink.png",
  2077. ":blush:": "emoticons/blush.png",
  2078. ":cheerful:": "emoticons/cheerful.png",
  2079. ":devil:": "emoticons/devil.png",
  2080. ":dizzy:": "emoticons/dizzy.png",
  2081. ":getlost:": "emoticons/getlost.png",
  2082. ":happy:": "emoticons/happy.png",
  2083. ":kissing:": "emoticons/kissing.png",
  2084. ":ninja:": "emoticons/ninja.png",
  2085. ":pinch:": "emoticons/pinch.png",
  2086. ":pouty:": "emoticons/pouty.png",
  2087. ":sick:": "emoticons/sick.png",
  2088. ":sideways:": "emoticons/sideways.png",
  2089. ":silly:": "emoticons/silly.png",
  2090. ":sleeping:": "emoticons/sleeping.png",
  2091. ":unsure:": "emoticons/unsure.png",
  2092. ":woot:": "emoticons/w00t.png",
  2093. ":wassat:": "emoticons/wassat.png"
  2094. },
  2095. hidden: {
  2096. ":whistling:": "emoticons/whistling.png",
  2097. ":love:": "emoticons/wub.png"
  2098. }
  2099. },
  2100. // Width of the editor. Set to null for automatic with
  2101. width: null,
  2102. // Height of the editor including toolbat. Set to null for automatic height
  2103. height: null,
  2104. // If to allow the editor to be resized
  2105. resizeEnabled: true,
  2106. // Min resize to width, set to null for half textarea width or -1 for unlimited
  2107. resizeMinWidth: null,
  2108. // Min resize to height, set to null for half textarea height or -1 for unlimited
  2109. resizeMinHeight: null,
  2110. // Max resize to height, set to null for double textarea height or -1 for unlimited
  2111. resizeMaxHeight: null,
  2112. // Max resize to width, set to null for double textarea width or -1 for unlimited
  2113. resizeMaxWidth: null,
  2114. getHtmlHandler: null,
  2115. getTextHandler: null,
  2116. // date format. year, month and day will be replaced with the users current year, month and day.
  2117. dateFormat: "year-month-day",
  2118. toolbarContainer: null,
  2119. enablePasteFiltering: false,
  2120. //add css to dropdown menu (eg. z-index)
  2121. dropDownCss: { }
  2122. };
  2123. $.fn.sceditor = function (options) {
  2124. return this.each(function () {
  2125. (new $.sceditor(this, options));
  2126. });
  2127. };
  2128. })(jQuery);
  2129. (function($) {
  2130. var extensionMethods = {
  2131. InsertText: function(text, bClear) {
  2132. var bIsSource = this.inSourceMode();
  2133. // @TODO make it put the quote close to the current selection
  2134. if (!bIsSource)
  2135. this.toggleTextMode();
  2136. var current_value = bClear ? text + "\n" : this.getTextareaValue(false) + "\n" + text + "\n";
  2137. this.setTextareaValue(current_value);
  2138. if (!bIsSource)
  2139. this.toggleTextMode();
  2140. },
  2141. getText: function() {
  2142. if(this.inSourceMode())
  2143. var current_value = this.getTextareaValue(false);
  2144. else
  2145. var current_value = this.getWysiwygEditorValue();
  2146. return current_value;
  2147. },
  2148. appendEmoticon: function (code, emoticon) {
  2149. if (code == '')
  2150. line.append($('<br />'));
  2151. else
  2152. line.append($('<img />')
  2153. .attr({
  2154. src: emoticon,
  2155. alt: code,
  2156. })
  2157. .click(function (e) {
  2158. var start = '', end = '';
  2159. if (base.options.emoticonsCompat)
  2160. {
  2161. start = '<span> ';
  2162. end = ' </span>';
  2163. }
  2164. if (base.inSourceMode())
  2165. base.textEditorInsertText(' ' + $(this).attr('alt') + ' ');
  2166. else
  2167. base.wysiwygEditorInsertHtml(start + '<img src="' + $(this).attr("src") +
  2168. '" data-sceditor-emoticon="' + $(this).attr('alt') + '" />' + end);
  2169. e.preventDefault();
  2170. })
  2171. );
  2172. if (line.children().length > 0)
  2173. content.append(line);
  2174. $(".sceditor-toolbar").append(content);
  2175. },
  2176. storeLastState: function (){
  2177. this.wasSource = this.inSourceMode();
  2178. },
  2179. setTextMode: function () {
  2180. if (!this.inSourceMode())
  2181. this.toggleTextMode();
  2182. },
  2183. createPermanentDropDown: function() {
  2184. var emoticons = $.extend({}, this.options.emoticons.dropdown);
  2185. var popup_exists = false;
  2186. content = $('<div />').attr({class: "sceditor-insertemoticon"});
  2187. line = $('<div />');
  2188. base = this;
  2189. for (smiley_popup in this.options.emoticons.popup)
  2190. {
  2191. popup_exists = true;
  2192. break;
  2193. }
  2194. if (popup_exists)
  2195. {
  2196. this.options.emoticons.more = this.options.emoticons.popup;
  2197. moreButton = $('<div />').attr({class: "sceditor-more"}).text('[' + this._('More') + ']').click(function () {
  2198. if ($(".sceditor-smileyPopup").length > 0)
  2199. {
  2200. $(".sceditor-smileyPopup").fadeIn('fast');
  2201. }
  2202. else
  2203. {
  2204. var emoticons = $.extend({}, base.options.emoticons.popup);
  2205. var basement = $('<div />').attr({class: "sceditor-popup"});
  2206. allowHide = true;
  2207. popupContent = $('<div />');
  2208. line = $('<div />');
  2209. closeButton = $('<span />').text('[' + base._('Close') + ']').click(function () {
  2210. $(".sceditor-smileyPopup").fadeOut('fast');
  2211. });
  2212. $.each(emoticons, base.appendEmoticon);
  2213. if (line.children().length > 0)
  2214. popupContent.append(line);
  2215. if (typeof closeButton !== "undefined")
  2216. popupContent.append(closeButton);
  2217. // IE needs unselectable attr to stop it from unselecting the text in the editor.
  2218. // The editor can cope if IE does unselect the text it's just not nice.
  2219. if(base.ieUnselectable !== false) {
  2220. content = $(content);
  2221. content.find(':not(input,textarea)').filter(function() { return this.nodeType===1; }).attr('unselectable', 'on');
  2222. }
  2223. $dropdown = $('<div class="sceditor-dropdown sceditor-smileyPopup" />').append(popupContent);
  2224. $dropdown.appendTo($('body'));
  2225. dropdownIgnoreLastClick = true;
  2226. $dropdown.css({
  2227. position: "fixed",
  2228. top: $(window).height() * 0.2,
  2229. left: $(window).width() * 0.5 - ($dropdown.width() / 2),
  2230. "max-width": "50%"
  2231. });
  2232. // stop clicks within the dropdown from being handled
  2233. $dropdown.click(function (e) {
  2234. e.stopPropagation();
  2235. });
  2236. }
  2237. });
  2238. }
  2239. $.each(emoticons, base.appendEmoticon);
  2240. if (typeof moreButton !== "undefined")
  2241. content.append(moreButton);
  2242. }
  2243. };
  2244. $.extend(true, $['sceditor'].prototype, extensionMethods);
  2245. })(jQuery);
  2246. $.sceditor.setCommand(
  2247. 'ftp',
  2248. function (caller) {
  2249. var editor = this,
  2250. content = $(this._('<form><div><label for="link">{0}</label> <input type="text" id="link" value="ftp://" /></div>' +
  2251. '<div><label for="des">{1}</label> <input type="text" id="des" value="" /></div></form>',
  2252. this._("URL:"),
  2253. this._("Description (optional):")
  2254. ))
  2255. .submit(function () {return false;});
  2256. content.append($(
  2257. this._('<div><input type="button" class="button" value="{0}" /></div>',
  2258. this._("Insert")
  2259. )).click(function (e) {
  2260. var val = $(this).parent("form").find("#link").val(),
  2261. description = $(this).parent("form").find("#des").val();
  2262. if(val !== "" && val !== "ftp://") {
  2263. // needed for IE to reset the last range
  2264. editor.focus();
  2265. if(!editor.getRangeHelper().selectedHtml() || description)
  2266. {
  2267. if(!description)
  2268. description = val;
  2269. editor.wysiwygEditorInsertHtml('<a href="' + val + '">' + description + '</a>');
  2270. }
  2271. else
  2272. editor.execCommand("createlink", val);
  2273. }
  2274. editor.closeDropDown(true);
  2275. e.preventDefault();
  2276. }));
  2277. editor.createDropDown(caller, "insertlink", content);
  2278. },
  2279. 'Insert FTP Link'
  2280. );
  2281. $.sceditor.setCommand(
  2282. 'glow',
  2283. function () {
  2284. this.wysiwygEditorInsertHtml('[glow=red,2,300]', '[/glow]');
  2285. },
  2286. 'Glow'
  2287. );
  2288. $.sceditor.setCommand(
  2289. 'shadow',
  2290. function () {
  2291. this.wysiwygEditorInsertHtml('[shadow=red,left]', '[/shadow]');
  2292. },
  2293. 'Shadow'
  2294. );
  2295. $.sceditor.setCommand(
  2296. 'tt',
  2297. function () {
  2298. this.wysiwygEditorInsertHtml('<tt>', '</tt>');
  2299. },
  2300. 'Teletype'
  2301. );