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