[FIX] web: more backports for cleditor
[odoo/odoo.git] / addons / web / static / lib / cleditor / jquery.cleditor.js
1 /*!\r
2  CLEditor WYSIWYG HTML Editor v1.4.4\r
3  http://premiumsoftware.net/CLEditor\r
4  requires jQuery v1.4.2 or later\r
5 \r
6  Copyright 2010, Chris Landowski, Premium Software, LLC\r
7  Dual licensed under the MIT or GPL Version 2 licenses.\r
8 */\r
9 \r
10 (function ($) {\r
11 \r
12   //==============\r
13   // jQuery Plugin\r
14   //==============\r
15 \r
16   $.cleditor = {\r
17 \r
18     // Define the defaults used for all new cleditor instances\r
19     defaultOptions: {\r
20       width:        'auto', // width not including margins, borders or padding\r
21       height:       250, // height not including margins, borders or padding\r
22       controls:     // controls to add to the toolbar\r
23                     "bold italic underline strikethrough subscript superscript | font size " +\r
24                     "style | color highlight removeformat | bullets numbering | outdent " +\r
25                     "indent | alignleft center alignright justify | undo redo | " +\r
26                     "rule image link unlink | cut copy paste pastetext | print source",\r
27       colors:       // colors in the color popup\r
28                     "FFF FCC FC9 FF9 FFC 9F9 9FF CFF CCF FCF " +\r
29                     "CCC F66 F96 FF6 FF3 6F9 3FF 6FF 99F F9F " +\r
30                     "BBB F00 F90 FC6 FF0 3F3 6CC 3CF 66C C6C " +\r
31                     "999 C00 F60 FC3 FC0 3C0 0CC 36F 63F C3C " +\r
32                     "666 900 C60 C93 990 090 399 33F 60C 939 " +\r
33                     "333 600 930 963 660 060 366 009 339 636 " +\r
34                     "000 300 630 633 330 030 033 006 309 303",    \r
35       fonts:        // font names in the font popup\r
36                     "Arial,Arial Black,Comic Sans MS,Courier New,Narrow,Garamond," +\r
37                     "Georgia,Impact,Sans Serif,Serif,Tahoma,Trebuchet MS,Verdana",\r
38       sizes:        // sizes in the font size popup\r
39                     "1,2,3,4,5,6,7",\r
40       styles:       // styles in the style popup\r
41                     [["Paragraph", "<p>"], ["Header 1", "<h1>"], ["Header 2", "<h2>"],\r
42                     ["Header 3", "<h3>"],  ["Header 4","<h4>"],  ["Header 5","<h5>"],\r
43                     ["Header 6","<h6>"]],\r
44       useCSS:       true, // use CSS to style HTML when possible (not supported in ie)\r
45       docType:      // Document type contained within the editor\r
46                     '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',\r
47       docCSSFile:   // CSS file used to style the document contained within the editor\r
48                     "", \r
49       bodyStyle:    // style to assign to document body contained within the editor\r
50                     "margin:4px; color:#4c4c4c; font-size:13px; font-family:\"Lucida Grande\",Helvetica,Verdana,Arial,sans-serif; cursor:text"\r
51     },\r
52 \r
53     // Define all usable toolbar buttons - the init string property is \r
54     //   expanded during initialization back into the buttons object and \r
55     //   separate object properties are created for each button.\r
56     //   e.g. buttons.size.title = "Font Size"\r
57     buttons: {\r
58       // name,title,command,popupName (""=use name)\r
59       init:\r
60       "bold,,|" +\r
61       "italic,,|" +\r
62       "underline,,|" +\r
63       "strikethrough,,|" +\r
64       "subscript,,|" +\r
65       "superscript,,|" +\r
66       "font,,fontname,|" +\r
67       "size,Font Size,fontsize,|" +\r
68       "style,,formatblock,|" +\r
69       "color,Font Color,forecolor,|" +\r
70       "highlight,Text Highlight Color,hilitecolor,color|" +\r
71       "removeformat,Remove Formatting,|" +\r
72       "bullets,,insertunorderedlist|" +\r
73       "numbering,,insertorderedlist|" +\r
74       "outdent,,|" +\r
75       "indent,,|" +\r
76       "alignleft,Align Text Left,justifyleft|" +\r
77       "center,,justifycenter|" +\r
78       "alignright,Align Text Right,justifyright|" +\r
79       "justify,,justifyfull|" +\r
80       "undo,,|" +\r
81       "redo,,|" +\r
82       "rule,Insert Horizontal Rule,inserthorizontalrule|" +\r
83       "image,Insert Image,insertimage,url|" +\r
84       "link,Insert Hyperlink,createlink,url|" +\r
85       "unlink,Remove Hyperlink,|" +\r
86       "cut,,|" +\r
87       "copy,,|" +\r
88       "paste,,|" +\r
89       "pastetext,Paste as Text,inserthtml,|" +\r
90       "print,,|" +\r
91       "source,Show Source"\r
92     },\r
93 \r
94     // imagesPath - returns the path to the images folder\r
95     imagesPath: function() { return imagesPath(); }\r
96 \r
97   };\r
98 \r
99   // cleditor - creates a new editor for each of the matched textareas\r
100   $.fn.cleditor = function(options) {\r
101 \r
102     // Create a new jQuery object to hold the results\r
103     var $result = $([]);\r
104 \r
105     // Loop through all matching textareas and create the editors\r
106     this.each(function(idx, elem) {\r
107       if (elem.tagName.toUpperCase() === "TEXTAREA") {\r
108         var data = $.data(elem, CLEDITOR);\r
109         if (!data) data = new cleditor(elem, options);\r
110         $result = $result.add(data);\r
111       }\r
112     });\r
113 \r
114     // return the new jQuery object\r
115     return $result;\r
116 \r
117   };\r
118     \r
119   //==================\r
120   // Private Variables\r
121   //==================\r
122 \r
123   var\r
124 \r
125   // Misc constants\r
126   BACKGROUND_COLOR = "backgroundColor",\r
127   BLURRED          = "blurred",\r
128   BUTTON           = "button",\r
129   BUTTON_NAME      = "buttonName",\r
130   CHANGE           = "change",\r
131   CLEDITOR         = "cleditor",\r
132   CLICK            = "click",\r
133   DISABLED         = "disabled",\r
134   DIV_TAG          = "<div>",\r
135   FOCUSED          = "focused",\r
136   TRANSPARENT      = "transparent",\r
137   UNSELECTABLE     = "unselectable",\r
138 \r
139   // Class name constants\r
140   MAIN_CLASS       = "cleditorMain",    // main containing div\r
141   TOOLBAR_CLASS    = "cleditorToolbar", // toolbar div inside main div\r
142   GROUP_CLASS      = "cleditorGroup",   // group divs inside the toolbar div\r
143   BUTTON_CLASS     = "cleditorButton",  // button divs inside group div\r
144   DISABLED_CLASS   = "cleditorDisabled",// disabled button divs\r
145   DIVIDER_CLASS    = "cleditorDivider", // divider divs inside group div\r
146   POPUP_CLASS      = "cleditorPopup",   // popup divs inside body\r
147   LIST_CLASS       = "cleditorList",    // list popup divs inside body\r
148   COLOR_CLASS      = "cleditorColor",   // color popup div inside body\r
149   PROMPT_CLASS     = "cleditorPrompt",  // prompt popup divs inside body\r
150   MSG_CLASS        = "cleditorMsg",     // message popup div inside body\r
151 \r
152   // Browser detection\r
153   ua = navigator.userAgent.toLowerCase(),\r
154   ie = /msie/.test(ua),\r
155   ie6 = /msie\s6/.test(ua),\r
156   iege11 = /(trident)(?:.*rv:([\w.]+))?/.test(ua),\r
157   webkit = /webkit/.test(ua),\r
158 \r
159   // Test for iPhone/iTouch/iPad\r
160   iOS = /iphone|ipad|ipod/i.test(ua),\r
161 \r
162   // Popups are created once as needed and shared by all editor instances\r
163   popups = {},\r
164 \r
165   // Used to prevent the document click event from being bound more than once\r
166   documentClickAssigned,\r
167 \r
168   // Local copy of the buttons object\r
169   buttons = $.cleditor.buttons;\r
170 \r
171   //===============\r
172   // Initialization\r
173   //===============\r
174 \r
175   // Expand the buttons.init string back into the buttons object\r
176   //   and create seperate object properties for each button.\r
177   //   e.g. buttons.size.title = "Font Size"\r
178   $.each(buttons.init.split("|"), function(idx, button) {\r
179     var items = button.split(","), name = items[0];\r
180     buttons[name] = {\r
181       stripIndex: idx,\r
182       name: name,\r
183       title: items[1] === "" ? name.charAt(0).toUpperCase() + name.substr(1) : items[1],\r
184       command: items[2] === "" ? name : items[2],\r
185       popupName: items[3] === "" ? name : items[3]\r
186     };\r
187   });\r
188   delete buttons.init;\r
189 \r
190   //============\r
191   // Constructor\r
192   //============\r
193 \r
194   // cleditor - creates a new editor for the passed in textarea element\r
195   cleditor = function(area, options) {\r
196 \r
197     var editor = this;\r
198 \r
199     // Get the defaults and override with options\r
200     editor.options = options = $.extend({}, $.cleditor.defaultOptions, options);\r
201 \r
202     // Hide the textarea and associate it with this editor\r
203     var $area = editor.$area = $(area)\r
204       .hide()\r
205       .data(CLEDITOR, editor)\r
206       .blur(function() {\r
207         // Update the iframe when the textarea loses focus\r
208         updateFrame(editor, true);\r
209       });\r
210 \r
211     // Create the main container and append the textarea\r
212     var $main = editor.$main = $(DIV_TAG)\r
213       .addClass(MAIN_CLASS)\r
214       .width(options.width)\r
215       .height(options.height);\r
216 \r
217     // Create the toolbar\r
218     var $toolbar = editor.$toolbar = $(DIV_TAG)\r
219       .addClass(TOOLBAR_CLASS)\r
220       .appendTo($main);\r
221 \r
222     // Add the first group to the toolbar\r
223     var $group = $(DIV_TAG)\r
224       .addClass(GROUP_CLASS)\r
225       .appendTo($toolbar);\r
226 \r
227     // Initialize the group width\r
228     var groupWidth = 0;\r
229     \r
230     // Add the buttons to the toolbar\r
231     $.each(options.controls.split(" "), function(idx, buttonName) {\r
232       if (buttonName === "") return true;\r
233 \r
234       // Divider\r
235       if (buttonName === "|") {\r
236 \r
237         // Add a new divider to the group\r
238         var $div = $(DIV_TAG)\r
239           .addClass(DIVIDER_CLASS)\r
240           .appendTo($group);\r
241 \r
242         // Update the group width\r
243         $group.width(groupWidth + 1);\r
244         groupWidth = 0;\r
245 \r
246         // Create a new group\r
247         $group = $(DIV_TAG)\r
248           .addClass(GROUP_CLASS)\r
249           .appendTo($toolbar);\r
250 \r
251       }\r
252 \r
253       // Button\r
254       else {\r
255         \r
256         // Get the button definition\r
257         var button = buttons[buttonName];\r
258 \r
259         // Add a new button to the group\r
260         var $buttonDiv = $(DIV_TAG)\r
261           .data(BUTTON_NAME, button.name)\r
262           .addClass(BUTTON_CLASS)\r
263           .attr("title", button.title)\r
264           .bind(CLICK, $.proxy(buttonClick, editor))\r
265           .appendTo($group)\r
266           .hover(hoverEnter, hoverLeave);\r
267 \r
268         // Update the group width\r
269         groupWidth += 24;\r
270         $group.width(groupWidth + 1);\r
271 \r
272         // Prepare the button image\r
273         var map = {};\r
274         if (button.css) map = button.css;\r
275         else if (button.image) map.backgroundImage = imageUrl(button.image);\r
276         if (button.stripIndex) map.backgroundPosition = button.stripIndex * -24;\r
277         $buttonDiv.css(map);\r
278 \r
279         // Add the unselectable attribute for ie\r
280         if (ie)\r
281           $buttonDiv.attr(UNSELECTABLE, "on");\r
282 \r
283         // Create the popup\r
284         if (button.popupName)\r
285           createPopup(button.popupName, options, button.popupClass,\r
286             button.popupContent, button.popupHover);\r
287         \r
288       }\r
289 \r
290     });\r
291 \r
292     // Add the main div to the DOM and append the textarea\r
293     $main.insertBefore($area)\r
294       .append($area);\r
295 \r
296     // Bind the document click event handler\r
297     if (!documentClickAssigned) {\r
298       $(document).click(function(e) {\r
299         // Dismiss all non-prompt popups\r
300         var $target = $(e.target);\r
301         if (!$target.add($target.parents()).is("." + PROMPT_CLASS))\r
302           hidePopups();\r
303       });\r
304       documentClickAssigned = true;\r
305     }\r
306 \r
307     // Bind the window resize event when the width or height is auto or %\r
308     if (/auto|%/.test("" + options.width + options.height))\r
309       $(window).bind('resize.cleditor', function () {\r
310         //Forcefully blurred iframe contentWindow, chrome, IE, safari doesn't trigger blur on window resize and due to which text disappears\r
311         var contentWindow = editor.$frame[0].contentWindow;\r
312         if(!$.browser.mozilla && contentWindow){\r
313           $(contentWindow).trigger('blur');\r
314         }\r
315         // CHM Note MonkeyPatch: if the DOM is not remove, refresh the cleditor\r
316         if(editor.$main.parent().parent().size()) {\r
317           refresh(editor);\r
318         }\r
319       });\r
320     // Create the iframe and resize the controls\r
321     refresh(editor);\r
322 \r
323   };\r
324 \r
325   //===============\r
326   // Public Methods\r
327   //===============\r
328 \r
329   var fn = cleditor.prototype,\r
330 \r
331   // Expose the following private functions as methods on the cleditor object.\r
332   // The closure compiler will rename the private functions. However, the\r
333   // exposed method names on the cleditor object will remain fixed.\r
334   methods = [\r
335     ["clear", clear],\r
336     ["disable", disable],\r
337     ["execCommand", execCommand],\r
338     ["focus", focus],\r
339     ["hidePopups", hidePopups],\r
340     ["sourceMode", sourceMode, true],\r
341     ["refresh", refresh],\r
342     ["select", select],\r
343     ["selectedHTML", selectedHTML, true],\r
344     ["selectedText", selectedText, true],\r
345     ["showMessage", showMessage],\r
346     ["updateFrame", updateFrame],\r
347     ["updateTextArea", updateTextArea]\r
348   ];\r
349 \r
350   $.each(methods, function(idx, method) {\r
351     fn[method[0]] = function() {\r
352       var editor = this, args = [editor];\r
353       // using each here would cast booleans into objects!\r
354       for(var x = 0; x < arguments.length; x++) {args.push(arguments[x]);}\r
355       var result = method[1].apply(editor, args);\r
356       if (method[2]) return result;\r
357       return editor;\r
358     };\r
359   });\r
360   \r
361   // blurred - shortcut for .bind("blurred", handler) or .trigger("blurred")\r
362   fn.blurred = function(handler) {\r
363     var $this = $(this);\r
364     return handler ? $this.bind(BLURRED, handler) : $this.trigger(BLURRED);\r
365   };\r
366 \r
367   // change - shortcut for .bind("change", handler) or .trigger("change")\r
368   fn.change = function change(handler) {\r
369     var $this = $(this);\r
370     return handler ? $this.bind(CHANGE, handler) : $this.trigger(CHANGE);\r
371   };\r
372 \r
373   // focused - shortcut for .bind("focused", handler) or .trigger("focused")\r
374   fn.focused = function(handler) {\r
375     var $this = $(this);\r
376     return handler ? $this.bind(FOCUSED, handler) : $this.trigger(FOCUSED);\r
377   };\r
378 \r
379   //===============\r
380   // Event Handlers\r
381   //===============\r
382 \r
383   // buttonClick - click event handler for toolbar buttons\r
384   function buttonClick(e) {\r
385 \r
386     var editor = this,\r
387         buttonDiv = e.target,\r
388         buttonName = $.data(buttonDiv, BUTTON_NAME),\r
389         button = buttons[buttonName],\r
390         popupName = button.popupName,\r
391         popup = popups[popupName];\r
392 \r
393     // Check if disabled\r
394     if (editor.disabled || $(buttonDiv).attr(DISABLED) === DISABLED)\r
395       return;\r
396 \r
397     // Fire the buttonClick event\r
398     var data = {\r
399       editor: editor,\r
400       button: buttonDiv,\r
401       buttonName: buttonName,\r
402       popup: popup,\r
403       popupName: popupName,\r
404       command: button.command,\r
405       useCSS: editor.options.useCSS\r
406     };\r
407 \r
408     if (button.buttonClick && button.buttonClick(e, data) === false)\r
409       return false;\r
410 \r
411     // Toggle source\r
412     if (buttonName === "source") {\r
413 \r
414       // Show the iframe\r
415       if (sourceMode(editor)) {\r
416         delete editor.range;\r
417         editor.$area.hide();\r
418         editor.$frame.show();\r
419         buttonDiv.title = button.title;\r
420       }\r
421 \r
422       // Show the textarea\r
423       else {\r
424         editor.$frame.hide();\r
425         editor.$area.show();\r
426         buttonDiv.title = "Show Rich Text";\r
427       }\r
428 \r
429       // Enable or disable the toolbar buttons\r
430       // IE requires the timeout\r
431       setTimeout(function() {refreshButtons(editor);}, 100);\r
432 \r
433     }\r
434 \r
435     // Check for rich text mode\r
436     else if (!sourceMode(editor)) {\r
437 \r
438       // Handle popups\r
439       if (popupName) {\r
440         var $popup = $(popup);\r
441 \r
442         // URL\r
443         if (popupName === "url") {\r
444 \r
445           // Check for selection before showing the link url popup\r
446           if (buttonName === "link" && selectedText(editor) === "") {\r
447             showMessage(editor, "A selection is required when inserting a link.", buttonDiv);\r
448             return false;\r
449           }\r
450 \r
451           // Wire up the submit button click event handler\r
452           $popup.children(":button")\r
453             .unbind(CLICK)\r
454             .bind(CLICK, function() {\r
455 \r
456               // Insert the image or link if a url was entered\r
457               var $text = $popup.find(":text"),\r
458                 url = $.trim($text.val());\r
459               if (url !== "")\r
460                 execCommand(editor, data.command, url, null, data.button);\r
461 \r
462               // Reset the text, hide the popup and set focus\r
463               $text.val("http://");\r
464               hidePopups();\r
465               focus(editor);\r
466 \r
467             });\r
468 \r
469         }\r
470 \r
471         // Paste as Text\r
472         else if (popupName === "pastetext") {\r
473 \r
474           // Wire up the submit button click event handler\r
475           $popup.children(":button")\r
476             .unbind(CLICK)\r
477             .bind(CLICK, function() {\r
478 \r
479               // Insert the unformatted text replacing new lines with break tags\r
480               var $textarea = $popup.find("textarea"),\r
481                 text = $textarea.val().replace(/\n/g, "<br />");\r
482               if (text !== "")\r
483                 execCommand(editor, data.command, text, null, data.button);\r
484 \r
485               // Reset the text, hide the popup and set focus\r
486               $textarea.val("");\r
487               hidePopups();\r
488               focus(editor);\r
489 \r
490             });\r
491 \r
492         }\r
493 \r
494         // Show the popup if not already showing for this button\r
495         if (buttonDiv !== $.data(popup, BUTTON)) {\r
496           showPopup(editor, popup, buttonDiv);\r
497           return false; // stop propagination to document click\r
498         }\r
499 \r
500         // propaginate to document click\r
501         return;\r
502 \r
503       }\r
504 \r
505       // Print\r
506       else if (buttonName === "print")\r
507         editor.$frame[0].contentWindow.print();\r
508 \r
509       // All other buttons\r
510       else if (!execCommand(editor, data.command, data.value, data.useCSS, buttonDiv))\r
511         return false;\r
512 \r
513     }\r
514 \r
515     // Focus the editor\r
516     focus(editor);\r
517 \r
518   }\r
519 \r
520   // hoverEnter - mouseenter event handler for buttons and popup items\r
521   function hoverEnter(e) {\r
522     var $div = $(e.target).closest("div");\r
523     $div.css(BACKGROUND_COLOR, $div.data(BUTTON_NAME) ? "#FFF" : "#FFC");\r
524   }\r
525 \r
526   // hoverLeave - mouseleave event handler for buttons and popup items\r
527   function hoverLeave(e) {\r
528     $(e.target).closest("div").css(BACKGROUND_COLOR, "transparent");\r
529   }\r
530 \r
531   // popupClick - click event handler for popup items\r
532   function popupClick(e) {\r
533 \r
534     var editor = this,\r
535         popup = e.data.popup,\r
536         target = e.target;\r
537 \r
538     // Check for message and prompt popups\r
539     if (popup === popups.msg || $(popup).hasClass(PROMPT_CLASS))\r
540       return;\r
541 \r
542     // Get the button info\r
543     var buttonDiv = $.data(popup, BUTTON),\r
544         buttonName = $.data(buttonDiv, BUTTON_NAME),\r
545         button = buttons[buttonName],\r
546         command = button.command,\r
547         value,\r
548         useCSS = editor.options.useCSS;\r
549 \r
550     // Get the command value\r
551     if (buttonName === "font")\r
552       // Opera returns the fontfamily wrapped in quotes\r
553       value = target.style.fontFamily.replace(/"/g, "");\r
554     else if (buttonName === "size") {\r
555       if (target.tagName.toUpperCase() === "DIV")\r
556         target = target.children[0];\r
557       value = target.innerHTML;\r
558     }\r
559     else if (buttonName === "style")\r
560       value = "<" + target.tagName + ">";\r
561     else if (buttonName === "color")\r
562       value = hex(target.style.backgroundColor);\r
563     else if (buttonName === "highlight") {\r
564       value = hex(target.style.backgroundColor);\r
565       if (ie) command = 'backcolor';\r
566       else useCSS = true;\r
567     }\r
568 \r
569     // Fire the popupClick event\r
570     var data = {\r
571       editor: editor,\r
572       button: buttonDiv,\r
573       buttonName: buttonName,\r
574       popup: popup,\r
575       popupName: button.popupName,\r
576       command: command,\r
577       value: value,\r
578       useCSS: useCSS\r
579     };\r
580 \r
581     if (button.popupClick && button.popupClick(e, data) === false)\r
582       return;\r
583 \r
584     // Execute the command\r
585     if (data.command && !execCommand(editor, data.command, data.value, data.useCSS, buttonDiv))\r
586       return false;\r
587 \r
588     // Hide the popup and focus the editor\r
589     hidePopups();\r
590     focus(editor);\r
591 \r
592   }\r
593 \r
594   //==================\r
595   // Private Functions\r
596   //==================\r
597 \r
598   // checksum - returns a checksum using the Adler-32 method\r
599   function checksum(text)\r
600   {\r
601     var a = 1, b = 0;\r
602     for (var index = 0; index < text.length; ++index) {\r
603       a = (a + text.charCodeAt(index)) % 65521;\r
604       b = (b + a) % 65521;\r
605     }\r
606     return (b << 16) | a;\r
607   }\r
608 \r
609   // clear - clears the contents of the editor\r
610   function clear(editor) {\r
611     editor.$area.val("");\r
612     updateFrame(editor);\r
613   }\r
614 \r
615   // createPopup - creates a popup and adds it to the body\r
616   function createPopup(popupName, options, popupTypeClass, popupContent, popupHover) {\r
617 \r
618     // Check if popup already exists\r
619     if (popups[popupName])\r
620       return popups[popupName];\r
621 \r
622     // Create the popup\r
623     var $popup = $(DIV_TAG)\r
624       .hide()\r
625       .addClass(POPUP_CLASS)\r
626       .appendTo("body");\r
627 \r
628     // Add the content\r
629 \r
630     // Custom popup\r
631     if (popupContent)\r
632       $popup.html(popupContent);\r
633 \r
634     // Color\r
635     else if (popupName === "color") {\r
636       var colors = options.colors.split(" ");\r
637       if (colors.length < 10)\r
638         $popup.width("auto");\r
639       $.each(colors, function(idx, color) {\r
640         $(DIV_TAG).appendTo($popup)\r
641           .css(BACKGROUND_COLOR, "#" + color);\r
642       });\r
643       popupTypeClass = COLOR_CLASS;\r
644     }\r
645 \r
646     // Font\r
647     else if (popupName === "font")\r
648       $.each(options.fonts.split(","), function(idx, font) {\r
649         $(DIV_TAG).appendTo($popup)\r
650           .css("fontFamily", font)\r
651           .html(font);\r
652       });\r
653 \r
654     // Size\r
655     else if (popupName === "size")\r
656       $.each(options.sizes.split(","), function(idx, size) {\r
657         $(DIV_TAG).appendTo($popup)\r
658           .html('<font size="' + size + '">' + size + '</font>');\r
659       });\r
660 \r
661     // Style\r
662     else if (popupName === "style")\r
663       $.each(options.styles, function(idx, style) {\r
664         $(DIV_TAG).appendTo($popup)\r
665           .html(style[1] + style[0] + style[1].replace("<", "</"));\r
666       });\r
667 \r
668     // URL\r
669     else if (popupName === "url") {\r
670       $popup.html('Enter URL:<br /><input type="text" value="http://" size="35" /><br /><input type="button" value="Submit" />');\r
671       popupTypeClass = PROMPT_CLASS;\r
672     }\r
673 \r
674     // Paste as Text\r
675     else if (popupName === "pastetext") {\r
676       $popup.html('Paste your content here and click submit.<br /><textarea cols="40" rows="3"></textarea><br /><input type="button" value="Submit" />');\r
677       popupTypeClass = PROMPT_CLASS;\r
678     }\r
679 \r
680     // Add the popup type class name\r
681     if (!popupTypeClass && !popupContent)\r
682       popupTypeClass = LIST_CLASS;\r
683     $popup.addClass(popupTypeClass);\r
684 \r
685     // Add the unselectable attribute to all items\r
686     if (ie) {\r
687       $popup.attr(UNSELECTABLE, "on")\r
688         .find("div,font,p,h1,h2,h3,h4,h5,h6")\r
689         .attr(UNSELECTABLE, "on");\r
690     }\r
691 \r
692     // Add the hover effect to all items\r
693     if ($popup.hasClass(LIST_CLASS) || popupHover === true)\r
694       $popup.children().hover(hoverEnter, hoverLeave);\r
695 \r
696     // Add the popup to the array and return it\r
697     popups[popupName] = $popup[0];\r
698     return $popup[0];\r
699 \r
700   }\r
701 \r
702   // disable - enables or disables the editor\r
703   function disable(editor, disabled) {\r
704 \r
705     // Update the textarea and save the state\r
706     if (disabled) {\r
707       editor.$area.attr(DISABLED, DISABLED);\r
708       editor.disabled = true;\r
709     }\r
710     else {\r
711       editor.$area.removeAttr(DISABLED);\r
712       delete editor.disabled;\r
713     }\r
714 \r
715     // Switch the iframe into design mode.\r
716     // ie6 does not support designMode.\r
717     // ie7 & ie8 do not properly support designMode="off".\r
718     try {\r
719       if (ie) editor.doc.body.contentEditable = !disabled;\r
720       else editor.doc.designMode = !disabled ? "on" : "off";\r
721     }\r
722     // Firefox 1.5 throws an exception that can be ignored\r
723     // when toggling designMode from off to on.\r
724     catch (err) {}\r
725 \r
726     // Enable or disable the toolbar buttons\r
727     refreshButtons(editor);\r
728 \r
729   }\r
730 \r
731   // execCommand - executes a designMode command\r
732   function execCommand(editor, command, value, useCSS, button) {\r
733 \r
734     // Restore the current ie selection\r
735     restoreRange(editor);\r
736 \r
737     // Set the styling method\r
738     if (!ie) {\r
739       if (useCSS === undefined || useCSS === null)\r
740         useCSS = editor.options.useCSS;\r
741       editor.doc.execCommand("styleWithCSS", 0, useCSS.toString());\r
742     }\r
743 \r
744     // Execute the command and check for error\r
745     var success = true, message;\r
746     if (ie && command.toLowerCase() === "inserthtml")\r
747       getRange(editor).pasteHTML(value);\r
748     else {\r
749       try { success = editor.doc.execCommand(command, 0, value || null); }\r
750       catch (err) { message = err.message; success = false; }\r
751       if (!success) {\r
752         if ("cutcopypaste".indexOf(command) > -1)\r
753           showMessage(editor, "For security reasons, your browser does not support the " +\r
754             command + " command. Try using the keyboard shortcut or context menu instead.",\r
755             button);\r
756         else\r
757           showMessage(editor,\r
758             (message ? message : "Error executing the " + command + " command."),\r
759             button);\r
760       }\r
761     }\r
762 \r
763     // Enable the buttons and update the textarea\r
764     refreshButtons(editor);\r
765     updateTextArea(editor, true);\r
766     return success;\r
767 \r
768   }\r
769 \r
770   // focus - sets focus to either the textarea or iframe\r
771   function focus(editor) {\r
772     setTimeout(function() {\r
773       if (sourceMode(editor)) editor.$area.focus();\r
774       else editor.$frame[0].contentWindow.focus();\r
775       refreshButtons(editor);\r
776     }, 0);\r
777   }\r
778 \r
779   // getRange - gets the current text range object\r
780   function getRange(editor) {\r
781     if (ie) return getSelection(editor).createRange();\r
782     return getSelection(editor).getRangeAt(0);\r
783   }\r
784 \r
785   // getSelection - gets the current text range object\r
786   function getSelection(editor) {\r
787     if (ie) return editor.doc.selection;\r
788     return editor.$frame[0].contentWindow.getSelection();\r
789   }\r
790 \r
791   // hex - returns the hex value for the passed in color string\r
792   function hex(s) {\r
793 \r
794     // hex("rgb(255, 0, 0)") returns #FF0000\r
795     var m = /rgba?\((\d+), (\d+), (\d+)/.exec(s);\r
796     if (m) {\r
797       s = (m[1] << 16 | m[2] << 8 | m[3]).toString(16);\r
798       while (s.length < 6)\r
799         s = "0" + s;\r
800       return "#" + s;\r
801     }\r
802 \r
803     // hex("#F00") returns #FF0000\r
804     var c = s.split("");\r
805     if (s.length === 4)\r
806       return "#" + c[1] + c[1] + c[2] + c[2] + c[3] + c[3];\r
807 \r
808     // hex("#FF0000") returns #FF0000\r
809     return s;\r
810 \r
811   }\r
812 \r
813   // hidePopups - hides all popups\r
814   function hidePopups() {\r
815     $.each(popups, function(idx, popup) {\r
816       $(popup)\r
817         .hide()\r
818         .unbind(CLICK)\r
819         .removeData(BUTTON);\r
820     });\r
821   }\r
822 \r
823   // imagesPath - returns the path to the images folder\r
824   function imagesPath() {\r
825     var href = $("link[href*=cleditor]").attr("href");\r
826     return href.replace(/^(.*\/)[^\/]+$/, '$1') + "images/";\r
827   }\r
828 \r
829   // imageUrl - Returns the css url string for a filemane\r
830   function imageUrl(filename) {\r
831     return "url(" + imagesPath() + filename + ")";\r
832   }\r
833 \r
834   // refresh - creates the iframe and resizes the controls\r
835   function refresh(editor) {\r
836 \r
837     var $main = editor.$main,\r
838       options = editor.options;\r
839 \r
840     // Remove the old iframe\r
841     if (editor.$frame) \r
842       editor.$frame.remove();\r
843 \r
844     // Create a new iframe\r
845     var $frame = editor.$frame = $('<iframe frameborder="0" src="javascript:true;" />')\r
846       .hide()\r
847       .appendTo($main);\r
848 \r
849     // Load the iframe document content\r
850     var contentWindow = $frame[0].contentWindow,\r
851       doc = editor.doc = contentWindow.document,\r
852       $doc = $(doc);\r
853 \r
854     doc.open();\r
855     doc.write(\r
856       options.docType +\r
857       '<html>' +\r
858       ((options.docCSSFile === '') ? '' : '<head><link rel="stylesheet" type="text/css" href="' + options.docCSSFile + '" /></head>') +\r
859       '<body style="' + options.bodyStyle + '"></body></html>'\r
860     );\r
861     doc.close();\r
862 \r
863     // Work around for bug in IE which causes the editor to lose\r
864     // focus when clicking below the end of the document.\r
865     if (ie || iege11)\r
866       $doc.click(function() {focus(editor);});\r
867 \r
868     // Load the content\r
869     updateFrame(editor);\r
870 \r
871     // Bind the ie specific iframe event handlers\r
872     if (ie || iege11) {\r
873 \r
874       // Save the current user selection. This code is needed since IE will\r
875       // reset the selection just after the beforedeactivate event and just\r
876       // before the beforeactivate event.\r
877       $doc.bind("beforedeactivate beforeactivate selectionchange keypress", function(e) {\r
878         \r
879         // Flag the editor as inactive\r
880         if (e.type === "beforedeactivate")\r
881           editor.inactive = true;\r
882 \r
883           // Get rid of the bogus selection and flag the editor as active\r
884         else if (e.type === "beforeactivate") {\r
885           if (!editor.inactive && editor.range && editor.range.length > 1)\r
886             editor.range.shift();\r
887           delete editor.inactive;\r
888         }\r
889 \r
890           // Save the selection when the editor is active\r
891         else if (!editor.inactive) {\r
892           if (!editor.range)\r
893             editor.range = [];\r
894           editor.range.unshift(getRange(editor));\r
895 \r
896           // We only need the last 2 selections\r
897           while (editor.range.length > 2)\r
898             editor.range.pop();\r
899         }\r
900 \r
901       });\r
902 \r
903       // Restore the text range and trigger focused event when the iframe gains focus\r
904       $frame.focus(function() {\r
905         restoreRange(editor);\r
906         $(editor).triggerHandler(FOCUSED);\r
907       });\r
908 \r
909       // Trigger blurred event when the iframe looses focus\r
910       $frame.blur(function() {\r
911         $(editor).triggerHandler(BLURRED);\r
912       });\r
913 \r
914     }\r
915 \r
916       // Trigger focused and blurred events for all other browsers\r
917     else {\r
918       $(editor.$frame[0].contentWindow)\r
919         .focus(function () { $(editor).triggerHandler(FOCUSED); })\r
920         .blur(function () { $(editor).triggerHandler(BLURRED); });\r
921     }\r
922 \r
923     // Enable the toolbar buttons and update the textarea as the user types or clicks\r
924     $doc.click(hidePopups)\r
925       .bind("keyup mouseup", function() {\r
926         refreshButtons(editor);\r
927         updateTextArea(editor, true);\r
928       });\r
929 \r
930     // Show the textarea for iPhone/iTouch/iPad or\r
931     // the iframe when design mode is supported.\r
932     if (iOS) editor.$area.show();\r
933     else $frame.show();\r
934 \r
935     // Wait for the layout to finish - shortcut for $(document).ready()\r
936     $(function() {\r
937 \r
938       var $toolbar = editor.$toolbar,\r
939           $group = $toolbar.children("div:last"),\r
940           wid = /%/.test("" + options.width) ? options.width : $main.width();\r
941 \r
942       // Resize the toolbar\r
943       var hgt = $group.offset().top + $group.outerHeight() - $toolbar.offset().top + 1;\r
944       $toolbar.height(hgt);\r
945 \r
946       // Resize the iframe\r
947       hgt = (/%/.test("" + options.height) ? $main.height() : parseInt(options.height, 10)) - hgt;\r
948       $frame.width(wid).height(hgt);\r
949 \r
950       // Resize the textarea. IE6 textareas have a 1px top\r
951       // & bottom margin that cannot be removed using css.\r
952       editor.$area.width(wid).height(ie6 ? hgt - 2 : hgt);\r
953 \r
954       // Switch the iframe into design mode if enabled\r
955       disable(editor, editor.disabled);\r
956 \r
957       // Enable or disable the toolbar buttons\r
958       refreshButtons(editor);\r
959 \r
960     });\r
961 \r
962   }\r
963 \r
964   // refreshButtons - enables or disables buttons based on availability\r
965   function refreshButtons(editor) {\r
966 \r
967     // Webkit requires focus before queryCommandEnabled will return anything but false\r
968     if (!iOS && webkit && !editor.focused) {\r
969       editor.$frame[0].contentWindow.focus();\r
970       window.focus();\r
971       editor.focused = true;\r
972     }\r
973 \r
974     // Get the object used for checking queryCommandEnabled\r
975     var queryObj = editor.doc;\r
976     if (ie) queryObj = getRange(editor);\r
977 \r
978     // Loop through each button\r
979     var inSourceMode = sourceMode(editor);\r
980     $.each(editor.$toolbar.find("." + BUTTON_CLASS), function(idx, elem) {\r
981 \r
982       var $elem = $(elem),\r
983         button = $.cleditor.buttons[$.data(elem, BUTTON_NAME)],\r
984         command = button.command,\r
985         enabled = true;\r
986 \r
987       // Determine the state\r
988       if (editor.disabled)\r
989         enabled = false;\r
990       else if (button.getEnabled) {\r
991         var data = {\r
992           editor: editor,\r
993           button: elem,\r
994           buttonName: button.name,\r
995           popup: popups[button.popupName],\r
996           popupName: button.popupName,\r
997           command: button.command,\r
998           useCSS: editor.options.useCSS\r
999         };\r
1000         enabled = button.getEnabled(data);\r
1001         if (enabled === undefined)\r
1002           enabled = true;\r
1003       }\r
1004       else if (((inSourceMode || iOS) && button.name !== "source") ||\r
1005       (ie && (command === "undo" || command === "redo")))\r
1006         enabled = false;\r
1007       else if (command && command !== "print") {\r
1008         if (ie && command === "hilitecolor")\r
1009           command = "backcolor";\r
1010         // IE does not support inserthtml, so it's always enabled\r
1011         if (!ie || command !== "inserthtml") {\r
1012           try {enabled = queryObj.queryCommandEnabled(command);}\r
1013           catch (err) {enabled = false;}\r
1014         }\r
1015       }\r
1016 \r
1017       // Enable or disable the button\r
1018       if (enabled) {\r
1019         $elem.removeClass(DISABLED_CLASS);\r
1020         $elem.removeAttr(DISABLED);\r
1021       }\r
1022       else {\r
1023         $elem.addClass(DISABLED_CLASS);\r
1024         $elem.attr(DISABLED, DISABLED);\r
1025       }\r
1026 \r
1027     });\r
1028   }\r
1029 \r
1030   // restoreRange - restores the current ie selection\r
1031   function restoreRange(editor) {\r
1032     if (editor.range) {\r
1033       if (ie)\r
1034         editor.range[0].select();\r
1035       else if (iege11)\r
1036         getSelection(editor).addRange(editor.range[0]);\r
1037     }\r
1038   }\r
1039 \r
1040   // select - selects all the text in either the textarea or iframe\r
1041   function select(editor) {\r
1042     setTimeout(function() {\r
1043       if (sourceMode(editor)) editor.$area.select();\r
1044       else execCommand(editor, "selectall");\r
1045     }, 0);\r
1046   }\r
1047 \r
1048   // selectedHTML - returns the current HTML selection or and empty string\r
1049   function selectedHTML(editor) {\r
1050     restoreRange(editor);\r
1051     var range = getRange(editor);\r
1052     if (ie)\r
1053       return range.htmlText;\r
1054     var layer = $("<layer>")[0];\r
1055     layer.appendChild(range.cloneContents());\r
1056     var html = layer.innerHTML;\r
1057     layer = null;\r
1058     return html;\r
1059   }\r
1060 \r
1061   // selectedText - returns the current text selection or and empty string\r
1062   function selectedText(editor) {\r
1063     restoreRange(editor);\r
1064     if (ie) return getRange(editor).text;\r
1065     return getSelection(editor).toString();\r
1066   }\r
1067 \r
1068   // showMessage - alert replacement\r
1069   function showMessage(editor, message, button) {\r
1070     var popup = createPopup("msg", editor.options, MSG_CLASS);\r
1071     popup.innerHTML = message;\r
1072     showPopup(editor, popup, button);\r
1073   }\r
1074 \r
1075   // showPopup - shows a popup\r
1076   function showPopup(editor, popup, button) {\r
1077 \r
1078     var offset, left, top, $popup = $(popup);\r
1079 \r
1080     // Determine the popup location\r
1081     if (button) {\r
1082       var $button = $(button);\r
1083       offset = $button.offset();\r
1084       left = --offset.left;\r
1085       top = offset.top + $button.height();\r
1086     }\r
1087     else {\r
1088       var $toolbar = editor.$toolbar;\r
1089       offset = $toolbar.offset();\r
1090       left = Math.floor(($toolbar.width() - $popup.width()) / 2) + offset.left;\r
1091       top = offset.top + $toolbar.height() - 2;\r
1092     }\r
1093 \r
1094     // Position and show the popup\r
1095     hidePopups();\r
1096     $popup.css({left: left, top: top})\r
1097       .show();\r
1098 \r
1099     // Assign the popup button and click event handler\r
1100     if (button) {\r
1101       $.data(popup, BUTTON, button);\r
1102       $popup.bind(CLICK, {popup: popup}, $.proxy(popupClick, editor));\r
1103     }\r
1104 \r
1105     // Focus the first input element if any\r
1106     setTimeout(function() {\r
1107       $popup.find(":text,textarea").eq(0).focus().select();\r
1108     }, 100);\r
1109 \r
1110   }\r
1111 \r
1112   // sourceMode - returns true if the textarea is showing\r
1113   function sourceMode(editor) {\r
1114     return editor.$area.is(":visible");\r
1115   }\r
1116 \r
1117   // updateFrame - updates the iframe with the textarea contents\r
1118   function updateFrame(editor, checkForChange) {\r
1119     \r
1120     var code = editor.$area.val(),\r
1121       options = editor.options,\r
1122       updateFrameCallback = options.updateFrame,\r
1123       $body = $(editor.doc.body);\r
1124 \r
1125     // Check for textarea change to avoid unnecessary firing\r
1126     // of potentially heavy updateFrame callbacks.\r
1127     if (updateFrameCallback) {\r
1128       var sum = checksum(code);\r
1129       if (checkForChange && editor.areaChecksum === sum)\r
1130         return;\r
1131       editor.areaChecksum = sum;\r
1132     }\r
1133 \r
1134     // Convert the textarea source code into iframe html\r
1135     var html = updateFrameCallback ? updateFrameCallback(code) : code;\r
1136 \r
1137     // Prevent script injection attacks by html encoding script tags\r
1138     html = html.replace(/<(?=\/?script)/ig, "&lt;");\r
1139 \r
1140     // Update the iframe checksum\r
1141     if (options.updateTextArea)\r
1142       editor.frameChecksum = checksum(html);\r
1143 \r
1144     // Update the iframe and trigger the change event\r
1145     if (html !== $body.html()) {\r
1146       $body.html(html);\r
1147       $(editor).triggerHandler(CHANGE);\r
1148     }\r
1149 \r
1150   }\r
1151 \r
1152   // updateTextArea - updates the textarea with the iframe contents\r
1153   function updateTextArea(editor, checkForChange) {\r
1154 \r
1155     var html = $(editor.doc.body).html(),\r
1156       options = editor.options,\r
1157       updateTextAreaCallback = options.updateTextArea,\r
1158       $area = editor.$area;\r
1159 \r
1160     // Check for iframe change to avoid unnecessary firing\r
1161     // of potentially heavy updateTextArea callbacks.\r
1162     if (updateTextAreaCallback) {\r
1163       var sum = checksum(html);\r
1164       if (checkForChange && editor.frameChecksum === sum)\r
1165         return;\r
1166       editor.frameChecksum = sum;\r
1167     }\r
1168 \r
1169     // Convert the iframe html into textarea source code\r
1170     var code = updateTextAreaCallback ? updateTextAreaCallback(html) : html;\r
1171 \r
1172     // Update the textarea checksum\r
1173     if (options.updateFrame)\r
1174       editor.areaChecksum = checksum(code);\r
1175 \r
1176     // Update the textarea and trigger the change event\r
1177     if (code !== $area.val()) {\r
1178       $area.val(code);\r
1179       $(editor).triggerHandler(CHANGE);\r
1180     }\r
1181 \r
1182   }\r
1183 \r
1184 })(jQuery);\r