4 * WYMeditor : what you see is What You Mean web-based editor
5 * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/
6 * Dual licensed under the MIT (MIT-license.txt)
7 * and GPL (GPL-license.txt) licenses.
9 * For further information visit:
10 * http://www.wymeditor.org/
12 * File: jquery.wymeditor.js
14 * Main JS file with core classes and functions.
15 * See the documentation for more info.
19 * Jean-Francois Hovinne (jf.hovinne a-t wymeditor dotorg)
20 * Volker Mische (vmx a-t gmx dotde)
21 * Scott Lewis (lewiscot a-t gmail dotcom)
22 * Bermi Ferrer (wymeditor a-t bermi dotorg)
23 * Daniel Reszka (d.reszka a-t wymeditor dotorg)
24 * Jonatan Lundin (jonatan.lundin _at_ gmail.com)
29 Global WYMeditor namespace.
31 if(!WYMeditor) var WYMeditor = {};
33 //Wrap the Firebug console in WYMeditor.console
35 if ( !window.console || !console.firebug ) {
36 var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml",
37 "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"];
39 WYMeditor.console = {};
40 for (var i = 0; i < names.length; ++i)
41 WYMeditor.console[names[i]] = function() {}
43 } else WYMeditor.console = window.console;
46 jQuery.extend(WYMeditor, {
49 Constants: Global WYMeditor constants.
51 VERSION - Defines WYMeditor version.
52 INSTANCES - An array of loaded WYMeditor.editor instances.
53 STRINGS - An array of loaded WYMeditor language pairs/values.
54 SKINS - An array of loaded WYMeditor skins.
55 NAME - The "name" attribute.
56 INDEX - A string replaced by the instance index.
57 WYM_INDEX - A string used to get/set the instance index.
58 BASE_PATH - A string replaced by WYMeditor's base path.
59 SKIN_PATH - A string replaced by WYMeditor's skin path.
60 WYM_PATH - A string replaced by WYMeditor's main JS file path.
61 SKINS_DEFAULT_PATH - The skins default base path.
62 SKINS_DEFAULT_CSS - The skins default CSS file.
63 LANG_DEFAULT_PATH - The language files default path.
64 IFRAME_BASE_PATH - A string replaced by the designmode iframe's base path.
65 IFRAME_DEFAULT - The iframe's default base path.
66 JQUERY_PATH - A string replaced by the computed jQuery path.
67 DIRECTION - A string replaced by the text direction (rtl or ltr).
68 LOGO - A string replaced by WYMeditor logo.
69 TOOLS - A string replaced by the toolbar's HTML.
70 TOOLS_ITEMS - A string replaced by the toolbar items.
71 TOOL_NAME - A string replaced by a toolbar item's name.
72 TOOL_TITLE - A string replaced by a toolbar item's title.
73 TOOL_CLASS - A string replaced by a toolbar item's class.
74 CLASSES - A string replaced by the classes panel's HTML.
75 CLASSES_ITEMS - A string replaced by the classes items.
76 CLASS_NAME - A string replaced by a class item's name.
77 CLASS_TITLE - A string replaced by a class item's title.
78 CONTAINERS - A string replaced by the containers panel's HTML.
79 CONTAINERS_ITEMS - A string replaced by the containers items.
80 CONTAINER_NAME - A string replaced by a container item's name.
81 CONTAINER_TITLE - A string replaced by a container item's title.
82 CONTAINER_CLASS - A string replaced by a container item's class.
83 HTML - A string replaced by the HTML view panel's HTML.
84 IFRAME - A string replaced by the designmode iframe.
85 STATUS - A string replaced by the status panel's HTML.
86 DIALOG_TITLE - A string replaced by a dialog's title.
87 DIALOG_BODY - A string replaced by a dialog's HTML body.
88 BODY - The BODY element.
89 STRING - The "string" type.
95 UL,OL,LI - HTML elements string representation.
97 TITLE,ALT - HTML attributes string representation.
98 DIALOG_LINK - A link dialog type.
99 DIALOG_IMAGE - An image dialog type.
100 DIALOG_TABLE - A table dialog type.
101 DIALOG_PASTE - A 'Paste from Word' dialog type.
102 BOLD - Command: (un)set selection to <strong>.
103 ITALIC - Command: (un)set selection to <em>.
104 CREATE_LINK - Command: open the link dialog or (un)set link.
105 INSERT_IMAGE - Command: open the image dialog or insert an image.
106 INSERT_TABLE - Command: open the table dialog.
107 PASTE - Command: open the paste dialog.
108 INDENT - Command: nest a list item.
109 OUTDENT - Command: unnest a list item.
110 TOGGLE_HTML - Command: display/hide the HTML view.
111 FORMAT_BLOCK - Command: set a block element to another type.
112 PREVIEW - Command: open the preview dialog.
113 UNLINK - Command: unset a link.
114 INSERT_UNORDEREDLIST- Command: insert an unordered list.
115 INSERT_ORDEREDLIST - Command: insert an ordered list.
116 MAIN_CONTAINERS - An array of the main HTML containers used in WYMeditor.
117 BLOCKS - An array of the HTML block elements.
118 KEY - Standard key codes.
128 INDEX : "{Wym_Index}",
129 WYM_INDEX : "wym_index",
130 BASE_PATH : "{Wym_Base_Path}",
131 CSS_PATH : "{Wym_Css_Path}",
132 WYM_PATH : "{Wym_Wym_Path}",
133 SKINS_DEFAULT_PATH : "skins/",
134 SKINS_DEFAULT_CSS : "skin.css",
135 SKINS_DEFAULT_JS : "skin.js",
136 LANG_DEFAULT_PATH : "lang/",
137 IFRAME_BASE_PATH : "{Wym_Iframe_Base_Path}",
138 IFRAME_DEFAULT : "iframe/default/",
139 JQUERY_PATH : "{Wym_Jquery_Path}",
140 DIRECTION : "{Wym_Direction}",
142 TOOLS : "{Wym_Tools}",
143 TOOLS_ITEMS : "{Wym_Tools_Items}",
144 TOOL_NAME : "{Wym_Tool_Name}",
145 TOOL_TITLE : "{Wym_Tool_Title}",
146 TOOL_CLASS : "{Wym_Tool_Class}",
147 CLASSES : "{Wym_Classes}",
148 CLASSES_ITEMS : "{Wym_Classes_Items}",
149 CLASS_NAME : "{Wym_Class_Name}",
150 CLASS_TITLE : "{Wym_Class_Title}",
151 CONTAINERS : "{Wym_Containers}",
152 CONTAINERS_ITEMS : "{Wym_Containers_Items}",
153 CONTAINER_NAME : "{Wym_Container_Name}",
154 CONTAINER_TITLE : "{Wym_Containers_Title}",
155 CONTAINER_CLASS : "{Wym_Container_Class}",
157 IFRAME : "{Wym_Iframe}",
158 STATUS : "{Wym_Status}",
159 DIALOG_TITLE : "{Wym_Dialog_Title}",
160 DIALOG_BODY : "{Wym_Dialog_Body}",
172 BLOCKQUOTE : "blockquote",
187 DIALOG_LINK : "Link",
188 DIALOG_IMAGE : "Image",
189 DIALOG_TABLE : "Table",
190 DIALOG_PASTE : "Paste_From_Word",
193 CREATE_LINK : "CreateLink",
194 INSERT_IMAGE : "InsertImage",
195 INSERT_TABLE : "InsertTable",
196 INSERT_HTML : "InsertHTML",
200 TOGGLE_HTML : "ToggleHtml",
201 FORMAT_BLOCK : "FormatBlock",
204 INSERT_UNORDEREDLIST: "InsertUnorderedList",
205 INSERT_ORDEREDLIST : "InsertOrderedList",
209 MAIN_CONTAINERS : new Array("p","h1","h2","h3","h4","h5","h6","pre","blockquote"),
211 BLOCKS : new Array("address", "blockquote", "div", "dl",
212 "fieldset", "form", "h1", "h2", "h3", "h4", "h5", "h6", "hr",
213 "noscript", "ol", "p", "pre", "table", "ul", "dd", "dt",
214 "li", "tbody", "td", "tfoot", "th", "thead", "tr"),
225 CURSOR: new Array(37, 38, 39, 40),
236 Class: WYMeditor.editor
237 WYMeditor editor main class, instanciated for each editor occurrence.
240 editor : function(elem, options) {
243 Constructor: WYMeditor.editor
245 Initializes main values (index, elements, paths, ...)
246 and call WYMeditor.editor.init which initializes the editor.
250 elem - The HTML element to be replaced by the editor.
251 options - The hash of options.
259 <WYMeditor.editor.init>
262 //store the instance in the INSTANCES array and store the index
263 this._index = WYMeditor.INSTANCES.push(this) - 1;
264 //store the element replaced by the editor
265 this._element = elem;
267 this._options = options;
268 //store the element's inner value
269 this._html = jQuery(elem).val();
271 //store the HTML option, if any
272 if(this._options.html) this._html = this._options.html;
273 //get or compute the base path (where the main JS file is located)
274 this._options.basePath = this._options.basePath
275 || this.computeBasePath();
276 //get or set the skin path (where the skin files are located)
277 this._options.skinPath = this._options.skinPath
278 || this._options.basePath + WYMeditor.SKINS_DEFAULT_PATH
279 + this._options.skin + '/';
280 //get or compute the main JS file location
281 this._options.wymPath = this._options.wymPath
282 || this.computeWymPath();
283 //get or set the language files path
284 this._options.langPath = this._options.langPath
285 || this._options.basePath + WYMeditor.LANG_DEFAULT_PATH;
286 //get or set the designmode iframe's base path
287 this._options.iframeBasePath = this._options.iframeBasePath
288 || this._options.basePath + WYMeditor.IFRAME_DEFAULT;
289 //get or compute the jQuery JS file location
290 this._options.jQueryPath = this._options.jQueryPath
291 || this.computeJqueryPath();
293 //initialize the editor instance
301 /********** JQUERY **********/
304 * Replace an HTML element by WYMeditor
306 * @example jQuery(".wymeditor").wymeditor(
311 * @desc Example description here
314 * @description WYMeditor is a web-based WYSIWYM XHTML editor
315 * @param Hash hash A hash of parameters
316 * @option Integer iExample Description here
317 * @option String sExample Description here
320 * @cat Plugins/WYMeditor
321 * @author Jean-Francois Hovinne
323 jQuery.fn.wymeditor = function(options) {
325 options = jQuery.extend({
335 iframeBasePath: false,
351 boxHtml: "<div class='wym_box'>"
352 + "<div class='wym_area_top'>"
355 + "<div class='wym_area_left'></div>"
356 + "<div class='wym_area_right'>"
357 + WYMeditor.CONTAINERS
360 + "<div class='wym_area_main'>"
365 + "<div class='wym_area_bottom'>"
370 logoHtml: "<a class='wym_wymeditor_link' "
371 + "href='http://www.wymeditor.org/'>WYMeditor</a>",
373 iframeHtml:"<div class='wym_iframe wym_section'>"
376 + WYMeditor.IFRAME_BASE_PATH
378 + "onload='this.contentWindow.parent.WYMeditor.INSTANCES["
379 + WYMeditor.INDEX + "].initIframe(this)'"
385 toolsHtml: "<div class='wym_tools wym_section'>"
388 + WYMeditor.TOOLS_ITEMS
392 toolsItemHtml: "<li class='"
393 + WYMeditor.TOOL_CLASS
394 + "'><a href='#' name='"
395 + WYMeditor.TOOL_NAME
397 + WYMeditor.TOOL_TITLE
399 + WYMeditor.TOOL_TITLE
403 {'name': 'Bold', 'title': 'Strong', 'css': 'wym_tools_strong'},
404 {'name': 'Italic', 'title': 'Emphasis', 'css': 'wym_tools_emphasis'},
405 {'name': 'Superscript', 'title': 'Superscript',
406 'css': 'wym_tools_superscript'},
407 {'name': 'Subscript', 'title': 'Subscript',
408 'css': 'wym_tools_subscript'},
409 {'name': 'InsertOrderedList', 'title': 'Ordered_List',
410 'css': 'wym_tools_ordered_list'},
411 {'name': 'InsertUnorderedList', 'title': 'Unordered_List',
412 'css': 'wym_tools_unordered_list'},
413 {'name': 'Indent', 'title': 'Indent', 'css': 'wym_tools_indent'},
414 {'name': 'Outdent', 'title': 'Outdent', 'css': 'wym_tools_outdent'},
415 {'name': 'Undo', 'title': 'Undo', 'css': 'wym_tools_undo'},
416 {'name': 'Redo', 'title': 'Redo', 'css': 'wym_tools_redo'},
417 {'name': 'CreateLink', 'title': 'Link', 'css': 'wym_tools_link'},
418 {'name': 'Unlink', 'title': 'Unlink', 'css': 'wym_tools_unlink'},
419 {'name': 'InsertImage', 'title': 'Image', 'css': 'wym_tools_image'},
420 {'name': 'InsertTable', 'title': 'Table', 'css': 'wym_tools_table'},
421 {'name': 'Paste', 'title': 'Paste_From_Word',
422 'css': 'wym_tools_paste'},
423 {'name': 'ToggleHtml', 'title': 'HTML', 'css': 'wym_tools_html'},
424 {'name': 'Preview', 'title': 'Preview', 'css': 'wym_tools_preview'}
427 containersHtml: "<div class='wym_containers wym_section'>"
428 + "<h2>{Containers}</h2>"
430 + WYMeditor.CONTAINERS_ITEMS
434 containersItemHtml:"<li class='"
435 + WYMeditor.CONTAINER_CLASS
437 + "<a href='#' name='"
438 + WYMeditor.CONTAINER_NAME
440 + WYMeditor.CONTAINER_TITLE
444 {'name': 'P', 'title': 'Paragraph', 'css': 'wym_containers_p'},
445 {'name': 'H1', 'title': 'Heading_1', 'css': 'wym_containers_h1'},
446 {'name': 'H2', 'title': 'Heading_2', 'css': 'wym_containers_h2'},
447 {'name': 'H3', 'title': 'Heading_3', 'css': 'wym_containers_h3'},
448 {'name': 'H4', 'title': 'Heading_4', 'css': 'wym_containers_h4'},
449 {'name': 'H5', 'title': 'Heading_5', 'css': 'wym_containers_h5'},
450 {'name': 'H6', 'title': 'Heading_6', 'css': 'wym_containers_h6'},
451 {'name': 'PRE', 'title': 'Preformatted', 'css': 'wym_containers_pre'},
452 {'name': 'BLOCKQUOTE', 'title': 'Blockquote',
453 'css': 'wym_containers_blockquote'},
454 {'name': 'TH', 'title': 'Table_Header', 'css': 'wym_containers_th'}
457 classesHtml: "<div class='wym_classes wym_section'>"
458 + "<h2>{Classes}</h2><ul>"
459 + WYMeditor.CLASSES_ITEMS
462 classesItemHtml: "<li><a href='#' name='"
463 + WYMeditor.CLASS_NAME
465 + WYMeditor.CLASS_TITLE
470 statusHtml: "<div class='wym_status wym_section'>"
471 + "<h2>{Status}</h2>"
474 htmlHtml: "<div class='wym_html wym_section'>"
475 + "<h2>{Source_Code}</h2>"
476 + "<textarea class='wym_html_val'></textarea>"
479 boxSelector: ".wym_box",
480 toolsSelector: ".wym_tools",
481 toolsListSelector: " ul",
482 containersSelector:".wym_containers",
483 classesSelector: ".wym_classes",
484 htmlSelector: ".wym_html",
485 iframeSelector: ".wym_iframe iframe",
486 iframeBodySelector:".wym_iframe",
487 statusSelector: ".wym_status",
488 toolSelector: ".wym_tools a",
489 containerSelector: ".wym_containers a",
490 classSelector: ".wym_classes a",
491 htmlValSelector: ".wym_html_val",
493 hrefSelector: ".wym_href",
494 srcSelector: ".wym_src",
495 titleSelector: ".wym_title",
496 altSelector: ".wym_alt",
497 textSelector: ".wym_text",
499 rowsSelector: ".wym_rows",
500 colsSelector: ".wym_cols",
501 captionSelector: ".wym_caption",
502 summarySelector: ".wym_summary",
504 submitSelector: ".wym_submit",
505 cancelSelector: ".wym_cancel",
508 dialogTypeSelector: ".wym_dialog_type",
509 dialogLinkSelector: ".wym_dialog_link",
510 dialogImageSelector: ".wym_dialog_image",
511 dialogTableSelector: ".wym_dialog_table",
512 dialogPasteSelector: ".wym_dialog_paste",
513 dialogPreviewSelector: ".wym_dialog_preview",
515 updateSelector: ".wymupdate",
516 updateEvent: "click",
519 targetSelector: ".wym_target",
521 dialogFeatures: "menubar=no,titlebar=no,toolbar=no,resizable=no"
522 + ",width=560,height=300,top=0,left=0",
523 dialogFeaturesPreview: "menubar=no,titlebar=no,toolbar=no,resizable=no"
524 + ",scrollbars=yes,width=560,height=300,top=0,left=0",
526 dialogHtml: "<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Strict//EN'"
527 + " 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'>"
529 + WYMeditor.DIRECTION
531 + "<link rel='stylesheet' type='text/css' media='screen'"
536 + WYMeditor.DIALOG_TITLE
538 + "<script type='text/javascript'"
540 + WYMeditor.JQUERY_PATH
542 + "<script type='text/javascript'"
547 + WYMeditor.DIALOG_BODY
550 dialogLinkHtml: "<body class='wym_dialog wym_dialog_link'"
551 + " onload='WYMeditor.INIT_DIALOG(" + WYMeditor.INDEX + ")'"
555 + "<input type='hidden' class='wym_dialog_type' value='"
556 + WYMeditor.DIALOG_LINK
558 + "<legend>{Link}</legend>"
559 + "<div class='row'>"
560 + "<label>{URL}</label>"
561 + "<input type='text' class='wym_href' value='' size='40' />"
563 + "<div class='row'>"
564 + "<label>{Title}</label>"
565 + "<input type='text' class='wym_title' value='' size='40' />"
568 + "<div class='row'>"
569 + "<label>Target</label>"
570 + "<input type='checkbox' class='wym_target' value='_blank' size='40' /> New window"
573 + "<div class='row row-indent'>"
574 + "<input class='wym_submit' type='button'"
575 + " value='{Submit}' />"
576 + "<input class='wym_cancel' type='button'"
577 + "value='{Cancel}' />"
583 dialogImageHtml: "<body class='wym_dialog wym_dialog_image'"
584 + " onload='WYMeditor.INIT_DIALOG(" + WYMeditor.INDEX + ")'"
588 + "<input type='hidden' class='wym_dialog_type' value='"
589 + WYMeditor.DIALOG_IMAGE
591 + "<legend>{Image}</legend>"
592 + "<div class='row'>"
593 + "<label>{URL}</label>"
594 + "<input type='text' class='wym_src' value='' size='40' />"
596 + "<div class='row'>"
597 + "<label>{Alternative_Text}</label>"
598 + "<input type='text' class='wym_alt' value='' size='40' />"
600 + "<div class='row'>"
601 + "<label>{Title}</label>"
602 + "<input type='text' class='wym_title' value='' size='40' />"
604 + "<div class='row row-indent'>"
605 + "<input class='wym_submit' type='button'"
606 + " value='{Submit}' />"
607 + "<input class='wym_cancel' type='button'"
608 + "value='{Cancel}' />"
614 dialogTableHtml: "<body class='wym_dialog wym_dialog_table'"
615 + " onload='WYMeditor.INIT_DIALOG(" + WYMeditor.INDEX + ")'"
619 + "<input type='hidden' class='wym_dialog_type' value='"
620 + WYMeditor.DIALOG_TABLE
622 + "<legend>{Table}</legend>"
623 + "<div class='row'>"
624 + "<label>{Caption}</label>"
625 + "<input type='text' class='wym_caption' value='' size='40' />"
627 + "<div class='row'>"
628 + "<label>{Summary}</label>"
629 + "<input type='text' class='wym_summary' value='' size='40' />"
631 + "<div class='row'>"
632 + "<label>{Number_Of_Rows}</label>"
633 + "<input type='text' class='wym_rows' value='3' size='3' />"
635 + "<div class='row'>"
636 + "<label>{Number_Of_Cols}</label>"
637 + "<input type='text' class='wym_cols' value='2' size='3' />"
639 + "<div class='row row-indent'>"
640 + "<input class='wym_submit' type='button'"
641 + " value='{Submit}' />"
642 + "<input class='wym_cancel' type='button'"
643 + "value='{Cancel}' />"
649 dialogPasteHtml: "<body class='wym_dialog wym_dialog_paste'"
650 + " onload='WYMeditor.INIT_DIALOG(" + WYMeditor.INDEX + ")'"
653 + "<input type='hidden' class='wym_dialog_type' value='"
654 + WYMeditor.DIALOG_PASTE
657 + "<legend>{Paste_From_Word}</legend>"
658 + "<div class='row'>"
659 + "<textarea class='wym_text' rows='10' cols='50'></textarea>"
661 + "<div class='row'>"
662 + "<input class='wym_submit' type='button'"
663 + " value='{Submit}' />"
664 + "<input class='wym_cancel' type='button'"
665 + "value='{Cancel}' />"
671 dialogPreviewHtml: "<body class='wym_dialog wym_dialog_preview'"
672 + " onload='WYMeditor.INIT_DIALOG(" + WYMeditor.INDEX + ")'"
677 stringDelimiterLeft: "{",
678 stringDelimiterRight:"}",
689 return this.each(function() {
691 new WYMeditor.editor(jQuery(this),options);
696 * @description Returns the WYMeditor instance based on its index
699 wymeditors: function(i) {
700 return (WYMeditor.INSTANCES[i]);
705 /********** WYMeditor **********/
708 * @description WYMeditor class
712 * @description Initializes a WYMeditor instance
714 WYMeditor.editor.prototype.init = function() {
716 //load subclass - browser specific
717 //unsupported browsers: do nothing
718 if (jQuery.browser.msie) {
719 var WymClass = new WYMeditor.WymClassExplorer(this);
721 else if (jQuery.browser.mozilla) {
722 var WymClass = new WYMeditor.WymClassMozilla(this);
724 else if (jQuery.browser.opera) {
725 var WymClass = new WYMeditor.WymClassOpera(this);
727 else if (jQuery.browser.safari) {
728 var WymClass = new WYMeditor.WymClassSafari(this);
733 if(jQuery.isFunction(this._options.preInit)) this._options.preInit(this);
735 var SaxListener = new WYMeditor.XhtmlSaxListener();
736 jQuery.extend(SaxListener, WymClass);
737 this.parser = new WYMeditor.XhtmlParser(SaxListener);
739 if(this._options.styles || this._options.stylesheet){
740 this.configureEditorUsingRawCss();
743 this.helper = new WYMeditor.XmlHelper();
745 //extend the Wymeditor object
746 //don't use jQuery.extend since 1.1.4
747 //jQuery.extend(this, WymClass);
748 for (var prop in WymClass) { this[prop] = WymClass[prop]; }
751 this._box = jQuery(this._element).hide().after(this._options.boxHtml).next().addClass('wym_box_' + this._index);
753 //store the instance index in wymbox and element replaced by editor instance
754 //but keep it compatible with jQuery < 1.2.3, see #122
755 if( jQuery.isFunction( jQuery.fn.data ) ) {
756 jQuery.data(this._box.get(0), WYMeditor.WYM_INDEX, this._index);
757 jQuery.data(this._element.get(0), WYMeditor.WYM_INDEX, this._index);
760 var h = WYMeditor.Helper;
762 //construct the iframe
763 var iframeHtml = this._options.iframeHtml;
764 iframeHtml = h.replaceAll(iframeHtml, WYMeditor.INDEX, this._index);
765 iframeHtml = h.replaceAll(iframeHtml, WYMeditor.IFRAME_BASE_PATH, this._options.iframeBasePath);
768 var boxHtml = jQuery(this._box).html();
770 boxHtml = h.replaceAll(boxHtml, WYMeditor.LOGO, this._options.logoHtml);
771 boxHtml = h.replaceAll(boxHtml, WYMeditor.TOOLS, this._options.toolsHtml);
772 boxHtml = h.replaceAll(boxHtml, WYMeditor.CONTAINERS,this._options.containersHtml);
773 boxHtml = h.replaceAll(boxHtml, WYMeditor.CLASSES, this._options.classesHtml);
774 boxHtml = h.replaceAll(boxHtml, WYMeditor.HTML, this._options.htmlHtml);
775 boxHtml = h.replaceAll(boxHtml, WYMeditor.IFRAME, iframeHtml);
776 boxHtml = h.replaceAll(boxHtml, WYMeditor.STATUS, this._options.statusHtml);
778 //construct tools list
779 var aTools = eval(this._options.toolsItems);
782 for(var i = 0; i < aTools.length; i++) {
783 var oTool = aTools[i];
784 if(oTool.name && oTool.title)
785 var sTool = this._options.toolsItemHtml;
786 var sTool = h.replaceAll(sTool, WYMeditor.TOOL_NAME, oTool.name);
787 sTool = h.replaceAll(sTool, WYMeditor.TOOL_TITLE, this._options.stringDelimiterLeft
789 + this._options.stringDelimiterRight);
790 sTool = h.replaceAll(sTool, WYMeditor.TOOL_CLASS, oTool.css);
794 boxHtml = h.replaceAll(boxHtml, WYMeditor.TOOLS_ITEMS, sTools);
796 //construct classes list
797 var aClasses = eval(this._options.classesItems);
800 for(var i = 0; i < aClasses.length; i++) {
801 var oClass = aClasses[i];
802 if(oClass.name && oClass.title)
803 var sClass = this._options.classesItemHtml;
804 sClass = h.replaceAll(sClass, WYMeditor.CLASS_NAME, oClass.name);
805 sClass = h.replaceAll(sClass, WYMeditor.CLASS_TITLE, oClass.title);
809 boxHtml = h.replaceAll(boxHtml, WYMeditor.CLASSES_ITEMS, sClasses);
811 //construct containers list
812 var aContainers = eval(this._options.containersItems);
813 var sContainers = "";
815 for(var i = 0; i < aContainers.length; i++) {
816 var oContainer = aContainers[i];
817 if(oContainer.name && oContainer.title)
818 var sContainer = this._options.containersItemHtml;
819 sContainer = h.replaceAll(sContainer, WYMeditor.CONTAINER_NAME, oContainer.name);
820 sContainer = h.replaceAll(sContainer, WYMeditor.CONTAINER_TITLE,
821 this._options.stringDelimiterLeft
823 + this._options.stringDelimiterRight);
824 sContainer = h.replaceAll(sContainer, WYMeditor.CONTAINER_CLASS, oContainer.css);
825 sContainers += sContainer;
828 boxHtml = h.replaceAll(boxHtml, WYMeditor.CONTAINERS_ITEMS, sContainers);
831 boxHtml = this.replaceStrings(boxHtml);
833 //load html in wymbox
834 jQuery(this._box).html(boxHtml);
836 //hide the html value
837 jQuery(this._box).find(this._options.htmlSelector).hide();
845 WYMeditor.editor.prototype.bindEvents = function() {
850 //handle click event on tools buttons
851 jQuery(this._box).find(this._options.toolSelector).click(function() {
852 wym._iframe.contentWindow.focus(); //See #154
853 wym.exec(jQuery(this).attr(WYMeditor.NAME));
857 //handle click event on containers buttons
858 jQuery(this._box).find(this._options.containerSelector).click(function() {
859 wym.container(jQuery(this).attr(WYMeditor.NAME));
863 //handle keyup event on html value: set the editor value
864 //handle focus/blur events to check if the element has focus, see #147
865 jQuery(this._box).find(this._options.htmlValSelector)
866 .keyup(function() { jQuery(wym._doc.body).html(jQuery(this).val());})
867 .focus(function() { jQuery(this).toggleClass('hasfocus'); })
868 .blur(function() { jQuery(this).toggleClass('hasfocus'); });
870 //handle click event on classes buttons
871 jQuery(this._box).find(this._options.classSelector).click(function() {
873 var aClasses = eval(wym._options.classesItems);
874 var sName = jQuery(this).attr(WYMeditor.NAME);
876 var oClass = WYMeditor.Helper.findByName(aClasses, sName);
879 var jqexpr = oClass.expr;
880 wym.toggleClass(sName, jqexpr);
882 wym._iframe.contentWindow.focus(); //See #154
886 //handle event on update element
887 jQuery(this._options.updateSelector)
888 .bind(this._options.updateEvent, function() {
893 WYMeditor.editor.prototype.ready = function() {
894 return(this._doc != null);
898 /********** METHODS **********/
901 * @description Returns the WYMeditor container
903 WYMeditor.editor.prototype.box = function() {
908 * @description Get/Set the html value
910 WYMeditor.editor.prototype.html = function(html) {
912 if(typeof html === 'string') jQuery(this._doc.body).html(html);
913 else return(jQuery(this._doc.body).html());
917 * @description Cleans up the HTML
919 WYMeditor.editor.prototype.xhtml = function() {
920 return this.parser.parse(this.html());
924 * @description Executes a button command
926 WYMeditor.editor.prototype.exec = function(cmd) {
928 //base function for execCommand
929 //open a dialog or exec
931 case WYMeditor.CREATE_LINK:
932 var container = this.container();
933 if(container || this._selected_image) this.dialog(WYMeditor.DIALOG_LINK);
936 case WYMeditor.INSERT_IMAGE:
937 this.dialog(WYMeditor.DIALOG_IMAGE);
940 case WYMeditor.INSERT_TABLE:
941 this.dialog(WYMeditor.DIALOG_TABLE);
944 case WYMeditor.PASTE:
945 this.dialog(WYMeditor.DIALOG_PASTE);
948 case WYMeditor.TOGGLE_HTML:
952 //partially fixes #121 when the user manually inserts an image
953 if(!jQuery(this._box).find(this._options.htmlSelector).is(':visible'))
957 case WYMeditor.PREVIEW:
958 this.dialog(WYMeditor.PREVIEW, this._options.dialogFeaturesPreview);
968 * @description Get/Set the selected container
970 WYMeditor.editor.prototype.container = function(sType) {
974 var container = null;
976 if(sType.toLowerCase() == WYMeditor.TH) {
978 container = this.container();
980 //find the TD or TH container
981 switch(container.tagName.toLowerCase()) {
983 case WYMeditor.TD: case WYMeditor.TH:
986 var aTypes = new Array(WYMeditor.TD,WYMeditor.TH);
987 container = this.findUp(this.container(), aTypes);
991 //if it exists, switch
992 if(container!=null) {
994 sType = (container.tagName.toLowerCase() == WYMeditor.TD)? WYMeditor.TH: WYMeditor.TD;
995 this.switchTo(container,sType);
1000 //set the container type
1001 var aTypes=new Array(WYMeditor.P,WYMeditor.H1,WYMeditor.H2,WYMeditor.H3,WYMeditor.H4,WYMeditor.H5,
1002 WYMeditor.H6,WYMeditor.PRE,WYMeditor.BLOCKQUOTE);
1003 container = this.findUp(this.container(), aTypes);
1009 //blockquotes must contain a block level element
1010 if(sType.toLowerCase() == WYMeditor.BLOCKQUOTE) {
1012 var blockquote = this.findUp(this.container(), WYMeditor.BLOCKQUOTE);
1014 if(blockquote == null) {
1016 newNode = this._doc.createElement(sType);
1017 container.parentNode.insertBefore(newNode,container);
1018 newNode.appendChild(container);
1019 this.setFocusToNode(newNode.firstChild);
1023 var nodes = blockquote.childNodes;
1024 var lgt = nodes.length;
1025 var firstNode = null;
1027 if(lgt > 0) firstNode = nodes.item(0);
1028 for(var x=0; x<lgt; x++) {
1029 blockquote.parentNode.insertBefore(nodes.item(0),blockquote);
1031 blockquote.parentNode.removeChild(blockquote);
1032 if(firstNode) this.setFocusToNode(firstNode);
1036 else this.switchTo(container,sType);
1042 else return(this.selected());
1045 /* @name toggleClass
1046 * @description Toggles class on selected element, or one of its parents
1048 WYMeditor.editor.prototype.toggleClass = function(sClass, jqexpr) {
1050 var container = (this._selected_image
1051 ? this._selected_image
1052 : jQuery(this.selected()));
1053 container = jQuery(container).parentsOrSelf(jqexpr);
1054 jQuery(container).toggleClass(sClass);
1056 if(!jQuery(container).attr(WYMeditor.CLASS)) jQuery(container).removeAttr(this._class);
1061 * @description Returns the first parent or self container, based on its type
1063 WYMeditor.editor.prototype.findUp = function(node, filter) {
1065 //filter is a string or an array of strings
1069 var tagname = node.tagName.toLowerCase();
1071 if(typeof(filter) == WYMeditor.STRING) {
1073 while(tagname != filter && tagname != WYMeditor.BODY) {
1075 node = node.parentNode;
1076 tagname = node.tagName.toLowerCase();
1083 while(!bFound && tagname != WYMeditor.BODY) {
1084 for(var i = 0; i < filter.length; i++) {
1085 if(tagname == filter[i]) {
1091 node = node.parentNode;
1092 tagname = node.tagName.toLowerCase();
1097 if(tagname != WYMeditor.BODY) return(node);
1100 } else return(null);
1104 * @description Switch the node's type
1106 WYMeditor.editor.prototype.switchTo = function(node,sType) {
1108 var newNode = this._doc.createElement(sType);
1109 var html = jQuery(node).html();
1110 node.parentNode.replaceChild(newNode,node);
1111 jQuery(newNode).html(html);
1112 this.setFocusToNode(newNode);
1115 WYMeditor.editor.prototype.replaceStrings = function(sVal) {
1116 //check if the language file has already been loaded
1117 //if not, get it via a synchronous ajax call
1118 if(!WYMeditor.STRINGS[this._options.lang]) {
1120 eval(jQuery.ajax({url:this._options.langPath
1121 + this._options.lang + '.js', async:false}).responseText);
1123 WYMeditor.console.error("WYMeditor: error while parsing language file.");
1128 //replace all the strings in sVal and return it
1129 for (var key in WYMeditor.STRINGS[this._options.lang]) {
1130 sVal = WYMeditor.Helper.replaceAll(sVal, this._options.stringDelimiterLeft + key
1131 + this._options.stringDelimiterRight,
1132 WYMeditor.STRINGS[this._options.lang][key]);
1137 WYMeditor.editor.prototype.encloseString = function(sVal) {
1139 return(this._options.stringDelimiterLeft
1141 + this._options.stringDelimiterRight);
1145 * @description Prints a status message
1147 WYMeditor.editor.prototype.status = function(sMessage) {
1149 //print status message
1150 jQuery(this._box).find(this._options.statusSelector).html(sMessage);
1154 * @description Updates the element and textarea values
1156 WYMeditor.editor.prototype.update = function() {
1158 var html = this.xhtml();
1159 jQuery(this._element).val(html);
1160 jQuery(this._box).find(this._options.htmlValSelector).not('.hasfocus').val(html); //#147
1164 * @description Opens a dialog box
1166 WYMeditor.editor.prototype.dialog = function( dialogType, dialogFeatures, bodyHtml ) {
1168 var features = dialogFeatures || this._wym._options.dialogFeatures;
1169 var wDialog = window.open('', 'dialog', features);
1175 switch( dialogType ) {
1177 case(WYMeditor.DIALOG_LINK):
1178 sBodyHtml = this._options.dialogLinkHtml;
1180 case(WYMeditor.DIALOG_IMAGE):
1181 sBodyHtml = this._options.dialogImageHtml;
1183 case(WYMeditor.DIALOG_TABLE):
1184 sBodyHtml = this._options.dialogTableHtml;
1186 case(WYMeditor.DIALOG_PASTE):
1187 sBodyHtml = this._options.dialogPasteHtml;
1189 case(WYMeditor.PREVIEW):
1190 sBodyHtml = this._options.dialogPreviewHtml;
1194 sBodyHtml = bodyHtml;
1197 var h = WYMeditor.Helper;
1199 //construct the dialog
1200 var dialogHtml = this._options.dialogHtml;
1201 dialogHtml = h.replaceAll(dialogHtml, WYMeditor.BASE_PATH, this._options.basePath);
1202 dialogHtml = h.replaceAll(dialogHtml, WYMeditor.DIRECTION, this._options.direction);
1203 dialogHtml = h.replaceAll(dialogHtml, WYMeditor.CSS_PATH, this._options.skinPath + WYMeditor.SKINS_DEFAULT_CSS);
1204 dialogHtml = h.replaceAll(dialogHtml, WYMeditor.WYM_PATH, this._options.wymPath);
1205 dialogHtml = h.replaceAll(dialogHtml, WYMeditor.JQUERY_PATH, this._options.jQueryPath);
1206 dialogHtml = h.replaceAll(dialogHtml, WYMeditor.DIALOG_TITLE, this.encloseString( dialogType ));
1207 dialogHtml = h.replaceAll(dialogHtml, WYMeditor.DIALOG_BODY, sBodyHtml);
1208 dialogHtml = h.replaceAll(dialogHtml, WYMeditor.INDEX, this._index);
1210 dialogHtml = this.replaceStrings(dialogHtml);
1212 var doc = wDialog.document;
1213 doc.write(dialogHtml);
1219 * @description Shows/Hides the HTML
1221 WYMeditor.editor.prototype.toggleHtml = function() {
1222 jQuery(this._box).find(this._options.htmlSelector).toggle();
1225 WYMeditor.editor.prototype.uniqueStamp = function() {
1226 var now = new Date();
1227 return("wym-" + now.getTime());
1230 WYMeditor.editor.prototype.paste = function(sData) {
1233 var container = this.selected();
1235 //split the data, using double newlines as the separator
1236 var aP = sData.split(this._newLine + this._newLine);
1237 var rExp = new RegExp(this._newLine, "g");
1239 //add a P for each item
1240 if(container && container.tagName.toLowerCase() != WYMeditor.BODY) {
1241 for(x = aP.length - 1; x >= 0; x--) {
1243 //simple newlines are replaced by a break
1244 sTmp = sTmp.replace(rExp, "<br />");
1245 jQuery(container).after("<p>" + sTmp + "</p>");
1248 for(x = 0; x < aP.length; x++) {
1250 //simple newlines are replaced by a break
1251 sTmp = sTmp.replace(rExp, "<br />");
1252 jQuery(this._doc.body).append("<p>" + sTmp + "</p>");
1258 WYMeditor.editor.prototype.insert = function(html) {
1259 // Do we have a selection?
1260 if (this._iframe.contentWindow.getSelection().focusNode != null) {
1261 // Overwrite selection with provided html
1262 this._exec( WYMeditor.INSERT_HTML, html);
1264 // Fall back to the internal paste function if there's no selection
1269 WYMeditor.editor.prototype.wrap = function(left, right) {
1270 // Do we have a selection?
1271 if (this._iframe.contentWindow.getSelection().focusNode != null) {
1272 // Wrap selection with provided html
1273 this._exec( WYMeditor.INSERT_HTML, left + this._iframe.contentWindow.getSelection().toString() + right);
1277 WYMeditor.editor.prototype.unwrap = function() {
1278 // Do we have a selection?
1279 if (this._iframe.contentWindow.getSelection().focusNode != null) {
1281 this._exec( WYMeditor.INSERT_HTML, this._iframe.contentWindow.getSelection().toString() );
1285 WYMeditor.editor.prototype.addCssRules = function(doc, aCss) {
1286 var styles = doc.styleSheets[0];
1288 for(var i = 0; i < aCss.length; i++) {
1290 if(oCss.name && oCss.css) this.addCssRule(styles, oCss);
1295 /********** CONFIGURATION **********/
1297 WYMeditor.editor.prototype.computeBasePath = function() {
1298 return jQuery(jQuery.grep(jQuery('script'), function(s){
1299 return (s.src && s.src.match(/jquery\.wymeditor(\.pack|\.min|\.packed)?\.js(\?.*)?$/ ))
1300 })).attr('src').replace(/jquery\.wymeditor(\.pack|\.min|\.packed)?\.js(\?.*)?$/, '');
1303 WYMeditor.editor.prototype.computeWymPath = function() {
1304 return jQuery(jQuery.grep(jQuery('script'), function(s){
1305 return (s.src && s.src.match(/jquery\.wymeditor(\.pack|\.min|\.packed)?\.js(\?.*)?$/ ))
1309 WYMeditor.editor.prototype.computeJqueryPath = function() {
1310 return jQuery(jQuery.grep(jQuery('script'), function(s){
1311 return (s.src && s.src.match(/jquery(-(.*)){0,1}(\.pack|\.min|\.packed)?\.js(\?.*)?$/ ))
1315 WYMeditor.editor.prototype.computeCssPath = function() {
1316 return jQuery(jQuery.grep(jQuery('link'), function(s){
1317 return (s.href && s.href.match(/wymeditor\/skins\/(.*)screen\.css(\?.*)?$/ ))
1321 WYMeditor.editor.prototype.configureEditorUsingRawCss = function() {
1323 var CssParser = new WYMeditor.WymCssParser();
1324 if(this._options.stylesheet){
1325 CssParser.parse(jQuery.ajax({url: this._options.stylesheet,async:false}).responseText);
1327 CssParser.parse(this._options.styles, false);
1330 if(this._options.classesItems.length == 0) {
1331 this._options.classesItems = CssParser.css_settings.classesItems;
1333 if(this._options.editorStyles.length == 0) {
1334 this._options.editorStyles = CssParser.css_settings.editorStyles;
1336 if(this._options.dialogStyles.length == 0) {
1337 this._options.dialogStyles = CssParser.css_settings.dialogStyles;
1341 /********** EVENTS **********/
1343 WYMeditor.editor.prototype.listen = function() {
1345 //don't use jQuery.find() on the iframe body
1346 //because of MSIE + jQuery + expando issue (#JQ1143)
1347 //jQuery(this._doc.body).find("*").bind("mouseup", this.mouseup);
1349 jQuery(this._doc.body).bind("mousedown", this.mousedown);
1350 var images = this._doc.body.getElementsByTagName("img");
1351 for(var i=0; i < images.length; i++) {
1352 jQuery(images[i]).bind("mousedown", this.mousedown);
1356 WYMeditor.editor.prototype.mousedown = function(evt) {
1358 var wym = WYMeditor.INSTANCES[this.ownerDocument.title];
1359 wym._selected_image = (this.tagName.toLowerCase() == WYMeditor.IMG) ? this : null;
1360 evt.stopPropagation();
1363 /********** SKINS **********/
1366 * Function: WYMeditor.loadCss
1367 * Loads a stylesheet in the document.
1370 * href - The CSS path.
1372 WYMeditor.loadCss = function(href) {
1374 var link = document.createElement('link');
1375 link.rel = 'stylesheet';
1378 var head = jQuery('head').get(0);
1379 head.appendChild(link);
1383 * Function: WYMeditor.editor.loadSkin
1384 * Loads the skin CSS and initialization script (if needed).
1386 WYMeditor.editor.prototype.loadSkin = function() {
1388 //does the user want to automatically load the CSS (default: yes)?
1389 //we also test if it hasn't been already loaded by another instance
1390 //see below for a better (second) test
1391 if(this._options.loadSkin && !WYMeditor.SKINS[this._options.skin]) {
1393 //check if it hasn't been already loaded
1394 //so we don't load it more than once
1395 //(we check the existing <link> elements)
1398 var rExp = new RegExp(this._options.skin
1399 + '\/' + WYMeditor.SKINS_DEFAULT_CSS + '$');
1401 jQuery('link').each( function() {
1402 if(this.href.match(rExp)) found = true;
1405 //load it, using the skin path
1406 if(!found) WYMeditor.loadCss( this._options.skinPath
1407 + WYMeditor.SKINS_DEFAULT_CSS );
1410 //put the classname (ex. wym_skin_default) on wym_box
1411 jQuery(this._box).addClass( "wym_skin_" + this._options.skin );
1413 //does the user want to use some JS to initialize the skin (default: yes)?
1414 //also check if it hasn't already been loaded by another instance
1415 if(this._options.initSkin && !WYMeditor.SKINS[this._options.skin]) {
1417 eval(jQuery.ajax({url:this._options.skinPath
1418 + WYMeditor.SKINS_DEFAULT_JS, async:false}).responseText);
1421 //init the skin, if needed
1422 if(WYMeditor.SKINS[this._options.skin]
1423 && WYMeditor.SKINS[this._options.skin].init)
1424 WYMeditor.SKINS[this._options.skin].init(this);
1429 /********** DIALOGS **********/
1431 WYMeditor.INIT_DIALOG = function(index) {
1433 var wym = window.opener.WYMeditor.INSTANCES[index];
1434 var doc = window.document;
1435 var selected = wym.selected();
1436 var dialogType = jQuery(wym._options.dialogTypeSelector).val();
1437 var sStamp = wym.uniqueStamp();
1439 switch(dialogType) {
1441 case WYMeditor.DIALOG_LINK:
1442 //ensure that we select the link to populate the fields
1443 if(selected && selected.tagName && selected.tagName.toLowerCase != WYMeditor.A)
1444 selected = jQuery(selected).parentsOrSelf(WYMeditor.A);
1446 //fix MSIE selection if link image has been clicked
1447 if(!selected && wym._selected_image)
1448 selected = jQuery(wym._selected_image).parentsOrSelf(WYMeditor.A);
1454 //pre-init functions
1455 if(jQuery.isFunction(wym._options.preInitDialog))
1456 wym._options.preInitDialog(wym,window);
1458 //add css rules from options
1459 var styles = doc.styleSheets[0];
1460 var aCss = eval(wym._options.dialogStyles);
1462 wym.addCssRules(doc, aCss);
1464 //auto populate fields if selected container (e.g. A)
1466 jQuery(wym._options.hrefSelector).val(jQuery(selected).attr(WYMeditor.HREF));
1467 jQuery(wym._options.srcSelector).val(jQuery(selected).attr(WYMeditor.SRC));
1468 jQuery(wym._options.titleSelector).val(jQuery(selected).attr(WYMeditor.TITLE));
1469 jQuery(wym._options.altSelector).val(jQuery(selected).attr(WYMeditor.ALT));
1471 if (jQuery(selected).attr(WYMeditor.TARGET) == "_blank")
1472 jQuery(wym._options.targetSelector).attr("checked", "checked");
1475 //auto populate image fields if selected image
1476 if(wym._selected_image) {
1477 jQuery(wym._options.dialogImageSelector + " " + wym._options.srcSelector)
1478 .val(jQuery(wym._selected_image).attr(WYMeditor.SRC));
1479 jQuery(wym._options.dialogImageSelector + " " + wym._options.titleSelector)
1480 .val(jQuery(wym._selected_image).attr(WYMeditor.TITLE));
1481 jQuery(wym._options.dialogImageSelector + " " + wym._options.altSelector)
1482 .val(jQuery(wym._selected_image).attr(WYMeditor.ALT));
1485 jQuery(wym._options.dialogLinkSelector + " "
1486 + wym._options.submitSelector).click(function() {
1489 var sUrl = jQuery(wym._options.hrefSelector).val();
1490 var target = jQuery(wym._options.targetSelector).attr('checked');
1491 if(sUrl.length > 0 && target) {
1493 wym._exec(WYMeditor.CREATE_LINK, sStamp);
1494 jQuery("a[href=" + sStamp + "]", wym._doc.body)
1495 .attr(WYMeditor.HREF, sUrl)
1496 .attr(WYMeditor.TITLE, jQuery(wym._options.titleSelector).val())
1497 .attr(WYMeditor.TARGET, "_blank");
1501 wym._exec(WYMeditor.CREATE_LINK, sStamp);
1502 jQuery("a[href=" + sStamp + "]", wym._doc.body)
1503 .attr(WYMeditor.HREF, sUrl)
1504 .attr(WYMeditor.TITLE, jQuery(wym._options.titleSelector).val())
1505 .removeAttr(WYMeditor.TARGET);
1512 jQuery(wym._options.dialogImageSelector + " "
1513 + wym._options.submitSelector).click(function() {
1515 var sUrl = jQuery(wym._options.srcSelector).val();
1516 if(sUrl.length > 0) {
1518 wym._exec(WYMeditor.INSERT_IMAGE, sStamp);
1520 jQuery("img[src$=" + sStamp + "]", wym._doc.body)
1521 .attr(WYMeditor.SRC, sUrl)
1522 .attr(WYMeditor.TITLE, jQuery(wym._options.titleSelector).val())
1523 .attr(WYMeditor.ALT, jQuery(wym._options.altSelector).val());
1528 jQuery(wym._options.dialogTableSelector + " "
1529 + wym._options.submitSelector).click(function() {
1531 var iRows = jQuery(wym._options.rowsSelector).val();
1532 var iCols = jQuery(wym._options.colsSelector).val();
1534 if(iRows > 0 && iCols > 0) {
1536 var table = wym._doc.createElement(WYMeditor.TABLE);
1540 var sCaption = jQuery(wym._options.captionSelector).val();
1542 //we create the caption
1543 var newCaption = table.createCaption();
1544 newCaption.innerHTML = sCaption;
1546 //we create the rows and cells
1547 for(x=0; x<iRows; x++) {
1548 newRow = table.insertRow(x);
1549 for(y=0; y<iCols; y++) {newRow.insertCell(y);}
1552 //set the summary attr
1553 jQuery(table).attr('summary',
1554 jQuery(wym._options.summarySelector).val());
1556 //append the table after the selected container
1557 var node = jQuery(wym.findUp(wym.container(),
1558 WYMeditor.MAIN_CONTAINERS)).get(0);
1559 if(!node || !node.parentNode) jQuery(wym._doc.body).append(table);
1560 else jQuery(node).after(table);
1565 jQuery(wym._options.dialogPasteSelector + " "
1566 + wym._options.submitSelector).click(function() {
1568 var sText = jQuery(wym._options.textSelector).val();
1573 jQuery(wym._options.dialogPreviewSelector + " "
1574 + wym._options.previewSelector)
1578 jQuery(wym._options.cancelSelector).mousedown(function() {
1582 //pre-init functions
1583 if(jQuery.isFunction(wym._options.postInitDialog))
1584 wym._options.postInitDialog(wym,window);
1588 /********** XHTML LEXER/PARSER **********/
1592 * @description Use these methods to generate XML and XHTML compliant tags and
1593 * escape tag attributes correctly
1594 * @author Bermi Ferrer - http://bermi.org
1595 * @author David Heinemeier Hansson http://loudthinking.com
1597 WYMeditor.XmlHelper = function()
1599 this._entitiesDiv = document.createElement('div');
1607 * Returns an empty HTML tag of type *name* which by default is XHTML
1608 * compliant. Setting *open* to true will create an open tag compatible
1609 * with HTML 4.0 and below. Add HTML attributes by passing an attributes
1610 * array to *options*. For attributes with no value like (disabled and
1611 * readonly), give it a value of true in the *options* array.
1617 * this.tag ('br', false, true)
1619 * this.tag ('input', jQuery({type:'text',disabled:true }) )
1620 * # => <input type="text" disabled="disabled" />
1622 WYMeditor.XmlHelper.prototype.tag = function(name, options, open)
1624 options = options || false;
1625 open = open || false;
1626 return '<'+name+(options ? this.tagOptions(options) : '')+(open ? '>' : ' />');
1632 * Returns a XML block tag of type *name* surrounding the *content*. Add
1633 * XML attributes by passing an attributes array to *options*. For attributes
1634 * with no value like (disabled and readonly), give it a value of true in
1635 * the *options* array. You can use symbols or strings for the attribute names.
1637 * this.contentTag ('p', 'Hello world!' )
1638 * # => <p>Hello world!</p>
1639 * this.contentTag('div', this.contentTag('p', "Hello world!"), jQuery({class : "strong"}))
1640 * # => <div class="strong"><p>Hello world!</p></div>
1641 * this.contentTag("select", options, jQuery({multiple : true}))
1642 * # => <select multiple="multiple">...options...</select>
1644 WYMeditor.XmlHelper.prototype.contentTag = function(name, content, options)
1646 options = options || false;
1647 return '<'+name+(options ? this.tagOptions(options) : '')+'>'+content+'</'+name+'>';
1651 * @name cdataSection
1653 * Returns a CDATA section for the given +content+. CDATA sections
1654 * are used to escape blocks of text containing characters which would
1655 * otherwise be recognized as markup. CDATA sections begin with the string
1656 * <tt><![CDATA[</tt> and } with (and may not contain) the string
1659 WYMeditor.XmlHelper.prototype.cdataSection = function(content)
1661 return '<![CDATA['+content+']]>';
1668 * Returns the escaped +xml+ without affecting existing escaped entities.
1670 * this.escapeOnce( "1 > 2 & 3")
1671 * # => "1 > 2 & 3"
1673 WYMeditor.XmlHelper.prototype.escapeOnce = function(xml)
1675 return this._fixDoubleEscape(this.escapeEntities(xml));
1679 * @name _fixDoubleEscape
1681 * Fix double-escaped entities, such as &amp;, &#123;, etc.
1683 WYMeditor.XmlHelper.prototype._fixDoubleEscape = function(escaped)
1685 return escaped.replace(/&([a-z]+|(#\d+));/ig, "&$1;");
1691 * Takes an array like the one generated by Tag.parseAttributes
1692 * [["src", "http://www.editam.com/?a=b&c=d&f=g"], ["title", "Editam, <Simplified> CMS"]]
1693 * or an object like {src:"http://www.editam.com/?a=b&c=d&f=g", title:"Editam, <Simplified> CMS"}
1694 * and returns a string properly escaped like
1695 * ' src = "http://www.editam.com/?a=b&c=d&f=g" title = "Editam, <Simplified> CMS"'
1696 * which is valid for strict XHTML
1698 WYMeditor.XmlHelper.prototype.tagOptions = function(options)
1701 xml._formated_options = '';
1703 for (var key in options) {
1704 var formated_options = '';
1705 var value = options[key];
1706 if(typeof value != 'function' && value.length > 0) {
1708 if(parseInt(key) == key && typeof value == 'object'){
1709 key = value.shift();
1710 value = value.pop();
1712 if(key != '' && value != ''){
1713 xml._formated_options += ' '+key+'="'+xml.escapeOnce(value)+'"';
1717 return xml._formated_options;
1721 * @name escapeEntities
1723 * Escapes XML/HTML entities <, >, & and ". If seccond parameter is set to false it
1724 * will not escape ". If set to true it will also escape '
1726 WYMeditor.XmlHelper.prototype.escapeEntities = function(string, escape_quotes)
1728 this._entitiesDiv.innerHTML = string;
1729 this._entitiesDiv.textContent = string;
1730 var result = this._entitiesDiv.innerHTML;
1731 if(typeof escape_quotes == 'undefined'){
1732 if(escape_quotes != false) result = result.replace('"', '"');
1733 if(escape_quotes == true) result = result.replace('"', ''');
1739 * Parses a string conatining tag attributes and values an returns an array formated like
1740 * [["src", "http://www.editam.com"], ["title", "Editam, Simplified CMS"]]
1742 WYMeditor.XmlHelper.prototype.parseAttributes = function(tag_attributes)
1744 // Use a compounded regex to match single quoted, double quoted and unquoted attribute pairs
1746 var matches = tag_attributes.split(/((=\s*")(")("))|((=\s*\')(\')(\'))|((=\s*[^>\s]*))/g);
1747 if(matches.toString() != tag_attributes){
1748 for (var k in matches) {
1750 if(typeof v != 'function' && v.length != 0){
1751 var re = new RegExp('(\\w+)\\s*'+v);
1752 if(match = tag_attributes.match(re) ){
1753 var value = v.replace(/^[\s=]+/, "");
1754 var delimiter = value.charAt(0);
1755 delimiter = delimiter == '"' ? '"' : (delimiter=="'"?"'":'');
1756 if(delimiter != ''){
1757 value = delimiter == '"' ? value.replace(/^"|"+$/g, '') : value.replace(/^'|'+$/g, '');
1759 tag_attributes = tag_attributes.replace(match[0],'');
1760 result.push([match[1] , value]);
1769 * XhtmlValidator for validating tag attributes
1771 * @author Bermi Ferrer - http://bermi.org
1773 WYMeditor.XhtmlValidator = {
1821 "accesskey":/^(\w){1}$/,
1822 "tabindex":/^(\d)+$/
1917 "rel":/^(alternate|designates|stylesheet|start|next|prev|contents|index|glossary|copyright|chapter|section|subsection|appendix|help|bookmark| |shortcut|icon)+$/,
1918 "rev":/^(alternate|designates|stylesheet|start|next|prev|contents|index|glossary|copyright|chapter|section|subsection|appendix|help|bookmark| |shortcut|icon)+$/,
1919 "shape":/^(rect|rectangle|circ|circle|poly|polygon)$/,
1935 "nohref":/^(true|false)$/,
1936 "shape":/^(rect|rectangle|circ|circle|poly|polygon)$/
1975 "disabled":/^(disabled)$/,
1976 "type":/^(button|reset|submit)$/,
1988 "align":/^(right|left|center|justify)$/,
1992 "valign":/^(top|middle|bottom|baseline)$/,
2001 "align":/^(right|left|center|justify)$/,
2005 "valign":/^(top|middle|bottom|baseline)$/,
2015 "datetime":/^([0-9]){8}/
2033 "2":"accept-charset",
2035 "method":/^(get|post)$/
2083 "checked":/^(checked)$/,
2084 "disabled":/^(disabled)$/,
2085 "maxlength":/^(\d)+$/,
2087 "readonly":/^(readonly)$/,
2090 "type":/^(button|checkbox|file|hidden|image|password|radio|reset|submit|text)$/,
2100 "datetime":/^([0-9]){8}/
2120 "media":/^(all|braille|print|projection|screen|speech|,|;| )+$/i,
2121 //next comment line required by Opera!
2122 /*"rel":/^(alternate|appendix|bookmark|chapter|contents|copyright|glossary|help|home|index|next|prev|section|start|stylesheet|subsection| |shortcut|icon)+$/i,*/
2123 "rel":/^(alternate|appendix|bookmark|chapter|contents|copyright|glossary|help|home|index|next|prev|section|start|stylesheet|subsection| |shortcut|icon)+$/i,
2124 "rev":/^(alternate|appendix|bookmark|chapter|contents|copyright|glossary|help|home|index|next|prev|section|start|stylesheet|subsection| |shortcut|icon)+$/i,
2144 "http-equiv":/^(content\-type|expires|refresh|set\-cookie)$/i,
2176 "disabled": /^(disabled)$/
2187 "disabled":/^(disabled)$/,
2188 "selected":/^(selected)$/,
2199 "valuetype":/^(data|ref|object)$/,
2219 "type":/^(text\/ecmascript|text\/javascript|text\/jscript|text\/vbscript|text\/vbs|text\/xml)$/,
2221 "defer":/^(defer)$/,
2232 "disabled":/^(disabled)$/,
2233 "multiple":/^(multiple)$/,
2247 "media":/^(screen|tty|tv|projection|handheld|print|braille|aural|all)$/
2262 "frame":/^(void|above|below|hsides|lhs|rhs|vsides|box|border)$/,
2263 "rules":/^(none|groups|rows|cols|all)$/,
2272 "align":/^(right|left|center|justify)$/,
2275 "valign":/^(top|middle|bottom|baseline)$/
2283 "align":/^(left|right|center|justify|char)$/,
2287 "colspan":/^(\d)+$/,
2289 "rowspan":/^(\d)+$/,
2290 "scope":/^(col|colgroup|row|rowgroup)$/,
2291 "valign":/^(top|middle|bottom|baseline)$/
2313 "align":/^(right|left|center|justify)$/,
2316 "valign":/^(top|middle|bottom)$/,
2325 "align":/^(left|right|center|justify|char)$/,
2329 "colspan":/^(\d)+$/,
2331 "rowspan":/^(\d)+$/,
2332 "scope":/^(col|colgroup|row|rowgroup)$/,
2333 "valign":/^(top|middle|bottom|baseline)$/
2340 "align":/^(right|left|center|justify)$/,
2343 "valign":/^(top|middle|bottom|baseline)$/
2351 "align":/^(right|left|center|justify|char)$/,
2354 "valign":/^(top|middle|bottom|baseline)$/
2362 // Temporary skiped attributes
2363 skiped_attributes : [],
2364 skiped_attribute_values : [],
2366 getValidTagAttributes: function(tag, attributes)
2368 var valid_attributes = {};
2369 var possible_attributes = this.getPossibleTagAttributes(tag);
2370 for(var attribute in attributes) {
2371 var value = attributes[attribute];
2372 var h = WYMeditor.Helper;
2373 if(!h.contains(this.skiped_attributes, attribute) && !h.contains(this.skiped_attribute_values, value)){
2374 if (typeof value != 'function' && h.contains(possible_attributes, attribute)) {
2375 if (this.doesAttributeNeedsValidation(tag, attribute)) {
2376 if(this.validateAttribute(tag, attribute, value)){
2377 valid_attributes[attribute] = value;
2380 valid_attributes[attribute] = value;
2385 return valid_attributes;
2387 getUniqueAttributesAndEventsForTag : function(tag)
2391 if (this._tags[tag] && this._tags[tag]['attributes']) {
2392 for (k in this._tags[tag]['attributes']) {
2393 result.push(parseInt(k) == k ? this._tags[tag]['attributes'][k] : k);
2398 getDefaultAttributesAndEventsForTags : function()
2401 for (var key in this._events){
2402 result.push(this._events[key]);
2404 for (var key in this._attributes){
2405 result.push(this._attributes[key]);
2409 isValidTag : function(tag)
2411 if(this._tags[tag]){
2414 for(var key in this._tags){
2415 if(this._tags[key] == tag){
2421 getDefaultAttributesAndEventsForTag : function(tag)
2423 var default_attributes = [];
2424 if (this.isValidTag(tag)) {
2425 var default_attributes_and_events = this.getDefaultAttributesAndEventsForTags();
2427 for(var key in default_attributes_and_events) {
2428 var defaults = default_attributes_and_events[key];
2429 if(typeof defaults == 'object'){
2430 var h = WYMeditor.Helper;
2431 if ((defaults['except'] && h.contains(defaults['except'], tag)) || (defaults['only'] && !h.contains(defaults['only'], tag))) {
2435 var tag_defaults = defaults['attributes'] ? defaults['attributes'] : defaults['events'];
2436 for(var k in tag_defaults) {
2437 default_attributes.push(typeof tag_defaults[k] != 'string' ? k : tag_defaults[k]);
2442 return default_attributes;
2444 doesAttributeNeedsValidation: function(tag, attribute)
2446 return this._tags[tag] && ((this._tags[tag]['attributes'] && this._tags[tag]['attributes'][attribute]) || (this._tags[tag]['required'] &&
2447 WYMeditor.Helper.contains(this._tags[tag]['required'], attribute)));
2449 validateAttribute : function(tag, attribute, value)
2451 if ( this._tags[tag] &&
2452 (this._tags[tag]['attributes'] && this._tags[tag]['attributes'][attribute] && value.length > 0 && !value.match(this._tags[tag]['attributes'][attribute])) || // invalid format
2453 (this._tags[tag] && this._tags[tag]['required'] && WYMeditor.Helper.contains(this._tags[tag]['required'], attribute) && value.length == 0) // required attribute
2457 return typeof this._tags[tag] != 'undefined';
2459 getPossibleTagAttributes : function(tag)
2461 if (!this._possible_tag_attributes) {
2462 this._possible_tag_attributes = {};
2464 if (!this._possible_tag_attributes[tag]) {
2465 this._possible_tag_attributes[tag] = this.getUniqueAttributesAndEventsForTag(tag).concat(this.getDefaultAttributesAndEventsForTag(tag));
2467 return this._possible_tag_attributes[tag];
2473 * Compounded regular expression. Any of
2474 * the contained patterns could match and
2475 * when one does, it's label is returned.
2477 * Constructor. Starts with no patterns.
2478 * @param boolean case True for case sensitive, false
2481 * @author Marcus Baker (http://lastcraft.com)
2482 * @author Bermi Ferrer (http://bermi.org)
2484 WYMeditor.ParallelRegex = function(case_sensitive)
2486 this._case = case_sensitive;
2487 this._patterns = [];
2495 * Adds a pattern with an optional label.
2496 * @param string pattern Perl style regex, but ( and )
2497 * lose the usual meaning.
2498 * @param string label Label of regex to be returned
2502 WYMeditor.ParallelRegex.prototype.addPattern = function(pattern, label)
2504 label = label || true;
2505 var count = this._patterns.length;
2506 this._patterns[count] = pattern;
2507 this._labels[count] = label;
2512 * Attempts to match all patterns at once against
2514 * @param string subject String to match against.
2516 * @return boolean True on success.
2517 * @return string match First matched portion of
2521 WYMeditor.ParallelRegex.prototype.match = function(subject)
2523 if (this._patterns.length == 0) {
2526 var matches = subject.match(this._getCompoundedRegex());
2531 var match = matches[0];
2532 for (var i = 1; i < matches.length; i++) {
2534 return [this._labels[i-1], match];
2537 return [true, matches[0]];
2541 * Compounds the patterns into a single
2542 * regular expression separated with the
2543 * "or" operator. Caches the regex.
2544 * Will automatically escape (, ) and / tokens.
2545 * @param array patterns List of patterns in order.
2548 WYMeditor.ParallelRegex.prototype._getCompoundedRegex = function()
2550 if (this._regex == null) {
2551 for (var i = 0, count = this._patterns.length; i < count; i++) {
2552 this._patterns[i] = '(' + this._untokenizeRegex(this._tokenizeRegex(this._patterns[i]).replace(/([\/\(\)])/g,'\\$1')) + ')';
2554 this._regex = new RegExp(this._patterns.join("|") ,this._getPerlMatchingFlags());
2560 * Escape lookahead/lookbehind blocks
2562 WYMeditor.ParallelRegex.prototype._tokenizeRegex = function(regex)
2565 replace(/\(\?(i|m|s|x|U)\)/, '~~~~~~Tk1\$1~~~~~~').
2566 replace(/\(\?(\-[i|m|s|x|U])\)/, '~~~~~~Tk2\$1~~~~~~').
2567 replace(/\(\?\=(.*)\)/, '~~~~~~Tk3\$1~~~~~~').
2568 replace(/\(\?\!(.*)\)/, '~~~~~~Tk4\$1~~~~~~').
2569 replace(/\(\?\<\=(.*)\)/, '~~~~~~Tk5\$1~~~~~~').
2570 replace(/\(\?\<\!(.*)\)/, '~~~~~~Tk6\$1~~~~~~').
2571 replace(/\(\?\:(.*)\)/, '~~~~~~Tk7\$1~~~~~~');
2575 * Unscape lookahead/lookbehind blocks
2577 WYMeditor.ParallelRegex.prototype._untokenizeRegex = function(regex)
2580 replace(/~~~~~~Tk1(.{1})~~~~~~/, "(?\$1)").
2581 replace(/~~~~~~Tk2(.{2})~~~~~~/, "(?\$1)").
2582 replace(/~~~~~~Tk3(.*)~~~~~~/, "(?=\$1)").
2583 replace(/~~~~~~Tk4(.*)~~~~~~/, "(?!\$1)").
2584 replace(/~~~~~~Tk5(.*)~~~~~~/, "(?<=\$1)").
2585 replace(/~~~~~~Tk6(.*)~~~~~~/, "(?<!\$1)").
2586 replace(/~~~~~~Tk7(.*)~~~~~~/, "(?:\$1)");
2591 * Accessor for perl regex mode flags to use.
2592 * @return string Perl regex flags.
2595 WYMeditor.ParallelRegex.prototype._getPerlMatchingFlags = function()
2597 return (this._case ? "m" : "mi");
2603 * States for a stack machine.
2605 * Constructor. Starts in named state.
2606 * @param string start Starting state name.
2608 * @author Marcus Baker (http://lastcraft.com)
2609 * @author Bermi Ferrer (http://bermi.org)
2611 WYMeditor.StateStack = function(start)
2613 this._stack = [start];
2618 * Accessor for current state.
2619 * @return string State.
2622 WYMeditor.StateStack.prototype.getCurrent = function()
2624 return this._stack[this._stack.length - 1];
2628 * Adds a state to the stack and sets it
2629 * to be the current state.
2630 * @param string state New state.
2633 WYMeditor.StateStack.prototype.enter = function(state)
2635 this._stack.push(state);
2639 * Leaves the current state and reverts
2640 * to the previous one.
2641 * @return boolean False if we drop off
2642 * the bottom of the list.
2645 WYMeditor.StateStack.prototype.leave = function()
2647 if (this._stack.length == 1) {
2656 WYMeditor.LEXER_ENTER = 1;
2657 WYMeditor.LEXER_MATCHED = 2;
2658 WYMeditor.LEXER_UNMATCHED = 3;
2659 WYMeditor.LEXER_EXIT = 4;
2660 WYMeditor.LEXER_SPECIAL = 5;
2664 * Accepts text and breaks it into tokens.
2665 * Some optimisation to make the sure the
2666 * content is only scanned by the PHP regex
2667 * parser once. Lexer modes must not start
2668 * with leading underscores.
2670 * Sets up the lexer in case insensitive matching
2672 * @param Parser parser Handling strategy by reference.
2673 * @param string start Starting handler.
2674 * @param boolean case True for case sensitive.
2676 * @author Marcus Baker (http://lastcraft.com)
2677 * @author Bermi Ferrer (http://bermi.org)
2679 WYMeditor.Lexer = function(parser, start, case_sensitive)
2681 start = start || 'accept';
2682 this._case = case_sensitive || false;
2684 this._parser = parser;
2685 this._mode = new WYMeditor.StateStack(start);
2686 this._mode_handlers = {};
2687 this._mode_handlers[start] = start;
2692 * Adds a token search pattern for a particular
2693 * parsing mode. The pattern does not change the
2695 * @param string pattern Perl style regex, but ( and )
2696 * lose the usual meaning.
2697 * @param string mode Should only apply this
2698 * pattern when dealing with
2699 * this type of input.
2702 WYMeditor.Lexer.prototype.addPattern = function(pattern, mode)
2704 var mode = mode || "accept";
2705 if (typeof this._regexes[mode] == 'undefined') {
2706 this._regexes[mode] = new WYMeditor.ParallelRegex(this._case);
2708 this._regexes[mode].addPattern(pattern);
2709 if (typeof this._mode_handlers[mode] == 'undefined') {
2710 this._mode_handlers[mode] = mode;
2715 * Adds a pattern that will enter a new parsing
2716 * mode. Useful for entering parenthesis, strings,
2718 * @param string pattern Perl style regex, but ( and )
2719 * lose the usual meaning.
2720 * @param string mode Should only apply this
2721 * pattern when dealing with
2722 * this type of input.
2723 * @param string new_mode Change parsing to this new
2727 WYMeditor.Lexer.prototype.addEntryPattern = function(pattern, mode, new_mode)
2729 if (typeof this._regexes[mode] == 'undefined') {
2730 this._regexes[mode] = new WYMeditor.ParallelRegex(this._case);
2732 this._regexes[mode].addPattern(pattern, new_mode);
2733 if (typeof this._mode_handlers[new_mode] == 'undefined') {
2734 this._mode_handlers[new_mode] = new_mode;
2739 * Adds a pattern that will exit the current mode
2740 * and re-enter the previous one.
2741 * @param string pattern Perl style regex, but ( and )
2742 * lose the usual meaning.
2743 * @param string mode Mode to leave.
2746 WYMeditor.Lexer.prototype.addExitPattern = function(pattern, mode)
2748 if (typeof this._regexes[mode] == 'undefined') {
2749 this._regexes[mode] = new WYMeditor.ParallelRegex(this._case);
2751 this._regexes[mode].addPattern(pattern, "__exit");
2752 if (typeof this._mode_handlers[mode] == 'undefined') {
2753 this._mode_handlers[mode] = mode;
2758 * Adds a pattern that has a special mode. Acts as an entry
2759 * and exit pattern in one go, effectively calling a special
2760 * parser handler for this token only.
2761 * @param string pattern Perl style regex, but ( and )
2762 * lose the usual meaning.
2763 * @param string mode Should only apply this
2764 * pattern when dealing with
2765 * this type of input.
2766 * @param string special Use this mode for this one token.
2769 WYMeditor.Lexer.prototype.addSpecialPattern = function(pattern, mode, special)
2771 if (typeof this._regexes[mode] == 'undefined') {
2772 this._regexes[mode] = new WYMeditor.ParallelRegex(this._case);
2774 this._regexes[mode].addPattern(pattern, '_'+special);
2775 if (typeof this._mode_handlers[special] == 'undefined') {
2776 this._mode_handlers[special] = special;
2781 * Adds a mapping from a mode to another handler.
2782 * @param string mode Mode to be remapped.
2783 * @param string handler New target handler.
2786 WYMeditor.Lexer.prototype.mapHandler = function(mode, handler)
2788 this._mode_handlers[mode] = handler;
2792 * Splits the page text into tokens. Will fail
2793 * if the handlers report an error or if no
2794 * content is consumed. If successful then each
2795 * unparsed and parsed token invokes a call to the
2797 * @param string raw Raw HTML text.
2798 * @return boolean True on success, else false.
2801 WYMeditor.Lexer.prototype.parse = function(raw)
2803 if (typeof this._parser == 'undefined') {
2807 var length = raw.length;
2809 while (typeof (parsed = this._reduce(raw)) == 'object') {
2810 var raw = parsed[0];
2811 var unmatched = parsed[1];
2812 var matched = parsed[2];
2813 var mode = parsed[3];
2815 if (! this._dispatchTokens(unmatched, matched, mode)) {
2822 if (raw.length == length) {
2825 length = raw.length;
2831 return this._invokeParser(raw, WYMeditor.LEXER_UNMATCHED);
2835 * Sends the matched token and any leading unmatched
2836 * text to the parser changing the lexer to a new
2837 * mode if one is listed.
2838 * @param string unmatched Unmatched leading portion.
2839 * @param string matched Actual token match.
2840 * @param string mode Mode after match. A boolean
2841 * false mode causes no change.
2842 * @return boolean False if there was any error
2846 WYMeditor.Lexer.prototype._dispatchTokens = function(unmatched, matched, mode)
2848 mode = mode || false;
2850 if (! this._invokeParser(unmatched, WYMeditor.LEXER_UNMATCHED)) {
2854 if (typeof mode == 'boolean') {
2855 return this._invokeParser(matched, WYMeditor.LEXER_MATCHED);
2857 if (this._isModeEnd(mode)) {
2858 if (! this._invokeParser(matched, WYMeditor.LEXER_EXIT)) {
2861 return this._mode.leave();
2863 if (this._isSpecialMode(mode)) {
2864 this._mode.enter(this._decodeSpecial(mode));
2865 if (! this._invokeParser(matched, WYMeditor.LEXER_SPECIAL)) {
2868 return this._mode.leave();
2870 this._mode.enter(mode);
2872 return this._invokeParser(matched, WYMeditor.LEXER_ENTER);
2876 * Tests to see if the new mode is actually to leave
2877 * the current mode and pop an item from the matching
2879 * @param string mode Mode to test.
2880 * @return boolean True if this is the exit mode.
2883 WYMeditor.Lexer.prototype._isModeEnd = function(mode)
2885 return (mode === "__exit");
2889 * Test to see if the mode is one where this mode
2890 * is entered for this token only and automatically
2891 * leaves immediately afterwoods.
2892 * @param string mode Mode to test.
2893 * @return boolean True if this is the exit mode.
2896 WYMeditor.Lexer.prototype._isSpecialMode = function(mode)
2898 return (mode.substring(0,1) == "_");
2902 * Strips the magic underscore marking single token
2904 * @param string mode Mode to decode.
2905 * @return string Underlying mode name.
2908 WYMeditor.Lexer.prototype._decodeSpecial = function(mode)
2910 return mode.substring(1);
2914 * Calls the parser method named after the current
2915 * mode. Empty content will be ignored. The lexer
2916 * has a parser handler for each mode in the lexer.
2917 * @param string content Text parsed.
2918 * @param boolean is_match Token is recognised rather
2919 * than unparsed data.
2922 WYMeditor.Lexer.prototype._invokeParser = function(content, is_match)
2925 if (!/ +/.test(content) && ((content === '') || (content == false))) {
2928 var current = this._mode.getCurrent();
2929 var handler = this._mode_handlers[current];
2931 eval('result = this._parser.' + handler + '(content, is_match);');
2936 * Tries to match a chunk of text and if successful
2937 * removes the recognised chunk and any leading
2938 * unparsed data. Empty strings will not be matched.
2939 * @param string raw The subject to parse. This is the
2940 * content that will be eaten.
2941 * @return array/boolean Three item list of unparsed
2942 * content followed by the
2943 * recognised token and finally the
2944 * action the parser is to take.
2945 * True if no match, false if there
2946 * is a parsing error.
2949 WYMeditor.Lexer.prototype._reduce = function(raw)
2951 var matched = this._regexes[this._mode.getCurrent()].match(raw);
2952 var match = matched[1];
2953 var action = matched[0];
2955 var unparsed_character_count = raw.indexOf(match);
2956 var unparsed = raw.substr(0, unparsed_character_count);
2957 raw = raw.substring(unparsed_character_count + match.length);
2958 return [raw, unparsed, match, action];
2966 * This are the rules for breaking the XHTML code into events
2967 * handled by the provided parser.
2969 * @author Marcus Baker (http://lastcraft.com)
2970 * @author Bermi Ferrer (http://bermi.org)
2972 WYMeditor.XhtmlLexer = function(parser)
2974 jQuery.extend(this, new WYMeditor.Lexer(parser, 'Text'));
2976 this.mapHandler('Text', 'Text');
2986 WYMeditor.XhtmlLexer.prototype.init = function()
2990 WYMeditor.XhtmlLexer.prototype.addTokens = function()
2992 this.addCommentTokens('Text');
2993 this.addScriptTokens('Text');
2994 this.addCssTokens('Text');
2995 this.addTagTokens('Text');
2998 WYMeditor.XhtmlLexer.prototype.addCommentTokens = function(scope)
3000 this.addEntryPattern("<!--", scope, 'Comment');
3001 this.addExitPattern("-->", 'Comment');
3004 WYMeditor.XhtmlLexer.prototype.addScriptTokens = function(scope)
3006 this.addEntryPattern("<script", scope, 'Script');
3007 this.addExitPattern("</script>", 'Script');
3010 WYMeditor.XhtmlLexer.prototype.addCssTokens = function(scope)
3012 this.addEntryPattern("<style", scope, 'Css');
3013 this.addExitPattern("</style>", 'Css');
3016 WYMeditor.XhtmlLexer.prototype.addTagTokens = function(scope)
3018 this.addSpecialPattern("<\\s*[a-z0-9:\-]+\\s*>", scope, 'OpeningTag');
3019 this.addEntryPattern("<[a-z0-9:\-]+"+'[\\\/ \\\>]+', scope, 'OpeningTag');
3020 this.addInTagDeclarationTokens('OpeningTag');
3022 this.addSpecialPattern("</\\s*[a-z0-9:\-]+\\s*>", scope, 'ClosingTag');
3026 WYMeditor.XhtmlLexer.prototype.addInTagDeclarationTokens = function(scope)
3028 this.addSpecialPattern('\\s+', scope, 'Ignore');
3030 this.addAttributeTokens(scope);
3032 this.addExitPattern('/>', scope);
3033 this.addExitPattern('>', scope);
3037 WYMeditor.XhtmlLexer.prototype.addAttributeTokens = function(scope)
3039 this.addSpecialPattern("\\s*[a-z-_0-9]*:?[a-z-_0-9]+\\s*(?=\=)\\s*", scope, 'TagAttributes');
3041 this.addEntryPattern('=\\s*"', scope, 'DoubleQuotedAttribute');
3042 this.addPattern("\\\\\"", 'DoubleQuotedAttribute');
3043 this.addExitPattern('"', 'DoubleQuotedAttribute');
3045 this.addEntryPattern("=\\s*'", scope, 'SingleQuotedAttribute');
3046 this.addPattern("\\\\'", 'SingleQuotedAttribute');
3047 this.addExitPattern("'", 'SingleQuotedAttribute');
3049 this.addSpecialPattern('=\\s*[^>\\s]*', scope, 'UnquotedAttribute');
3057 * This XHTML parser will trigger the events available on on
3058 * current SaxListener
3060 * @author Bermi Ferrer (http://bermi.org)
3062 WYMeditor.XhtmlParser = function(Listener, mode)
3064 var mode = mode || 'Text';
3065 this._Lexer = new WYMeditor.XhtmlLexer(this);
3066 this._Listener = Listener;
3069 this._last_match = '';
3070 this._current_match = '';
3075 WYMeditor.XhtmlParser.prototype.parse = function(raw)
3077 this._Lexer.parse(this.beforeParsing(raw));
3078 return this.afterParsing(this._Listener.getResult());
3081 WYMeditor.XhtmlParser.prototype.beforeParsing = function(raw)
3083 if(raw.match(/class="MsoNormal"/) || raw.match(/ns = "urn:schemas-microsoft-com/)){
3084 // Usefull for cleaning up content pasted from other sources (MSWord)
3085 this._Listener.avoidStylingTagsAndAttributes();
3087 return this._Listener.beforeParsing(raw);
3090 WYMeditor.XhtmlParser.prototype.afterParsing = function(parsed)
3092 if(this._Listener._avoiding_tags_implicitly){
3093 this._Listener.allowStylingTagsAndAttributes();
3095 return this._Listener.afterParsing(parsed);
3099 WYMeditor.XhtmlParser.prototype.Ignore = function(match, state)
3104 WYMeditor.XhtmlParser.prototype.Text = function(text)
3106 this._Listener.addContent(text);
3110 WYMeditor.XhtmlParser.prototype.Comment = function(match, status)
3112 return this._addNonTagBlock(match, status, 'addComment');
3115 WYMeditor.XhtmlParser.prototype.Script = function(match, status)
3117 return this._addNonTagBlock(match, status, 'addScript');
3120 WYMeditor.XhtmlParser.prototype.Css = function(match, status)
3122 return this._addNonTagBlock(match, status, 'addCss');
3125 WYMeditor.XhtmlParser.prototype._addNonTagBlock = function(match, state, type)
3128 case WYMeditor.LEXER_ENTER:
3129 this._non_tag = match;
3131 case WYMeditor.LEXER_UNMATCHED:
3132 this._non_tag += match;
3134 case WYMeditor.LEXER_EXIT:
3137 this._Listener.addComment(this._non_tag+match);
3140 this._Listener.addScript(this._non_tag+match);
3143 this._Listener.addCss(this._non_tag+match);
3150 WYMeditor.XhtmlParser.prototype.OpeningTag = function(match, state)
3153 case WYMeditor.LEXER_ENTER:
3154 this._tag = this.normalizeTag(match);
3155 this._tag_attributes = {};
3157 case WYMeditor.LEXER_SPECIAL:
3158 this._callOpenTagListener(this.normalizeTag(match));
3160 case WYMeditor.LEXER_EXIT:
3161 this._callOpenTagListener(this._tag, this._tag_attributes);
3166 WYMeditor.XhtmlParser.prototype.ClosingTag = function(match, state)
3168 this._callCloseTagListener(this.normalizeTag(match));
3172 WYMeditor.XhtmlParser.prototype._callOpenTagListener = function(tag, attributes)
3174 var attributes = attributes || {};
3175 this.autoCloseUnclosedBeforeNewOpening(tag);
3177 if(this._Listener.isBlockTag(tag)){
3178 this._Listener._tag_stack.push(tag);
3179 this._Listener.fixNestingBeforeOpeningBlockTag(tag, attributes);
3180 this._Listener.openBlockTag(tag, attributes);
3181 this._increaseOpenTagCounter(tag);
3182 }else if(this._Listener.isInlineTag(tag)){
3183 this._Listener.inlineTag(tag, attributes);
3185 this._Listener.openUnknownTag(tag, attributes);
3186 this._increaseOpenTagCounter(tag);
3188 this._Listener.last_tag = tag;
3189 this._Listener.last_tag_opened = true;
3190 this._Listener.last_tag_attributes = attributes;
3193 WYMeditor.XhtmlParser.prototype._callCloseTagListener = function(tag)
3195 if(this._decreaseOpenTagCounter(tag)){
3196 this.autoCloseUnclosedBeforeTagClosing(tag);
3198 if(this._Listener.isBlockTag(tag)){
3199 var expected_tag = this._Listener._tag_stack.pop();
3200 if(expected_tag == false){
3202 }else if(expected_tag != tag){
3205 this._Listener.closeBlockTag(tag);
3207 this._Listener.closeUnknownTag(tag);
3210 this._Listener.closeUnopenedTag(tag);
3212 this._Listener.last_tag = tag;
3213 this._Listener.last_tag_opened = false;
3216 WYMeditor.XhtmlParser.prototype._increaseOpenTagCounter = function(tag)
3218 this._Listener._open_tags[tag] = this._Listener._open_tags[tag] || 0;
3219 this._Listener._open_tags[tag]++;
3222 WYMeditor.XhtmlParser.prototype._decreaseOpenTagCounter = function(tag)
3224 if(this._Listener._open_tags[tag]){
3225 this._Listener._open_tags[tag]--;
3226 if(this._Listener._open_tags[tag] == 0){
3227 this._Listener._open_tags[tag] = undefined;
3234 WYMeditor.XhtmlParser.prototype.autoCloseUnclosedBeforeNewOpening = function(new_tag)
3236 this._autoCloseUnclosed(new_tag, false);
3239 WYMeditor.XhtmlParser.prototype.autoCloseUnclosedBeforeTagClosing = function(tag)
3241 this._autoCloseUnclosed(tag, true);
3244 WYMeditor.XhtmlParser.prototype._autoCloseUnclosed = function(new_tag, closing)
3246 var closing = closing || false;
3247 if(this._Listener._open_tags){
3248 for (var tag in this._Listener._open_tags) {
3249 var counter = this._Listener._open_tags[tag];
3250 if(counter > 0 && this._Listener.shouldCloseTagAutomatically(tag, new_tag, closing)){
3251 this._callCloseTagListener(tag, true);
3257 WYMeditor.XhtmlParser.prototype.getTagReplacements = function()
3259 return this._Listener.getTagReplacements();
3262 WYMeditor.XhtmlParser.prototype.normalizeTag = function(tag)
3264 tag = tag.replace(/^([\s<\/>]*)|([\s<\/>]*)$/gm,'').toLowerCase();
3265 var tags = this._Listener.getTagReplacements();
3272 WYMeditor.XhtmlParser.prototype.TagAttributes = function(match, state)
3274 if(WYMeditor.LEXER_SPECIAL == state){
3275 this._current_attribute = match;
3280 WYMeditor.XhtmlParser.prototype.DoubleQuotedAttribute = function(match, state)
3282 if(WYMeditor.LEXER_UNMATCHED == state){
3283 this._tag_attributes[this._current_attribute] = match;
3288 WYMeditor.XhtmlParser.prototype.SingleQuotedAttribute = function(match, state)
3290 if(WYMeditor.LEXER_UNMATCHED == state){
3291 this._tag_attributes[this._current_attribute] = match;
3296 WYMeditor.XhtmlParser.prototype.UnquotedAttribute = function(match, state)
3298 this._tag_attributes[this._current_attribute] = match.replace(/^=/,'');
3307 * @author Bermi Ferrer (http://bermi.org)
3309 WYMeditor.XhtmlSaxListener = function()
3312 this.helper = new WYMeditor.XmlHelper();
3313 this._open_tags = {};
3314 this.validator = WYMeditor.XhtmlValidator;
3315 this._tag_stack = [];
3316 this.avoided_tags = [];
3319 ' ':' ','¡':'¡','¢':'¢',
3320 '£':'£','¤':'¤','¥':'¥',
3321 '¦':'¦','§':'§','¨':'¨',
3322 '©':'©','ª':'ª','«':'«',
3323 '¬':'¬','­':'­','®':'®',
3324 '¯':'¯','°':'°','±':'±',
3325 '²':'²','³':'³','´':'´',
3326 'µ':'µ','¶':'¶','·':'·',
3327 '¸':'¸','¹':'¹','º':'º',
3328 '»':'»','¼':'¼','½':'½',
3329 '¾':'¾','¿':'¿','À':'À',
3330 'Á':'Á','Â':'Â','Ã':'Ã',
3331 'Ä':'Ä','Å':'Å','Æ':'Æ',
3332 'Ç':'Ç','È':'È','É':'É',
3333 'Ê':'Ê','Ë':'Ë','Ì':'Ì',
3334 'Í':'Í','Î':'Î','Ï':'Ï',
3335 'Ð':'Ð','Ñ':'Ñ','Ò':'Ò',
3336 'Ó':'Ó','Ô':'Ô','Õ':'Õ',
3337 'Ö':'Ö','×':'×','Ø':'Ø',
3338 'Ù':'Ù','Ú':'Ú','Û':'Û',
3339 'Ü':'Ü','Ý':'Ý','Þ':'Þ',
3340 'ß':'ß','à':'à','á':'á',
3341 'â':'â','ã':'ã','ä':'ä',
3342 'å':'å','æ':'æ','ç':'ç',
3343 'è':'è','é':'é','ê':'ê',
3344 'ë':'ë','ì':'ì','í':'í',
3345 'î':'î','ï':'ï','ð':'ð',
3346 'ñ':'ñ','ò':'ò','ó':'ó',
3347 'ô':'ô','õ':'õ','ö':'ö',
3348 '÷':'÷','ø':'ø','ù':'ù',
3349 'ú':'ú','û':'û','ü':'ü',
3350 'ý':'ý','þ':'þ','ÿ':'ÿ',
3351 'Œ':'Œ','œ':'œ','Š':'Š',
3352 'š':'š','Ÿ':'Ÿ','ƒ':'ƒ',
3353 'ˆ':'ˆ','˜':'˜','Α':'Α',
3354 'Β':'Β','Γ':'Γ','Δ':'Δ',
3355 'Ε':'Ε','Ζ':'Ζ','Η':'Η',
3356 'Θ':'Θ','Ι':'Ι','Κ':'Κ',
3357 'Λ':'Λ','Μ':'Μ','Ν':'Ν',
3358 'Ξ':'Ξ','Ο':'Ο','Π':'Π',
3359 'Ρ':'Ρ','Σ':'Σ','Τ':'Τ',
3360 'Υ':'Υ','Φ':'Φ','Χ':'Χ',
3361 'Ψ':'Ψ','Ω':'Ω','α':'α',
3362 'β':'β','γ':'γ','δ':'δ',
3363 'ε':'ε','ζ':'ζ','η':'η',
3364 'θ':'θ','ι':'ι','κ':'κ',
3365 'λ':'λ','μ':'μ','ν':'ν',
3366 'ξ':'ξ','ο':'ο','π':'π',
3367 'ρ':'ρ','ς':'ς','σ':'σ',
3368 'τ':'τ','υ':'υ','φ':'φ',
3369 'χ':'χ','ψ':'ψ','ω':'ω',
3370 'ϑ':'ϑ','ϒ':'ϒ','ϖ':'ϖ',
3371 ' ':' ',' ':' ',' ':' ',
3372 '‌':'‌','‍':'‍','‎':'‎',
3373 '‏':'‏','–':'–','—':'—',
3374 '‘':'‘','’':'’','‚':'‚',
3375 '“':'“','”':'”','„':'„',
3376 '†':'†','‡':'‡','•':'•',
3377 '…':'…','‰':'‰','′':'′',
3378 '″':'″','‹':'‹','›':'›',
3379 '‾':'‾','⁄':'⁄','€':'€',
3380 'ℑ':'ℑ','℘':'℘','ℜ':'ℜ',
3381 '™':'™','ℵ':'ℵ','←':'←',
3382 '↑':'↑','→':'→','↓':'↓',
3383 '↔':'↔','↵':'↵','⇐':'⇐',
3384 '⇑':'⇑','⇒':'⇒','⇓':'⇓',
3385 '⇔':'⇔','∀':'∀','∂':'∂',
3386 '∃':'∃','∅':'∅','∇':'∇',
3387 '∈':'∈','∉':'∉','∋':'∋',
3388 '∏':'∏','∑':'∑','−':'−',
3389 '∗':'∗','√':'√','∝':'∝',
3390 '∞':'∞','∠':'∠','∧':'∧',
3391 '∨':'∨','∩':'∩','∪':'∪',
3392 '∫':'∫','∴':'∴','∼':'∼',
3393 '≅':'≅','≈':'≈','≠':'≠',
3394 '≡':'≡','≤':'≤','≥':'≥',
3395 '⊂':'⊂','⊃':'⊃','⊄':'⊄',
3396 '⊆':'⊆','⊇':'⊇','⊕':'⊕',
3397 '⊗':'⊗','⊥':'⊥','⋅':'⋅',
3398 '⌈':'⌈','⌉':'⌉','⌊':'⌊',
3399 '⌋':'⌋','⟨':'〈','⟩':'〉',
3400 '◊':'◊','♠':'♠','♣':'♣',
3401 '♥':'♥','♦':'♦'};
3403 this.block_tags = ["a", "abbr", "acronym", "address", "area", "b",
3404 "base", "bdo", "big", "blockquote", "body", "button",
3405 "caption", "cite", "code", "col", "colgroup", "dd", "del", "div",
3406 "dfn", "dl", "dt", "em", "fieldset", "form", "head", "h1", "h2",
3407 "h3", "h4", "h5", "h6", "html", "i", "ins",
3408 "kbd", "label", "legend", "li", "map", "noscript",
3409 "object", "ol", "optgroup", "option", "p", "param", "pre", "q",
3410 "samp", "script", "select", "small", "span", "strong", "style",
3411 "sub", "sup", "table", "tbody", "td", "textarea", "tfoot", "th",
3412 "thead", "title", "tr", "tt", "ul", "var", "extends"];
3415 this.inline_tags = ["br", "hr", "img", "input"];
3420 WYMeditor.XhtmlSaxListener.prototype.shouldCloseTagAutomatically = function(tag, now_on_tag, closing)
3422 var closing = closing || false;
3424 if((closing && now_on_tag == 'tr') || (!closing && now_on_tag == 'td')){
3428 if(tag == 'option'){
3429 if((closing && now_on_tag == 'select') || (!closing && now_on_tag == 'option')){
3436 WYMeditor.XhtmlSaxListener.prototype.beforeParsing = function(raw)
3442 WYMeditor.XhtmlSaxListener.prototype.afterParsing = function(xhtml)
3444 xhtml = this.replaceNamedEntities(xhtml);
3445 xhtml = this.joinRepeatedEntities(xhtml);
3446 xhtml = this.removeEmptyTags(xhtml);
3447 xhtml = this.removeBrInPre(xhtml);
3451 WYMeditor.XhtmlSaxListener.prototype.replaceNamedEntities = function(xhtml)
3453 for (var entity in this.entities) {
3454 xhtml = xhtml.replace(new RegExp(entity, 'g'), this.entities[entity]);
3459 WYMeditor.XhtmlSaxListener.prototype.joinRepeatedEntities = function(xhtml)
3461 var tags = 'em|strong|sub|sup|acronym|pre|del|address';
3462 return xhtml.replace(new RegExp('<\/('+tags+')><\\1>' ,''),'').
3463 replace(new RegExp('(\s*<('+tags+')>\s*){2}(.*)(\s*<\/\\2>\s*){2}' ,''),'<\$2>\$3<\$2>');
3466 WYMeditor.XhtmlSaxListener.prototype.removeEmptyTags = function(xhtml)
3468 return xhtml.replace(new RegExp('<('+this.block_tags.join("|").replace(/\|td/,'').replace(/\|th/, '')+')>(<br \/>| | |\\s)*<\/\\1>' ,'g'),'');
3471 WYMeditor.XhtmlSaxListener.prototype.removeBrInPre = function(xhtml)
3473 var matches = xhtml.match(new RegExp('<pre[^>]*>(.*?)<\/pre>','gmi'));
3475 for(var i=0; i<matches.length; i++) {
3476 xhtml = xhtml.replace(matches[i], matches[i].replace(new RegExp('<br \/>', 'g'), String.fromCharCode(13,10)));
3482 WYMeditor.XhtmlSaxListener.prototype.getResult = function()
3487 WYMeditor.XhtmlSaxListener.prototype.getTagReplacements = function()
3489 return {'b':'strong', 'i':'em'};
3492 WYMeditor.XhtmlSaxListener.prototype.addContent = function(text)
3494 this.output += text;
3497 WYMeditor.XhtmlSaxListener.prototype.addComment = function(text)
3499 if(this.remove_comments){
3500 this.output += text;
3504 WYMeditor.XhtmlSaxListener.prototype.addScript = function(text)
3506 if(!this.remove_scripts){
3507 this.output += text;
3511 WYMeditor.XhtmlSaxListener.prototype.addCss = function(text)
3513 if(!this.remove_embeded_styles){
3514 this.output += text;
3518 WYMeditor.XhtmlSaxListener.prototype.openBlockTag = function(tag, attributes)
3520 this.output += this.helper.tag(tag, this.validator.getValidTagAttributes(tag, attributes), true);
3523 WYMeditor.XhtmlSaxListener.prototype.inlineTag = function(tag, attributes)
3525 this.output += this.helper.tag(tag, this.validator.getValidTagAttributes(tag, attributes));
3528 WYMeditor.XhtmlSaxListener.prototype.openUnknownTag = function(tag, attributes)
3530 //this.output += this.helper.tag(tag, attributes, true);
3533 WYMeditor.XhtmlSaxListener.prototype.closeBlockTag = function(tag)
3535 this.output = this.output.replace(/<br \/>$/, '')+this._getClosingTagContent('before', tag)+"</"+tag+">"+this._getClosingTagContent('after', tag);
3538 WYMeditor.XhtmlSaxListener.prototype.closeUnknownTag = function(tag)
3540 //this.output += "</"+tag+">";
3543 WYMeditor.XhtmlSaxListener.prototype.closeUnopenedTag = function(tag)
3545 this.output += "</"+tag+">";
3548 WYMeditor.XhtmlSaxListener.prototype.avoidStylingTagsAndAttributes = function()
3550 this.avoided_tags = ['div','span'];
3551 this.validator.skiped_attributes = ['style'];
3552 this.validator.skiped_attribute_values = ['MsoNormal','main1']; // MS Word attributes for class
3553 this._avoiding_tags_implicitly = true;
3556 WYMeditor.XhtmlSaxListener.prototype.allowStylingTagsAndAttributes = function()
3558 this.avoided_tags = [];
3559 this.validator.skiped_attributes = [];
3560 this.validator.skiped_attribute_values = [];
3561 this._avoiding_tags_implicitly = false;
3564 WYMeditor.XhtmlSaxListener.prototype.isBlockTag = function(tag)
3566 return !WYMeditor.Helper.contains(this.avoided_tags, tag) && WYMeditor.Helper.contains(this.block_tags, tag);
3569 WYMeditor.XhtmlSaxListener.prototype.isInlineTag = function(tag)
3571 return !WYMeditor.Helper.contains(this.avoided_tags, tag) && WYMeditor.Helper.contains(this.inline_tags, tag);
3574 WYMeditor.XhtmlSaxListener.prototype.insertContentAfterClosingTag = function(tag, content)
3576 this._insertContentWhenClosingTag('after', tag, content);
3579 WYMeditor.XhtmlSaxListener.prototype.insertContentBeforeClosingTag = function(tag, content)
3581 this._insertContentWhenClosingTag('before', tag, content);
3584 WYMeditor.XhtmlSaxListener.prototype.fixNestingBeforeOpeningBlockTag = function(tag, attributes)
3586 if(tag != 'li' && (tag == 'ul' || tag == 'ol') && this.last_tag && !this.last_tag_opened && this.last_tag == 'li'){
3587 this.output = this.output.replace(/<\/li>$/, '');
3588 this.insertContentAfterClosingTag(tag, '</li>');
3592 WYMeditor.XhtmlSaxListener.prototype._insertContentWhenClosingTag = function(position, tag, content)
3594 if(!this['_insert_'+position+'_closing']){
3595 this['_insert_'+position+'_closing'] = [];
3597 if(!this['_insert_'+position+'_closing'][tag]){
3598 this['_insert_'+position+'_closing'][tag] = [];
3600 this['_insert_'+position+'_closing'][tag].push(content);
3603 WYMeditor.XhtmlSaxListener.prototype._getClosingTagContent = function(position, tag)
3605 if( this['_insert_'+position+'_closing'] &&
3606 this['_insert_'+position+'_closing'][tag] &&
3607 this['_insert_'+position+'_closing'][tag].length > 0){
3608 return this['_insert_'+position+'_closing'][tag].pop();
3614 /********** CSS PARSER **********/
3617 WYMeditor.WymCssLexer = function(parser, only_wym_blocks)
3619 var only_wym_blocks = (typeof only_wym_blocks == 'undefined' ? true : only_wym_blocks);
3621 jQuery.extend(this, new WYMeditor.Lexer(parser, (only_wym_blocks?'Ignore':'WymCss')));
3623 this.mapHandler('WymCss', 'Ignore');
3625 if(only_wym_blocks == true){
3626 this.addEntryPattern("/\\\x2a[<\\s]*WYMeditor[>\\s]*\\\x2a/", 'Ignore', 'WymCss');
3627 this.addExitPattern("/\\\x2a[<\/\\s]*WYMeditor[>\\s]*\\\x2a/", 'WymCss');
3630 this.addSpecialPattern("[\\sa-z1-6]*\\\x2e[a-z-_0-9]+", 'WymCss', 'WymCssStyleDeclaration');
3632 this.addEntryPattern("/\\\x2a", 'WymCss', 'WymCssComment');
3633 this.addExitPattern("\\\x2a/", 'WymCssComment');
3635 this.addEntryPattern("\x7b", 'WymCss', 'WymCssStyle');
3636 this.addExitPattern("\x7d", 'WymCssStyle');
3638 this.addEntryPattern("/\\\x2a", 'WymCssStyle', 'WymCssFeedbackStyle');
3639 this.addExitPattern("\\\x2a/", 'WymCssFeedbackStyle');
3644 WYMeditor.WymCssParser = function()
3646 this._in_style = false;
3647 this._has_title = false;
3648 this.only_wym_blocks = true;
3649 this.css_settings = {'classesItems':[], 'editorStyles':[], 'dialogStyles':[]};
3653 WYMeditor.WymCssParser.prototype.parse = function(raw, only_wym_blocks)
3655 var only_wym_blocks = (typeof only_wym_blocks == 'undefined' ? this.only_wym_blocks : only_wym_blocks);
3656 this._Lexer = new WYMeditor.WymCssLexer(this, only_wym_blocks);
3657 this._Lexer.parse(raw);
3660 WYMeditor.WymCssParser.prototype.Ignore = function(match, state)
3665 WYMeditor.WymCssParser.prototype.WymCssComment = function(text, status)
3667 if(text.match(/end[a-z0-9\s]*wym[a-z0-9\s]*/mi)){
3670 if(status == WYMeditor.LEXER_UNMATCHED){
3671 if(!this._in_style){
3672 this._has_title = true;
3673 this._current_item = {'title':WYMeditor.Helper.trim(text)};
3675 if(this._current_item[this._current_element]){
3676 if(!this._current_item[this._current_element].expressions){
3677 this._current_item[this._current_element].expressions = [text];
3679 this._current_item[this._current_element].expressions.push(text);
3683 this._in_style = true;
3688 WYMeditor.WymCssParser.prototype.WymCssStyle = function(match, status)
3690 if(status == WYMeditor.LEXER_UNMATCHED){
3691 match = WYMeditor.Helper.trim(match);
3693 this._current_item[this._current_element].style = match;
3695 }else if (status == WYMeditor.LEXER_EXIT){
3696 this._in_style = false;
3697 this._has_title = false;
3698 this.addStyleSetting(this._current_item);
3703 WYMeditor.WymCssParser.prototype.WymCssFeedbackStyle = function(match, status)
3705 if(status == WYMeditor.LEXER_UNMATCHED){
3706 this._current_item[this._current_element].feedback_style = match.replace(/^([\s\/\*]*)|([\s\/\*]*)$/gm,'');
3711 WYMeditor.WymCssParser.prototype.WymCssStyleDeclaration = function(match)
3713 match = match.replace(/^([\s\.]*)|([\s\.*]*)$/gm, '');
3716 if(match.indexOf('.') > 0){
3717 var parts = match.split('.');
3718 this._current_element = parts[1];
3721 this._current_element = match;
3724 if(!this._has_title){
3725 this._current_item = {'title':(!tag?'':tag.toUpperCase()+': ')+this._current_element};
3726 this._has_title = true;
3729 if(!this._current_item[this._current_element]){
3730 this._current_item[this._current_element] = {'name':this._current_element};
3733 if(!this._current_item[this._current_element].tags){
3734 this._current_item[this._current_element].tags = [tag];
3736 this._current_item[this._current_element].tags.push(tag);
3742 WYMeditor.WymCssParser.prototype.addStyleSetting = function(style_details)
3744 for (var name in style_details){
3745 var details = style_details[name];
3746 if(typeof details == 'object' && name != 'title'){
3748 this.css_settings.classesItems.push({
3749 'name': WYMeditor.Helper.trim(details.name),
3750 'title': style_details.title,
3751 'expr' : WYMeditor.Helper.trim((details.expressions||details.tags).join(', '))
3753 if(details.feedback_style){
3754 this.css_settings.editorStyles.push({
3755 'name': '.'+ WYMeditor.Helper.trim(details.name),
3756 'css': details.feedback_style
3760 this.css_settings.dialogStyles.push({
3761 'name': '.'+ WYMeditor.Helper.trim(details.name),
3762 'css': details.style
3769 /********** HELPERS **********/
3771 // Returns true if it is a text node with whitespaces only
3772 jQuery.fn.isPhantomNode = function() {
3773 if (this[0].nodeType == 3)
3774 return !(/[^\t\n\r ]/.test(this[0].data));
3779 WYMeditor.isPhantomNode = function(n) {
3780 if (n.nodeType == 3)
3781 return !(/[^\t\n\r ]/.test(n.data));
3786 WYMeditor.isPhantomString = function(str) {
3787 return !(/[^\t\n\r ]/.test(str));
3790 // Returns the Parents or the node itself
3791 // jqexpr = a jQuery expression
3792 jQuery.fn.parentsOrSelf = function(jqexpr) {
3795 if (n[0].nodeType == 3)
3796 n = n.parents().slice(0,1);
3798 // if (n.is(jqexpr)) // XXX should work, but doesn't (probably a jQuery bug)
3799 if (n.filter(jqexpr).size() == 1)
3802 return n.parents(jqexpr).slice(0,1);
3805 // String & array helpers
3807 WYMeditor.Helper = {
3809 //replace all instances of 'old' by 'rep' in 'str' string
3810 replaceAll: function(str, old, rep) {
3811 var rExp = new RegExp(old, "g");
3812 return(str.replace(rExp, rep));
3815 //insert 'inserted' at position 'pos' in 'str' string
3816 insertAt: function(str, inserted, pos) {
3817 return(str.substr(0,pos) + inserted + str.substring(pos));
3821 trim: function(str) {
3822 return str.replace(/^(\s*)|(\s*)$/gm,'');
3825 //return true if 'arr' array contains 'elem', or false
3826 contains: function(arr, elem) {
3827 for (var i = 0; i < arr.length; i++) {
3828 if (arr[i] === elem) return true;
3833 //return 'item' position in 'arr' array, or -1
3834 indexOf: function(arr, item) {
3836 for(var i = 0; i < arr.length; i++) {
3837 if (arr[i] == item) {
3845 //return 'item' object in 'arr' array, checking its 'name' property, or null
3846 findByName: function(arr, name) {
3847 for(var i = 0; i < arr.length; i++) {
3849 if(item.name == name) return(item);
3857 * WYMeditor : what you see is What You Mean web-based editor
3858 * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/
3859 * Dual licensed under the MIT (MIT-license.txt)
3860 * and GPL (GPL-license.txt) licenses.
3862 * For further information visit:
3863 * http://www.wymeditor.org/
3866 * jquery.wymeditor.explorer.js
3867 * MSIE specific class and functions.
3868 * See the documentation for more info.
3871 * Jean-Francois Hovinne (jf.hovinne a-t wymeditor dotorg)
3872 * Bermi Ferrer (wymeditor a-t bermi dotorg)
3873 * Frédéric Palluel-Lafleur (fpalluel a-t gmail dotcom)
3874 * Jonatan Lundin (jonatan.lundin _at_ gmail.com)
3877 WYMeditor.WymClassExplorer = function(wym) {
3880 this._class = "className";
3881 this._newLine = "\r\n";
3885 WYMeditor.WymClassExplorer.prototype.initIframe = function(iframe) {
3887 //This function is executed twice, though it is called once!
3888 //But MSIE needs that, otherwise designMode won't work.
3891 this._iframe = iframe;
3892 this._doc = iframe.contentWindow.document;
3894 //add css rules from options
3895 var styles = this._doc.styleSheets[0];
3896 var aCss = eval(this._options.editorStyles);
3898 this.addCssRules(this._doc, aCss);
3900 this._doc.title = this._wym._index;
3902 //set the text direction
3903 jQuery('html', this._doc).attr('dir', this._options.direction);
3906 jQuery(this._doc.body).html(this._wym._html);
3911 this._doc.body.onfocus = function()
3912 {wym._doc.designMode = "on"; wym._doc = iframe.contentWindow.document;};
3913 this._doc.onbeforedeactivate = function() {wym.saveCaret();};
3914 this._doc.onkeyup = function() {
3918 this._doc.onclick = function() {wym.saveCaret();};
3920 this._doc.body.onbeforepaste = function() {
3921 wym._iframe.contentWindow.event.returnValue = false;
3924 this._doc.body.onpaste = function() {
3925 wym._iframe.contentWindow.event.returnValue = false;
3926 wym.paste(window.clipboardData.getData("Text"));
3929 //callback can't be executed twice, so we check
3930 if(this._initialized) {
3932 //pre-bind functions
3933 if(jQuery.isFunction(this._options.preBind)) this._options.preBind(this);
3935 //bind external events
3936 this._wym.bindEvents();
3938 //post-init functions
3939 if(jQuery.isFunction(this._options.postInit)) this._options.postInit(this);
3941 //add event listeners to doc elements, e.g. images
3945 this._initialized = true;
3948 this._doc.designMode="on";
3950 // (bermi's note) noticed when running unit tests on IE6
3951 // Is this really needed, it trigger an unexisting property on IE6
3952 this._doc = iframe.contentWindow.document;
3956 WYMeditor.WymClassExplorer.prototype._exec = function(cmd,param) {
3960 case WYMeditor.INDENT: case WYMeditor.OUTDENT:
3962 var container = this.findUp(this.container(), WYMeditor.LI);
3964 var ancestor = container.parentNode.parentNode;
3965 if(container.parentNode.childNodes.length>1
3966 || ancestor.tagName.toLowerCase() == WYMeditor.OL
3967 || ancestor.tagName.toLowerCase() == WYMeditor.UL)
3968 this._doc.execCommand(cmd);
3972 if(param) this._doc.execCommand(cmd,false,param);
3973 else this._doc.execCommand(cmd);
3980 WYMeditor.WymClassExplorer.prototype.selected = function() {
3982 var caretPos = this._iframe.contentWindow.document.caretPos;
3983 if(caretPos!=null) {
3984 if(caretPos.parentElement!=undefined)
3985 return(caretPos.parentElement());
3989 WYMeditor.WymClassExplorer.prototype.saveCaret = function() {
3991 this._doc.caretPos = this._doc.selection.createRange();
3994 WYMeditor.WymClassExplorer.prototype.addCssRule = function(styles, oCss) {
3996 styles.addRule(oCss.name, oCss.css);
3999 WYMeditor.WymClassExplorer.prototype.insert = function(html) {
4001 // Get the current selection
4002 var range = this._doc.selection.createRange();
4004 // Check if the current selection is inside the editor
4005 if ( jQuery(range.parentElement()).parents( this._options.iframeBodySelector ).is('*') ) {
4007 // Overwrite selection with provided html
4008 range.pasteHTML(html);
4011 // Fall back to the internal paste function if there's no selection
4016 WYMeditor.WymClassExplorer.prototype.wrap = function(left, right) {
4018 // Get the current selection
4019 var range = this._doc.selection.createRange();
4021 // Check if the current selection is inside the editor
4022 if ( jQuery(range.parentElement()).parents( this._options.iframeBodySelector ).is('*') ) {
4024 // Overwrite selection with provided html
4025 range.pasteHTML(left + range.text + right);
4030 WYMeditor.WymClassExplorer.prototype.unwrap = function() {
4032 // Get the current selection
4033 var range = this._doc.selection.createRange();
4035 // Check if the current selection is inside the editor
4036 if ( jQuery(range.parentElement()).parents( this._options.iframeBodySelector ).is('*') ) {
4039 var text = range.text;
4040 this._exec( 'Cut' );
4041 range.pasteHTML( text );
4047 WYMeditor.WymClassExplorer.prototype.keyup = function() {
4048 this._selected_image = null;
4051 WYMeditor.WymClassExplorer.prototype.setFocusToNode = function(node) {
4052 var range = this._doc.selection.createRange();
4053 range.moveToElementText(node);
4054 range.collapse(false);
4055 range.move('character',-1);
4061 * WYMeditor : what you see is What You Mean web-based editor
4062 * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/
4063 * Dual licensed under the MIT (MIT-license.txt)
4064 * and GPL (GPL-license.txt) licenses.
4066 * For further information visit:
4067 * http://www.wymeditor.org/
4070 * jquery.wymeditor.mozilla.js
4071 * Gecko specific class and functions.
4072 * See the documentation for more info.
4075 * Jean-Francois Hovinne (jf.hovinne a-t wymeditor dotorg)
4076 * Volker Mische (vmx a-t gmx dotde)
4077 * Bermi Ferrer (wymeditor a-t bermi dotorg)
4078 * Frédéric Palluel-Lafleur (fpalluel a-t gmail dotcom)
4081 WYMeditor.WymClassMozilla = function(wym) {
4084 this._class = "class";
4085 this._newLine = "\n";
4088 WYMeditor.WymClassMozilla.prototype.initIframe = function(iframe) {
4090 this._iframe = iframe;
4091 this._doc = iframe.contentDocument;
4093 //add css rules from options
4095 var styles = this._doc.styleSheets[0];
4096 var aCss = eval(this._options.editorStyles);
4098 this.addCssRules(this._doc, aCss);
4100 this._doc.title = this._wym._index;
4102 //set the text direction
4103 jQuery('html', this._doc).attr('dir', this._options.direction);
4106 this.html(this._wym._html);
4109 this.enableDesignMode();
4111 //pre-bind functions
4112 if(jQuery.isFunction(this._options.preBind)) this._options.preBind(this);
4114 //bind external events
4115 this._wym.bindEvents();
4117 //bind editor keydown events
4118 jQuery(this._doc).bind("keydown", this.keydown);
4120 //bind editor keyup events
4121 jQuery(this._doc).bind("keyup", this.keyup);
4123 //bind editor focus events (used to reset designmode - Gecko bug)
4124 jQuery(this._doc).bind("focus", this.enableDesignMode);
4126 //post-init functions
4127 if(jQuery.isFunction(this._options.postInit)) this._options.postInit(this);
4129 //add event listeners to doc elements, e.g. images
4134 * @description Get/Set the html value
4136 WYMeditor.WymClassMozilla.prototype.html = function(html) {
4138 if(typeof html === 'string') {
4140 //disable designMode
4141 try { this._doc.designMode = "off"; } catch(e) { };
4143 //replace em by i and strong by bold
4144 //(designMode issue)
4145 html = html.replace(/<em(\b[^>]*)>/gi, "<i$1>")
4146 .replace(/<\/em>/gi, "</i>")
4147 .replace(/<strong(\b[^>]*)>/gi, "<b$1>")
4148 .replace(/<\/strong>/gi, "</b>");
4150 //update the html body
4151 jQuery(this._doc.body).html(html);
4153 //re-init designMode
4154 this.enableDesignMode();
4156 else return(jQuery(this._doc.body).html());
4159 WYMeditor.WymClassMozilla.prototype._exec = function(cmd,param) {
4161 if(!this.selected()) return(false);
4165 case WYMeditor.INDENT: case WYMeditor.OUTDENT:
4167 var focusNode = this.selected();
4168 var sel = this._iframe.contentWindow.getSelection();
4169 var anchorNode = sel.anchorNode;
4170 if(anchorNode.nodeName == "#text") anchorNode = anchorNode.parentNode;
4172 focusNode = this.findUp(focusNode, WYMeditor.BLOCKS);
4173 anchorNode = this.findUp(anchorNode, WYMeditor.BLOCKS);
4175 if(focusNode && focusNode == anchorNode
4176 && focusNode.tagName.toLowerCase() == WYMeditor.LI) {
4178 var ancestor = focusNode.parentNode.parentNode;
4180 if(focusNode.parentNode.childNodes.length>1
4181 || ancestor.tagName.toLowerCase() == WYMeditor.OL
4182 || ancestor.tagName.toLowerCase() == WYMeditor.UL)
4183 this._doc.execCommand(cmd,'',null);
4190 if(param) this._doc.execCommand(cmd,'',param);
4191 else this._doc.execCommand(cmd,'',null);
4194 //set to P if parent = BODY
4195 var container = this.selected();
4196 if(container.tagName.toLowerCase() == WYMeditor.BODY)
4197 this._exec(WYMeditor.FORMAT_BLOCK, WYMeditor.P);
4199 //add event handlers on doc elements
4205 * @description Returns the selected container
4207 WYMeditor.WymClassMozilla.prototype.selected = function() {
4209 var sel = this._iframe.contentWindow.getSelection();
4210 var node = sel.focusNode;
4212 if(node.nodeName == "#text") return(node.parentNode);
4214 } else return(null);
4217 WYMeditor.WymClassMozilla.prototype.addCssRule = function(styles, oCss) {
4219 styles.insertRule(oCss.name + " {" + oCss.css + "}",
4220 styles.cssRules.length);
4224 //keydown handler, mainly used for keyboard shortcuts
4225 WYMeditor.WymClassMozilla.prototype.keydown = function(evt) {
4228 var wym = WYMeditor.INSTANCES[this.title];
4229 var container = null;
4232 if(evt.keyCode == 66){
4234 wym._exec(WYMeditor.BOLD);
4237 if(evt.keyCode == 73){
4238 //CTRL+i => EMPHASIS
4239 wym._exec(WYMeditor.ITALIC);
4244 else if(evt.keyCode == 13) {
4247 container = wym.selected();
4248 if(container && container.tagName.toLowerCase() == WYMeditor.PRE) {
4249 evt.preventDefault();
4250 wym.insert('<p></p>');
4256 //keyup handler, mainly used for cleanups
4257 WYMeditor.WymClassMozilla.prototype.keyup = function(evt) {
4260 var wym = WYMeditor.INSTANCES[this.title];
4262 wym._selected_image = null;
4263 var container = null;
4265 if(evt.keyCode == 13 && !evt.shiftKey) {
4268 //cleanup <br><br> between paragraphs
4269 jQuery(wym._doc.body).children(WYMeditor.BR).remove();
4272 else if(evt.keyCode != 8
4273 && evt.keyCode != 17
4274 && evt.keyCode != 46
4275 && evt.keyCode != 224
4279 //NOT BACKSPACE, NOT DELETE, NOT CTRL, NOT COMMAND
4280 //text nodes replaced by P
4282 container = wym.selected();
4283 var name = container.tagName.toLowerCase();
4285 //fix forbidden main containers
4295 ) name = container.parentNode.tagName.toLowerCase();
4297 if(name == WYMeditor.BODY) wym._exec(WYMeditor.FORMAT_BLOCK, WYMeditor.P);
4301 WYMeditor.WymClassMozilla.prototype.enableDesignMode = function() {
4302 if(this.designMode == "off") {
4304 this.designMode = "on";
4305 this.execCommand("styleWithCSS", '', false);
4310 WYMeditor.WymClassMozilla.prototype.setFocusToNode = function(node) {
4311 var range = document.createRange();
4312 range.selectNode(node);
4313 var selected = this._iframe.contentWindow.getSelection();
4314 selected.addRange(range);
4315 selected.collapse(node, node.childNodes.length);
4316 this._iframe.contentWindow.focus();
4319 WYMeditor.WymClassMozilla.prototype.openBlockTag = function(tag, attributes)
4321 var attributes = this.validator.getValidTagAttributes(tag, attributes);
4323 // Handle Mozilla styled spans
4324 if(tag == 'span' && attributes.style){
4325 var new_tag = this.getTagForStyle(attributes.style);
4327 this._tag_stack.pop();
4329 this._tag_stack.push(new_tag);
4330 attributes.style = '';
4336 this.output += this.helper.tag(tag, attributes, true);
4339 WYMeditor.WymClassMozilla.prototype.getTagForStyle = function(style) {
4341 if(/bold/.test(style)) return 'strong';
4342 if(/italic/.test(style)) return 'em';
4343 if(/sub/.test(style)) return 'sub';
4344 if(/sub/.test(style)) return 'super';
4349 * WYMeditor : what you see is What You Mean web-based editor
4350 * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/
4351 * Dual licensed under the MIT (MIT-license.txt)
4352 * and GPL (GPL-license.txt) licenses.
4354 * For further information visit:
4355 * http://www.wymeditor.org/
4358 * jquery.wymeditor.opera.js
4359 * Opera specific class and functions.
4360 * See the documentation for more info.
4363 * Jean-Francois Hovinne (jf.hovinne a-t wymeditor dotorg)
4366 WYMeditor.WymClassOpera = function(wym) {
4369 this._class = "class";
4370 this._newLine = "\r\n";
4373 WYMeditor.WymClassOpera.prototype.initIframe = function(iframe) {
4375 this._iframe = iframe;
4376 this._doc = iframe.contentWindow.document;
4378 //add css rules from options
4379 var styles = this._doc.styleSheets[0];
4380 var aCss = eval(this._options.editorStyles);
4382 this.addCssRules(this._doc, aCss);
4384 this._doc.title = this._wym._index;
4386 //set the text direction
4387 jQuery('html', this._doc).attr('dir', this._options.direction);
4390 this._doc.designMode = "on";
4393 this.html(this._wym._html);
4395 //pre-bind functions
4396 if(jQuery.isFunction(this._options.preBind)) this._options.preBind(this);
4398 //bind external events
4399 this._wym.bindEvents();
4401 //bind editor keydown events
4402 jQuery(this._doc).bind("keydown", this.keydown);
4404 //bind editor events
4405 jQuery(this._doc).bind("keyup", this.keyup);
4407 //post-init functions
4408 if(jQuery.isFunction(this._options.postInit)) this._options.postInit(this);
4410 //add event listeners to doc elements, e.g. images
4414 WYMeditor.WymClassOpera.prototype._exec = function(cmd,param) {
4416 if(param) this._doc.execCommand(cmd,false,param);
4417 else this._doc.execCommand(cmd);
4422 WYMeditor.WymClassOpera.prototype.selected = function() {
4424 var sel=this._iframe.contentWindow.getSelection();
4425 var node=sel.focusNode;
4427 if(node.nodeName=="#text")return(node.parentNode);
4429 } else return(null);
4432 WYMeditor.WymClassOpera.prototype.addCssRule = function(styles, oCss) {
4434 styles.insertRule(oCss.name + " {" + oCss.css + "}",
4435 styles.cssRules.length);
4439 WYMeditor.WymClassOpera.prototype.keydown = function(evt) {
4442 var wym = WYMeditor.INSTANCES[this.title];
4443 var sel = wym._iframe.contentWindow.getSelection();
4444 startNode = sel.getRangeAt(0).startContainer;
4446 //Get a P instead of no container
4447 if(!jQuery(startNode).parentsOrSelf(
4448 WYMeditor.MAIN_CONTAINERS.join(","))[0]
4449 && !jQuery(startNode).parentsOrSelf('li')
4450 && evt.keyCode != WYMeditor.KEY.ENTER
4451 && evt.keyCode != WYMeditor.KEY.LEFT
4452 && evt.keyCode != WYMeditor.KEY.UP
4453 && evt.keyCode != WYMeditor.KEY.RIGHT
4454 && evt.keyCode != WYMeditor.KEY.DOWN
4455 && evt.keyCode != WYMeditor.KEY.BACKSPACE
4456 && evt.keyCode != WYMeditor.KEY.DELETE)
4457 wym._exec(WYMeditor.FORMAT_BLOCK, WYMeditor.P);
4462 WYMeditor.WymClassOpera.prototype.keyup = function(evt) {
4465 var wym = WYMeditor.INSTANCES[this.title];
4466 wym._selected_image = null;
4469 // TODO: implement me
4470 WYMeditor.WymClassOpera.prototype.setFocusToNode = function(node) {
4475 * WYMeditor : what you see is What You Mean web-based editor
4476 * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/
4477 * Dual licensed under the MIT (MIT-license.txt)
4478 * and GPL (GPL-license.txt) licenses.
4480 * For further information visit:
4481 * http://www.wymeditor.org/
4484 * jquery.wymeditor.safari.js
4485 * Safari specific class and functions.
4486 * See the documentation for more info.
4489 * Jean-Francois Hovinne (jf.hovinne a-t wymeditor dotorg)
4490 * Scott Lewis (lewiscot a-t gmail dotcom)
4493 WYMeditor.WymClassSafari = function(wym) {
4496 this._class = "class";
4497 this._newLine = "\n";
4500 WYMeditor.WymClassSafari.prototype.initIframe = function(iframe) {
4502 this._iframe = iframe;
4503 this._doc = iframe.contentDocument;
4505 //add css rules from options
4507 var styles = this._doc.styleSheets[0];
4508 var aCss = eval(this._options.editorStyles);
4510 this.addCssRules(this._doc, aCss);
4512 this._doc.title = this._wym._index;
4514 //set the text direction
4515 jQuery('html', this._doc).attr('dir', this._options.direction);
4518 this._doc.designMode = "on";
4521 this.html(this._wym._html);
4523 //pre-bind functions
4524 if(jQuery.isFunction(this._options.preBind)) this._options.preBind(this);
4526 //bind external events
4527 this._wym.bindEvents();
4529 //bind editor keydown events
4530 jQuery(this._doc).bind("keydown", this.keydown);
4532 //bind editor keyup events
4533 jQuery(this._doc).bind("keyup", this.keyup);
4535 //post-init functions
4536 if(jQuery.isFunction(this._options.postInit)) this._options.postInit(this);
4538 //add event listeners to doc elements, e.g. images
4542 WYMeditor.WymClassSafari.prototype._exec = function(cmd,param) {
4544 if(!this.selected()) return(false);
4548 case WYMeditor.INDENT: case WYMeditor.OUTDENT:
4550 var focusNode = this.selected();
4551 var sel = this._iframe.contentWindow.getSelection();
4552 var anchorNode = sel.anchorNode;
4553 if(anchorNode.nodeName == "#text") anchorNode = anchorNode.parentNode;
4555 focusNode = this.findUp(focusNode, WYMeditor.BLOCKS);
4556 anchorNode = this.findUp(anchorNode, WYMeditor.BLOCKS);
4558 if(focusNode && focusNode == anchorNode
4559 && focusNode.tagName.toLowerCase() == WYMeditor.LI) {
4561 var ancestor = focusNode.parentNode.parentNode;
4563 if(focusNode.parentNode.childNodes.length>1
4564 || ancestor.tagName.toLowerCase() == WYMeditor.OL
4565 || ancestor.tagName.toLowerCase() == WYMeditor.UL)
4566 this._doc.execCommand(cmd,'',null);
4571 case WYMeditor.INSERT_ORDEREDLIST: case WYMeditor.INSERT_UNORDEREDLIST:
4573 this._doc.execCommand(cmd,'',null);
4575 //Safari creates lists in e.g. paragraphs.
4576 //Find the container, and remove it.
4577 var focusNode = this.selected();
4578 var container = this.findUp(focusNode, WYMeditor.MAIN_CONTAINERS);
4579 if(container) jQuery(container).replaceWith(jQuery(container).html());
4585 if(param) this._doc.execCommand(cmd,'',param);
4586 else this._doc.execCommand(cmd,'',null);
4589 //set to P if parent = BODY
4590 var container = this.selected();
4591 if(container && container.tagName.toLowerCase() == WYMeditor.BODY)
4592 this._exec(WYMeditor.FORMAT_BLOCK, WYMeditor.P);
4594 //add event handlers on doc elements
4599 * @description Returns the selected container
4601 WYMeditor.WymClassSafari.prototype.selected = function() {
4603 var sel = this._iframe.contentWindow.getSelection();
4604 var node = sel.focusNode;
4606 if(node.nodeName == "#text") return(node.parentNode);
4608 } else return(null);
4611 WYMeditor.WymClassSafari.prototype.addCssRule = function(styles, oCss) {
4613 styles.insertRule(oCss.name + " {" + oCss.css + "}",
4614 styles.cssRules.length);
4618 //keydown handler, mainly used for keyboard shortcuts
4619 WYMeditor.WymClassSafari.prototype.keydown = function(evt) {
4622 var wym = WYMeditor.INSTANCES[this.title];
4625 if(evt.keyCode == 66){
4627 wym._exec(WYMeditor.BOLD);
4630 if(evt.keyCode == 73){
4631 //CTRL+i => EMPHASIS
4632 wym._exec(WYMeditor.ITALIC);
4638 //keyup handler, mainly used for cleanups
4639 WYMeditor.WymClassSafari.prototype.keyup = function(evt) {
4642 var wym = WYMeditor.INSTANCES[this.title];
4644 wym._selected_image = null;
4645 var container = null;
4647 if(evt.keyCode == 13 && !evt.shiftKey) {
4650 //cleanup <br><br> between paragraphs
4651 jQuery(wym._doc.body).children(WYMeditor.BR).remove();
4654 container = wym.selected();
4655 if(container && container.tagName.toLowerCase() == WYMeditor.PRE)
4656 wym._exec(WYMeditor.FORMAT_BLOCK, WYMeditor.P); //create P after PRE
4660 if(evt.keyCode == 13 && evt.shiftKey) {
4661 wym._exec('InsertLineBreak');
4665 && evt.keyCode != 17
4666 && evt.keyCode != 46
4667 && evt.keyCode != 224
4671 //NOT BACKSPACE, NOT DELETE, NOT CTRL, NOT COMMAND
4672 //text nodes replaced by P
4674 container = wym.selected();
4675 var name = container.tagName.toLowerCase();
4677 //fix forbidden main containers
4686 name == "span" //fix #110
4688 ) name = container.parentNode.tagName.toLowerCase();
4690 if(name == WYMeditor.BODY || name == WYMeditor.DIV) wym._exec(WYMeditor.FORMAT_BLOCK, WYMeditor.P); //fix #110 for DIV
4694 WYMeditor.WymClassSafari.prototype.setFocusToNode = function(node) {
4695 var range = this._iframe.contentDocument.createRange();
4696 range.selectNode(node);
4697 var selected = this._iframe.contentWindow.getSelection();
4698 selected.addRange(range);
4699 selected.collapse(node, node.childNodes.length);
4700 this._iframe.contentWindow.focus();
4703 WYMeditor.WymClassSafari.prototype.openBlockTag = function(tag, attributes)
4705 var attributes = this.validator.getValidTagAttributes(tag, attributes);
4707 // Handle Safari styled spans
4708 if(tag == 'span' && attributes.style) {
4709 var new_tag = this.getTagForStyle(attributes.style);
4711 this._tag_stack.pop();
4713 this._tag_stack.push(new_tag);
4714 attributes.style = '';
4716 //should fix #125 - also removed the xhtml() override
4717 if(typeof attributes['class'] == 'string')
4718 attributes['class'] = attributes['class'].replace(/apple-style-span/gi, '');
4725 this.output += this.helper.tag(tag, attributes, true);
4728 WYMeditor.WymClassSafari.prototype.getTagForStyle = function(style) {
4730 if(/bold/.test(style)) return 'strong';
4731 if(/italic/.test(style)) return 'em';
4732 if(/sub/.test(style)) return 'sub';
4733 if(/super/.test(style)) return 'sup';