commit dc04a1231cad3b8e092d963c53fc926b3fdf315d
parent ab959cd85865c3fc31baff0996bb54318d10af15
Author: Dan Stillman <dstillman@zotero.org>
Date: Sun, 25 Dec 2016 23:03:41 -0500
Upgrade to TinyMCE 4.5.1
- New flat theme (with padding tightened a bit from the default to fit
in right-hand pane)
- Adds search/replace within notes
- Adds URL autolinking
- Image pasting/dragging is now properly disallowed (though TinyMCE 4
has hooks that may allow us to actually support this by automatically
creating attachments)
- New blockquote style with color bar
- Replaces custom context menu on link click with built-in version
To-do:
- Fix display of pop-ups, which are now modal dialogs within the note
frame instead of pop-up windows, to stay fully within the frame
- Localize (more important now that there are tooltips)
- Support image dragging
- Update elements list for HTML5, for better drag-and-drop?
- Move directionality control to context menu instead of taking up
toolbar space?
- Evaluate other plugins for potential inclusion
- Show additional controls in separate note window?
- Fix opacity of text in tooltips
Closes #451, closes #421
Diffstat:
67 files changed, 54829 insertions(+), 25013 deletions(-)
diff --git a/chrome/content/zotero/bindings/styled-textbox.xml b/chrome/content/zotero/bindings/styled-textbox.xml
@@ -48,6 +48,7 @@
<constructor><![CDATA[
this.mode = this.getAttribute('mode');
+ this._onInitCallbacks = [];
this._iframe = document.getAnonymousElementByAttribute(this, "anonid", "rt-view");
this._htmlRTFmap = [
@@ -68,8 +69,10 @@
[/(?:\\par{}|\\\r?\n)/g, "</p><p>"]
];
- this.init = function() {
- if (this.initialized) return;
+ this.prepare = function() {
+ // DEBUG: Does this actually happen?
+ if (this.prepared) return;
+
// Tag data
var _rexData = [
[
@@ -282,9 +285,9 @@
this.rtfHTMLtagRegistry = tagRegistryMaker(1);
this.htmlRTFtagRegistry = tagRegistryMaker(0);
- this.initialized = true;
+ this.prepared = true;
}
- this.init();
+ this.prepare();
this.getSplit = function(mode, txt) {
if (!txt) return [];
@@ -375,14 +378,14 @@
Zotero.debug("Setting mode to " + val);
switch (val) {
case 'note':
- var self = this;
-
this._eventHandler = function (event) {
// Necessary in Fx32+
if (event.wrappedJSObject) {
event = event.wrappedJSObject;
}
+ var commandEvent = false;
+
//Zotero.debug(event.type);
switch (event.type) {
case 'keydown':
@@ -392,7 +395,7 @@
&& !event.altKey && event.keyCode == 90) {
event.stopPropagation();
event.preventDefault();
- self.redo();
+ this.redo();
return;
}
break;
@@ -407,38 +410,36 @@
//Zotero.debug("Not a char");
return;
}
+ commandEvent = true;
break;
+ // 'change' includes text added via drag-and-drop
case 'change':
+ case 'undo':
+ case 'redo':
+ commandEvent = true;
break;
- case 'openlink':
- var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
- .getService(Components.interfaces.nsIWindowMediator);
- win = wm.getMostRecentWindow('navigator:browser');
- win.ZoteroPane.loadURI(event.target.href, event.modifierKeys);
- break;
-
default:
return;
}
- if (self._timer) {
- clearTimeout(self._timer);
- }
-
- // Trigger command event on change
- if (event.type == 'keypress' || event.type == 'change') {
- self._timer = self.timeout && setTimeout(function () {
- var attr = self.getAttribute('oncommand');
+ // Trigger command on change
+ if (commandEvent && this.timeout) {
+ if (this._timer) {
+ clearTimeout(this._timer);
+ }
+
+ this._timer = setTimeout(function () {
+ var attr = this.getAttribute('oncommand');
attr = attr.replace('this', 'thisObj');
var func = new Function('thisObj', 'event', attr);
- func(self, event);
- }, self.timeout);
+ func(this, event);
+ }.bind(this), this.timeout);
}
return true;
- };
+ }.bind(this);
break;
case 'integration':
@@ -583,6 +584,18 @@
</body>
</method>
+ <method name="onInit">
+ <parameter name="callback"/>
+ <body><![CDATA[
+ if (this.initialized) {
+ this._editor.once(event, callback);
+ }
+ else {
+ this._onInitCallbacks.push(callback);
+ }
+ ]]></body>
+ </method>
+
<field name="_loaded"/>
<method name="_load">
<body>
@@ -660,15 +673,15 @@
if (fontSize < 6) {
fontSize = 11;
}
- var css = "body#zotero-tinymce-note.mceContentBody, "
- + "body#zotero-tinymce-note.mceContentBody p, "
- + "body#zotero-tinymce-note.mceContentBody th, "
- + "body#zotero-tinymce-note.mceContentBody td, "
- + "body#zotero-tinymce-note.mceContentBody pre { "
+ var css = "body#zotero-tinymce-note, "
+ + "body#zotero-tinymce-note p, "
+ + "body#zotero-tinymce-note th, "
+ + "body#zotero-tinymce-note td, "
+ + "body#zotero-tinymce-note pre { "
+ "font-size: " + fontSize + "px; "
+ "} "
- + "body#zotero-tinymce-note.mceContentBody, "
- + "body#zotero-tinymce-note.mceContentBody p { "
+ + "body#zotero-tinymce-note, "
+ + "body#zotero-tinymce-note p { "
+ "font-family: "
+ Zotero.Prefs.get('note.fontFamily') + "; "
+ "}"
@@ -681,11 +694,11 @@
head.appendChild(style);
}
- // Dispatch a tinymceInitialized event
- var ev = document.createEvent('HTMLEvents');
- ev.initEvent('tinymceInitialized', true, true);
- self.dispatchEvent(ev);
- };
+ let cb;
+ while (cb = this._onInitCallbacks.shift()) {
+ cb(this._editor);
+ }
+ }.bind(this);
}
var editor = SJOW.tinyMCE.get("tinymce");
@@ -701,11 +714,7 @@
return;
}
- if(window.ZoteroTab) {
- ZoteroTab.containerWindow.gBrowser.removeEventListener("DOMContentLoaded", listener, true);
- } else {
- self._iframe.removeEventListener("DOMContentLoaded", listener, false);
- }
+ self._iframe.removeEventListener("DOMContentLoaded", listener, false);
if (self._eventHandler) {
win.wrappedJSObject.zoteroHandleEvent = self._eventHandler;
@@ -715,20 +724,9 @@
win.wrappedJSObject.zoteroExecCommand = function (doc, command, ui, value) {
return doc.execCommand(command, ui, value);
}
-
- win.wrappedJSObject.zoteroFixWindow = function (win) {
- win.locationbar.visible = false;
- win.statusbar.visible = false;
- }
- };
+ }.bind(this);
- if(window.ZoteroTab) {
- // I'm not sure why it's necessary to attach the event listener to the
- // container window to get it to fire on the tab, but apparently it is...
- ZoteroTab.containerBrowser.addEventListener("DOMContentLoaded", listener, true);
- } else {
- this._iframe.addEventListener("DOMContentLoaded", listener, false);
- }
+ this._iframe.addEventListener("DOMContentLoaded", listener, false);
this._iframe.webNavigation.loadURI(uri.spec,
Components.interfaces.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
diff --git a/chrome/content/zotero/integration/addCitationDialog.js b/chrome/content/zotero/integration/addCitationDialog.js
@@ -623,14 +623,11 @@ var Zotero_Citation_Dialog = new function () {
io.preview().then(function(preview) {
editor.value = preview;
- if(editor.initialized) {
+ if (editor.initialized) {
_originalHTML = editor.value;
- } else {
- var eventListener = function() {
- _originalHTML = editor.value;
- editor.removeEventListener("tinymceInitialized", eventListener, false);
- };
- editor.addEventListener("tinymceInitialized", eventListener, false);
+ }
+ else {
+ editor.onInit(() => _originalHTML = editor.value);
}
});
} else {
diff --git a/chrome/skin/default/zotero/overlay.css b/chrome/skin/default/zotero/overlay.css
@@ -247,7 +247,7 @@
#zotero-item-pane
{
width: 338px;
- min-width: 250px;
+ min-width: 320px;
}
#zotero-layout-switcher
diff --git a/resource/tinymce/css/integration-content.css b/resource/tinymce/css/integration-content.css
diff --git a/resource/tinymce/css/note-content.css b/resource/tinymce/css/note-content.css
@@ -1,16 +1,7 @@
-pre {
- font-family: -moz-fixed;
-}
-
blockquote {
- margin-left: 2em;
-}
-
-/* Add quotation marks around blockquote */
-blockquote p:not(:empty):before {
- content: '“';
-}
-
-blockquote p:not(:empty):last-child:after {
- content: '”';
+ margin-top: 1.5em;
+ margin-bottom: 1.5em;
+ margin-left: 1em;
+ padding-left: .75em;
+ border-left: 3px solid lightblue;
}
diff --git a/resource/tinymce/css/note-ui.css b/resource/tinymce/css/note-ui.css
@@ -2,40 +2,55 @@ html, body {
height: 100%;
margin: 0;
}
-#tinymce_parent {
- display: block;
- height: 100%;
-}
-#tinymce_tbl {
+
+/* Stretch editor to fit frame */
+#tinymce_ifr, .mce-tinymce:not(.mce-floatpanel) {
height: 100% !important;
- width: 100% !important;
+ border: 0 !important;
}
-table.mceLayout > tbody > tr.mceLast {
- position: absolute;
- display: block;
- top: 54px;
- bottom: 2px;
- left: 1px;
- right: 1px;
+.mce-container-body {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
}
-td.mceIframeContainer {
- display: block;
- height: 100% !important;
- width: 100% !important;
+.mce-container-body .mce-edit-area {
+ position: absolute;
+ top: 57px;
+ bottom: 1px;
+ left: 0;
+ right: 0;
}
-#tinymce_ifr {
- height: 100% !important;
- width: 100% !important;
+
+/* Shrink the buttons a bit */
+.mce-listbox button {
+ padding-right: 8px !important;
+}
+
+.mce-btn-small button {
+ padding-left: 4px !important;
+ padding-right: 4px !important;
+}
+
+/* Tighten some padding */
+.mce-toolbar:first-child > div > :nth-child(3) {
+ margin-left: 0;
+}
+
+.mce-toolbar:last-child > div > :nth-child(2) {
+ margin-left: 0;
}
-#tinymce_formatselect_text {
- width: 65px;
+/* Keep popup windows within frame */
+.mce-window {
+ max-width: calc(100% - 15px) !important;
+ overflow-x: hidden;
}
#noScriptWarning {
padding: 4px;
- font-family: "Lucida Sans Unicode", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
+ font-family: sans-serif;
font-size: 12px;
}
diff --git a/resource/tinymce/integration.html b/resource/tinymce/integration.html
@@ -13,41 +13,30 @@ html, body {
height: 100%;
min-height: 130px;
}
-#tinymce_tbl {
- height: 100% !important;
- width: 100% !important;
-}
#noScriptWarning {
padding: 10px 8px 4px;
- font-family: "Lucida Sans Unicode", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
+ font-family: sans-serif;
font-size: 12px;
}
</style>
-<script type="text/javascript" src="tiny_mce.js"></script>
+<script type="text/javascript" src="tinymce.js"></script>
<script type="text/javascript">
tinyMCE.init({
- // General options
- mode : "none",
- theme : "advanced",
- content_css : "css/integration-content.css",
-
- // Theme options
- theme_advanced_buttons1 : "bold,italic,underline,|,sub,sup,|,removeformat",
- theme_advanced_buttons2 : "",
- theme_advanced_buttons3 : "",
- theme_advanced_toolbar_location : "top",
- theme_advanced_toolbar_align : "left",
- theme_advanced_resizing : true,
+ content_css: "css/integration-content.css",
+
+ toolbar: "bold italic underline | sub sup | removeformat",
+ toolbar_items_size: 'small',
+ menubar: false,
+ resize: false,
+ statusbar: false,
- setup : function (ed) {
- ed.onInit.add(function (ed) {
- zoteroInit(ed);
- });
+ init_instance_callback: function (ed) {
+ zoteroInit(ed);
}
});
- tinyMCE.execCommand("mceAddControl", true, "tinymce");
+ tinyMCE.execCommand("mceAddEditor", true, "tinymce");
</script>
</head>
<body>
diff --git a/resource/tinymce/langs/en.js b/resource/tinymce/langs/en.js
@@ -1 +0,0 @@
-tinyMCE.addI18n({en:{common:{"more_colors":"More Colors...","invalid_data":"Error: Invalid values entered, these are marked in red.","popup_blocked":"Sorry, but we have noticed that your popup-blocker has disabled a window that provides application functionality. You will need to disable popup blocking on this site in order to fully utilize this tool.","clipboard_no_support":"Currently not supported by your browser, use keyboard shortcuts instead.","clipboard_msg":"Copy/Cut/Paste is not available in Mozilla and Firefox.\nDo you want more information about this issue?","not_set":"-- Not Set --","class_name":"Class",browse:"Browse",close:"Close",cancel:"Cancel",update:"Update",insert:"Insert",apply:"Apply","edit_confirm":"Do you want to use the WYSIWYG mode for this textarea?","invalid_data_number":"{#field} must be a number","invalid_data_min":"{#field} must be a number greater than {#min}","invalid_data_size":"{#field} must be a number or percentage",value:"(value)"},contextmenu:{full:"Full",right:"Right",center:"Center",left:"Left",align:"Alignment"},insertdatetime:{"day_short":"Sun,Mon,Tue,Wed,Thu,Fri,Sat,Sun","day_long":"Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday","months_short":"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec","months_long":"January,February,March,April,May,June,July,August,September,October,November,December","inserttime_desc":"Insert Time","insertdate_desc":"Insert Date","time_fmt":"%H:%M:%S","date_fmt":"%Y-%m-%d"},print:{"print_desc":"Print"},preview:{"preview_desc":"Preview"},directionality:{"rtl_desc":"Direction Right to Left","ltr_desc":"Direction Left to Right"},layer:{content:"New layer...","absolute_desc":"Toggle Absolute Positioning","backward_desc":"Move Backward","forward_desc":"Move Forward","insertlayer_desc":"Insert New Layer"},save:{"save_desc":"Save","cancel_desc":"Cancel All Changes"},nonbreaking:{"nonbreaking_desc":"Insert Non-Breaking Space Character"},iespell:{download:"ieSpell not detected. Do you want to install it now?","iespell_desc":"Check Spelling"},advhr:{"delta_height":"","delta_width":"","advhr_desc":"Insert Horizontal Line"},emotions:{"delta_height":"","delta_width":"","emotions_desc":"Emotions"},searchreplace:{"replace_desc":"Find/Replace","delta_width":"","delta_height":"","search_desc":"Find"},advimage:{"delta_width":"","image_desc":"Insert/Edit Image","delta_height":""},advlink:{"delta_height":"","delta_width":"","link_desc":"Insert/Edit Link"},xhtmlxtras:{"attribs_delta_height":"","attribs_delta_width":"","ins_delta_height":"","ins_delta_width":"","del_delta_height":"","del_delta_width":"","acronym_delta_height":"","acronym_delta_width":"","abbr_delta_height":"","abbr_delta_width":"","cite_delta_height":"","cite_delta_width":"","attribs_desc":"Insert/Edit Attributes","ins_desc":"Insertion","del_desc":"Deletion","acronym_desc":"Acronym","abbr_desc":"Abbreviation","cite_desc":"Citation"},style:{"delta_height":"","delta_width":"",desc:"Edit CSS Style"},paste:{"plaintext_mode_stick":"Paste is now in plain text mode. Click again to toggle back to regular paste mode.","plaintext_mode":"Paste is now in plain text mode. Click again to toggle back to regular paste mode. After you paste something you will be returned to regular paste mode.","selectall_desc":"Select All","paste_word_desc":"Paste from Word","paste_text_desc":"Paste as Plain Text"},"paste_dlg":{"word_title":"Use Ctrl+V on your keyboard to paste the text into the window.","text_linebreaks":"Keep Linebreaks","text_title":"Use Ctrl+V on your keyboard to paste the text into the window."},table:{"merge_cells_delta_height":"","merge_cells_delta_width":"","table_delta_height":"","table_delta_width":"","cellprops_delta_height":"","cellprops_delta_width":"","rowprops_delta_height":"","rowprops_delta_width":"",cell:"Cell",col:"Column",row:"Row",del:"Delete Table","copy_row_desc":"Copy Table Row","cut_row_desc":"Cut Table Row","paste_row_after_desc":"Paste Table Row After","paste_row_before_desc":"Paste Table Row Before","props_desc":"Table Properties","cell_desc":"Table Cell Properties","row_desc":"Table Row Properties","merge_cells_desc":"Merge Table Cells","split_cells_desc":"Split Merged Table Cells","delete_col_desc":"Delete Column","col_after_desc":"Insert Column After","col_before_desc":"Insert Column Before","delete_row_desc":"Delete Row","row_after_desc":"Insert Row After","row_before_desc":"Insert Row Before",desc:"Insert/Edit Table"},autosave:{"warning_message":"If you restore the saved content, you will lose all the content that is currently in the editor.\n\nAre you sure you want to restore the saved content?","restore_content":"Restore auto-saved content.","unload_msg":"The changes you made will be lost if you navigate away from this page."},fullscreen:{desc:"Toggle Full Screen Mode"},media:{"delta_height":"","delta_width":"",edit:"Edit Embedded Media",desc:"Insert/Edit Embedded Media"},fullpage:{desc:"Document Properties","delta_width":"","delta_height":""},template:{desc:"Insert Predefined Template Content"},visualchars:{desc:"Show/Hide Visual Control Characters"},spellchecker:{desc:"Toggle Spell Checker",menu:"Spell Checker Settings","ignore_word":"Ignore Word","ignore_words":"Ignore All",langs:"Languages",wait:"Please wait...",sug:"Suggestions","no_sug":"No Suggestions","no_mpell":"No misspellings found.","learn_word":"Learn word"},pagebreak:{desc:"Insert Page Break for Printing"},advlist:{types:"Types",def:"Default","lower_alpha":"Lower Alpha","lower_greek":"Lower Greek","lower_roman":"Lower Roman","upper_alpha":"Upper Alpha","upper_roman":"Upper Roman",circle:"Circle",disc:"Disc",square:"Square"},colors:{"333300":"Dark olive","993300":"Burnt orange","000000":"Black","003300":"Dark green","003366":"Dark azure","000080":"Navy Blue","333399":"Indigo","333333":"Very dark gray","800000":"Maroon",FF6600:"Orange","808000":"Olive","008000":"Green","008080":"Teal","0000FF":"Blue","666699":"Grayish blue","808080":"Gray",FF0000:"Red",FF9900:"Amber","99CC00":"Yellow green","339966":"Sea green","33CCCC":"Turquoise","3366FF":"Royal blue","800080":"Purple","999999":"Medium gray",FF00FF:"Magenta",FFCC00:"Gold",FFFF00:"Yellow","00FF00":"Lime","00FFFF":"Aqua","00CCFF":"Sky blue","993366":"Brown",C0C0C0:"Silver",FF99CC:"Pink",FFCC99:"Peach",FFFF99:"Light yellow",CCFFCC:"Pale green",CCFFFF:"Pale cyan","99CCFF":"Light sky blue",CC99FF:"Plum",FFFFFF:"White"},aria:{"rich_text_area":"Rich Text Area"},wordcount:{words:"Words:"},visualblocks:{desc:'Show/hide block elements'}}});
-\ No newline at end of file
diff --git a/resource/tinymce/note.html b/resource/tinymce/note.html
@@ -3,41 +3,48 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link type="text/css" rel="stylesheet" href="css/note-ui.css"/>
-<script type="text/javascript;version=1.8" src="tiny_mce.js"></script>
-<script type="text/javascript;version=1.8">
- tinyMCE.init({
- // General options
- body_id : "zotero-tinymce-note",
- mode : "none",
- theme : "advanced",
- content_css : "css/note-content.css",
- button_tile_map : true,
- language : "en", // TODO: localize
- entities : "160,nbsp",
- gecko_spellcheck : true,
- convert_urls : false,
+<script type="text/javascript" src="tinymce.js"></script>
+<script type="text/javascript">
+ tinymce.init({
+ body_id: "zotero-tinymce-note",
+ content_css: "css/note-content.css",
+ language: "en", // TODO: localize
+ entities: "160,nbsp",
+ browser_spellcheck: true,
+ convert_urls: false,
+ fix_list_elements: true,
- handle_event_callback : function (event) {
- zoteroHandleEvent(event);
- },
+ plugins: "autolink,code,contextmenu,directionality,link,lists,paste,searchreplace",
- onchange_callback : function () {
- zoteroHandleEvent({ type: 'change' });
- },
+ toolbar1: "bold italic underline strikethrough | subscript superscript | forecolor backcolor | blockquote link | %DIR% | removeformat",
+ toolbar2: "formatselect | alignleft aligncenter alignright | bullist numlist outdent indent | searchreplace",
+ toolbar_items_size: 'small',
+ menubar: false,
+ resize: false,
+ statusbar: false,
+
+ contextmenu: "link | code",
- setup : function (ed) {
+ link_context_toolbar: true,
+ link_assume_external_targets: true,
+
+ setup: function (ed) {
// Set text direction
var dir = window.location.href.match(/dir=(ltr|rtl)/)[1];
ed.settings.directionality = dir;
// Include button for opposite direction, to function as a toggle
- ed.settings.theme_advanced_buttons1 = ed.settings.theme_advanced_buttons1.replace(
+ ed.settings.toolbar1 = ed.settings.toolbar1.replace(
"%DIR%",
- "," + dir.split("").reverse().join("")
+ dir.split("").reverse().join("")
);
+ },
+
+ init_instance_callback: function (ed) {
+ zoteroInit(ed);
- ed.onInit.add(function (ed) {
- zoteroInit(ed);
- });
+ ['Change', 'KeyDown', 'KeyPress', 'Undo', 'Redo'].forEach(eventName =>
+ ed.on(eventName, event => zoteroHandleEvent(event))
+ );
["Cut", "Copy", "Paste"].forEach(function (command) {
let cmd = command;
@@ -47,20 +54,8 @@
});
},
- fix_list_elements : true,
- fix_table_elements : true,
- plugins : "paste,contextmenu,linksmenu,directionality,autolink",
-
- // Theme options
- theme_advanced_buttons1 : "bold,italic,underline,strikethrough,|,sub,sup,|,forecolor,backcolor,|,blockquote,|,link,|,%DIR%",
- theme_advanced_buttons2 : "formatselect,|,justifyleft,justifycenter,justifyright,|,bullist,numlist,outdent,indent,|,removeformat,code",
- theme_advanced_buttons3 : "",
- theme_advanced_toolbar_location : "top",
- theme_advanced_toolbar_align : "left",
- theme_advanced_statusbar_location : "none",
-
// More restrictive version of default set, with JS/etc. removed
- valid_elements : "@[id|class|style|title|dir<ltr?rtl|lang|xml::lang],"
+ valid_elements: "@[id|class|style|title|dir<ltr?rtl|lang|xml::lang],"
+ "a[rel|rev|charset|hreflang|tabindex|accesskey|type|"
+ "name|href|target|title|class],strong/b,em/i,strike,u,"
+ "#p,-ol[type|compact],-ul[type|compact],-li,br,img[longdesc|usemap|"
@@ -80,15 +75,8 @@
+ "q[cite],samp,select[disabled|multiple|name|size],small,"
+ "textarea[cols|rows|disabled|name|readonly],tt,var,big"
});
- tinyMCE.execCommand("mceAddControl", true, "tinymce");
+ tinymce.execCommand("mceAddEditor", true, "tinymce");
</script>
-<style>
-table.mceLayout {
- border-left: 0 !important;
- border-right: 0 !important;
- border-top: 0 !important;
-}
-</style>
</head>
<body>
<div id="tinymce"></div>
diff --git a/resource/tinymce/noteview.html b/resource/tinymce/noteview.html
@@ -3,48 +3,26 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link type="text/css" rel="stylesheet" href="css/note-ui.css"/>
-<style>
-table.mceLayout {
- border-left: 0 !important;
- border-right: 0 !important;
- border-top: 0 !important;
-}
-table.mceLayout > tbody > tr.mceLast {
- top: 0;
-}
-</style>
-<script type="text/javascript" src="tiny_mce.js"></script>
+<script type="text/javascript" src="tinymce.js"></script>
<script type="text/javascript">
tinyMCE.init({
- // General options
- body_id : "zotero-tinymce-note",
- mode : "none",
- theme : "advanced",
- content_css : "css/note-content.css",
- button_tile_map : true,
- language : "en", // TODO: localize
- entity_encoding : "raw",
- readonly : true,
+ body_id: "zotero-tinymce-note",
+ content_css: "css/note-content.css",
+ language: "en", // TODO: localize
+ entity_encoding: "raw",
+ fix_list_elements: true,
+ readonly: true,
- fix_list_elements : true,
- fix_table_elements : true,
+ menubar: false,
+ resize: false,
+ statusbar: false,
- setup : function (ed) {
- ed.onInit.add(function (ed) {
- zoteroInit(ed);
- });
+ init_instance_callback: function (ed) {
+ zoteroInit(ed);
},
- // Theme options
- theme_advanced_buttons1 : "",
- theme_advanced_buttons2 : "",
- theme_advanced_buttons3 : "",
- theme_advanced_toolbar_location : "top",
- theme_advanced_toolbar_align : "left",
- theme_advanced_statusbar_location : "none",
-
// More restrictive version of default set, with JS/etc. removed
- valid_elements : "@[id|class|style|title|dir<ltr?rtl|lang|xml::lang],"
+ valid_elements: "@[id|class|style|title|dir<ltr?rtl|lang|xml::lang],"
+ "a[rel|rev|charset|hreflang|tabindex|accesskey|type|"
+ "name|href|target|title|class],strong/b,em/i,strike,u,"
+ "#p,-ol[type|compact],-ul[type|compact],-li,br,img[longdesc|usemap|"
@@ -64,7 +42,7 @@ table.mceLayout > tbody > tr.mceLast {
+ "q[cite],samp,select[disabled|multiple|name|size],small,"
+ "textarea[cols|rows|disabled|name|readonly],tt,var,big"
});
- tinyMCE.execCommand("mceAddControl", true, "tinymce");
+ tinyMCE.execCommand("mceAddEditor", true, "tinymce");
</script>
</head>
<body>
diff --git a/resource/tinymce/plugins/autolink/editor_plugin.js b/resource/tinymce/plugins/autolink/editor_plugin.js
@@ -1,184 +0,0 @@
-/**
- * editor_plugin_src.js
- *
- * Copyright 2011, Moxiecode Systems AB
- * Released under LGPL License.
- *
- * License: http://tinymce.moxiecode.com/license
- * Contributing: http://tinymce.moxiecode.com/contributing
- */
-
-(function() {
- tinymce.create('tinymce.plugins.AutolinkPlugin', {
- /**
- * Initializes the plugin, this will be executed after the plugin has been created.
- * This call is done before the editor instance has finished it's initialization so use the onInit event
- * of the editor instance to intercept that event.
- *
- * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.
- * @param {string} url Absolute URL to where the plugin is located.
- */
-
- init : function(ed, url) {
- var t = this;
-
- // Add a key down handler
- ed.onKeyDown.addToTop(function(ed, e) {
- if (e.keyCode == 13)
- return t.handleEnter(ed);
- });
-
- // Internet Explorer has built-in automatic linking for most cases
- if (tinyMCE.isIE)
- return;
-
- ed.onKeyPress.add(function(ed, e) {
- if (e.which == 41)
- return t.handleEclipse(ed);
- });
-
- // Add a key up handler
- ed.onKeyUp.add(function(ed, e) {
- if (e.keyCode == 32)
- return t.handleSpacebar(ed);
- });
- },
-
- handleEclipse : function(ed) {
- this.parseCurrentLine(ed, -1, '(', true);
- },
-
- handleSpacebar : function(ed) {
- this.parseCurrentLine(ed, 0, '', true);
- },
-
- handleEnter : function(ed) {
- this.parseCurrentLine(ed, -1, '', false);
- },
-
- parseCurrentLine : function(ed, end_offset, delimiter, goback) {
- var r, end, start, endContainer, bookmark, text, matches, prev, len;
-
- // We need at least five characters to form a URL,
- // hence, at minimum, five characters from the beginning of the line.
- r = ed.selection.getRng(true).cloneRange();
- if (r.startOffset < 5) {
- // During testing, the caret is placed inbetween two text nodes.
- // The previous text node contains the URL.
- prev = r.endContainer.previousSibling;
- if (prev == null) {
- if (r.endContainer.firstChild == null || r.endContainer.firstChild.nextSibling == null)
- return;
-
- prev = r.endContainer.firstChild.nextSibling;
- }
- len = prev.length;
- r.setStart(prev, len);
- r.setEnd(prev, len);
-
- if (r.endOffset < 5)
- return;
-
- end = r.endOffset;
- endContainer = prev;
- } else {
- endContainer = r.endContainer;
-
- // Get a text node
- if (endContainer.nodeType != 3 && endContainer.firstChild) {
- while (endContainer.nodeType != 3 && endContainer.firstChild)
- endContainer = endContainer.firstChild;
-
- // Move range to text node
- if (endContainer.nodeType == 3) {
- r.setStart(endContainer, 0);
- r.setEnd(endContainer, endContainer.nodeValue.length);
- }
- }
-
- if (r.endOffset == 1)
- end = 2;
- else
- end = r.endOffset - 1 - end_offset;
- }
-
- start = end;
-
- do
- {
- // Move the selection one character backwards.
- r.setStart(endContainer, end - 2);
- r.setEnd(endContainer, end - 1);
- end -= 1;
-
- // Loop until one of the following is found: a blank space, , delimeter, (end-2) >= 0
- } while (r.toString() != ' ' && r.toString() != '' && r.toString().charCodeAt(0) != 160 && (end -2) >= 0 && r.toString() != delimiter);
-
- if (r.toString() == delimiter || r.toString().charCodeAt(0) == 160) {
- r.setStart(endContainer, end);
- r.setEnd(endContainer, start);
- end += 1;
- } else if (r.startOffset == 0) {
- r.setStart(endContainer, 0);
- r.setEnd(endContainer, start);
- }
- else {
- r.setStart(endContainer, end);
- r.setEnd(endContainer, start);
- }
-
- // Exclude last . from word like "www.site.com."
- var text = r.toString();
- if (text.charAt(text.length - 1) == '.') {
- r.setEnd(endContainer, start - 1);
- }
-
- text = r.toString();
- matches = text.match(/^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+-]+@)(.+)$/i);
-
- if (matches) {
- if (matches[1] == 'www.') {
- matches[1] = 'http://www.';
- } else if (/@$/.test(matches[1]) && !/^mailto:/.test(matches[1])) {
- matches[1] = 'mailto:' + matches[1];
- }
-
- bookmark = ed.selection.getBookmark();
-
- ed.selection.setRng(r);
- tinyMCE.execCommand('createlink',false, matches[1] + matches[2]);
- ed.selection.moveToBookmark(bookmark);
- ed.nodeChanged();
-
- // TODO: Determine if this is still needed.
- if (tinyMCE.isWebKit) {
- // move the caret to its original position
- ed.selection.collapse(false);
- var max = Math.min(endContainer.length, start + 1);
- r.setStart(endContainer, max);
- r.setEnd(endContainer, max);
- ed.selection.setRng(r);
- }
- }
- },
-
- /**
- * Returns information about the plugin as a name/value array.
- * The current keys are longname, author, authorurl, infourl and version.
- *
- * @return {Object} Name/value array containing information about the plugin.
- */
- getInfo : function() {
- return {
- longname : 'Autolink',
- author : 'Moxiecode Systems AB',
- authorurl : 'http://tinymce.moxiecode.com',
- infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/autolink',
- version : tinymce.majorVersion + "." + tinymce.minorVersion
- };
- }
- });
-
- // Register plugin
- tinymce.PluginManager.add('autolink', tinymce.plugins.AutolinkPlugin);
-})();
diff --git a/resource/tinymce/plugins/autolink/plugin.js b/resource/tinymce/plugins/autolink/plugin.js
@@ -0,0 +1,209 @@
+/**
+ * plugin.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*global tinymce:true */
+
+tinymce.PluginManager.add('autolink', function(editor) {
+ var AutoUrlDetectState;
+ var AutoLinkPattern = /^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+\-]+@)(.+)$/i;
+
+ if (editor.settings.autolink_pattern) {
+ AutoLinkPattern = editor.settings.autolink_pattern;
+ }
+
+ editor.on("keydown", function(e) {
+ if (e.keyCode == 13) {
+ return handleEnter(editor);
+ }
+ });
+
+ // Internet Explorer has built-in automatic linking for most cases
+ if (tinymce.Env.ie) {
+ editor.on("focus", function() {
+ if (!AutoUrlDetectState) {
+ AutoUrlDetectState = true;
+
+ try {
+ editor.execCommand('AutoUrlDetect', false, true);
+ } catch (ex) {
+ // Ignore
+ }
+ }
+ });
+
+ return;
+ }
+
+ editor.on("keypress", function(e) {
+ if (e.keyCode == 41) {
+ return handleEclipse(editor);
+ }
+ });
+
+ editor.on("keyup", function(e) {
+ if (e.keyCode == 32) {
+ return handleSpacebar(editor);
+ }
+ });
+
+ function handleEclipse(editor) {
+ parseCurrentLine(editor, -1, '(', true);
+ }
+
+ function handleSpacebar(editor) {
+ parseCurrentLine(editor, 0, '', true);
+ }
+
+ function handleEnter(editor) {
+ parseCurrentLine(editor, -1, '', false);
+ }
+
+ function parseCurrentLine(editor, end_offset, delimiter) {
+ var rng, end, start, endContainer, bookmark, text, matches, prev, len, rngText;
+
+ function scopeIndex(container, index) {
+ if (index < 0) {
+ index = 0;
+ }
+
+ if (container.nodeType == 3) {
+ var len = container.data.length;
+
+ if (index > len) {
+ index = len;
+ }
+ }
+
+ return index;
+ }
+
+ function setStart(container, offset) {
+ if (container.nodeType != 1 || container.hasChildNodes()) {
+ rng.setStart(container, scopeIndex(container, offset));
+ } else {
+ rng.setStartBefore(container);
+ }
+ }
+
+ function setEnd(container, offset) {
+ if (container.nodeType != 1 || container.hasChildNodes()) {
+ rng.setEnd(container, scopeIndex(container, offset));
+ } else {
+ rng.setEndAfter(container);
+ }
+ }
+
+ // Never create a link when we are inside a link
+ if (editor.selection.getNode().tagName == 'A') {
+ return;
+ }
+
+ // We need at least five characters to form a URL,
+ // hence, at minimum, five characters from the beginning of the line.
+ rng = editor.selection.getRng(true).cloneRange();
+ if (rng.startOffset < 5) {
+ // During testing, the caret is placed between two text nodes.
+ // The previous text node contains the URL.
+ prev = rng.endContainer.previousSibling;
+ if (!prev) {
+ if (!rng.endContainer.firstChild || !rng.endContainer.firstChild.nextSibling) {
+ return;
+ }
+
+ prev = rng.endContainer.firstChild.nextSibling;
+ }
+
+ len = prev.length;
+ setStart(prev, len);
+ setEnd(prev, len);
+
+ if (rng.endOffset < 5) {
+ return;
+ }
+
+ end = rng.endOffset;
+ endContainer = prev;
+ } else {
+ endContainer = rng.endContainer;
+
+ // Get a text node
+ if (endContainer.nodeType != 3 && endContainer.firstChild) {
+ while (endContainer.nodeType != 3 && endContainer.firstChild) {
+ endContainer = endContainer.firstChild;
+ }
+
+ // Move range to text node
+ if (endContainer.nodeType == 3) {
+ setStart(endContainer, 0);
+ setEnd(endContainer, endContainer.nodeValue.length);
+ }
+ }
+
+ if (rng.endOffset == 1) {
+ end = 2;
+ } else {
+ end = rng.endOffset - 1 - end_offset;
+ }
+ }
+
+ start = end;
+
+ do {
+ // Move the selection one character backwards.
+ setStart(endContainer, end >= 2 ? end - 2 : 0);
+ setEnd(endContainer, end >= 1 ? end - 1 : 0);
+ end -= 1;
+ rngText = rng.toString();
+
+ // Loop until one of the following is found: a blank space, , delimiter, (end-2) >= 0
+ } while (rngText != ' ' && rngText !== '' && rngText.charCodeAt(0) != 160 && (end - 2) >= 0 && rngText != delimiter);
+
+ if (rng.toString() == delimiter || rng.toString().charCodeAt(0) == 160) {
+ setStart(endContainer, end);
+ setEnd(endContainer, start);
+ end += 1;
+ } else if (rng.startOffset === 0) {
+ setStart(endContainer, 0);
+ setEnd(endContainer, start);
+ } else {
+ setStart(endContainer, end);
+ setEnd(endContainer, start);
+ }
+
+ // Exclude last . from word like "www.site.com."
+ text = rng.toString();
+ if (text.charAt(text.length - 1) == '.') {
+ setEnd(endContainer, start - 1);
+ }
+
+ text = rng.toString();
+ matches = text.match(AutoLinkPattern);
+
+ if (matches) {
+ if (matches[1] == 'www.') {
+ matches[1] = 'http://www.';
+ } else if (/@$/.test(matches[1]) && !/^mailto:/.test(matches[1])) {
+ matches[1] = 'mailto:' + matches[1];
+ }
+
+ bookmark = editor.selection.getBookmark();
+
+ editor.selection.setRng(rng);
+ editor.execCommand('createlink', false, matches[1] + matches[2]);
+
+ if (editor.settings.default_link_target) {
+ editor.dom.setAttrib(editor.selection.getNode(), 'target', editor.settings.default_link_target);
+ }
+
+ editor.selection.moveToBookmark(bookmark);
+ editor.nodeChanged();
+ }
+ }
+});
diff --git a/resource/tinymce/plugins/code/plugin.js b/resource/tinymce/plugins/code/plugin.js
@@ -0,0 +1,60 @@
+/**
+ * plugin.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*global tinymce:true */
+
+tinymce.PluginManager.add('code', function(editor) {
+ function showDialog() {
+ var win = editor.windowManager.open({
+ title: "Source code",
+ body: {
+ type: 'textbox',
+ name: 'code',
+ multiline: true,
+ minWidth: editor.getParam("code_dialog_width", 600),
+ minHeight: editor.getParam("code_dialog_height", Math.min(tinymce.DOM.getViewPort().h - 200, 500)),
+ spellcheck: false,
+ style: 'direction: ltr; text-align: left'
+ },
+ onSubmit: function(e) {
+ // We get a lovely "Wrong document" error in IE 11 if we
+ // don't move the focus to the editor before creating an undo
+ // transation since it tries to make a bookmark for the current selection
+ editor.focus();
+
+ editor.undoManager.transact(function() {
+ editor.setContent(e.data.code);
+ });
+
+ editor.selection.setCursorLocation();
+ editor.nodeChanged();
+ }
+ });
+
+ // Gecko has a major performance issue with textarea
+ // contents so we need to set it when all reflows are done
+ win.find('#code').value(editor.getContent({source_view: true}));
+ }
+
+ editor.addCommand("mceCodeEditor", showDialog);
+
+ editor.addButton('code', {
+ icon: 'code',
+ tooltip: 'Source code',
+ onclick: showDialog
+ });
+
+ editor.addMenuItem('code', {
+ icon: 'code',
+ text: 'Source code',
+ context: 'tools',
+ onclick: showDialog
+ });
+});
+\ No newline at end of file
diff --git a/resource/tinymce/plugins/contextmenu/editor_plugin.js b/resource/tinymce/plugins/contextmenu/editor_plugin.js
@@ -1,165 +0,0 @@
-/**
- * editor_plugin_src.js
- *
- * Copyright 2009, Moxiecode Systems AB
- * Released under LGPL License.
- *
- * License: http://tinymce.moxiecode.com/license
- * Contributing: http://tinymce.moxiecode.com/contributing
- */
-
-(function() {
- var Event = tinymce.dom.Event, each = tinymce.each, DOM = tinymce.DOM;
-
- /**
- * This plugin a context menu to TinyMCE editor instances.
- *
- * @class tinymce.plugins.ContextMenu
- */
- tinymce.create('tinymce.plugins.ContextMenu', {
- /**
- * Initializes the plugin, this will be executed after the plugin has been created.
- * This call is done before the editor instance has finished it's initialization so use the onInit event
- * of the editor instance to intercept that event.
- *
- * @method init
- * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.
- * @param {string} url Absolute URL to where the plugin is located.
- */
- init : function(ed) {
- var t = this, showMenu, contextmenuNeverUseNative, realCtrlKey, hideMenu;
-
- t.editor = ed;
-
- contextmenuNeverUseNative = ed.settings.contextmenu_never_use_native;
-
- /**
- * This event gets fired when the context menu is shown.
- *
- * @event onContextMenu
- * @param {tinymce.plugins.ContextMenu} sender Plugin instance sending the event.
- * @param {tinymce.ui.DropMenu} menu Drop down menu to fill with more items if needed.
- */
- t.onContextMenu = new tinymce.util.Dispatcher(this);
-
- hideMenu = function(e) {
- hide(ed, e);
- };
-
- showMenu = ed.onContextMenu.add(function(ed, e) {
- // Block TinyMCE menu on ctrlKey and work around Safari issue
- if ((realCtrlKey !== 0 ? realCtrlKey : e.ctrlKey) && !contextmenuNeverUseNative)
- return;
-
- Event.cancel(e);
-
- // Select the image if it's clicked. WebKit would other wise expand the selection
- if (e.target.nodeName == 'IMG')
- ed.selection.select(e.target);
-
- t._getMenu(ed).showMenu(e.clientX || e.pageX, e.clientY || e.pageY);
- Event.add(ed.getDoc(), 'click', hideMenu);
-
- ed.nodeChanged();
- });
-
- ed.onRemove.add(function() {
- if (t._menu)
- t._menu.removeAll();
- });
-
- function hide(ed, e) {
- realCtrlKey = 0;
-
- // Since the contextmenu event moves
- // the selection we need to store it away
- if (e && e.button == 2) {
- realCtrlKey = e.ctrlKey;
- return;
- }
-
- if (t._menu) {
- t._menu.removeAll();
- t._menu.destroy();
- Event.remove(ed.getDoc(), 'click', hideMenu);
- t._menu = null;
- }
- };
-
- ed.onMouseDown.add(hide);
- ed.onKeyDown.add(hide);
- ed.onKeyDown.add(function(ed, e) {
- if (e.shiftKey && !e.ctrlKey && !e.altKey && e.keyCode === 121) {
- Event.cancel(e);
- showMenu(ed, e);
- }
- });
- },
-
- /**
- * Returns information about the plugin as a name/value array.
- * The current keys are longname, author, authorurl, infourl and version.
- *
- * @method getInfo
- * @return {Object} Name/value array containing information about the plugin.
- */
- getInfo : function() {
- return {
- longname : 'Contextmenu',
- author : 'Moxiecode Systems AB',
- authorurl : 'http://tinymce.moxiecode.com',
- infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/contextmenu',
- version : tinymce.majorVersion + "." + tinymce.minorVersion
- };
- },
-
- _getMenu : function(ed) {
- var t = this, m = t._menu, se = ed.selection, col = se.isCollapsed(), el = se.getNode() || ed.getBody(), am, p;
-
- if (m) {
- m.removeAll();
- m.destroy();
- }
-
- p = DOM.getPos(ed.getContentAreaContainer());
-
- m = ed.controlManager.createDropMenu('contextmenu', {
- offset_x : p.x + ed.getParam('contextmenu_offset_x', 0),
- offset_y : p.y + ed.getParam('contextmenu_offset_y', 0),
- constrain : 1,
- keyboard_focus: true
- });
-
- t._menu = m;
-
- m.add({title : 'advanced.cut_desc', icon : 'cut', cmd : 'Cut'}).setDisabled(col);
- m.add({title : 'advanced.copy_desc', icon : 'copy', cmd : 'Copy'}).setDisabled(col);
- m.add({title : 'advanced.paste_desc', icon : 'paste', cmd : 'Paste'});
-
- if ((el.nodeName == 'A' && !ed.dom.getAttrib(el, 'name')) || !col) {
- m.addSeparator();
- // Added by Zotero
- if(el.nodeName == 'A') m.add({title : 'Open Link', icon : 'link', cmd : 'openlink', ui : true });
- m.add({title : 'advanced.link_desc', icon : 'link', cmd : ed.plugins.advlink ? 'mceAdvLink' : 'mceLink', ui : true});
- m.add({title : 'advanced.unlink_desc', icon : 'unlink', cmd : 'UnLink'});
- }
-
- m.addSeparator();
- m.add({title : 'advanced.image_desc', icon : 'image', cmd : ed.plugins.advimage ? 'mceAdvImage' : 'mceImage', ui : true});
-
- m.addSeparator();
- am = m.addMenu({title : 'contextmenu.align'});
- am.add({title : 'contextmenu.left', icon : 'justifyleft', cmd : 'JustifyLeft'});
- am.add({title : 'contextmenu.center', icon : 'justifycenter', cmd : 'JustifyCenter'});
- am.add({title : 'contextmenu.right', icon : 'justifyright', cmd : 'JustifyRight'});
- am.add({title : 'contextmenu.full', icon : 'justifyfull', cmd : 'JustifyFull'});
-
- t.onContextMenu.dispatch(t, m, el, col);
-
- return m;
- }
- });
-
- // Register plugin
- tinymce.PluginManager.add('contextmenu', tinymce.plugins.ContextMenu);
-})();
-\ No newline at end of file
diff --git a/resource/tinymce/plugins/contextmenu/plugin.js b/resource/tinymce/plugins/contextmenu/plugin.js
@@ -0,0 +1,116 @@
+/**
+ * plugin.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*global tinymce:true */
+
+tinymce.PluginManager.add('contextmenu', function(editor) {
+ var menu, visibleState, contextmenuNeverUseNative = editor.settings.contextmenu_never_use_native;
+
+ var isNativeOverrideKeyEvent = function (e) {
+ return e.ctrlKey && !contextmenuNeverUseNative;
+ };
+
+ var isMacWebKit = function () {
+ return tinymce.Env.mac && tinymce.Env.webkit;
+ };
+
+ var isContextMenuVisible = function () {
+ return visibleState === true;
+ };
+
+ /**
+ * This takes care of a os x native issue where it expands the selection
+ * to the word at the caret position to do "lookups". Since we are overriding
+ * the context menu we also need to override this expanding so the behavior becomes
+ * normalized. Firefox on os x doesn't expand to the word when using the context menu.
+ */
+ editor.on('mousedown', function (e) {
+ if (isMacWebKit() && e.button === 2 && !isNativeOverrideKeyEvent(e)) {
+ if (editor.selection.isCollapsed()) {
+ editor.once('contextmenu', function (e) {
+ editor.selection.placeCaretAt(e.clientX, e.clientY);
+ });
+ }
+ }
+ });
+
+ editor.on('contextmenu', function(e) {
+ var contextmenu;
+
+ if (isNativeOverrideKeyEvent(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ contextmenu = editor.settings.contextmenu || 'link openlink image inserttable | cell row column deletetable';
+
+ // Render menu
+ if (!menu) {
+ var items = [];
+
+ tinymce.each(contextmenu.split(/[ ,]/), function(name) {
+ var item = editor.menuItems[name];
+
+ if (name == '|') {
+ item = {text: name};
+ }
+
+ if (item) {
+ item.shortcut = ''; // Hide shortcuts
+ items.push(item);
+ }
+ });
+
+ for (var i = 0; i < items.length; i++) {
+ if (items[i].text == '|') {
+ if (i === 0 || i == items.length - 1) {
+ items.splice(i, 1);
+ }
+ }
+ }
+
+ menu = new tinymce.ui.Menu({
+ items: items,
+ context: 'contextmenu',
+ classes: 'contextmenu'
+ }).renderTo();
+
+ menu.on('hide', function (e) {
+ if (e.control === this) {
+ visibleState = false;
+ }
+ });
+
+ editor.on('remove', function() {
+ menu.remove();
+ menu = null;
+ });
+
+ } else {
+ menu.show();
+ }
+
+ // Position menu
+ var pos = {x: e.pageX, y: e.pageY};
+
+ if (!editor.inline) {
+ pos = tinymce.DOM.getPos(editor.getContentAreaContainer());
+ pos.x += e.clientX;
+ pos.y += e.clientY;
+ }
+
+ menu.moveTo(pos.x, pos.y);
+ visibleState = true;
+ });
+
+ return {
+ isContextMenuVisible: isContextMenuVisible
+ };
+});
+\ No newline at end of file
diff --git a/resource/tinymce/plugins/directionality/editor_plugin.js b/resource/tinymce/plugins/directionality/editor_plugin.js
@@ -1,85 +0,0 @@
-/**
- * editor_plugin_src.js
- *
- * Copyright 2009, Moxiecode Systems AB
- * Released under LGPL License.
- *
- * License: http://tinymce.moxiecode.com/license
- * Contributing: http://tinymce.moxiecode.com/contributing
- */
-
-(function() {
- tinymce.create('tinymce.plugins.Directionality', {
- init : function(ed, url) {
- var t = this;
-
- t.editor = ed;
-
- function setDir(dir) {
- var dom = ed.dom, curDir, blocks = ed.selection.getSelectedBlocks();
-
- if (blocks.length) {
- curDir = dom.getAttrib(blocks[0], "dir");
-
- tinymce.each(blocks, function(block) {
- // Add dir to block if the parent block doesn't already have that dir
- if (!dom.getParent(block.parentNode, "*[dir='" + dir + "']", dom.getRoot())) {
- if (curDir != dir) {
- dom.setAttrib(block, "dir", dir);
- } else {
- dom.setAttrib(block, "dir", null);
- }
- }
- });
-
- ed.nodeChanged();
- }
- }
-
- ed.addCommand('mceDirectionLTR', function() {
- setDir("ltr");
- });
-
- ed.addCommand('mceDirectionRTL', function() {
- setDir("rtl");
- });
-
- ed.addButton('ltr', {title : 'directionality.ltr_desc', cmd : 'mceDirectionLTR'});
- ed.addButton('rtl', {title : 'directionality.rtl_desc', cmd : 'mceDirectionRTL'});
-
- ed.onNodeChange.add(t._nodeChange, t);
- },
-
- getInfo : function() {
- return {
- longname : 'Directionality',
- author : 'Moxiecode Systems AB',
- authorurl : 'http://tinymce.moxiecode.com',
- infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/directionality',
- version : tinymce.majorVersion + "." + tinymce.minorVersion
- };
- },
-
- // Private methods
-
- _nodeChange : function(ed, cm, n) {
- var dom = ed.dom, dir;
-
- n = dom.getParent(n, dom.isBlock);
- if (!n) {
- cm.setDisabled('ltr', 1);
- cm.setDisabled('rtl', 1);
- return;
- }
-
- dir = dom.getAttrib(n, 'dir');
- cm.setActive('ltr', dir == "ltr");
- cm.setDisabled('ltr', 0);
- cm.setActive('rtl', dir == "rtl");
- cm.setDisabled('rtl', 0);
- }
- });
-
- // Register plugin
- tinymce.PluginManager.add('directionality', tinymce.plugins.Directionality);
-})();
-\ No newline at end of file
diff --git a/resource/tinymce/plugins/directionality/plugin.js b/resource/tinymce/plugins/directionality/plugin.js
@@ -0,0 +1,64 @@
+/**
+ * plugin.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*global tinymce:true */
+
+tinymce.PluginManager.add('directionality', function(editor) {
+ function setDir(dir) {
+ var dom = editor.dom, curDir, blocks = editor.selection.getSelectedBlocks();
+
+ if (blocks.length) {
+ curDir = dom.getAttrib(blocks[0], "dir");
+
+ tinymce.each(blocks, function(block) {
+ // Add dir to block if the parent block doesn't already have that dir
+ if (!dom.getParent(block.parentNode, "*[dir='" + dir + "']", dom.getRoot())) {
+ if (curDir != dir) {
+ dom.setAttrib(block, "dir", dir);
+ } else {
+ dom.setAttrib(block, "dir", null);
+ }
+ }
+ });
+
+ editor.nodeChanged();
+ }
+ }
+
+ function generateSelector(dir) {
+ var selector = [];
+
+ tinymce.each('h1 h2 h3 h4 h5 h6 div p'.split(' '), function(name) {
+ selector.push(name + '[dir=' + dir + ']');
+ });
+
+ return selector.join(',');
+ }
+
+ editor.addCommand('mceDirectionLTR', function() {
+ setDir("ltr");
+ });
+
+ editor.addCommand('mceDirectionRTL', function() {
+ setDir("rtl");
+ });
+
+ editor.addButton('ltr', {
+ title: 'Left to right',
+ cmd: 'mceDirectionLTR',
+ stateSelector: generateSelector('ltr')
+ });
+
+ editor.addButton('rtl', {
+ title: 'Right to left',
+ cmd: 'mceDirectionRTL',
+ stateSelector: generateSelector('rtl')
+ });
+});
+\ No newline at end of file
diff --git a/resource/tinymce/plugins/link/plugin.js b/resource/tinymce/plugins/link/plugin.js
@@ -0,0 +1,608 @@
+/**
+ * plugin.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*global tinymce:true */
+
+tinymce.PluginManager.add('link', function(editor) {
+ var attachState = {};
+
+ function isLink(elm) {
+ return elm && elm.nodeName === 'A' && elm.href;
+ }
+
+ function hasLinks(elements) {
+ return tinymce.util.Tools.grep(elements, isLink).length > 0;
+ }
+
+ function getLink(elm) {
+ return editor.dom.getParent(elm, 'a[href]');
+ }
+
+ function getSelectedLink() {
+ return getLink(editor.selection.getStart());
+ }
+
+ function getHref(elm) {
+ // Returns the real href value not the resolved a.href value
+ var href = elm.getAttribute('data-mce-href');
+ return href ? href : elm.getAttribute('href');
+ }
+
+ function isContextMenuVisible() {
+ var contextmenu = editor.plugins.contextmenu;
+ return contextmenu ? contextmenu.isContextMenuVisible() : false;
+ }
+
+ var hasOnlyAltModifier = function (e) {
+ return e.altKey === true && e.shiftKey === false && e.ctrlKey === false && e.metaKey === false;
+ };
+
+ function leftClickedOnAHref(elm) {
+ var sel, rng, node;
+ if (editor.settings.link_context_toolbar && !isContextMenuVisible() && isLink(elm)) {
+ sel = editor.selection;
+ rng = sel.getRng();
+ node = rng.startContainer;
+ // ignore cursor positions at the beginning/end (to make context toolbar less noisy)
+ if (node.nodeType == 3 && sel.isCollapsed() && rng.startOffset > 0 && rng.startOffset < node.data.length) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function openDetachedWindow(url) {
+ // Chrome and Webkit has implemented noopener and works correctly with/without popup blocker
+ // Firefox has it implemented noopener but when the popup blocker is activated it doesn't work
+ // Edge has only implemented noreferrer and it seems to remove opener as well
+ // Older IE versions pre IE 11 falls back to a window.open approach
+ if (!tinymce.Env.ie || tinymce.Env.ie > 10) {
+ var link = document.createElement('a');
+ link.target = '_blank';
+ link.href = url;
+ link.rel = 'noreferrer noopener';
+
+ var evt = document.createEvent('MouseEvents');
+ evt.initMouseEvent('click', true, true, window, true, 0, 0, 0, 0, false, false, false, false, 0, null);
+ link.dispatchEvent(evt);
+ } else {
+ var win = window.open('', '_blank');
+ if (win) {
+ win.opener = null;
+ var doc = win.document;
+ doc.open();
+ doc.write('<meta http-equiv="refresh" content="0; url=' + tinymce.DOM.encode(url) + '">');
+ doc.close();
+ }
+ }
+ }
+
+ function gotoLink(a) {
+ if (a) {
+ var href = getHref(a);
+ if (/^#/.test(href)) {
+ var targetEl = editor.$(href);
+ if (targetEl.length) {
+ editor.selection.scrollIntoView(targetEl[0], true);
+ }
+ } else {
+ openDetachedWindow(a.href);
+ }
+ }
+ }
+
+ function gotoSelectedLink() {
+ gotoLink(getSelectedLink());
+ }
+
+ function toggleViewLinkState() {
+ var self = this;
+
+ var toggleVisibility = function (e) {
+ if (hasLinks(e.parents)) {
+ self.show();
+ } else {
+ self.hide();
+ }
+ };
+
+ if (!hasLinks(editor.dom.getParents(editor.selection.getStart()))) {
+ self.hide();
+ }
+
+ editor.on('nodechange', toggleVisibility);
+
+ self.on('remove', function () {
+ editor.off('nodechange', toggleVisibility);
+ });
+ }
+
+ function createLinkList(callback) {
+ return function() {
+ var linkList = editor.settings.link_list;
+
+ if (typeof linkList == "string") {
+ tinymce.util.XHR.send({
+ url: linkList,
+ success: function(text) {
+ callback(tinymce.util.JSON.parse(text));
+ }
+ });
+ } else if (typeof linkList == "function") {
+ linkList(callback);
+ } else {
+ callback(linkList);
+ }
+ };
+ }
+
+ function buildListItems(inputList, itemCallback, startItems) {
+ function appendItems(values, output) {
+ output = output || [];
+
+ tinymce.each(values, function(item) {
+ var menuItem = {text: item.text || item.title};
+
+ if (item.menu) {
+ menuItem.menu = appendItems(item.menu);
+ } else {
+ menuItem.value = item.value;
+
+ if (itemCallback) {
+ itemCallback(menuItem);
+ }
+ }
+
+ output.push(menuItem);
+ });
+
+ return output;
+ }
+
+ return appendItems(inputList, startItems || []);
+ }
+
+ function showDialog(linkList) {
+ var data = {}, selection = editor.selection, dom = editor.dom, selectedElm, anchorElm, initialText;
+ var win, onlyText, textListCtrl, linkListCtrl, relListCtrl, targetListCtrl, classListCtrl, linkTitleCtrl, value;
+
+ function linkListChangeHandler(e) {
+ var textCtrl = win.find('#text');
+
+ if (!textCtrl.value() || (e.lastControl && textCtrl.value() == e.lastControl.text())) {
+ textCtrl.value(e.control.text());
+ }
+
+ win.find('#href').value(e.control.value());
+ }
+
+ function buildAnchorListControl(url) {
+ var anchorList = [];
+
+ tinymce.each(editor.dom.select('a:not([href])'), function(anchor) {
+ var id = anchor.name || anchor.id;
+
+ if (id) {
+ anchorList.push({
+ text: id,
+ value: '#' + id,
+ selected: url.indexOf('#' + id) != -1
+ });
+ }
+ });
+
+ if (anchorList.length) {
+ anchorList.unshift({text: 'None', value: ''});
+
+ return {
+ name: 'anchor',
+ type: 'listbox',
+ label: 'Anchors',
+ values: anchorList,
+ onselect: linkListChangeHandler
+ };
+ }
+ }
+
+ function updateText() {
+ if (!initialText && data.text.length === 0 && onlyText) {
+ this.parent().parent().find('#text')[0].value(this.value());
+ }
+ }
+
+ function urlChange(e) {
+ var meta = e.meta || {};
+
+ if (linkListCtrl) {
+ linkListCtrl.value(editor.convertURL(this.value(), 'href'));
+ }
+
+ tinymce.each(e.meta, function(value, key) {
+ var inp = win.find('#' + key);
+
+ if (key === 'text') {
+ if (initialText.length === 0) {
+ inp.value(value);
+ data.text = value;
+ }
+ } else {
+ inp.value(value);
+ }
+ });
+
+ if (meta.attach) {
+ attachState = {
+ href: this.value(),
+ attach: meta.attach
+ };
+ }
+
+ if (!meta.text) {
+ updateText.call(this);
+ }
+ }
+
+ function isOnlyTextSelected(anchorElm) {
+ var html = selection.getContent();
+
+ // Partial html and not a fully selected anchor element
+ if (/</.test(html) && (!/^<a [^>]+>[^<]+<\/a>$/.test(html) || html.indexOf('href=') == -1)) {
+ return false;
+ }
+
+ if (anchorElm) {
+ var nodes = anchorElm.childNodes, i;
+
+ if (nodes.length === 0) {
+ return false;
+ }
+
+ for (i = nodes.length - 1; i >= 0; i--) {
+ if (nodes[i].nodeType != 3) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ function onBeforeCall(e) {
+ e.meta = win.toJSON();
+ }
+
+ selectedElm = selection.getNode();
+ anchorElm = dom.getParent(selectedElm, 'a[href]');
+ onlyText = isOnlyTextSelected();
+
+ data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({format: 'text'});
+ data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : '';
+
+ if (anchorElm) {
+ data.target = dom.getAttrib(anchorElm, 'target');
+ } else if (editor.settings.default_link_target) {
+ data.target = editor.settings.default_link_target;
+ }
+
+ if ((value = dom.getAttrib(anchorElm, 'rel'))) {
+ data.rel = value;
+ }
+
+ if ((value = dom.getAttrib(anchorElm, 'class'))) {
+ data['class'] = value;
+ }
+
+ if ((value = dom.getAttrib(anchorElm, 'title'))) {
+ data.title = value;
+ }
+
+ if (onlyText) {
+ textListCtrl = {
+ name: 'text',
+ type: 'textbox',
+ size: 40,
+ label: 'Text to display',
+ onchange: function() {
+ data.text = this.value();
+ }
+ };
+ }
+
+ if (linkList) {
+ linkListCtrl = {
+ type: 'listbox',
+ label: 'Link list',
+ values: buildListItems(
+ linkList,
+ function(item) {
+ item.value = editor.convertURL(item.value || item.url, 'href');
+ },
+ [{text: 'None', value: ''}]
+ ),
+ onselect: linkListChangeHandler,
+ value: editor.convertURL(data.href, 'href'),
+ onPostRender: function() {
+ /*eslint consistent-this:0*/
+ linkListCtrl = this;
+ }
+ };
+ }
+
+ if (editor.settings.target_list !== false) {
+ if (!editor.settings.target_list) {
+ editor.settings.target_list = [
+ {text: 'None', value: ''},
+ {text: 'New window', value: '_blank'}
+ ];
+ }
+
+ targetListCtrl = {
+ name: 'target',
+ type: 'listbox',
+ label: 'Target',
+ values: buildListItems(editor.settings.target_list)
+ };
+ }
+
+ if (editor.settings.rel_list) {
+ relListCtrl = {
+ name: 'rel',
+ type: 'listbox',
+ label: 'Rel',
+ values: buildListItems(editor.settings.rel_list)
+ };
+ }
+
+ if (editor.settings.link_class_list) {
+ classListCtrl = {
+ name: 'class',
+ type: 'listbox',
+ label: 'Class',
+ values: buildListItems(
+ editor.settings.link_class_list,
+ function(item) {
+ if (item.value) {
+ item.textStyle = function() {
+ return editor.formatter.getCssText({inline: 'a', classes: [item.value]});
+ };
+ }
+ }
+ )
+ };
+ }
+
+ if (editor.settings.link_title !== false) {
+ linkTitleCtrl = {
+ name: 'title',
+ type: 'textbox',
+ label: 'Title',
+ value: data.title
+ };
+ }
+
+ win = editor.windowManager.open({
+ title: 'Insert link',
+ data: data,
+ body: [
+ {
+ name: 'href',
+ type: 'filepicker',
+ filetype: 'file',
+ size: 40,
+ autofocus: true,
+ label: 'Url',
+ onchange: urlChange,
+ onkeyup: updateText,
+ onbeforecall: onBeforeCall
+ },
+ textListCtrl,
+ linkTitleCtrl,
+ buildAnchorListControl(data.href),
+ linkListCtrl,
+ relListCtrl,
+ targetListCtrl,
+ classListCtrl
+ ],
+ onSubmit: function(e) {
+ /*eslint dot-notation: 0*/
+ var href;
+
+ data = tinymce.extend(data, e.data);
+ href = data.href;
+
+ // Delay confirm since onSubmit will move focus
+ function delayedConfirm(message, callback) {
+ var rng = editor.selection.getRng();
+
+ tinymce.util.Delay.setEditorTimeout(editor, function() {
+ editor.windowManager.confirm(message, function(state) {
+ editor.selection.setRng(rng);
+ callback(state);
+ });
+ });
+ }
+
+ function toggleTargetRules(rel, isUnsafe) {
+ var rules = 'noopener noreferrer';
+
+ function addTargetRules(rel) {
+ rel = removeTargetRules(rel);
+ return rel ? [rel, rules].join(' ') : rules;
+ }
+
+ function removeTargetRules(rel) {
+ var regExp = new RegExp('(' + rules.replace(' ', '|') + ')', 'g');
+ if (rel) {
+ rel = tinymce.trim(rel.replace(regExp, ''));
+ }
+ return rel ? rel : null;
+ }
+
+ return isUnsafe ? addTargetRules(rel) : removeTargetRules(rel);
+ }
+
+ function createLink() {
+ var linkAttrs = {
+ href: href,
+ target: data.target ? data.target : null,
+ rel: data.rel ? data.rel : null,
+ "class": data["class"] ? data["class"] : null,
+ title: data.title ? data.title : null
+ };
+
+ if (!editor.settings.allow_unsafe_link_target) {
+ linkAttrs.rel = toggleTargetRules(linkAttrs.rel, linkAttrs.target == '_blank');
+ }
+
+ if (href === attachState.href) {
+ attachState.attach();
+ attachState = {};
+ }
+
+ if (anchorElm) {
+ editor.focus();
+
+ if (onlyText && data.text != initialText) {
+ if ("innerText" in anchorElm) {
+ anchorElm.innerText = data.text;
+ } else {
+ anchorElm.textContent = data.text;
+ }
+ }
+
+ dom.setAttribs(anchorElm, linkAttrs);
+
+ selection.select(anchorElm);
+ editor.undoManager.add();
+ } else {
+ if (onlyText) {
+ editor.insertContent(dom.createHTML('a', linkAttrs, dom.encode(data.text)));
+ } else {
+ editor.execCommand('mceInsertLink', false, linkAttrs);
+ }
+ }
+ }
+
+ function insertLink() {
+ editor.undoManager.transact(createLink);
+ }
+
+ if (!href) {
+ editor.execCommand('unlink');
+ return;
+ }
+
+ // Is email and not //user@domain.com
+ if (href.indexOf('@') > 0 && href.indexOf('//') == -1 && href.indexOf('mailto:') == -1) {
+ delayedConfirm(
+ 'The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?',
+ function(state) {
+ if (state) {
+ href = 'mailto:' + href;
+ }
+
+ insertLink();
+ }
+ );
+
+ return;
+ }
+
+ // Is not protocol prefixed
+ if ((editor.settings.link_assume_external_targets && !/^\w+:/i.test(href)) ||
+ (!editor.settings.link_assume_external_targets && /^\s*www[\.|\d\.]/i.test(href))) {
+ delayedConfirm(
+ 'The URL you entered seems to be an external link. Do you want to add the required http:// prefix?',
+ function(state) {
+ if (state) {
+ href = 'http://' + href;
+ }
+
+ insertLink();
+ }
+ );
+
+ return;
+ }
+
+ insertLink();
+ }
+ });
+ }
+
+ editor.addButton('link', {
+ icon: 'link',
+ tooltip: 'Insert/edit link',
+ shortcut: 'Meta+K',
+ onclick: createLinkList(showDialog),
+ stateSelector: 'a[href]'
+ });
+
+ editor.addButton('unlink', {
+ icon: 'unlink',
+ tooltip: 'Remove link',
+ cmd: 'unlink',
+ stateSelector: 'a[href]'
+ });
+
+
+ if (editor.addContextToolbar) {
+ editor.addButton('openlink', {
+ icon: 'newtab',
+ tooltip: 'Open link',
+ onclick: gotoSelectedLink
+ });
+
+ editor.addContextToolbar(
+ leftClickedOnAHref,
+ 'openlink | link unlink'
+ );
+ }
+
+
+ editor.addShortcut('Meta+K', '', createLinkList(showDialog));
+ editor.addCommand('mceLink', createLinkList(showDialog));
+
+ editor.on('click', function (e) {
+ var link = getLink(e.target);
+ if (link && tinymce.util.VK.metaKeyPressed(e)) {
+ e.preventDefault();
+ gotoLink(link);
+ }
+ });
+
+ editor.on('keydown', function (e) {
+ var link = getSelectedLink();
+ if (link && e.keyCode === 13 && hasOnlyAltModifier(e)) {
+ e.preventDefault();
+ gotoLink(link);
+ }
+ });
+
+ this.showDialog = showDialog;
+
+ editor.addMenuItem('openlink', {
+ text: 'Open link',
+ icon: 'newtab',
+ onclick: gotoSelectedLink,
+ onPostRender: toggleViewLinkState,
+ prependToContext: true
+ });
+
+ editor.addMenuItem('link', {
+ icon: 'link',
+ text: 'Link',
+ shortcut: 'Meta+K',
+ onclick: createLinkList(showDialog),
+ stateSelector: 'a[href]',
+ context: 'insert',
+ prependToContext: true
+ });
+});
diff --git a/resource/tinymce/plugins/linksmenu/editor_plugin.js b/resource/tinymce/plugins/linksmenu/editor_plugin.js
@@ -1,176 +0,0 @@
-(function() {
- var Event = tinymce.dom.Event, each = tinymce.each, DOM = tinymce.DOM;
-
- /**
- * This plugin adds a left-click context menu to links in the TinyMCE editor for Zotero.
- * Code adopted and modified from TinyMCE contextmenu plugin.
- *
- * @class tinymce.plugins.LinksMenu
- */
- tinymce.create('tinymce.plugins.LinksMenu', {
- /**
- * Initializes the plugin, this will be executed after the plugin has been created.
- * This call is done before the editor instance has finished it's initialization so use the onInit event
- * of the editor instance to intercept that event.
- *
- * @method init
- * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.
- * @param {string} url Absolute URL to where the plugin is located.
- */
- init : function(ed) {
- var t = this, showMenu, contextmenuNeverUseNative, realCtrlKey, hideMenu;
-
- t.editor = ed;
-
- contextmenuNeverUseNative = ed.settings.contextmenu_never_use_native;
-
- // add editor command to open links through zoteroHandleEvent
- ed.addCommand('openlink', function(command) {
- var ed = tinyMCE.activeEditor;
- var node = ed.selection.getNode();
- if (node.nodeName == 'A') {
- zoteroHandleEvent({
- type: 'openlink',
- target: node,
- // We don't seem to be able to access the click event that triggered this
- // command in order to check the modifier keys used, so instead we save
- // the keys on every menu click in tiny_mce.js and pass them on here
- // for use by loadURI().
- modifierKeys: ed.lastClickModifierKeys
- });
- }
- });
-
- /**
- * This event gets fired when the context menu is shown.
- *
- * @event onClick
- * @param {tinymce.plugins.LinksMenu} sender Plugin instance sending the event.
- * @param {tinymce.ui.DropMenu} menu Drop down menu to fill with more items if needed.
- */
- t.onClick = new tinymce.util.Dispatcher(this);
-
- hideMenu = function(e) {
- hide(ed, e);
- };
-
- showMenu = ed.onClick.add(function(ed, e) {
- // Only show on left-click
- if (e.button != 0) {
- return;
- }
-
- // Only show when <a> node
- if (e.target.nodeName != 'A') {
- return;
- }
-
- // Block TinyMCE menu on ctrlKey and work around Safari issue
- if ((realCtrlKey !== 0 ? realCtrlKey : e.ctrlKey) && !contextmenuNeverUseNative) {
- return;
- }
-
- Event.cancel(e);
-
- t._getMenu(ed).showMenu(e.clientX || e.pageX, e.clientY || e.pageY);
- Event.add(ed.getDoc(), 'click', hideMenu);
-
- ed.nodeChanged();
- });
-
- ed.onRemove.add(function() {
- if (t._menu)
- t._menu.removeAll();
- });
-
- function hide(ed, e) {
- realCtrlKey = 0;
-
- // Since the contextmenu event moves
- // the selection we need to store it away
- if (e && e.button == 2) {
- realCtrlKey = e.ctrlKey;
- return;
- }
-
- if (t._menu) {
- t._menu.removeAll();
- t._menu.destroy();
- Event.remove(ed.getDoc(), 'click', hideMenu);
- t._menu = null;
- }
- };
-
- ed.onMouseDown.add(hide);
- ed.onKeyDown.add(hide);
- ed.onKeyDown.add(function(ed, e) {
- if (e.shiftKey && !e.ctrlKey && !e.altKey && e.keyCode === 121) {
- Event.cancel(e);
- showMenu(ed, e);
- }
- });
- },
-
- /**
- * Returns information about the plugin as a name/value array.
- * The current keys are longname, author, authorurl, infourl and version.
- *
- * @method getInfo
- * @return {Object} Name/value array containing information about the plugin.
- */
- getInfo : function() {
- return {
- longname : 'Linksmenu',
- author : '',
- authorurl : '',
- infourl : '',
- version : tinymce.majorVersion + "." + tinymce.minorVersion
- };
- },
-
- _getMenu : function(ed) {
- var t = this, m = t._menu, se = ed.selection, col = se.isCollapsed(), el = se.getNode() || ed.getBody(), am, p;
-
- if (m) {
- m.removeAll();
- m.destroy();
- }
-
- p = DOM.getPos(ed.getContentAreaContainer());
-
- m = ed.controlManager.createDropMenu('linksmenu', {
- offset_x : p.x + ed.getParam('contextmenu_offset_x', 0),
- offset_y : p.y + ed.getParam('contextmenu_offset_y', 0),
- constrain : 1,
- keyboard_focus: true
- });
-
- t._menu = m;
-
- m.add({
- title : 'Open Link',
- icon : 'link',
- cmd : 'openlink',
- ui : true
- });
- m.add({
- title : 'Edit Link',
- icon : 'link',
- cmd : ed.plugins.advlink ? 'mceAdvLink' : 'mceLink',
- ui : true
- });
- m.add({
- title : 'advanced.unlink_desc',
- icon : 'unlink',
- cmd : 'UnLink'
- });
-
- t.onClick.dispatch(t, m, el, col);
-
- return m;
- }
- });
-
- // Register plugin
- tinymce.PluginManager.add('linksmenu', tinymce.plugins.LinksMenu);
-})();
diff --git a/resource/tinymce/plugins/lists/plugin.js b/resource/tinymce/plugins/lists/plugin.js
@@ -0,0 +1,1006 @@
+/**
+ * plugin.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*global tinymce:true */
+/*eslint consistent-this:0 */
+
+tinymce.PluginManager.add('lists', function(editor) {
+ var self = this;
+
+ function isChildOfBody(elm) {
+ return editor.$.contains(editor.getBody(), elm);
+ }
+
+ function isBr(node) {
+ return node && node.nodeName == 'BR';
+ }
+
+ function isListNode(node) {
+ return node && (/^(OL|UL|DL)$/).test(node.nodeName) && isChildOfBody(node);
+ }
+
+ function isListItemNode(node) {
+ return node && /^(LI|DT|DD)$/.test(node.nodeName);
+ }
+
+ function isFirstChild(node) {
+ return node.parentNode.firstChild == node;
+ }
+
+ function isLastChild(node) {
+ return node.parentNode.lastChild == node;
+ }
+
+ function isTextBlock(node) {
+ return node && !!editor.schema.getTextBlockElements()[node.nodeName];
+ }
+
+ function isEditorBody(elm) {
+ return elm === editor.getBody();
+ }
+
+ function isTextNode(node) {
+ return node && node.nodeType === 3;
+ }
+
+ function getNormalizedEndPoint(container, offset) {
+ var node = tinymce.dom.RangeUtils.getNode(container, offset);
+
+ if (isListItemNode(container) && isTextNode(node)) {
+ var textNodeOffset = offset >= container.childNodes.length ? node.data.length : 0;
+ return {container: node, offset: textNodeOffset};
+ }
+
+ return {container: container, offset: offset};
+ }
+
+ function normalizeRange(rng) {
+ var outRng = rng.cloneRange();
+
+ var rangeStart = getNormalizedEndPoint(rng.startContainer, rng.startOffset);
+ outRng.setStart(rangeStart.container, rangeStart.offset);
+
+ var rangeEnd = getNormalizedEndPoint(rng.endContainer, rng.endOffset);
+ outRng.setEnd(rangeEnd.container, rangeEnd.offset);
+
+ return outRng;
+ }
+
+ editor.on('init', function() {
+ var dom = editor.dom, selection = editor.selection;
+
+ function isEmpty(elm, keepBookmarks) {
+ var empty = dom.isEmpty(elm);
+
+ if (keepBookmarks && dom.select('span[data-mce-type=bookmark]').length > 0) {
+ return false;
+ }
+
+ return empty;
+ }
+
+ /**
+ * Returns a range bookmark. This will convert indexed bookmarks into temporary span elements with
+ * index 0 so that they can be restored properly after the DOM has been modified. Text bookmarks will not have spans
+ * added to them since they can be restored after a dom operation.
+ *
+ * So this: <p><b>|</b><b>|</b></p>
+ * becomes: <p><b><span data-mce-type="bookmark">|</span></b><b data-mce-type="bookmark">|</span></b></p>
+ *
+ * @param {DOMRange} rng DOM Range to get bookmark on.
+ * @return {Object} Bookmark object.
+ */
+ function createBookmark(rng) {
+ var bookmark = {};
+
+ function setupEndPoint(start) {
+ var offsetNode, container, offset;
+
+ container = rng[start ? 'startContainer' : 'endContainer'];
+ offset = rng[start ? 'startOffset' : 'endOffset'];
+
+ if (container.nodeType == 1) {
+ offsetNode = dom.create('span', {'data-mce-type': 'bookmark'});
+
+ if (container.hasChildNodes()) {
+ offset = Math.min(offset, container.childNodes.length - 1);
+
+ if (start) {
+ container.insertBefore(offsetNode, container.childNodes[offset]);
+ } else {
+ dom.insertAfter(offsetNode, container.childNodes[offset]);
+ }
+ } else {
+ container.appendChild(offsetNode);
+ }
+
+ container = offsetNode;
+ offset = 0;
+ }
+
+ bookmark[start ? 'startContainer' : 'endContainer'] = container;
+ bookmark[start ? 'startOffset' : 'endOffset'] = offset;
+ }
+
+ setupEndPoint(true);
+
+ if (!rng.collapsed) {
+ setupEndPoint();
+ }
+
+ return bookmark;
+ }
+
+ /**
+ * Moves the selection to the current bookmark and removes any selection container wrappers.
+ *
+ * @param {Object} bookmark Bookmark object to move selection to.
+ */
+ function moveToBookmark(bookmark) {
+ function restoreEndPoint(start) {
+ var container, offset, node;
+
+ function nodeIndex(container) {
+ var node = container.parentNode.firstChild, idx = 0;
+
+ while (node) {
+ if (node == container) {
+ return idx;
+ }
+
+ // Skip data-mce-type=bookmark nodes
+ if (node.nodeType != 1 || node.getAttribute('data-mce-type') != 'bookmark') {
+ idx++;
+ }
+
+ node = node.nextSibling;
+ }
+
+ return -1;
+ }
+
+ container = node = bookmark[start ? 'startContainer' : 'endContainer'];
+ offset = bookmark[start ? 'startOffset' : 'endOffset'];
+
+ if (!container) {
+ return;
+ }
+
+ if (container.nodeType == 1) {
+ offset = nodeIndex(container);
+ container = container.parentNode;
+ dom.remove(node);
+ }
+
+ bookmark[start ? 'startContainer' : 'endContainer'] = container;
+ bookmark[start ? 'startOffset' : 'endOffset'] = offset;
+ }
+
+ restoreEndPoint(true);
+ restoreEndPoint();
+
+ var rng = dom.createRng();
+
+ rng.setStart(bookmark.startContainer, bookmark.startOffset);
+
+ if (bookmark.endContainer) {
+ rng.setEnd(bookmark.endContainer, bookmark.endOffset);
+ }
+
+ selection.setRng(normalizeRange(rng));
+ }
+
+ function createNewTextBlock(contentNode, blockName) {
+ var node, textBlock, fragment = dom.createFragment(), hasContentNode;
+ var blockElements = editor.schema.getBlockElements();
+
+ if (editor.settings.forced_root_block) {
+ blockName = blockName || editor.settings.forced_root_block;
+ }
+
+ if (blockName) {
+ textBlock = dom.create(blockName);
+
+ if (textBlock.tagName === editor.settings.forced_root_block) {
+ dom.setAttribs(textBlock, editor.settings.forced_root_block_attrs);
+ }
+
+ fragment.appendChild(textBlock);
+ }
+
+ if (contentNode) {
+ while ((node = contentNode.firstChild)) {
+ var nodeName = node.nodeName;
+
+ if (!hasContentNode && (nodeName != 'SPAN' || node.getAttribute('data-mce-type') != 'bookmark')) {
+ hasContentNode = true;
+ }
+
+ if (blockElements[nodeName]) {
+ fragment.appendChild(node);
+ textBlock = null;
+ } else {
+ if (blockName) {
+ if (!textBlock) {
+ textBlock = dom.create(blockName);
+ fragment.appendChild(textBlock);
+ }
+
+ textBlock.appendChild(node);
+ } else {
+ fragment.appendChild(node);
+ }
+ }
+ }
+ }
+
+ if (!editor.settings.forced_root_block) {
+ fragment.appendChild(dom.create('br'));
+ } else {
+ // BR is needed in empty blocks on non IE browsers
+ if (!hasContentNode && (!tinymce.Env.ie || tinymce.Env.ie > 10)) {
+ textBlock.appendChild(dom.create('br', {'data-mce-bogus': '1'}));
+ }
+ }
+
+ return fragment;
+ }
+
+ function getSelectedListItems() {
+ return tinymce.grep(selection.getSelectedBlocks(), function(block) {
+ return isListItemNode(block);
+ });
+ }
+
+ function splitList(ul, li, newBlock) {
+ var tmpRng, fragment, bookmarks, node;
+
+ function removeAndKeepBookmarks(targetNode) {
+ tinymce.each(bookmarks, function(node) {
+ targetNode.parentNode.insertBefore(node, li.parentNode);
+ });
+
+ dom.remove(targetNode);
+ }
+
+ bookmarks = dom.select('span[data-mce-type="bookmark"]', ul);
+ newBlock = newBlock || createNewTextBlock(li);
+ tmpRng = dom.createRng();
+ tmpRng.setStartAfter(li);
+ tmpRng.setEndAfter(ul);
+ fragment = tmpRng.extractContents();
+
+ for (node = fragment.firstChild; node; node = node.firstChild) {
+ if (node.nodeName == 'LI' && dom.isEmpty(node)) {
+ dom.remove(node);
+ break;
+ }
+ }
+
+ if (!dom.isEmpty(fragment)) {
+ dom.insertAfter(fragment, ul);
+ }
+
+ dom.insertAfter(newBlock, ul);
+
+ if (isEmpty(li.parentNode)) {
+ removeAndKeepBookmarks(li.parentNode);
+ }
+
+ dom.remove(li);
+
+ if (isEmpty(ul)) {
+ dom.remove(ul);
+ }
+ }
+
+ var shouldMerge = function (listBlock, sibling) {
+ var targetStyle = editor.dom.getStyle(listBlock, 'list-style-type', true);
+ var style = editor.dom.getStyle(sibling, 'list-style-type', true);
+ return targetStyle === style;
+ };
+
+ function mergeWithAdjacentLists(listBlock) {
+ var sibling, node;
+
+ sibling = listBlock.nextSibling;
+ if (sibling && isListNode(sibling) && sibling.nodeName == listBlock.nodeName && shouldMerge(listBlock, sibling)) {
+ while ((node = sibling.firstChild)) {
+ listBlock.appendChild(node);
+ }
+
+ dom.remove(sibling);
+ }
+
+ sibling = listBlock.previousSibling;
+ if (sibling && isListNode(sibling) && sibling.nodeName == listBlock.nodeName && shouldMerge(listBlock, sibling)) {
+ while ((node = sibling.lastChild)) {
+ listBlock.insertBefore(node, listBlock.firstChild);
+ }
+
+ dom.remove(sibling);
+ }
+ }
+
+ function normalizeLists(element) {
+ tinymce.each(tinymce.grep(dom.select('ol,ul', element)), normalizeList);
+ }
+
+ function normalizeList(ul) {
+ var sibling, parentNode = ul.parentNode;
+
+ // Move UL/OL to previous LI if it's the only child of a LI
+ if (parentNode.nodeName == 'LI' && parentNode.firstChild == ul) {
+ sibling = parentNode.previousSibling;
+ if (sibling && sibling.nodeName == 'LI') {
+ sibling.appendChild(ul);
+
+ if (isEmpty(parentNode)) {
+ dom.remove(parentNode);
+ }
+ } else {
+ dom.setStyle(parentNode, 'listStyleType', 'none');
+ }
+ }
+
+ // Append OL/UL to previous LI if it's in a parent OL/UL i.e. old HTML4
+ if (isListNode(parentNode)) {
+ sibling = parentNode.previousSibling;
+ if (sibling && sibling.nodeName == 'LI') {
+ sibling.appendChild(ul);
+ }
+ }
+ }
+
+ function outdent(li) {
+ var ul = li.parentNode, ulParent = ul.parentNode, newBlock;
+
+ function removeEmptyLi(li) {
+ if (isEmpty(li)) {
+ dom.remove(li);
+ }
+ }
+
+ if (isEditorBody(ul)) {
+ return true;
+ }
+
+ if (li.nodeName == 'DD') {
+ dom.rename(li, 'DT');
+ return true;
+ }
+
+ if (isFirstChild(li) && isLastChild(li)) {
+ if (ulParent.nodeName == "LI") {
+ dom.insertAfter(li, ulParent);
+ removeEmptyLi(ulParent);
+ dom.remove(ul);
+ } else if (isListNode(ulParent)) {
+ dom.remove(ul, true);
+ } else {
+ ulParent.insertBefore(createNewTextBlock(li), ul);
+ dom.remove(ul);
+ }
+
+ return true;
+ } else if (isFirstChild(li)) {
+ if (ulParent.nodeName == "LI") {
+ dom.insertAfter(li, ulParent);
+ li.appendChild(ul);
+ removeEmptyLi(ulParent);
+ } else if (isListNode(ulParent)) {
+ ulParent.insertBefore(li, ul);
+ } else {
+ ulParent.insertBefore(createNewTextBlock(li), ul);
+ dom.remove(li);
+ }
+
+ return true;
+ } else if (isLastChild(li)) {
+ if (ulParent.nodeName == "LI") {
+ dom.insertAfter(li, ulParent);
+ } else if (isListNode(ulParent)) {
+ dom.insertAfter(li, ul);
+ } else {
+ dom.insertAfter(createNewTextBlock(li), ul);
+ dom.remove(li);
+ }
+
+ return true;
+ }
+
+ if (ulParent.nodeName == 'LI') {
+ ul = ulParent;
+ newBlock = createNewTextBlock(li, 'LI');
+ } else if (isListNode(ulParent)) {
+ newBlock = createNewTextBlock(li, 'LI');
+ } else {
+ newBlock = createNewTextBlock(li);
+ }
+
+ splitList(ul, li, newBlock);
+ normalizeLists(ul.parentNode);
+
+ return true;
+ }
+
+ function indent(li) {
+ var sibling, newList, listStyle;
+
+ function mergeLists(from, to) {
+ var node;
+
+ if (isListNode(from)) {
+ while ((node = li.lastChild.firstChild)) {
+ to.appendChild(node);
+ }
+
+ dom.remove(from);
+ }
+ }
+
+ if (li.nodeName == 'DT') {
+ dom.rename(li, 'DD');
+ return true;
+ }
+
+ sibling = li.previousSibling;
+
+ if (sibling && isListNode(sibling)) {
+ sibling.appendChild(li);
+ return true;
+ }
+
+ if (sibling && sibling.nodeName == 'LI' && isListNode(sibling.lastChild)) {
+ sibling.lastChild.appendChild(li);
+ mergeLists(li.lastChild, sibling.lastChild);
+ return true;
+ }
+
+ sibling = li.nextSibling;
+
+ if (sibling && isListNode(sibling)) {
+ sibling.insertBefore(li, sibling.firstChild);
+ return true;
+ }
+
+ /*if (sibling && sibling.nodeName == 'LI' && isListNode(li.lastChild)) {
+ return false;
+ }*/
+
+ sibling = li.previousSibling;
+ if (sibling && sibling.nodeName == 'LI') {
+ newList = dom.create(li.parentNode.nodeName);
+ listStyle = dom.getStyle(li.parentNode, 'listStyleType');
+ if (listStyle) {
+ dom.setStyle(newList, 'listStyleType', listStyle);
+ }
+ sibling.appendChild(newList);
+ newList.appendChild(li);
+ mergeLists(li.lastChild, newList);
+ return true;
+ }
+
+ return false;
+ }
+
+ function indentSelection() {
+ var listElements = getSelectedListItems();
+
+ if (listElements.length) {
+ var bookmark = createBookmark(selection.getRng(true));
+
+ for (var i = 0; i < listElements.length; i++) {
+ if (!indent(listElements[i]) && i === 0) {
+ break;
+ }
+ }
+
+ moveToBookmark(bookmark);
+ editor.nodeChanged();
+
+ return true;
+ }
+ }
+
+ function outdentSelection() {
+ var listElements = getSelectedListItems();
+
+ if (listElements.length) {
+ var bookmark = createBookmark(selection.getRng(true));
+ var i, y, root = editor.getBody();
+
+ i = listElements.length;
+ while (i--) {
+ var node = listElements[i].parentNode;
+
+ while (node && node != root) {
+ y = listElements.length;
+ while (y--) {
+ if (listElements[y] === node) {
+ listElements.splice(i, 1);
+ break;
+ }
+ }
+
+ node = node.parentNode;
+ }
+ }
+
+ for (i = 0; i < listElements.length; i++) {
+ if (!outdent(listElements[i]) && i === 0) {
+ break;
+ }
+ }
+
+ moveToBookmark(bookmark);
+ editor.nodeChanged();
+
+ return true;
+ }
+ }
+
+ function applyList(listName, detail) {
+ var rng = selection.getRng(true), bookmark, listItemName = 'LI';
+
+ if (dom.getContentEditable(selection.getNode()) === "false") {
+ return;
+ }
+
+ listName = listName.toUpperCase();
+
+ if (listName == 'DL') {
+ listItemName = 'DT';
+ }
+
+ function getSelectedTextBlocks() {
+ var textBlocks = [], root = editor.getBody();
+
+ function getEndPointNode(start) {
+ var container, offset;
+
+ container = rng[start ? 'startContainer' : 'endContainer'];
+ offset = rng[start ? 'startOffset' : 'endOffset'];
+
+ // Resolve node index
+ if (container.nodeType == 1) {
+ container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
+ }
+
+ while (container.parentNode != root) {
+ if (isTextBlock(container)) {
+ return container;
+ }
+
+ if (/^(TD|TH)$/.test(container.parentNode.nodeName)) {
+ return container;
+ }
+
+ container = container.parentNode;
+ }
+
+ return container;
+ }
+
+ var startNode = getEndPointNode(true);
+ var endNode = getEndPointNode();
+ var block, siblings = [];
+
+ for (var node = startNode; node; node = node.nextSibling) {
+ siblings.push(node);
+
+ if (node == endNode) {
+ break;
+ }
+ }
+
+ tinymce.each(siblings, function(node) {
+ if (isTextBlock(node)) {
+ textBlocks.push(node);
+ block = null;
+ return;
+ }
+
+ if (dom.isBlock(node) || isBr(node)) {
+ if (isBr(node)) {
+ dom.remove(node);
+ }
+
+ block = null;
+ return;
+ }
+
+ var nextSibling = node.nextSibling;
+ if (tinymce.dom.BookmarkManager.isBookmarkNode(node)) {
+ if (isTextBlock(nextSibling) || (!nextSibling && node.parentNode == root)) {
+ block = null;
+ return;
+ }
+ }
+
+ if (!block) {
+ block = dom.create('p');
+ node.parentNode.insertBefore(block, node);
+ textBlocks.push(block);
+ }
+
+ block.appendChild(node);
+ });
+
+ return textBlocks;
+ }
+
+ bookmark = createBookmark(rng);
+
+ tinymce.each(getSelectedTextBlocks(), function(block) {
+ var listBlock, sibling;
+
+ var hasCompatibleStyle = function (sib) {
+ var sibStyle = dom.getStyle(sib, 'list-style-type');
+ var detailStyle = detail ? detail['list-style-type'] : '';
+
+ detailStyle = detailStyle === null ? '' : detailStyle;
+
+ return sibStyle === detailStyle;
+ };
+
+ sibling = block.previousSibling;
+ if (sibling && isListNode(sibling) && sibling.nodeName == listName && hasCompatibleStyle(sibling)) {
+ listBlock = sibling;
+ block = dom.rename(block, listItemName);
+ sibling.appendChild(block);
+ } else {
+ listBlock = dom.create(listName);
+ block.parentNode.insertBefore(listBlock, block);
+ listBlock.appendChild(block);
+ block = dom.rename(block, listItemName);
+ }
+
+ updateListStyle(listBlock, detail);
+ mergeWithAdjacentLists(listBlock);
+ });
+
+ moveToBookmark(bookmark);
+ }
+
+ var updateListStyle = function (el, detail) {
+ dom.setStyle(el, 'list-style-type', detail ? detail['list-style-type'] : null);
+ };
+
+ function removeList() {
+ var bookmark = createBookmark(selection.getRng(true)), root = editor.getBody();
+ var listItems = getSelectedListItems();
+ var emptyListItems = tinymce.util.Tools.grep(listItems, function (li) {
+ return isEmpty(li);
+ });
+
+ listItems = tinymce.util.Tools.grep(listItems, function (li) {
+ return !isEmpty(li);
+ });
+
+
+ tinymce.each(emptyListItems, function(li) {
+ if (isEmpty(li)) {
+ outdent(li);
+ return;
+ }
+ });
+
+ tinymce.each(listItems, function(li) {
+ var node, rootList;
+
+ if (isEditorBody(li.parentNode)) {
+ return;
+ }
+
+ for (node = li; node && node != root; node = node.parentNode) {
+ if (isListNode(node)) {
+ rootList = node;
+ }
+ }
+
+ splitList(rootList, li);
+ normalizeLists(rootList.parentNode);
+ });
+
+ moveToBookmark(bookmark);
+ }
+
+ function toggleList(listName, detail) {
+ var parentList = dom.getParent(selection.getStart(), 'OL,UL,DL');
+
+ if (isEditorBody(parentList)) {
+ return;
+ }
+
+ if (parentList) {
+ if (parentList.nodeName == listName) {
+ removeList(listName);
+ } else {
+ var bookmark = createBookmark(selection.getRng(true));
+ updateListStyle(parentList, detail);
+ mergeWithAdjacentLists(dom.rename(parentList, listName));
+
+ moveToBookmark(bookmark);
+ }
+ } else {
+ applyList(listName, detail);
+ }
+ }
+
+ function queryListCommandState(listName) {
+ return function() {
+ var parentList = dom.getParent(editor.selection.getStart(), 'UL,OL,DL');
+
+ return parentList && parentList.nodeName == listName;
+ };
+ }
+
+ function isBogusBr(node) {
+ if (!isBr(node)) {
+ return false;
+ }
+
+ if (dom.isBlock(node.nextSibling) && !isBr(node.previousSibling)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function findNextCaretContainer(rng, isForward) {
+ var node = rng.startContainer, offset = rng.startOffset;
+ var nonEmptyBlocks, walker;
+
+ if (node.nodeType == 3 && (isForward ? offset < node.data.length : offset > 0)) {
+ return node;
+ }
+
+ nonEmptyBlocks = editor.schema.getNonEmptyElements();
+ if (node.nodeType == 1) {
+ node = tinymce.dom.RangeUtils.getNode(node, offset);
+ }
+
+ walker = new tinymce.dom.TreeWalker(node, editor.getBody());
+
+ // Delete at <li>|<br></li> then jump over the bogus br
+ if (isForward) {
+ if (isBogusBr(node)) {
+ walker.next();
+ }
+ }
+
+ while ((node = walker[isForward ? 'next' : 'prev2']())) {
+ if (node.nodeName == 'LI' && !node.hasChildNodes()) {
+ return node;
+ }
+
+ if (nonEmptyBlocks[node.nodeName]) {
+ return node;
+ }
+
+ if (node.nodeType == 3 && node.data.length > 0) {
+ return node;
+ }
+ }
+ }
+
+ function mergeLiElements(fromElm, toElm) {
+ var node, listNode, ul = fromElm.parentNode;
+
+ if (!isChildOfBody(fromElm) || !isChildOfBody(toElm)) {
+ return;
+ }
+
+ if (isListNode(toElm.lastChild)) {
+ listNode = toElm.lastChild;
+ }
+
+ if (ul == toElm.lastChild) {
+ if (isBr(ul.previousSibling)) {
+ dom.remove(ul.previousSibling);
+ }
+ }
+
+ node = toElm.lastChild;
+ if (node && isBr(node) && fromElm.hasChildNodes()) {
+ dom.remove(node);
+ }
+
+ if (isEmpty(toElm, true)) {
+ dom.$(toElm).empty();
+ }
+
+ if (!isEmpty(fromElm, true)) {
+ while ((node = fromElm.firstChild)) {
+ toElm.appendChild(node);
+ }
+ }
+
+ if (listNode) {
+ toElm.appendChild(listNode);
+ }
+
+ dom.remove(fromElm);
+
+ if (isEmpty(ul) && !isEditorBody(ul)) {
+ dom.remove(ul);
+ }
+ }
+
+ function backspaceDeleteCaret(isForward) {
+ var li = dom.getParent(selection.getStart(), 'LI'), ul, rng, otherLi;
+
+ if (li) {
+ ul = li.parentNode;
+ if (isEditorBody(ul) && dom.isEmpty(ul)) {
+ return true;
+ }
+
+ rng = normalizeRange(selection.getRng(true));
+ otherLi = dom.getParent(findNextCaretContainer(rng, isForward), 'LI');
+
+ if (otherLi && otherLi != li) {
+ var bookmark = createBookmark(rng);
+
+ if (isForward) {
+ mergeLiElements(otherLi, li);
+ } else {
+ mergeLiElements(li, otherLi);
+ }
+
+ moveToBookmark(bookmark);
+
+ return true;
+ } else if (!otherLi) {
+ if (!isForward && removeList(ul.nodeName)) {
+ return true;
+ }
+ }
+ }
+ }
+
+ function backspaceDeleteRange() {
+ var startListParent = editor.dom.getParent(editor.selection.getStart(), 'LI,DT,DD');
+
+ if (startListParent || getSelectedListItems().length > 0) {
+ editor.undoManager.transact(function() {
+ editor.execCommand('Delete');
+ normalizeLists(editor.getBody());
+ });
+
+ return true;
+ }
+
+ return false;
+ }
+
+ self.backspaceDelete = function(isForward) {
+ return selection.isCollapsed() ? backspaceDeleteCaret(isForward) : backspaceDeleteRange();
+ };
+
+ editor.on('BeforeExecCommand', function(e) {
+ var cmd = e.command.toLowerCase(), isHandled;
+
+ if (cmd == "indent") {
+ if (indentSelection()) {
+ isHandled = true;
+ }
+ } else if (cmd == "outdent") {
+ if (outdentSelection()) {
+ isHandled = true;
+ }
+ }
+
+ if (isHandled) {
+ editor.fire('ExecCommand', {command: e.command});
+ e.preventDefault();
+ return true;
+ }
+ });
+
+ editor.addCommand('InsertUnorderedList', function(ui, detail) {
+ toggleList('UL', detail);
+ });
+
+ editor.addCommand('InsertOrderedList', function(ui, detail) {
+ toggleList('OL', detail);
+ });
+
+ editor.addCommand('InsertDefinitionList', function(ui, detail) {
+ toggleList('DL', detail);
+ });
+
+ editor.addQueryStateHandler('InsertUnorderedList', queryListCommandState('UL'));
+ editor.addQueryStateHandler('InsertOrderedList', queryListCommandState('OL'));
+ editor.addQueryStateHandler('InsertDefinitionList', queryListCommandState('DL'));
+
+ editor.on('keydown', function(e) {
+ // Check for tab but not ctrl/cmd+tab since it switches browser tabs
+ if (e.keyCode != 9 || tinymce.util.VK.metaKeyPressed(e)) {
+ return;
+ }
+
+ if (editor.dom.getParent(editor.selection.getStart(), 'LI,DT,DD')) {
+ e.preventDefault();
+
+ if (e.shiftKey) {
+ outdentSelection();
+ } else {
+ indentSelection();
+ }
+ }
+ });
+ });
+
+ var listState = function (listName) {
+ return function () {
+ var self = this;
+
+ editor.on('NodeChange', function (e) {
+ var lists = tinymce.util.Tools.grep(e.parents, isListNode);
+ self.active(lists.length > 0 && lists[0].nodeName === listName);
+ });
+ };
+ };
+
+ var hasPlugin = function (editor, plugin) {
+ var plugins = editor.settings.plugins ? editor.settings.plugins : '';
+ return tinymce.util.Tools.inArray(plugins.split(/[ ,]/), plugin) !== -1;
+ };
+
+ if (!hasPlugin(editor, 'advlist')) {
+ editor.addButton('numlist', {
+ title: 'Numbered list',
+ cmd: 'InsertOrderedList',
+ onPostRender: listState('OL')
+ });
+
+ editor.addButton('bullist', {
+ title: 'Bullet list',
+ cmd: 'InsertUnorderedList',
+ onPostRender: listState('UL')
+ });
+ }
+
+ editor.addButton('indent', {
+ icon: 'indent',
+ title: 'Increase indent',
+ cmd: 'Indent',
+ onPostRender: function() {
+ var ctrl = this;
+
+ editor.on('nodechange', function() {
+ var blocks = editor.selection.getSelectedBlocks();
+ var disable = false;
+
+ for (var i = 0, l = blocks.length; !disable && i < l; i++) {
+ var tag = blocks[i].nodeName;
+
+ disable = (tag == 'LI' && isFirstChild(blocks[i]) || tag == 'UL' || tag == 'OL' || tag == 'DD');
+ }
+
+ ctrl.disabled(disable);
+ });
+ }
+ });
+
+ editor.on('keydown', function(e) {
+ if (e.keyCode == tinymce.util.VK.BACKSPACE) {
+ if (self.backspaceDelete()) {
+ e.preventDefault();
+ }
+ } else if (e.keyCode == tinymce.util.VK.DELETE) {
+ if (self.backspaceDelete(true)) {
+ e.preventDefault();
+ }
+ }
+ });
+});
diff --git a/resource/tinymce/plugins/paste/editor_plugin.js b/resource/tinymce/plugins/paste/editor_plugin.js
@@ -1,896 +0,0 @@
-/**
- * editor_plugin_src.js
- *
- * Copyright 2009, Moxiecode Systems AB
- * Released under LGPL License.
- *
- * License: http://tinymce.moxiecode.com/license
- * Contributing: http://tinymce.moxiecode.com/contributing
- */
-
-(function() {
- var each = tinymce.each,
- defs = {
- paste_auto_cleanup_on_paste : true,
- paste_enable_default_filters : true,
- paste_block_drop : false,
- paste_retain_style_properties : "none",
- paste_strip_class_attributes : "mso",
- paste_remove_spans : false,
- paste_remove_styles : false,
- paste_remove_styles_if_webkit : true,
- paste_convert_middot_lists : true,
- paste_convert_headers_to_strong : false,
- paste_dialog_width : "450",
- paste_dialog_height : "400",
- paste_max_consecutive_linebreaks: 2,
- paste_text_use_dialog : false,
- paste_text_sticky : false,
- paste_text_sticky_default : false,
- paste_text_notifyalways : false,
- paste_text_linebreaktype : "combined",
- paste_text_replacements : [
- [/\u2026/g, "..."],
- [/[\x93\x94\u201c\u201d]/g, '"'],
- [/[\x60\x91\x92\u2018\u2019]/g, "'"]
- ]
- };
-
- function getParam(ed, name) {
- return ed.getParam(name, defs[name]);
- }
-
- tinymce.create('tinymce.plugins.PastePlugin', {
- init : function(ed, url) {
- var t = this;
-
- t.editor = ed;
- t.url = url;
-
- // Setup plugin events
- t.onPreProcess = new tinymce.util.Dispatcher(t);
- t.onPostProcess = new tinymce.util.Dispatcher(t);
-
- // Register default handlers
- t.onPreProcess.add(t._preProcess);
- t.onPostProcess.add(t._postProcess);
-
- // Register optional preprocess handler
- t.onPreProcess.add(function(pl, o) {
- ed.execCallback('paste_preprocess', pl, o);
- });
-
- // Register optional postprocess
- t.onPostProcess.add(function(pl, o) {
- ed.execCallback('paste_postprocess', pl, o);
- });
-
- ed.onKeyDown.addToTop(function(ed, e) {
- // Block ctrl+v from adding an undo level since the default logic in tinymce.Editor will add that
- if (((tinymce.isMac ? e.metaKey : e.ctrlKey) && e.keyCode == 86) || (e.shiftKey && e.keyCode == 45))
- return false; // Stop other listeners
- });
-
- // Initialize plain text flag
- ed.pasteAsPlainText = getParam(ed, 'paste_text_sticky_default');
-
- // This function executes the process handlers and inserts the contents
- // force_rich overrides plain text mode set by user, important for pasting with execCommand
- function process(o, force_rich) {
- var dom = ed.dom, rng;
-
- // Execute pre process handlers
- t.onPreProcess.dispatch(t, o);
-
- // Create DOM structure
- o.node = dom.create('div', 0, o.content);
-
- // If pasting inside the same element and the contents is only one block
- // remove the block and keep the text since Firefox will copy parts of pre and h1-h6 as a pre element
- if (tinymce.isGecko) {
- rng = ed.selection.getRng(true);
- if (rng.startContainer == rng.endContainer && rng.startContainer.nodeType == 3) {
- // Is only one block node and it doesn't contain word stuff
- if (o.node.childNodes.length === 1 && /^(p|h[1-6]|pre)$/i.test(o.node.firstChild.nodeName) && o.content.indexOf('__MCE_ITEM__') === -1)
- dom.remove(o.node.firstChild, true);
- }
- }
-
- // Execute post process handlers
- t.onPostProcess.dispatch(t, o);
-
- // Serialize content
- o.content = ed.serializer.serialize(o.node, {getInner : 1, forced_root_block : ''});
-
- // Plain text option active?
- if ((!force_rich) && (ed.pasteAsPlainText)) {
- t._insertPlainText(o.content);
-
- if (!getParam(ed, "paste_text_sticky")) {
- ed.pasteAsPlainText = false;
- ed.controlManager.setActive("pastetext", false);
- }
- } else {
- t._insert(o.content);
- }
- }
-
- // Add command for external usage
- ed.addCommand('mceInsertClipboardContent', function(u, o) {
- process(o, true);
- });
-
- if (!getParam(ed, "paste_text_use_dialog")) {
- ed.addCommand('mcePasteText', function(u, v) {
- var cookie = tinymce.util.Cookie;
-
- ed.pasteAsPlainText = !ed.pasteAsPlainText;
- ed.controlManager.setActive('pastetext', ed.pasteAsPlainText);
-
- if ((ed.pasteAsPlainText) && (!cookie.get("tinymcePasteText"))) {
- if (getParam(ed, "paste_text_sticky")) {
- ed.windowManager.alert(ed.translate('paste.plaintext_mode_sticky'));
- } else {
- ed.windowManager.alert(ed.translate('paste.plaintext_mode'));
- }
-
- if (!getParam(ed, "paste_text_notifyalways")) {
- cookie.set("tinymcePasteText", "1", new Date(new Date().getFullYear() + 1, 12, 31))
- }
- }
- });
- }
-
- ed.addButton('pastetext', {title: 'paste.paste_text_desc', cmd: 'mcePasteText'});
- ed.addButton('selectall', {title: 'paste.selectall_desc', cmd: 'selectall'});
-
- // This function grabs the contents from the clipboard by adding a
- // hidden div and placing the caret inside it and after the browser paste
- // is done it grabs that contents and processes that
- function grabContent(e) {
- var n, or, rng, oldRng, sel = ed.selection, dom = ed.dom, body = ed.getBody(), posY, textContent;
-
- // Check if browser supports direct plaintext access
- if (e.clipboardData || dom.doc.dataTransfer) {
- // Added by Zotero
- // Get HTML from the clipboard directly
- var html = e.clipboardData && e.clipboardData.getData('text/html');
- if (html) {
- e.preventDefault();
- process({content : html});
- return;
- }
-
- textContent = (e.clipboardData || dom.doc.dataTransfer).getData('Text');
-
- if (ed.pasteAsPlainText) {
- e.preventDefault();
- process({content : dom.encode(textContent).replace(/\r?\n/g, '<br />')});
- return;
- }
- }
-
- if (dom.get('_mcePaste'))
- return;
-
- // Create container to paste into
- n = dom.add(body, 'div', {id : '_mcePaste', 'class' : 'mcePaste', 'data-mce-bogus' : '1'}, '\uFEFF\uFEFF');
-
- // If contentEditable mode we need to find out the position of the closest element
- if (body != ed.getDoc().body)
- posY = dom.getPos(ed.selection.getStart(), body).y;
- else
- posY = body.scrollTop + dom.getViewPort(ed.getWin()).y;
-
- // Styles needs to be applied after the element is added to the document since WebKit will otherwise remove all styles
- // If also needs to be in view on IE or the paste would fail
- dom.setStyles(n, {
- position : 'absolute',
- left : tinymce.isGecko ? -40 : 0, // Need to move it out of site on Gecko since it will othewise display a ghost resize rect for the div
- top : posY - 25,
- width : 1,
- height : 1,
- overflow : 'hidden'
- });
-
- if (tinymce.isIE) {
- // Store away the old range
- oldRng = sel.getRng();
-
- // Select the container
- rng = dom.doc.body.createTextRange();
- rng.moveToElementText(n);
- rng.execCommand('Paste');
-
- // Remove container
- dom.remove(n);
-
- // Check if the contents was changed, if it wasn't then clipboard extraction failed probably due
- // to IE security settings so we pass the junk though better than nothing right
- if (n.innerHTML === '\uFEFF\uFEFF') {
- ed.execCommand('mcePasteWord');
- e.preventDefault();
- return;
- }
-
- // Restore the old range and clear the contents before pasting
- sel.setRng(oldRng);
- sel.setContent('');
-
- // For some odd reason we need to detach the the mceInsertContent call from the paste event
- // It's like IE has a reference to the parent element that you paste in and the selection gets messed up
- // when it tries to restore the selection
- setTimeout(function() {
- // Process contents
- process({content : n.innerHTML});
- }, 0);
-
- // Block the real paste event
- return tinymce.dom.Event.cancel(e);
- } else {
- function block(e) {
- e.preventDefault();
- };
-
- // Block mousedown and click to prevent selection change
- dom.bind(ed.getDoc(), 'mousedown', block);
- dom.bind(ed.getDoc(), 'keydown', block);
-
- or = ed.selection.getRng();
-
- // Move select contents inside DIV
- n = n.firstChild;
- rng = ed.getDoc().createRange();
- rng.setStart(n, 0);
- rng.setEnd(n, 2);
- sel.setRng(rng);
-
- // Wait a while and grab the pasted contents
- window.setTimeout(function() {
- var h = '', nl;
-
- // Paste divs duplicated in paste divs seems to happen when you paste plain text so lets first look for that broken behavior in WebKit
- if (!dom.select('div.mcePaste > div.mcePaste').length) {
- nl = dom.select('div.mcePaste');
-
- // WebKit will split the div into multiple ones so this will loop through then all and join them to get the whole HTML string
- each(nl, function(n) {
- var child = n.firstChild;
-
- // WebKit inserts a DIV container with lots of odd styles
- if (child && child.nodeName == 'DIV' && child.style.marginTop && child.style.backgroundColor) {
- dom.remove(child, 1);
- }
-
- // Remove apply style spans
- each(dom.select('span.Apple-style-span', n), function(n) {
- dom.remove(n, 1);
- });
-
- // Remove bogus br elements
- each(dom.select('br[data-mce-bogus]', n), function(n) {
- dom.remove(n);
- });
-
- // WebKit will make a copy of the DIV for each line of plain text pasted and insert them into the DIV
- if (n.parentNode.className != 'mcePaste')
- h += n.innerHTML;
- });
- } else {
- // Found WebKit weirdness so force the content into paragraphs this seems to happen when you paste plain text from Nodepad etc
- // So this logic will replace double enter with paragraphs and single enter with br so it kind of looks the same
- h = '<p>' + dom.encode(textContent).replace(/\r?\n\r?\n/g, '</p><p>').replace(/\r?\n/g, '<br />') + '</p>';
- }
-
- // Remove the nodes
- each(dom.select('div.mcePaste'), function(n) {
- dom.remove(n);
- });
-
- // Restore the old selection
- if (or)
- sel.setRng(or);
-
- process({content : h});
-
- // Unblock events ones we got the contents
- dom.unbind(ed.getDoc(), 'mousedown', block);
- dom.unbind(ed.getDoc(), 'keydown', block);
- }, 0);
- }
- }
-
- // Check if we should use the new auto process method
- if (getParam(ed, "paste_auto_cleanup_on_paste")) {
- // Is it's Opera or older FF use key handler
- if (tinymce.isOpera || /Firefox\/2/.test(navigator.userAgent)) {
- ed.onKeyDown.addToTop(function(ed, e) {
- if (((tinymce.isMac ? e.metaKey : e.ctrlKey) && e.keyCode == 86) || (e.shiftKey && e.keyCode == 45))
- grabContent(e);
- });
- } else {
- // Grab contents on paste event on Gecko and WebKit
- ed.onPaste.addToTop(function(ed, e) {
- return grabContent(e);
- });
- }
- }
-
- ed.onInit.add(function() {
- ed.controlManager.setActive("pastetext", ed.pasteAsPlainText);
-
- // Block all drag/drop events
- if (getParam(ed, "paste_block_drop")) {
- ed.dom.bind(ed.getBody(), ['dragend', 'dragover', 'draggesture', 'dragdrop', 'drop', 'drag'], function(e) {
- e.preventDefault();
- e.stopPropagation();
-
- return false;
- });
- }
- });
-
- // Add legacy support
- t._legacySupport();
- },
-
- getInfo : function() {
- return {
- longname : 'Paste text/word',
- author : 'Moxiecode Systems AB',
- authorurl : 'http://tinymce.moxiecode.com',
- infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/paste',
- version : tinymce.majorVersion + "." + tinymce.minorVersion
- };
- },
-
- _preProcess : function(pl, o) {
- var ed = this.editor,
- h = o.content,
- grep = tinymce.grep,
- explode = tinymce.explode,
- trim = tinymce.trim,
- len, stripClass;
-
- //console.log('Before preprocess:' + o.content);
-
- function process(items) {
- each(items, function(v) {
- // Remove or replace
- if (v.constructor == RegExp)
- h = h.replace(v, '');
- else
- h = h.replace(v[0], v[1]);
- });
- }
-
- if (ed.settings.paste_enable_default_filters == false) {
- return;
- }
-
- // IE9 adds BRs before/after block elements when contents is pasted from word or for example another browser
- if (tinymce.isIE && document.documentMode >= 9 && /<(h[1-6r]|p|div|address|pre|form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|caption|blockquote|center|dl|dt|dd|dir|fieldset)/.test(o.content)) {
- // IE9 adds BRs before/after block elements when contents is pasted from word or for example another browser
- process([[/(?:<br> [\s\r\n]+|<br>)*(<\/?(h[1-6r]|p|div|address|pre|form|table|tbody|thead|tfoot|th|tr|td|li|ol|ul|caption|blockquote|center|dl|dt|dd|dir|fieldset)[^>]*>)(?:<br> [\s\r\n]+|<br>)*/g, '$1']]);
-
- // IE9 also adds an extra BR element for each soft-linefeed and it also adds a BR for each word wrap break
- process([
- [/<br><br>/g, '<BR><BR>'], // Replace multiple BR elements with uppercase BR to keep them intact
- [/<br>/g, ' '], // Replace single br elements with space since they are word wrap BR:s
- [/<BR><BR>/g, '<br>'] // Replace back the double brs but into a single BR
- ]);
- }
-
- // Detect Word content and process it more aggressive
- if (/class="?Mso|style="[^"]*\bmso-|w:WordDocument/i.test(h) || o.wordContent) {
- o.wordContent = true; // Mark the pasted contents as word specific content
- //console.log('Word contents detected.');
-
- // Process away some basic content
- process([
- /^\s*( )+/gi, // entities at the start of contents
- /( |<br[^>]*>)+\s*$/gi // entities at the end of contents
- ]);
-
- if (getParam(ed, "paste_convert_headers_to_strong")) {
- h = h.replace(/<p [^>]*class="?MsoHeading"?[^>]*>(.*?)<\/p>/gi, "<p><strong>$1</strong></p>");
- }
-
- if (getParam(ed, "paste_convert_middot_lists")) {
- process([
- [/<!--\[if !supportLists\]-->/gi, '$&__MCE_ITEM__'], // Convert supportLists to a list item marker
- [/(<span[^>]+(?:mso-list:|:\s*symbol)[^>]+>)/gi, '$1__MCE_ITEM__'], // Convert mso-list and symbol spans to item markers
- [/(<p[^>]+(?:MsoListParagraph)[^>]+>)/gi, '$1__MCE_ITEM__'] // Convert mso-list and symbol paragraphs to item markers (FF)
- ]);
- }
-
- process([
- // Word comments like conditional comments etc
- /<!--[\s\S]+?-->/gi,
-
- // Remove comments, scripts (e.g., msoShowComment), XML tag, VML content, MS Office namespaced tags, and a few other tags
- /<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,
-
- // Convert <s> into <strike> for line-though
- [/<(\/?)s>/gi, "<$1strike>"],
-
- // Replace nsbp entites to char since it's easier to handle
- [/ /gi, "\u00a0"]
- ]);
-
- // Remove bad attributes, with or without quotes, ensuring that attribute text is really inside a tag.
- // If JavaScript had a RegExp look-behind, we could have integrated this with the last process() array and got rid of the loop. But alas, it does not, so we cannot.
- do {
- len = h.length;
- // Don't remove the type attribute for lists so that non-default list types display correctly.
- h = h.replace(/(<?!(ol|ul)[^>]*\s)(?:id|name|language|type|on\w+|\w+:\w+)=(?:"[^"]*"|\w+)\s?/gi, "$1");
- h = h.replace(/(<(ol|ul)[^>]*\s)(?:id|name|language|on\w+|\w+:\w+)=(?:"[^"]*"|\w+)\s?/gi, "$1");
- } while (len != h.length);
-
- // Remove all spans if no styles is to be retained
- if (getParam(ed, "paste_retain_style_properties").replace(/^none$/i, "").length == 0) {
- h = h.replace(/<\/?span[^>]*>/gi, "");
- } else {
- // We're keeping styles, so at least clean them up.
- // CSS Reference: http://msdn.microsoft.com/en-us/library/aa155477.aspx
-
- process([
- // Convert <span style="mso-spacerun:yes">___</span> to string of alternating breaking/non-breaking spaces of same length
- [/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi,
- function(str, spaces) {
- return (spaces.length > 0)? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : "";
- }
- ],
-
- // Examine all styles: delete junk, transform some, and keep the rest
- [/(<[a-z][^>]*)\sstyle="([^"]*)"/gi,
- function(str, tag, style) {
- var n = [],
- i = 0,
- s = explode(trim(style).replace(/"/gi, "'"), ";");
-
- // Examine each style definition within the tag's style attribute
- each(s, function(v) {
- var name, value,
- parts = explode(v, ":");
-
- function ensureUnits(v) {
- return v + ((v !== "0") && (/\d$/.test(v)))? "px" : "";
- }
-
- if (parts.length == 2) {
- name = parts[0].toLowerCase();
- value = parts[1].toLowerCase();
-
- // Translate certain MS Office styles into their CSS equivalents
- switch (name) {
- case "mso-padding-alt":
- case "mso-padding-top-alt":
- case "mso-padding-right-alt":
- case "mso-padding-bottom-alt":
- case "mso-padding-left-alt":
- case "mso-margin-alt":
- case "mso-margin-top-alt":
- case "mso-margin-right-alt":
- case "mso-margin-bottom-alt":
- case "mso-margin-left-alt":
- case "mso-table-layout-alt":
- case "mso-height":
- case "mso-width":
- case "mso-vertical-align-alt":
- n[i++] = name.replace(/^mso-|-alt$/g, "") + ":" + ensureUnits(value);
- return;
-
- case "horiz-align":
- n[i++] = "text-align:" + value;
- return;
-
- case "vert-align":
- n[i++] = "vertical-align:" + value;
- return;
-
- case "font-color":
- case "mso-foreground":
- n[i++] = "color:" + value;
- return;
-
- case "mso-background":
- case "mso-highlight":
- n[i++] = "background:" + value;
- return;
-
- case "mso-default-height":
- n[i++] = "min-height:" + ensureUnits(value);
- return;
-
- case "mso-default-width":
- n[i++] = "min-width:" + ensureUnits(value);
- return;
-
- case "mso-padding-between-alt":
- n[i++] = "border-collapse:separate;border-spacing:" + ensureUnits(value);
- return;
-
- case "text-line-through":
- if ((value == "single") || (value == "double")) {
- n[i++] = "text-decoration:line-through";
- }
- return;
-
- case "mso-zero-height":
- if (value == "yes") {
- n[i++] = "display:none";
- }
- return;
- }
-
- // Eliminate all MS Office style definitions that have no CSS equivalent by examining the first characters in the name
- if (/^(mso|column|font-emph|lang|layout|line-break|list-image|nav|panose|punct|row|ruby|sep|size|src|tab-|table-border|text-(?!align|decor|indent|trans)|top-bar|version|vnd|word-break)/.test(name)) {
- return;
- }
-
- // If it reached this point, it must be a valid CSS style
- n[i++] = name + ":" + parts[1]; // Lower-case name, but keep value case
- }
- });
-
- // If style attribute contained any valid styles the re-write it; otherwise delete style attribute.
- if (i > 0) {
- return tag + ' style="' + n.join(';') + '"';
- } else {
- return tag;
- }
- }
- ]
- ]);
- }
- }
-
- // Replace headers with <strong>
- if (getParam(ed, "paste_convert_headers_to_strong")) {
- process([
- [/<h[1-6][^>]*>/gi, "<p><strong>"],
- [/<\/h[1-6][^>]*>/gi, "</strong></p>"]
- ]);
- }
-
- process([
- // Copy paste from Java like Open Office will produce this junk on FF
- [/Version:[\d.]+\nStartHTML:\d+\nEndHTML:\d+\nStartFragment:\d+\nEndFragment:\d+/gi, '']
- ]);
-
- // Class attribute options are: leave all as-is ("none"), remove all ("all"), or remove only those starting with mso ("mso").
- // Note:- paste_strip_class_attributes: "none", verify_css_classes: true is also a good variation.
- stripClass = getParam(ed, "paste_strip_class_attributes");
-
- if (stripClass !== "none") {
- function removeClasses(match, g1) {
- if (stripClass === "all")
- return '';
-
- var cls = grep(explode(g1.replace(/^(["'])(.*)\1$/, "$2"), " "),
- function(v) {
- return (/^(?!mso)/i.test(v));
- }
- );
-
- return cls.length ? ' class="' + cls.join(" ") + '"' : '';
- };
-
- h = h.replace(/ class="([^"]+)"/gi, removeClasses);
- h = h.replace(/ class=([\-\w]+)/gi, removeClasses);
- }
-
- // Remove spans option
- if (getParam(ed, "paste_remove_spans")) {
- h = h.replace(/<\/?span[^>]*>/gi, "");
- }
-
- //console.log('After preprocess:' + h);
-
- o.content = h;
- },
-
- /**
- * Various post process items.
- */
- _postProcess : function(pl, o) {
- var t = this, ed = t.editor, dom = ed.dom, styleProps;
-
- if (ed.settings.paste_enable_default_filters == false) {
- return;
- }
-
- if (o.wordContent) {
- // Remove named anchors or TOC links
- each(dom.select('a', o.node), function(a) {
- if (!a.href || a.href.indexOf('#_Toc') != -1)
- dom.remove(a, 1);
- });
-
- if (getParam(ed, "paste_convert_middot_lists")) {
- t._convertLists(pl, o);
- }
-
- // Process styles
- styleProps = getParam(ed, "paste_retain_style_properties"); // retained properties
-
- // Process only if a string was specified and not equal to "all" or "*"
- if ((tinymce.is(styleProps, "string")) && (styleProps !== "all") && (styleProps !== "*")) {
- styleProps = tinymce.explode(styleProps.replace(/^none$/i, ""));
-
- // Retains some style properties
- each(dom.select('*', o.node), function(el) {
- var newStyle = {}, npc = 0, i, sp, sv;
-
- // Store a subset of the existing styles
- if (styleProps) {
- for (i = 0; i < styleProps.length; i++) {
- sp = styleProps[i];
- sv = dom.getStyle(el, sp);
-
- if (sv) {
- newStyle[sp] = sv;
- npc++;
- }
- }
- }
-
- // Remove all of the existing styles
- dom.setAttrib(el, 'style', '');
-
- if (styleProps && npc > 0)
- dom.setStyles(el, newStyle); // Add back the stored subset of styles
- else // Remove empty span tags that do not have class attributes
- if (el.nodeName == 'SPAN' && !el.className)
- dom.remove(el, true);
- });
- }
- }
-
- // Remove all style information or only specifically on WebKit to avoid the style bug on that browser
- if (getParam(ed, "paste_remove_styles") || (getParam(ed, "paste_remove_styles_if_webkit") && tinymce.isWebKit)) {
- each(dom.select('*[style]', o.node), function(el) {
- el.removeAttribute('style');
- el.removeAttribute('data-mce-style');
- });
- } else {
- if (tinymce.isWebKit) {
- // We need to compress the styles on WebKit since if you paste <img border="0" /> it will become <img border="0" style="... lots of junk ..." />
- // Removing the mce_style that contains the real value will force the Serializer engine to compress the styles
- each(dom.select('*', o.node), function(el) {
- el.removeAttribute('data-mce-style');
- });
- }
- }
- },
-
- /**
- * Converts the most common bullet and number formats in Office into a real semantic UL/LI list.
- */
- _convertLists : function(pl, o) {
- var dom = pl.editor.dom, listElm, li, lastMargin = -1, margin, levels = [], lastType, html;
-
- // Convert middot lists into real semantic lists
- each(dom.select('p', o.node), function(p) {
- var sib, val = '', type, html, idx, parents;
-
- // Get text node value at beginning of paragraph
- for (sib = p.firstChild; sib && sib.nodeType == 3; sib = sib.nextSibling)
- val += sib.nodeValue;
-
- val = p.innerHTML.replace(/<\/?\w+[^>]*>/gi, '').replace(/ /g, '\u00a0');
-
- // Detect unordered lists look for bullets
- if (/^(__MCE_ITEM__)+[\u2022\u00b7\u00a7\u00d8o\u25CF]\s*\u00a0*/.test(val))
- type = 'ul';
-
- // Detect ordered lists 1., a. or ixv.
- if (/^__MCE_ITEM__\s*\w+\.\s*\u00a0+/.test(val))
- type = 'ol';
-
- // Check if node value matches the list pattern: o
- if (type) {
- margin = parseFloat(p.style.marginLeft || 0);
-
- if (margin > lastMargin)
- levels.push(margin);
-
- if (!listElm || type != lastType) {
- listElm = dom.create(type);
- dom.insertAfter(listElm, p);
- } else {
- // Nested list element
- if (margin > lastMargin) {
- listElm = li.appendChild(dom.create(type));
- } else if (margin < lastMargin) {
- // Find parent level based on margin value
- idx = tinymce.inArray(levels, margin);
- parents = dom.getParents(listElm.parentNode, type);
- listElm = parents[parents.length - 1 - idx] || listElm;
- }
- }
-
- // Remove middot or number spans if they exists
- each(dom.select('span', p), function(span) {
- var html = span.innerHTML.replace(/<\/?\w+[^>]*>/gi, '');
-
- // Remove span with the middot or the number
- if (type == 'ul' && /^__MCE_ITEM__[\u2022\u00b7\u00a7\u00d8o\u25CF]/.test(html))
- dom.remove(span);
- else if (/^__MCE_ITEM__[\s\S]*\w+\.( |\u00a0)*\s*/.test(html))
- dom.remove(span);
- });
-
- html = p.innerHTML;
-
- // Remove middot/list items
- if (type == 'ul')
- html = p.innerHTML.replace(/__MCE_ITEM__/g, '').replace(/^[\u2022\u00b7\u00a7\u00d8o\u25CF]\s*( |\u00a0)+\s*/, '');
- else
- html = p.innerHTML.replace(/__MCE_ITEM__/g, '').replace(/^\s*\w+\.( |\u00a0)+\s*/, '');
-
- // Create li and add paragraph data into the new li
- li = listElm.appendChild(dom.create('li', 0, html));
- dom.remove(p);
-
- lastMargin = margin;
- lastType = type;
- } else
- listElm = lastMargin = 0; // End list element
- });
-
- // Remove any left over makers
- html = o.node.innerHTML;
- if (html.indexOf('__MCE_ITEM__') != -1)
- o.node.innerHTML = html.replace(/__MCE_ITEM__/g, '');
- },
-
- /**
- * Inserts the specified contents at the caret position.
- */
- _insert : function(h, skip_undo) {
- var ed = this.editor, r = ed.selection.getRng();
-
- // First delete the contents seems to work better on WebKit when the selection spans multiple list items or multiple table cells.
- if (!ed.selection.isCollapsed() && r.startContainer != r.endContainer)
- ed.getDoc().execCommand('Delete', false, null);
-
- ed.execCommand('mceInsertContent', false, h, {skip_undo : skip_undo});
- },
-
- /**
- * Instead of the old plain text method which tried to re-create a paste operation, the
- * new approach adds a plain text mode toggle switch that changes the behavior of paste.
- * This function is passed the same input that the regular paste plugin produces.
- * It performs additional scrubbing and produces (and inserts) the plain text.
- * This approach leverages all of the great existing functionality in the paste
- * plugin, and requires minimal changes to add the new functionality.
- * Speednet - June 2009
- */
- _insertPlainText : function(content) {
- var ed = this.editor,
- linebr = getParam(ed, "paste_text_linebreaktype"),
- rl = getParam(ed, "paste_text_replacements"),
- is = tinymce.is;
-
- function process(items) {
- each(items, function(v) {
- if (v.constructor == RegExp)
- content = content.replace(v, "");
- else
- content = content.replace(v[0], v[1]);
- });
- };
-
- if ((typeof(content) === "string") && (content.length > 0)) {
- // If HTML content with line-breaking tags, then remove all cr/lf chars because only tags will break a line
- if (/<(?:p|br|h[1-6]|ul|ol|dl|table|t[rdh]|div|blockquote|fieldset|pre|address|center)[^>]*>/i.test(content)) {
- process([
- /[\n\r]+/g
- ]);
- } else {
- // Otherwise just get rid of carriage returns (only need linefeeds)
- process([
- /\r+/g
- ]);
- }
-
- process([
- [/<\/(?:p|h[1-6]|ul|ol|dl|table|div|blockquote|fieldset|pre|address|center)>/gi, "\n\n"], // Block tags get a blank line after them
- [/<br[^>]*>|<\/tr>/gi, "\n"], // Single linebreak for <br /> tags and table rows
- [/<\/t[dh]>\s*<t[dh][^>]*>/gi, "\t"], // Table cells get tabs betweem them
- /<[a-z!\/?][^>]*>/gi, // Delete all remaining tags
- [/ /gi, " "], // Convert non-break spaces to regular spaces (remember, *plain text*)
- [/(?:(?!\n)\s)*(\n+)(?:(?!\n)\s)*/gi, "$1"] // Cool little RegExp deletes whitespace around linebreak chars.
- ]);
-
- var maxLinebreaks = Number(getParam(ed, "paste_max_consecutive_linebreaks"));
- if (maxLinebreaks > -1) {
- var maxLinebreaksRegex = new RegExp("\n{" + (maxLinebreaks + 1) + ",}", "g");
- var linebreakReplacement = "";
-
- while (linebreakReplacement.length < maxLinebreaks) {
- linebreakReplacement += "\n";
- }
-
- process([
- [maxLinebreaksRegex, linebreakReplacement] // Limit max consecutive linebreaks
- ]);
- }
-
- content = ed.dom.decode(tinymce.html.Entities.encodeRaw(content));
-
- // Perform default or custom replacements
- if (is(rl, "array")) {
- process(rl);
- } else if (is(rl, "string")) {
- process(new RegExp(rl, "gi"));
- }
-
- // Treat paragraphs as specified in the config
- if (linebr == "none") {
- // Convert all line breaks to space
- process([
- [/\n+/g, " "]
- ]);
- } else if (linebr == "br") {
- // Convert all line breaks to <br />
- process([
- [/\n/g, "<br />"]
- ]);
- } else if (linebr == "p") {
- // Convert all line breaks to <p>...</p>
- process([
- [/\n+/g, "</p><p>"],
- [/^(.*<\/p>)(<p>)$/, '<p>$1']
- ]);
- } else {
- // defaults to "combined"
- // Convert single line breaks to <br /> and double line breaks to <p>...</p>
- process([
- [/\n\n/g, "</p><p>"],
- [/^(.*<\/p>)(<p>)$/, '<p>$1'],
- [/\n/g, "<br />"]
- ]);
- }
-
- ed.execCommand('mceInsertContent', false, content);
- }
- },
-
- /**
- * This method will open the old style paste dialogs. Some users might want the old behavior but still use the new cleanup engine.
- */
- _legacySupport : function() {
- var t = this, ed = t.editor;
-
- // Register command(s) for backwards compatibility
- ed.addCommand("mcePasteWord", function() {
- ed.windowManager.open({
- file: t.url + "/pasteword.htm",
- width: parseInt(getParam(ed, "paste_dialog_width")),
- height: parseInt(getParam(ed, "paste_dialog_height")),
- inline: 1
- });
- });
-
- if (getParam(ed, "paste_text_use_dialog")) {
- ed.addCommand("mcePasteText", function() {
- ed.windowManager.open({
- file : t.url + "/pastetext.htm",
- width: parseInt(getParam(ed, "paste_dialog_width")),
- height: parseInt(getParam(ed, "paste_dialog_height")),
- inline : 1
- });
- });
- }
-
- // Register button for backwards compatibility
- ed.addButton("pasteword", {title : "paste.paste_word_desc", cmd : "mcePasteWord"});
- }
- });
-
- // Register plugin
- tinymce.PluginManager.add("paste", tinymce.plugins.PastePlugin);
-})();
-\ No newline at end of file
diff --git a/resource/tinymce/plugins/paste/plugin.js b/resource/tinymce/plugins/paste/plugin.js
@@ -0,0 +1,1856 @@
+/**
+ * Compiled inline version. (Library mode)
+ */
+
+/*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */
+/*globals $code */
+
+(function(exports, undefined) {
+ "use strict";
+
+ var modules = {};
+
+ function require(ids, callback) {
+ var module, defs = [];
+
+ for (var i = 0; i < ids.length; ++i) {
+ module = modules[ids[i]] || resolve(ids[i]);
+ if (!module) {
+ throw 'module definition dependecy not found: ' + ids[i];
+ }
+
+ defs.push(module);
+ }
+
+ callback.apply(null, defs);
+ }
+
+ function define(id, dependencies, definition) {
+ if (typeof id !== 'string') {
+ throw 'invalid module definition, module id must be defined and be a string';
+ }
+
+ if (dependencies === undefined) {
+ throw 'invalid module definition, dependencies must be specified';
+ }
+
+ if (definition === undefined) {
+ throw 'invalid module definition, definition function must be specified';
+ }
+
+ require(dependencies, function() {
+ modules[id] = definition.apply(null, arguments);
+ });
+ }
+
+ function defined(id) {
+ return !!modules[id];
+ }
+
+ function resolve(id) {
+ var target = exports;
+ var fragments = id.split(/[.\/]/);
+
+ for (var fi = 0; fi < fragments.length; ++fi) {
+ if (!target[fragments[fi]]) {
+ return;
+ }
+
+ target = target[fragments[fi]];
+ }
+
+ return target;
+ }
+
+ function expose(ids) {
+ var i, target, id, fragments, privateModules;
+
+ for (i = 0; i < ids.length; i++) {
+ target = exports;
+ id = ids[i];
+ fragments = id.split(/[.\/]/);
+
+ for (var fi = 0; fi < fragments.length - 1; ++fi) {
+ if (target[fragments[fi]] === undefined) {
+ target[fragments[fi]] = {};
+ }
+
+ target = target[fragments[fi]];
+ }
+
+ target[fragments[fragments.length - 1]] = modules[id];
+ }
+
+ // Expose private modules for unit tests
+ if (exports.AMDLC_TESTS) {
+ privateModules = exports.privateModules || {};
+
+ for (id in modules) {
+ privateModules[id] = modules[id];
+ }
+
+ for (i = 0; i < ids.length; i++) {
+ delete privateModules[ids[i]];
+ }
+
+ exports.privateModules = privateModules;
+ }
+ }
+
+// Included from: js/tinymce/plugins/paste/classes/Utils.js
+
+/**
+ * Utils.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class contails various utility functions for the paste plugin.
+ *
+ * @class tinymce.pasteplugin.Utils
+ */
+define("tinymce/pasteplugin/Utils", [
+ "tinymce/util/Tools",
+ "tinymce/html/DomParser",
+ "tinymce/html/Schema"
+], function(Tools, DomParser, Schema) {
+ function filter(content, items) {
+ Tools.each(items, function(v) {
+ if (v.constructor == RegExp) {
+ content = content.replace(v, '');
+ } else {
+ content = content.replace(v[0], v[1]);
+ }
+ });
+
+ return content;
+ }
+
+ /**
+ * Gets the innerText of the specified element. It will handle edge cases
+ * and works better than textContent on Gecko.
+ *
+ * @param {String} html HTML string to get text from.
+ * @return {String} String of text with line feeds.
+ */
+ function innerText(html) {
+ var schema = new Schema(), domParser = new DomParser({}, schema), text = '';
+ var shortEndedElements = schema.getShortEndedElements();
+ var ignoreElements = Tools.makeMap('script noscript style textarea video audio iframe object', ' ');
+ var blockElements = schema.getBlockElements();
+
+ function walk(node) {
+ var name = node.name, currentNode = node;
+
+ if (name === 'br') {
+ text += '\n';
+ return;
+ }
+
+ // img/input/hr
+ if (shortEndedElements[name]) {
+ text += ' ';
+ }
+
+ // Ingore script, video contents
+ if (ignoreElements[name]) {
+ text += ' ';
+ return;
+ }
+
+ if (node.type == 3) {
+ text += node.value;
+ }
+
+ // Walk all children
+ if (!node.shortEnded) {
+ if ((node = node.firstChild)) {
+ do {
+ walk(node);
+ } while ((node = node.next));
+ }
+ }
+
+ // Add \n or \n\n for blocks or P
+ if (blockElements[name] && currentNode.next) {
+ text += '\n';
+
+ if (name == 'p') {
+ text += '\n';
+ }
+ }
+ }
+
+ html = filter(html, [
+ /<!\[[^\]]+\]>/g // Conditional comments
+ ]);
+
+ walk(domParser.parse(html));
+
+ return text;
+ }
+
+ /**
+ * Trims the specified HTML by removing all WebKit fragments, all elements wrapping the body trailing BR elements etc.
+ *
+ * @param {String} html Html string to trim contents on.
+ * @return {String} Html contents that got trimmed.
+ */
+ function trimHtml(html) {
+ function trimSpaces(all, s1, s2) {
+ // WebKit meant to preserve multiple spaces but instead inserted around all inline tags,
+ // including the spans with inline styles created on paste
+ if (!s1 && !s2) {
+ return ' ';
+ }
+
+ return '\u00a0';
+ }
+
+ html = filter(html, [
+ /^[\s\S]*<body[^>]*>\s*|\s*<\/body[^>]*>[\s\S]*$/g, // Remove anything but the contents within the BODY element
+ /<!--StartFragment-->|<!--EndFragment-->/g, // Inner fragments (tables from excel on mac)
+ [/( ?)<span class="Apple-converted-space">\u00a0<\/span>( ?)/g, trimSpaces],
+ /<br class="Apple-interchange-newline">/g,
+ /<br>$/i // Trailing BR elements
+ ]);
+
+ return html;
+ }
+
+ // TODO: Should be in some global class
+ function createIdGenerator(prefix) {
+ var count = 0;
+
+ return function() {
+ return prefix + (count++);
+ };
+ }
+
+ return {
+ filter: filter,
+ innerText: innerText,
+ trimHtml: trimHtml,
+ createIdGenerator: createIdGenerator
+ };
+});
+
+// Included from: js/tinymce/plugins/paste/classes/SmartPaste.js
+
+/**
+ * SmartPaste.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Tries to be smart depending on what the user pastes if it looks like an url
+ * it will make a link out of the current selection. If it's an image url that looks
+ * like an image it will check if it's an image and insert it as an image.
+ *
+ * @class tinymce.pasteplugin.SmartPaste
+ * @private
+ */
+define("tinymce/pasteplugin/SmartPaste", [
+ "tinymce/util/Tools"
+], function (Tools) {
+ var isAbsoluteUrl = function (url) {
+ return /^https?:\/\/[\w\?\-\/+=.&%@~#]+$/i.test(url);
+ };
+
+ var isImageUrl = function (url) {
+ return isAbsoluteUrl(url) && /.(gif|jpe?g|png)$/.test(url);
+ };
+
+ var createImage = function (editor, url, pasteHtml) {
+ editor.undoManager.extra(function () {
+ pasteHtml(editor, url);
+ }, function () {
+ editor.insertContent('<img src="' + url + '">');
+ });
+
+ return true;
+ };
+
+ var createLink = function (editor, url, pasteHtml) {
+ editor.undoManager.extra(function () {
+ pasteHtml(editor, url);
+ }, function () {
+ editor.execCommand('mceInsertLink', false, url);
+ });
+
+ return true;
+ };
+
+ var linkSelection = function (editor, html, pasteHtml) {
+ return editor.selection.isCollapsed() === false && isAbsoluteUrl(html) ? createLink(editor, html, pasteHtml) : false;
+ };
+
+ var insertImage = function (editor, html, pasteHtml) {
+ return isImageUrl(html) ? createImage(editor, html, pasteHtml) : false;
+ };
+
+ var pasteHtml = function (editor, html) {
+ editor.insertContent(html, {
+ merge: editor.settings.paste_merge_formats !== false,
+ paste: true
+ });
+
+ return true;
+ };
+
+ var smartInsertContent = function (editor, html) {
+ Tools.each([
+ linkSelection,
+ insertImage,
+ pasteHtml
+ ], function (action) {
+ return action(editor, html, pasteHtml) !== true;
+ });
+ };
+
+ var insertContent = function (editor, html) {
+ if (editor.settings.smart_paste === false) {
+ pasteHtml(editor, html);
+ } else {
+ smartInsertContent(editor, html);
+ }
+ };
+
+ return {
+ isImageUrl: isImageUrl,
+ isAbsoluteUrl: isAbsoluteUrl,
+ insertContent: insertContent
+ };
+});
+
+// Included from: js/tinymce/plugins/paste/classes/Clipboard.js
+
+/**
+ * Clipboard.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class contains logic for getting HTML contents out of the clipboard.
+ *
+ * We need to make a lot of ugly hacks to get the contents out of the clipboard since
+ * the W3C Clipboard API is broken in all browsers that have it: Gecko/WebKit/Blink.
+ * We might rewrite this the way those API:s stabilize. Browsers doesn't handle pasting
+ * from applications like Word the same way as it does when pasting into a contentEditable area
+ * so we need to do lots of extra work to try to get to this clipboard data.
+ *
+ * Current implementation steps:
+ * 1. On keydown with paste keys Ctrl+V or Shift+Insert create
+ * a paste bin element and move focus to that element.
+ * 2. Wait for the browser to fire a "paste" event and get the contents out of the paste bin.
+ * 3. Check if the paste was successful if true, process the HTML.
+ * (4). If the paste was unsuccessful use IE execCommand, Clipboard API, document.dataTransfer old WebKit API etc.
+ *
+ * @class tinymce.pasteplugin.Clipboard
+ * @private
+ */
+define("tinymce/pasteplugin/Clipboard", [
+ "tinymce/Env",
+ "tinymce/dom/RangeUtils",
+ "tinymce/util/VK",
+ "tinymce/pasteplugin/Utils",
+ "tinymce/pasteplugin/SmartPaste",
+ "tinymce/util/Delay"
+], function(Env, RangeUtils, VK, Utils, SmartPaste, Delay) {
+ return function(editor) {
+ var self = this, pasteBinElm, lastRng, keyboardPasteTimeStamp = 0, draggingInternally = false;
+ var pasteBinDefaultContent = '%MCEPASTEBIN%', keyboardPastePlainTextState;
+ var mceInternalUrlPrefix = 'data:text/mce-internal,';
+ var uniqueId = Utils.createIdGenerator("mceclip");
+
+ /**
+ * Pastes the specified HTML. This means that the HTML is filtered and then
+ * inserted at the current selection in the editor. It will also fire paste events
+ * for custom user filtering.
+ *
+ * @param {String} html HTML code to paste into the current selection.
+ */
+ function pasteHtml(html) {
+ var args, dom = editor.dom;
+
+ args = editor.fire('BeforePastePreProcess', {content: html}); // Internal event used by Quirks
+ args = editor.fire('PastePreProcess', args);
+ html = args.content;
+
+ if (!args.isDefaultPrevented()) {
+ // User has bound PastePostProcess events then we need to pass it through a DOM node
+ // This is not ideal but we don't want to let the browser mess up the HTML for example
+ // some browsers add to P tags etc
+ if (editor.hasEventListeners('PastePostProcess') && !args.isDefaultPrevented()) {
+ // We need to attach the element to the DOM so Sizzle selectors work on the contents
+ var tempBody = dom.add(editor.getBody(), 'div', {style: 'display:none'}, html);
+ args = editor.fire('PastePostProcess', {node: tempBody});
+ dom.remove(tempBody);
+ html = args.node.innerHTML;
+ }
+
+ if (!args.isDefaultPrevented()) {
+ SmartPaste.insertContent(editor, html);
+ }
+ }
+ }
+
+ /**
+ * Pastes the specified text. This means that the plain text is processed
+ * and converted into BR and P elements. It will fire paste events for custom filtering.
+ *
+ * @param {String} text Text to paste as the current selection location.
+ */
+ function pasteText(text) {
+ text = editor.dom.encode(text).replace(/\r\n/g, '\n');
+
+ var startBlock = editor.dom.getParent(editor.selection.getStart(), editor.dom.isBlock);
+
+ // Create start block html for example <p attr="value">
+ var forcedRootBlockName = editor.settings.forced_root_block;
+ var forcedRootBlockStartHtml;
+ if (forcedRootBlockName) {
+ forcedRootBlockStartHtml = editor.dom.createHTML(forcedRootBlockName, editor.settings.forced_root_block_attrs);
+ forcedRootBlockStartHtml = forcedRootBlockStartHtml.substr(0, forcedRootBlockStartHtml.length - 3) + '>';
+ }
+
+ if ((startBlock && /^(PRE|DIV)$/.test(startBlock.nodeName)) || !forcedRootBlockName) {
+ text = Utils.filter(text, [
+ [/\n/g, "<br>"]
+ ]);
+ } else {
+ text = Utils.filter(text, [
+ [/\n\n/g, "</p>" + forcedRootBlockStartHtml],
+ [/^(.*<\/p>)(<p>)$/, forcedRootBlockStartHtml + '$1'],
+ [/\n/g, "<br />"]
+ ]);
+
+ if (text.indexOf('<p>') != -1) {
+ text = forcedRootBlockStartHtml + text;
+ }
+ }
+
+ pasteHtml(text);
+ }
+
+ /**
+ * Creates a paste bin element as close as possible to the current caret location and places the focus inside that element
+ * so that when the real paste event occurs the contents gets inserted into this element
+ * instead of the current editor selection element.
+ */
+ function createPasteBin() {
+ var dom = editor.dom, body = editor.getBody();
+ var viewport = editor.dom.getViewPort(editor.getWin()), scrollTop = viewport.y, top = 20;
+ var scrollContainer;
+
+ lastRng = editor.selection.getRng();
+
+ if (editor.inline) {
+ scrollContainer = editor.selection.getScrollContainer();
+
+ // Can't always rely on scrollTop returning a useful value.
+ // It returns 0 if the browser doesn't support scrollTop for the element or is non-scrollable
+ if (scrollContainer && scrollContainer.scrollTop > 0) {
+ scrollTop = scrollContainer.scrollTop;
+ }
+ }
+
+ /**
+ * Returns the rect of the current caret if the caret is in an empty block before a
+ * BR we insert a temporary invisible character that we get the rect this way we always get a proper rect.
+ *
+ * TODO: This might be useful in core.
+ */
+ function getCaretRect(rng) {
+ var rects, textNode, node, container = rng.startContainer;
+
+ rects = rng.getClientRects();
+ if (rects.length) {
+ return rects[0];
+ }
+
+ if (!rng.collapsed || container.nodeType != 1) {
+ return;
+ }
+
+ node = container.childNodes[lastRng.startOffset];
+
+ // Skip empty whitespace nodes
+ while (node && node.nodeType == 3 && !node.data.length) {
+ node = node.nextSibling;
+ }
+
+ if (!node) {
+ return;
+ }
+
+ // Check if the location is |<br>
+ // TODO: Might need to expand this to say |<table>
+ if (node.tagName == 'BR') {
+ textNode = dom.doc.createTextNode('\uFEFF');
+ node.parentNode.insertBefore(textNode, node);
+
+ rng = dom.createRng();
+ rng.setStartBefore(textNode);
+ rng.setEndAfter(textNode);
+
+ rects = rng.getClientRects();
+ dom.remove(textNode);
+ }
+
+ if (rects.length) {
+ return rects[0];
+ }
+ }
+
+ // Calculate top cordinate this is needed to avoid scrolling to top of document
+ // We want the paste bin to be as close to the caret as possible to avoid scrolling
+ if (lastRng.getClientRects) {
+ var rect = getCaretRect(lastRng);
+
+ if (rect) {
+ // Client rects gets us closes to the actual
+ // caret location in for example a wrapped paragraph block
+ top = scrollTop + (rect.top - dom.getPos(body).y);
+ } else {
+ top = scrollTop;
+
+ // Check if we can find a closer location by checking the range element
+ var container = lastRng.startContainer;
+ if (container) {
+ if (container.nodeType == 3 && container.parentNode != body) {
+ container = container.parentNode;
+ }
+
+ if (container.nodeType == 1) {
+ top = dom.getPos(container, scrollContainer || body).y;
+ }
+ }
+ }
+ }
+
+ // Create a pastebin
+ pasteBinElm = dom.add(editor.getBody(), 'div', {
+ id: "mcepastebin",
+ contentEditable: true,
+ "data-mce-bogus": "all",
+ style: 'position: absolute; top: ' + top + 'px;' +
+ 'width: 10px; height: 10px; overflow: hidden; opacity: 0'
+ }, pasteBinDefaultContent);
+
+ // Move paste bin out of sight since the controlSelection rect gets displayed otherwise on IE and Gecko
+ if (Env.ie || Env.gecko) {
+ dom.setStyle(pasteBinElm, 'left', dom.getStyle(body, 'direction', true) == 'rtl' ? 0xFFFF : -0xFFFF);
+ }
+
+ // Prevent focus events from bubbeling fixed FocusManager issues
+ dom.bind(pasteBinElm, 'beforedeactivate focusin focusout', function(e) {
+ e.stopPropagation();
+ });
+
+ pasteBinElm.focus();
+ editor.selection.select(pasteBinElm, true);
+ }
+
+ /**
+ * Removes the paste bin if it exists.
+ */
+ function removePasteBin() {
+ if (pasteBinElm) {
+ var pasteBinClone;
+
+ // WebKit/Blink might clone the div so
+ // lets make sure we remove all clones
+ // TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it!
+ while ((pasteBinClone = editor.dom.get('mcepastebin'))) {
+ editor.dom.remove(pasteBinClone);
+ editor.dom.unbind(pasteBinClone);
+ }
+
+ if (lastRng) {
+ editor.selection.setRng(lastRng);
+ }
+ }
+
+ pasteBinElm = lastRng = null;
+ }
+
+ /**
+ * Returns the contents of the paste bin as a HTML string.
+ *
+ * @return {String} Get the contents of the paste bin.
+ */
+ function getPasteBinHtml() {
+ var html = '', pasteBinClones, i, clone, cloneHtml;
+
+ // Since WebKit/Chrome might clone the paste bin when pasting
+ // for example: <img style="float: right"> we need to check if any of them contains some useful html.
+ // TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it!
+ pasteBinClones = editor.dom.select('div[id=mcepastebin]');
+ for (i = 0; i < pasteBinClones.length; i++) {
+ clone = pasteBinClones[i];
+
+ // Pasting plain text produces pastebins in pastebinds makes sence right!?
+ if (clone.firstChild && clone.firstChild.id == 'mcepastebin') {
+ clone = clone.firstChild;
+ }
+
+ cloneHtml = clone.innerHTML;
+ if (html != pasteBinDefaultContent) {
+ html += cloneHtml;
+ }
+ }
+
+ return html;
+ }
+
+ /**
+ * Gets various content types out of a datatransfer object.
+ *
+ * @param {DataTransfer} dataTransfer Event fired on paste.
+ * @return {Object} Object with mime types and data for those mime types.
+ */
+ function getDataTransferItems(dataTransfer) {
+ var items = {};
+
+ if (dataTransfer) {
+ // Use old WebKit/IE API
+ if (dataTransfer.getData) {
+ var legacyText = dataTransfer.getData('Text');
+ if (legacyText && legacyText.length > 0) {
+ if (legacyText.indexOf(mceInternalUrlPrefix) == -1) {
+ items['text/plain'] = legacyText;
+ }
+ }
+ }
+
+ if (dataTransfer.types) {
+ for (var i = 0; i < dataTransfer.types.length; i++) {
+ var contentType = dataTransfer.types[i];
+ items[contentType] = dataTransfer.getData(contentType);
+ }
+ }
+ }
+
+ return items;
+ }
+
+ /**
+ * Gets various content types out of the Clipboard API. It will also get the
+ * plain text using older IE and WebKit API:s.
+ *
+ * @param {ClipboardEvent} clipboardEvent Event fired on paste.
+ * @return {Object} Object with mime types and data for those mime types.
+ */
+ function getClipboardContent(clipboardEvent) {
+ return getDataTransferItems(clipboardEvent.clipboardData || editor.getDoc().dataTransfer);
+ }
+
+ function hasHtmlOrText(content) {
+ return hasContentType(content, 'text/html') || hasContentType(content, 'text/plain');
+ }
+
+ function getBase64FromUri(uri) {
+ var idx;
+
+ idx = uri.indexOf(',');
+ if (idx !== -1) {
+ return uri.substr(idx + 1);
+ }
+
+ return null;
+ }
+
+ function isValidDataUriImage(settings, imgElm) {
+ return settings.images_dataimg_filter ? settings.images_dataimg_filter(imgElm) : true;
+ }
+
+ function pasteImage(rng, reader, blob) {
+ if (rng) {
+ editor.selection.setRng(rng);
+ rng = null;
+ }
+
+ var dataUri = reader.result;
+ var base64 = getBase64FromUri(dataUri);
+
+ var img = new Image();
+ img.src = dataUri;
+
+ // TODO: Move the bulk of the cache logic to EditorUpload
+ if (isValidDataUriImage(editor.settings, img)) {
+ var blobCache = editor.editorUpload.blobCache;
+ var blobInfo, existingBlobInfo;
+
+ existingBlobInfo = blobCache.findFirst(function(cachedBlobInfo) {
+ return cachedBlobInfo.base64() === base64;
+ });
+
+ if (!existingBlobInfo) {
+ blobInfo = blobCache.create(uniqueId(), blob, base64);
+ blobCache.add(blobInfo);
+ } else {
+ blobInfo = existingBlobInfo;
+ }
+
+ pasteHtml('<img src="' + blobInfo.blobUri() + '">');
+ } else {
+ pasteHtml('<img src="' + dataUri + '">');
+ }
+ }
+
+ /**
+ * Checks if the clipboard contains image data if it does it will take that data
+ * and convert it into a data url image and paste that image at the caret location.
+ *
+ * @param {ClipboardEvent} e Paste/drop event object.
+ * @param {DOMRange} rng Rng object to move selection to.
+ * @return {Boolean} true/false if the image data was found or not.
+ */
+ function pasteImageData(e, rng) {
+ var dataTransfer = e.clipboardData || e.dataTransfer;
+
+ function processItems(items) {
+ var i, item, reader, hadImage = false;
+
+ if (items) {
+ for (i = 0; i < items.length; i++) {
+ item = items[i];
+
+ if (/^image\/(jpeg|png|gif|bmp)$/.test(item.type)) {
+ var blob = item.getAsFile ? item.getAsFile() : item;
+
+ reader = new FileReader();
+ reader.onload = pasteImage.bind(null, rng, reader, blob);
+ reader.readAsDataURL(blob);
+
+ e.preventDefault();
+ hadImage = true;
+ }
+ }
+ }
+
+ return hadImage;
+ }
+
+ if (editor.settings.paste_data_images && dataTransfer) {
+ return processItems(dataTransfer.items) || processItems(dataTransfer.files);
+ }
+ }
+
+ /**
+ * Chrome on Android doesn't support proper clipboard access so we have no choice but to allow the browser default behavior.
+ *
+ * @param {Event} e Paste event object to check if it contains any data.
+ * @return {Boolean} true/false if the clipboard is empty or not.
+ */
+ function isBrokenAndroidClipboardEvent(e) {
+ var clipboardData = e.clipboardData;
+
+ return navigator.userAgent.indexOf('Android') != -1 && clipboardData && clipboardData.items && clipboardData.items.length === 0;
+ }
+
+ function getCaretRangeFromEvent(e) {
+ return RangeUtils.getCaretRangeFromPoint(e.clientX, e.clientY, editor.getDoc());
+ }
+
+ function hasContentType(clipboardContent, mimeType) {
+ return mimeType in clipboardContent && clipboardContent[mimeType].length > 0;
+ }
+
+ function isKeyboardPasteEvent(e) {
+ return (VK.metaKeyPressed(e) && e.keyCode == 86) || (e.shiftKey && e.keyCode == 45);
+ }
+
+ function registerEventHandlers() {
+ editor.on('keydown', function(e) {
+ function removePasteBinOnKeyUp(e) {
+ // Ctrl+V or Shift+Insert
+ if (isKeyboardPasteEvent(e) && !e.isDefaultPrevented()) {
+ removePasteBin();
+ }
+ }
+
+ // Ctrl+V or Shift+Insert
+ if (isKeyboardPasteEvent(e) && !e.isDefaultPrevented()) {
+ keyboardPastePlainTextState = e.shiftKey && e.keyCode == 86;
+
+ // Edge case on Safari on Mac where it doesn't handle Cmd+Shift+V correctly
+ // it fires the keydown but no paste or keyup so we are left with a paste bin
+ if (keyboardPastePlainTextState && Env.webkit && navigator.userAgent.indexOf('Version/') != -1) {
+ return;
+ }
+
+ // Prevent undoManager keydown handler from making an undo level with the pastebin in it
+ e.stopImmediatePropagation();
+
+ keyboardPasteTimeStamp = new Date().getTime();
+
+ // IE doesn't support Ctrl+Shift+V and it doesn't even produce a paste event
+ // so lets fake a paste event and let IE use the execCommand/dataTransfer methods
+ if (Env.ie && keyboardPastePlainTextState) {
+ e.preventDefault();
+ editor.fire('paste', {ieFake: true});
+ return;
+ }
+
+ removePasteBin();
+ createPasteBin();
+
+ // Remove pastebin if we get a keyup and no paste event
+ // For example pasting a file in IE 11 will not produce a paste event
+ editor.once('keyup', removePasteBinOnKeyUp);
+ editor.once('paste', function() {
+ editor.off('keyup', removePasteBinOnKeyUp);
+ });
+ }
+ });
+
+ function insertClipboardContent(clipboardContent, isKeyBoardPaste, plainTextMode) {
+ var content;
+
+ // Grab HTML from Clipboard API or paste bin as a fallback
+ if (hasContentType(clipboardContent, 'text/html')) {
+ content = clipboardContent['text/html'];
+ } else {
+ content = getPasteBinHtml();
+
+ // If paste bin is empty try using plain text mode
+ // since that is better than nothing right
+ if (content == pasteBinDefaultContent) {
+ plainTextMode = true;
+ }
+ }
+
+ content = Utils.trimHtml(content);
+
+ // WebKit has a nice bug where it clones the paste bin if you paste from for example notepad
+ // so we need to force plain text mode in this case
+ if (pasteBinElm && pasteBinElm.firstChild && pasteBinElm.firstChild.id === 'mcepastebin') {
+ plainTextMode = true;
+ }
+
+ removePasteBin();
+
+ // If we got nothing from clipboard API and pastebin then we could try the last resort: plain/text
+ if (!content.length) {
+ plainTextMode = true;
+ }
+
+ // Grab plain text from Clipboard API or convert existing HTML to plain text
+ if (plainTextMode) {
+ // Use plain text contents from Clipboard API unless the HTML contains paragraphs then
+ // we should convert the HTML to plain text since works better when pasting HTML/Word contents as plain text
+ if (hasContentType(clipboardContent, 'text/plain') && content.indexOf('</p>') == -1) {
+ content = clipboardContent['text/plain'];
+ } else {
+ content = Utils.innerText(content);
+ }
+ }
+
+ // If the content is the paste bin default HTML then it was
+ // impossible to get the cliboard data out.
+ if (content == pasteBinDefaultContent) {
+ if (!isKeyBoardPaste) {
+ editor.windowManager.alert('Please use Ctrl+V/Cmd+V keyboard shortcuts to paste contents.');
+ }
+
+ return;
+ }
+
+ if (plainTextMode) {
+ pasteText(content);
+ } else {
+ pasteHtml(content);
+ }
+ }
+
+ var getLastRng = function() {
+ return lastRng || editor.selection.getRng();
+ };
+
+ editor.on('paste', function(e) {
+ // Getting content from the Clipboard can take some time
+ var clipboardTimer = new Date().getTime();
+ var clipboardContent = getClipboardContent(e);
+ var clipboardDelay = new Date().getTime() - clipboardTimer;
+
+ var isKeyBoardPaste = (new Date().getTime() - keyboardPasteTimeStamp - clipboardDelay) < 1000;
+ var plainTextMode = self.pasteFormat == "text" || keyboardPastePlainTextState;
+
+ keyboardPastePlainTextState = false;
+
+ if (e.isDefaultPrevented() || isBrokenAndroidClipboardEvent(e)) {
+ removePasteBin();
+ return;
+ }
+
+ if (!hasHtmlOrText(clipboardContent) && pasteImageData(e, getLastRng())) {
+ removePasteBin();
+ return;
+ }
+
+ // Not a keyboard paste prevent default paste and try to grab the clipboard contents using different APIs
+ if (!isKeyBoardPaste) {
+ e.preventDefault();
+ }
+
+ // Try IE only method if paste isn't a keyboard paste
+ if (Env.ie && (!isKeyBoardPaste || e.ieFake)) {
+ createPasteBin();
+
+ editor.dom.bind(pasteBinElm, 'paste', function(e) {
+ e.stopPropagation();
+ });
+
+ editor.getDoc().execCommand('Paste', false, null);
+ clipboardContent["text/html"] = getPasteBinHtml();
+ }
+
+ // If clipboard API has HTML then use that directly
+ if (hasContentType(clipboardContent, 'text/html')) {
+ e.preventDefault();
+ insertClipboardContent(clipboardContent, isKeyBoardPaste, plainTextMode);
+ } else {
+ Delay.setEditorTimeout(editor, function() {
+ insertClipboardContent(clipboardContent, isKeyBoardPaste, plainTextMode);
+ }, 0);
+ }
+ });
+
+ editor.on('dragstart dragend', function(e) {
+ draggingInternally = e.type == 'dragstart';
+ });
+
+ function isPlainTextFileUrl(content) {
+ var plainTextContent = content['text/plain'];
+ return plainTextContent ? plainTextContent.indexOf('file://') === 0 : false;
+ }
+
+ editor.on('drop', function(e) {
+ var dropContent, rng;
+
+ rng = getCaretRangeFromEvent(e);
+
+ if (e.isDefaultPrevented() || draggingInternally) {
+ return;
+ }
+
+ dropContent = getDataTransferItems(e.dataTransfer);
+
+ if ((!hasHtmlOrText(dropContent) || isPlainTextFileUrl(dropContent)) && pasteImageData(e, rng)) {
+ return;
+ }
+
+ if (rng && editor.settings.paste_filter_drop !== false) {
+ var content = dropContent['mce-internal'] || dropContent['text/html'] || dropContent['text/plain'];
+
+ if (content) {
+ e.preventDefault();
+
+ // FF 45 doesn't paint a caret when dragging in text in due to focus call by execCommand
+ Delay.setEditorTimeout(editor, function() {
+ editor.undoManager.transact(function() {
+ if (dropContent['mce-internal']) {
+ editor.execCommand('Delete');
+ }
+
+ editor.selection.setRng(rng);
+
+ content = Utils.trimHtml(content);
+
+ if (!dropContent['text/html']) {
+ pasteText(content);
+ } else {
+ pasteHtml(content);
+ }
+ });
+ });
+ }
+ }
+ });
+
+ editor.on('dragover dragend', function(e) {
+ if (editor.settings.paste_data_images) {
+ e.preventDefault();
+ }
+ });
+ }
+
+ self.pasteHtml = pasteHtml;
+ self.pasteText = pasteText;
+ self.pasteImageData = pasteImageData;
+
+ editor.on('preInit', function() {
+ registerEventHandlers();
+
+ // Remove all data images from paste for example from Gecko
+ // except internal images like video elements
+ editor.parser.addNodeFilter('img', function(nodes, name, args) {
+ function isPasteInsert(args) {
+ return args.data && args.data.paste === true;
+ }
+
+ function remove(node) {
+ if (!node.attr('data-mce-object') && src !== Env.transparentSrc) {
+ node.remove();
+ }
+ }
+
+ function isWebKitFakeUrl(src) {
+ return src.indexOf("webkit-fake-url") === 0;
+ }
+
+ function isDataUri(src) {
+ return src.indexOf("data:") === 0;
+ }
+
+ if (!editor.settings.paste_data_images && isPasteInsert(args)) {
+ var i = nodes.length;
+
+ while (i--) {
+ var src = nodes[i].attributes.map.src;
+
+ if (!src) {
+ continue;
+ }
+
+ // Safari on Mac produces webkit-fake-url see: https://bugs.webkit.org/show_bug.cgi?id=49141
+ if (isWebKitFakeUrl(src)) {
+ remove(nodes[i]);
+ } else if (!editor.settings.allow_html_data_urls && isDataUri(src)) {
+ remove(nodes[i]);
+ }
+ }
+ }
+ });
+ });
+ };
+});
+
+// Included from: js/tinymce/plugins/paste/classes/WordFilter.js
+
+/**
+ * WordFilter.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class parses word HTML into proper TinyMCE markup.
+ *
+ * @class tinymce.pasteplugin.WordFilter
+ * @private
+ */
+define("tinymce/pasteplugin/WordFilter", [
+ "tinymce/util/Tools",
+ "tinymce/html/DomParser",
+ "tinymce/html/Schema",
+ "tinymce/html/Serializer",
+ "tinymce/html/Node",
+ "tinymce/pasteplugin/Utils"
+], function(Tools, DomParser, Schema, Serializer, Node, Utils) {
+ /**
+ * Checks if the specified content is from any of the following sources: MS Word/Office 365/Google docs.
+ */
+ function isWordContent(content) {
+ return (
+ (/<font face="Times New Roman"|class="?Mso|style="[^"]*\bmso-|style='[^'']*\bmso-|w:WordDocument/i).test(content) ||
+ (/class="OutlineElement/).test(content) ||
+ (/id="?docs\-internal\-guid\-/.test(content))
+ );
+ }
+
+ /**
+ * Checks if the specified text starts with "1. " or "a. " etc.
+ */
+ function isNumericList(text) {
+ var found, patterns;
+
+ patterns = [
+ /^[IVXLMCD]{1,2}\.[ \u00a0]/, // Roman upper case
+ /^[ivxlmcd]{1,2}\.[ \u00a0]/, // Roman lower case
+ /^[a-z]{1,2}[\.\)][ \u00a0]/, // Alphabetical a-z
+ /^[A-Z]{1,2}[\.\)][ \u00a0]/, // Alphabetical A-Z
+ /^[0-9]+\.[ \u00a0]/, // Numeric lists
+ /^[\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d]+\.[ \u00a0]/, // Japanese
+ /^[\u58f1\u5f10\u53c2\u56db\u4f0d\u516d\u4e03\u516b\u4e5d\u62fe]+\.[ \u00a0]/ // Chinese
+ ];
+
+ text = text.replace(/^[\u00a0 ]+/, '');
+
+ Tools.each(patterns, function(pattern) {
+ if (pattern.test(text)) {
+ found = true;
+ return false;
+ }
+ });
+
+ return found;
+ }
+
+ function isBulletList(text) {
+ return /^[\s\u00a0]*[\u2022\u00b7\u00a7\u25CF]\s*/.test(text);
+ }
+
+ function WordFilter(editor) {
+ var settings = editor.settings;
+
+ editor.on('BeforePastePreProcess', function(e) {
+ var content = e.content, retainStyleProperties, validStyles;
+
+ // Remove google docs internal guid markers
+ content = content.replace(/<b[^>]+id="?docs-internal-[^>]*>/gi, '');
+ content = content.replace(/<br class="?Apple-interchange-newline"?>/gi, '');
+
+ retainStyleProperties = settings.paste_retain_style_properties;
+ if (retainStyleProperties) {
+ validStyles = Tools.makeMap(retainStyleProperties.split(/[, ]/));
+ }
+
+ /**
+ * Converts fake bullet and numbered lists to real semantic OL/UL.
+ *
+ * @param {tinymce.html.Node} node Root node to convert children of.
+ */
+ function convertFakeListsToProperLists(node) {
+ var currentListNode, prevListNode, lastLevel = 1;
+
+ function getText(node) {
+ var txt = '';
+
+ if (node.type === 3) {
+ return node.value;
+ }
+
+ if ((node = node.firstChild)) {
+ do {
+ txt += getText(node);
+ } while ((node = node.next));
+ }
+
+ return txt;
+ }
+
+ function trimListStart(node, regExp) {
+ if (node.type === 3) {
+ if (regExp.test(node.value)) {
+ node.value = node.value.replace(regExp, '');
+ return false;
+ }
+ }
+
+ if ((node = node.firstChild)) {
+ do {
+ if (!trimListStart(node, regExp)) {
+ return false;
+ }
+ } while ((node = node.next));
+ }
+
+ return true;
+ }
+
+ function removeIgnoredNodes(node) {
+ if (node._listIgnore) {
+ node.remove();
+ return;
+ }
+
+ if ((node = node.firstChild)) {
+ do {
+ removeIgnoredNodes(node);
+ } while ((node = node.next));
+ }
+ }
+
+ function convertParagraphToLi(paragraphNode, listName, start) {
+ var level = paragraphNode._listLevel || lastLevel;
+
+ // Handle list nesting
+ if (level != lastLevel) {
+ if (level < lastLevel) {
+ // Move to parent list
+ if (currentListNode) {
+ currentListNode = currentListNode.parent.parent;
+ }
+ } else {
+ // Create new list
+ prevListNode = currentListNode;
+ currentListNode = null;
+ }
+ }
+
+ if (!currentListNode || currentListNode.name != listName) {
+ prevListNode = prevListNode || currentListNode;
+ currentListNode = new Node(listName, 1);
+
+ if (start > 1) {
+ currentListNode.attr('start', '' + start);
+ }
+
+ paragraphNode.wrap(currentListNode);
+ } else {
+ currentListNode.append(paragraphNode);
+ }
+
+ paragraphNode.name = 'li';
+
+ // Append list to previous list if it exists
+ if (level > lastLevel && prevListNode) {
+ prevListNode.lastChild.append(currentListNode);
+ }
+
+ lastLevel = level;
+
+ // Remove start of list item "1. " or "· " etc
+ removeIgnoredNodes(paragraphNode);
+ trimListStart(paragraphNode, /^\u00a0+/);
+ trimListStart(paragraphNode, /^\s*([\u2022\u00b7\u00a7\u25CF]|\w+\.)/);
+ trimListStart(paragraphNode, /^\u00a0+/);
+ }
+
+ // Build a list of all root level elements before we start
+ // altering them in the loop below.
+ var elements = [], child = node.firstChild;
+ while (typeof child !== 'undefined' && child !== null) {
+ elements.push(child);
+
+ child = child.walk();
+ if (child !== null) {
+ while (typeof child !== 'undefined' && child.parent !== node) {
+ child = child.walk();
+ }
+ }
+ }
+
+ for (var i = 0; i < elements.length; i++) {
+ node = elements[i];
+
+ if (node.name == 'p' && node.firstChild) {
+ // Find first text node in paragraph
+ var nodeText = getText(node);
+
+ // Detect unordered lists look for bullets
+ if (isBulletList(nodeText)) {
+ convertParagraphToLi(node, 'ul');
+ continue;
+ }
+
+ // Detect ordered lists 1., a. or ixv.
+ if (isNumericList(nodeText)) {
+ // Parse OL start number
+ var matches = /([0-9]+)\./.exec(nodeText);
+ var start = 1;
+ if (matches) {
+ start = parseInt(matches[1], 10);
+ }
+
+ convertParagraphToLi(node, 'ol', start);
+ continue;
+ }
+
+ // Convert paragraphs marked as lists but doesn't look like anything
+ if (node._listLevel) {
+ convertParagraphToLi(node, 'ul', 1);
+ continue;
+ }
+
+ currentListNode = null;
+ } else {
+ // If the root level element isn't a p tag which can be
+ // processed by convertParagraphToLi, it interrupts the
+ // lists, causing a new list to start instead of having
+ // elements from the next list inserted above this tag.
+ prevListNode = currentListNode;
+ currentListNode = null;
+ }
+ }
+ }
+
+ function filterStyles(node, styleValue) {
+ var outputStyles = {}, matches, styles = editor.dom.parseStyle(styleValue);
+
+ Tools.each(styles, function(value, name) {
+ // Convert various MS styles to W3C styles
+ switch (name) {
+ case 'mso-list':
+ // Parse out list indent level for lists
+ matches = /\w+ \w+([0-9]+)/i.exec(styleValue);
+ if (matches) {
+ node._listLevel = parseInt(matches[1], 10);
+ }
+
+ // Remove these nodes <span style="mso-list:Ignore">o</span>
+ // Since the span gets removed we mark the text node and the span
+ if (/Ignore/i.test(value) && node.firstChild) {
+ node._listIgnore = true;
+ node.firstChild._listIgnore = true;
+ }
+
+ break;
+
+ case "horiz-align":
+ name = "text-align";
+ break;
+
+ case "vert-align":
+ name = "vertical-align";
+ break;
+
+ case "font-color":
+ case "mso-foreground":
+ name = "color";
+ break;
+
+ case "mso-background":
+ case "mso-highlight":
+ name = "background";
+ break;
+
+ case "font-weight":
+ case "font-style":
+ if (value != "normal") {
+ outputStyles[name] = value;
+ }
+ return;
+
+ case "mso-element":
+ // Remove track changes code
+ if (/^(comment|comment-list)$/i.test(value)) {
+ node.remove();
+ return;
+ }
+
+ break;
+ }
+
+ if (name.indexOf('mso-comment') === 0) {
+ node.remove();
+ return;
+ }
+
+ // Never allow mso- prefixed names
+ if (name.indexOf('mso-') === 0) {
+ return;
+ }
+
+ // Output only valid styles
+ if (retainStyleProperties == "all" || (validStyles && validStyles[name])) {
+ outputStyles[name] = value;
+ }
+ });
+
+ // Convert bold style to "b" element
+ if (/(bold)/i.test(outputStyles["font-weight"])) {
+ delete outputStyles["font-weight"];
+ node.wrap(new Node("b", 1));
+ }
+
+ // Convert italic style to "i" element
+ if (/(italic)/i.test(outputStyles["font-style"])) {
+ delete outputStyles["font-style"];
+ node.wrap(new Node("i", 1));
+ }
+
+ // Serialize the styles and see if there is something left to keep
+ outputStyles = editor.dom.serializeStyle(outputStyles, node.name);
+ if (outputStyles) {
+ return outputStyles;
+ }
+
+ return null;
+ }
+
+ if (settings.paste_enable_default_filters === false) {
+ return;
+ }
+
+ // Detect is the contents is Word junk HTML
+ if (isWordContent(e.content)) {
+ e.wordContent = true; // Mark it for other processors
+
+ // Remove basic Word junk
+ content = Utils.filter(content, [
+ // Word comments like conditional comments etc
+ /<!--[\s\S]+?-->/gi,
+
+ // Remove comments, scripts (e.g., msoShowComment), XML tag, VML content,
+ // MS Office namespaced tags, and a few other tags
+ /<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,
+
+ // Convert <s> into <strike> for line-though
+ [/<(\/?)s>/gi, "<$1strike>"],
+
+ // Replace nsbp entites to char since it's easier to handle
+ [/ /gi, "\u00a0"],
+
+ // Convert <span style="mso-spacerun:yes">___</span> to string of alternating
+ // breaking/non-breaking spaces of same length
+ [/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi,
+ function(str, spaces) {
+ return (spaces.length > 0) ?
+ spaces.replace(/./, " ").slice(Math.floor(spaces.length / 2)).split("").join("\u00a0") : "";
+ }
+ ]
+ ]);
+
+ var validElements = settings.paste_word_valid_elements;
+ if (!validElements) {
+ validElements = (
+ '-strong/b,-em/i,-u,-span,-p,-ol,-ul,-li,-h1,-h2,-h3,-h4,-h5,-h6,' +
+ '-p/div,-a[href|name],sub,sup,strike,br,del,table[width],tr,' +
+ 'td[colspan|rowspan|width],th[colspan|rowspan|width],thead,tfoot,tbody'
+ );
+ }
+
+ // Setup strict schema
+ var schema = new Schema({
+ valid_elements: validElements,
+ valid_children: '-li[p]'
+ });
+
+ // Add style/class attribute to all element rules since the user might have removed them from
+ // paste_word_valid_elements config option and we need to check them for properties
+ Tools.each(schema.elements, function(rule) {
+ /*eslint dot-notation:0*/
+ if (!rule.attributes["class"]) {
+ rule.attributes["class"] = {};
+ rule.attributesOrder.push("class");
+ }
+
+ if (!rule.attributes.style) {
+ rule.attributes.style = {};
+ rule.attributesOrder.push("style");
+ }
+ });
+
+ // Parse HTML into DOM structure
+ var domParser = new DomParser({}, schema);
+
+ // Filter styles to remove "mso" specific styles and convert some of them
+ domParser.addAttributeFilter('style', function(nodes) {
+ var i = nodes.length, node;
+
+ while (i--) {
+ node = nodes[i];
+ node.attr('style', filterStyles(node, node.attr('style')));
+
+ // Remove pointess spans
+ if (node.name == 'span' && node.parent && !node.attributes.length) {
+ node.unwrap();
+ }
+ }
+ });
+
+ // Check the class attribute for comments or del items and remove those
+ domParser.addAttributeFilter('class', function(nodes) {
+ var i = nodes.length, node, className;
+
+ while (i--) {
+ node = nodes[i];
+
+ className = node.attr('class');
+ if (/^(MsoCommentReference|MsoCommentText|msoDel)$/i.test(className)) {
+ node.remove();
+ }
+
+ node.attr('class', null);
+ }
+ });
+
+ // Remove all del elements since we don't want the track changes code in the editor
+ domParser.addNodeFilter('del', function(nodes) {
+ var i = nodes.length;
+
+ while (i--) {
+ nodes[i].remove();
+ }
+ });
+
+ // Keep some of the links and anchors
+ domParser.addNodeFilter('a', function(nodes) {
+ var i = nodes.length, node, href, name;
+
+ while (i--) {
+ node = nodes[i];
+ href = node.attr('href');
+ name = node.attr('name');
+
+ if (href && href.indexOf('#_msocom_') != -1) {
+ node.remove();
+ continue;
+ }
+
+ if (href && href.indexOf('file://') === 0) {
+ href = href.split('#')[1];
+ if (href) {
+ href = '#' + href;
+ }
+ }
+
+ if (!href && !name) {
+ node.unwrap();
+ } else {
+ // Remove all named anchors that aren't specific to TOC, Footnotes or Endnotes
+ if (name && !/^_?(?:toc|edn|ftn)/i.test(name)) {
+ node.unwrap();
+ continue;
+ }
+
+ node.attr({
+ href: href,
+ name: name
+ });
+ }
+ }
+ });
+
+ // Parse into DOM structure
+ var rootNode = domParser.parse(content);
+
+ // Process DOM
+ if (settings.paste_convert_word_fake_lists !== false) {
+ convertFakeListsToProperLists(rootNode);
+ }
+
+ // Serialize DOM back to HTML
+ e.content = new Serializer({
+ validate: settings.validate
+ }, schema).serialize(rootNode);
+ }
+ });
+ }
+
+ WordFilter.isWordContent = isWordContent;
+
+ return WordFilter;
+});
+
+// Included from: js/tinymce/plugins/paste/classes/Quirks.js
+
+/**
+ * Quirks.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class contains various fixes for browsers. These issues can not be feature
+ * detected since we have no direct control over the clipboard. However we might be able
+ * to remove some of these fixes once the browsers gets updated/fixed.
+ *
+ * @class tinymce.pasteplugin.Quirks
+ * @private
+ */
+define("tinymce/pasteplugin/Quirks", [
+ "tinymce/Env",
+ "tinymce/util/Tools",
+ "tinymce/pasteplugin/WordFilter",
+ "tinymce/pasteplugin/Utils"
+], function(Env, Tools, WordFilter, Utils) {
+ "use strict";
+
+ return function(editor) {
+ function addPreProcessFilter(filterFunc) {
+ editor.on('BeforePastePreProcess', function(e) {
+ e.content = filterFunc(e.content);
+ });
+ }
+
+ /**
+ * Removes BR elements after block elements. IE9 has a nasty bug where it puts a BR element after each
+ * block element when pasting from word. This removes those elements.
+ *
+ * This:
+ * <p>a</p><br><p>b</p>
+ *
+ * Becomes:
+ * <p>a</p><p>b</p>
+ */
+ function removeExplorerBrElementsAfterBlocks(html) {
+ // Only filter word specific content
+ if (!WordFilter.isWordContent(html)) {
+ return html;
+ }
+
+ // Produce block regexp based on the block elements in schema
+ var blockElements = [];
+
+ Tools.each(editor.schema.getBlockElements(), function(block, blockName) {
+ blockElements.push(blockName);
+ });
+
+ var explorerBlocksRegExp = new RegExp(
+ '(?:<br> [\\s\\r\\n]+|<br>)*(<\\/?(' + blockElements.join('|') + ')[^>]*>)(?:<br> [\\s\\r\\n]+|<br>)*',
+ 'g'
+ );
+
+ // Remove BR:s from: <BLOCK>X</BLOCK><BR>
+ html = Utils.filter(html, [
+ [explorerBlocksRegExp, '$1']
+ ]);
+
+ // IE9 also adds an extra BR element for each soft-linefeed and it also adds a BR for each word wrap break
+ html = Utils.filter(html, [
+ [/<br><br>/g, '<BR><BR>'], // Replace multiple BR elements with uppercase BR to keep them intact
+ [/<br>/g, ' '], // Replace single br elements with space since they are word wrap BR:s
+ [/<BR><BR>/g, '<br>'] // Replace back the double brs but into a single BR
+ ]);
+
+ return html;
+ }
+
+ /**
+ * WebKit has a nasty bug where the all computed styles gets added to style attributes when copy/pasting contents.
+ * This fix solves that by simply removing the whole style attribute.
+ *
+ * The paste_webkit_styles option can be set to specify what to keep:
+ * paste_webkit_styles: "none" // Keep no styles
+ * paste_webkit_styles: "all", // Keep all of them
+ * paste_webkit_styles: "font-weight color" // Keep specific ones
+ *
+ * @param {String} content Content that needs to be processed.
+ * @return {String} Processed contents.
+ */
+ function removeWebKitStyles(content) {
+ // Passthrough all styles from Word and let the WordFilter handle that junk
+ if (WordFilter.isWordContent(content)) {
+ return content;
+ }
+
+ // Filter away styles that isn't matching the target node
+ var webKitStyles = editor.settings.paste_webkit_styles;
+
+ if (editor.settings.paste_remove_styles_if_webkit === false || webKitStyles == "all") {
+ return content;
+ }
+
+ if (webKitStyles) {
+ webKitStyles = webKitStyles.split(/[, ]/);
+ }
+
+ // Keep specific styles that doesn't match the current node computed style
+ if (webKitStyles) {
+ var dom = editor.dom, node = editor.selection.getNode();
+
+ content = content.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi, function(all, before, value, after) {
+ var inputStyles = dom.parseStyle(value, 'span'), outputStyles = {};
+
+ if (webKitStyles === "none") {
+ return before + after;
+ }
+
+ for (var i = 0; i < webKitStyles.length; i++) {
+ var inputValue = inputStyles[webKitStyles[i]], currentValue = dom.getStyle(node, webKitStyles[i], true);
+
+ if (/color/.test(webKitStyles[i])) {
+ inputValue = dom.toHex(inputValue);
+ currentValue = dom.toHex(currentValue);
+ }
+
+ if (currentValue != inputValue) {
+ outputStyles[webKitStyles[i]] = inputValue;
+ }
+ }
+
+ outputStyles = dom.serializeStyle(outputStyles, 'span');
+ if (outputStyles) {
+ return before + ' style="' + outputStyles + '"' + after;
+ }
+
+ return before + after;
+ });
+ } else {
+ // Remove all external styles
+ content = content.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi, '$1$3');
+ }
+
+ // Keep internal styles
+ content = content.replace(/(<[^>]+) data-mce-style="([^"]+)"([^>]*>)/gi, function(all, before, value, after) {
+ return before + ' style="' + value + '"' + after;
+ });
+
+ return content;
+ }
+
+ // Sniff browsers and apply fixes since we can't feature detect
+ if (Env.webkit) {
+ addPreProcessFilter(removeWebKitStyles);
+ }
+
+ if (Env.ie) {
+ addPreProcessFilter(removeExplorerBrElementsAfterBlocks);
+ }
+ };
+});
+
+// Included from: js/tinymce/plugins/paste/classes/Plugin.js
+
+/**
+ * Plugin.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class contains the tinymce plugin logic for the paste plugin.
+ *
+ * @class tinymce.pasteplugin.Plugin
+ * @private
+ */
+define("tinymce/pasteplugin/Plugin", [
+ "tinymce/PluginManager",
+ "tinymce/pasteplugin/Clipboard",
+ "tinymce/pasteplugin/WordFilter",
+ "tinymce/pasteplugin/Quirks"
+], function(PluginManager, Clipboard, WordFilter, Quirks) {
+ var userIsInformed;
+
+ PluginManager.add('paste', function(editor) {
+ var self = this, clipboard, settings = editor.settings;
+
+ function isUserInformedAboutPlainText() {
+ return userIsInformed || editor.settings.paste_plaintext_inform === false;
+ }
+
+ function togglePlainTextPaste() {
+ if (clipboard.pasteFormat == "text") {
+ clipboard.pasteFormat = "html";
+ editor.fire('PastePlainTextToggle', {state: false});
+ } else {
+ clipboard.pasteFormat = "text";
+ editor.fire('PastePlainTextToggle', {state: true});
+
+ if (!isUserInformedAboutPlainText()) {
+ var message = editor.translate('Paste is now in plain text mode. Contents will now ' +
+ 'be pasted as plain text until you toggle this option off.');
+
+ editor.notificationManager.open({
+ text: message,
+ type: 'info'
+ });
+
+ userIsInformed = true;
+ }
+ }
+
+ editor.focus();
+ }
+
+ function stateChange() {
+ var self = this;
+
+ self.active(clipboard.pasteFormat === 'text');
+
+ editor.on('PastePlainTextToggle', function (e) {
+ self.active(e.state);
+ });
+ }
+
+ // draw back if power version is requested and registered
+ if (/(^|[ ,])powerpaste([, ]|$)/.test(settings.plugins) && PluginManager.get('powerpaste')) {
+ /*eslint no-console:0 */
+ if (typeof console !== "undefined" && console.log) {
+ console.log("PowerPaste is incompatible with Paste plugin! Remove 'paste' from the 'plugins' option.");
+ }
+ return;
+ }
+
+ self.clipboard = clipboard = new Clipboard(editor);
+ self.quirks = new Quirks(editor);
+ self.wordFilter = new WordFilter(editor);
+
+ if (editor.settings.paste_as_text) {
+ self.clipboard.pasteFormat = "text";
+ }
+
+ if (settings.paste_preprocess) {
+ editor.on('PastePreProcess', function(e) {
+ settings.paste_preprocess.call(self, self, e);
+ });
+ }
+
+ if (settings.paste_postprocess) {
+ editor.on('PastePostProcess', function(e) {
+ settings.paste_postprocess.call(self, self, e);
+ });
+ }
+
+ editor.addCommand('mceInsertClipboardContent', function(ui, value) {
+ if (value.content) {
+ self.clipboard.pasteHtml(value.content);
+ }
+
+ if (value.text) {
+ self.clipboard.pasteText(value.text);
+ }
+ });
+
+ // Block all drag/drop events
+ if (editor.settings.paste_block_drop) {
+ editor.on('dragend dragover draggesture dragdrop drop drag', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ });
+ }
+
+ // Prevent users from dropping data images on Gecko
+ if (!editor.settings.paste_data_images) {
+ editor.on('drop', function(e) {
+ var dataTransfer = e.dataTransfer;
+
+ if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) {
+ e.preventDefault();
+ }
+ });
+ }
+
+ editor.addCommand('mceTogglePlainTextPaste', togglePlainTextPaste);
+
+ editor.addButton('pastetext', {
+ icon: 'pastetext',
+ tooltip: 'Paste as text',
+ onclick: togglePlainTextPaste,
+ onPostRender: stateChange
+ });
+
+ editor.addMenuItem('pastetext', {
+ text: 'Paste as text',
+ selectable: true,
+ active: clipboard.pasteFormat,
+ onclick: togglePlainTextPaste,
+ onPostRender: stateChange
+ });
+ });
+});
+
+expose(["tinymce/pasteplugin/Utils"]);
+})(this);
+\ No newline at end of file
diff --git a/resource/tinymce/plugins/searchreplace/plugin.js b/resource/tinymce/plugins/searchreplace/plugin.js
@@ -0,0 +1,609 @@
+/**
+ * plugin.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*jshint smarttabs:true, undef:true, unused:true, latedef:true, curly:true, bitwise:true */
+/*eslint no-labels:0, no-constant-condition: 0 */
+/*global tinymce:true */
+
+(function() {
+ function isContentEditableFalse(node) {
+ return node && node.nodeType == 1 && node.contentEditable === "false";
+ }
+
+ // Based on work developed by: James Padolsey http://james.padolsey.com
+ // released under UNLICENSE that is compatible with LGPL
+ // TODO: Handle contentEditable edgecase:
+ // <p>text<span contentEditable="false">text<span contentEditable="true">text</span>text</span>text</p>
+ function findAndReplaceDOMText(regex, node, replacementNode, captureGroup, schema) {
+ var m, matches = [], text, count = 0, doc;
+ var blockElementsMap, hiddenTextElementsMap, shortEndedElementsMap;
+
+ doc = node.ownerDocument;
+ blockElementsMap = schema.getBlockElements(); // H1-H6, P, TD etc
+ hiddenTextElementsMap = schema.getWhiteSpaceElements(); // TEXTAREA, PRE, STYLE, SCRIPT
+ shortEndedElementsMap = schema.getShortEndedElements(); // BR, IMG, INPUT
+
+ function getMatchIndexes(m, captureGroup) {
+ captureGroup = captureGroup || 0;
+
+ if (!m[0]) {
+ throw 'findAndReplaceDOMText cannot handle zero-length matches';
+ }
+
+ var index = m.index;
+
+ if (captureGroup > 0) {
+ var cg = m[captureGroup];
+
+ if (!cg) {
+ throw 'Invalid capture group';
+ }
+
+ index += m[0].indexOf(cg);
+ m[0] = cg;
+ }
+
+ return [index, index + m[0].length, [m[0]]];
+ }
+
+ function getText(node) {
+ var txt;
+
+ if (node.nodeType === 3) {
+ return node.data;
+ }
+
+ if (hiddenTextElementsMap[node.nodeName] && !blockElementsMap[node.nodeName]) {
+ return '';
+ }
+
+ txt = '';
+
+ if (isContentEditableFalse(node)) {
+ return '\n';
+ }
+
+ if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) {
+ txt += '\n';
+ }
+
+ if ((node = node.firstChild)) {
+ do {
+ txt += getText(node);
+ } while ((node = node.nextSibling));
+ }
+
+ return txt;
+ }
+
+ function stepThroughMatches(node, matches, replaceFn) {
+ var startNode, endNode, startNodeIndex,
+ endNodeIndex, innerNodes = [], atIndex = 0, curNode = node,
+ matchLocation = matches.shift(), matchIndex = 0;
+
+ out: while (true) {
+ if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName] || isContentEditableFalse(curNode)) {
+ atIndex++;
+ }
+
+ if (curNode.nodeType === 3) {
+ if (!endNode && curNode.length + atIndex >= matchLocation[1]) {
+ // We've found the ending
+ endNode = curNode;
+ endNodeIndex = matchLocation[1] - atIndex;
+ } else if (startNode) {
+ // Intersecting node
+ innerNodes.push(curNode);
+ }
+
+ if (!startNode && curNode.length + atIndex > matchLocation[0]) {
+ // We've found the match start
+ startNode = curNode;
+ startNodeIndex = matchLocation[0] - atIndex;
+ }
+
+ atIndex += curNode.length;
+ }
+
+ if (startNode && endNode) {
+ curNode = replaceFn({
+ startNode: startNode,
+ startNodeIndex: startNodeIndex,
+ endNode: endNode,
+ endNodeIndex: endNodeIndex,
+ innerNodes: innerNodes,
+ match: matchLocation[2],
+ matchIndex: matchIndex
+ });
+
+ // replaceFn has to return the node that replaced the endNode
+ // and then we step back so we can continue from the end of the
+ // match:
+ atIndex -= (endNode.length - endNodeIndex);
+ startNode = null;
+ endNode = null;
+ innerNodes = [];
+ matchLocation = matches.shift();
+ matchIndex++;
+
+ if (!matchLocation) {
+ break; // no more matches
+ }
+ } else if ((!hiddenTextElementsMap[curNode.nodeName] || blockElementsMap[curNode.nodeName]) && curNode.firstChild) {
+ if (!isContentEditableFalse(curNode)) {
+ // Move down
+ curNode = curNode.firstChild;
+ continue;
+ }
+ } else if (curNode.nextSibling) {
+ // Move forward:
+ curNode = curNode.nextSibling;
+ continue;
+ }
+
+ // Move forward or up:
+ while (true) {
+ if (curNode.nextSibling) {
+ curNode = curNode.nextSibling;
+ break;
+ } else if (curNode.parentNode !== node) {
+ curNode = curNode.parentNode;
+ } else {
+ break out;
+ }
+ }
+ }
+ }
+
+ /**
+ * Generates the actual replaceFn which splits up text nodes
+ * and inserts the replacement element.
+ */
+ function genReplacer(nodeName) {
+ var makeReplacementNode;
+
+ if (typeof nodeName != 'function') {
+ var stencilNode = nodeName.nodeType ? nodeName : doc.createElement(nodeName);
+
+ makeReplacementNode = function(fill, matchIndex) {
+ var clone = stencilNode.cloneNode(false);
+
+ clone.setAttribute('data-mce-index', matchIndex);
+
+ if (fill) {
+ clone.appendChild(doc.createTextNode(fill));
+ }
+
+ return clone;
+ };
+ } else {
+ makeReplacementNode = nodeName;
+ }
+
+ return function(range) {
+ var before, after, parentNode, startNode = range.startNode,
+ endNode = range.endNode, matchIndex = range.matchIndex;
+
+ if (startNode === endNode) {
+ var node = startNode;
+
+ parentNode = node.parentNode;
+ if (range.startNodeIndex > 0) {
+ // Add `before` text node (before the match)
+ before = doc.createTextNode(node.data.substring(0, range.startNodeIndex));
+ parentNode.insertBefore(before, node);
+ }
+
+ // Create the replacement node:
+ var el = makeReplacementNode(range.match[0], matchIndex);
+ parentNode.insertBefore(el, node);
+ if (range.endNodeIndex < node.length) {
+ // Add `after` text node (after the match)
+ after = doc.createTextNode(node.data.substring(range.endNodeIndex));
+ parentNode.insertBefore(after, node);
+ }
+
+ node.parentNode.removeChild(node);
+
+ return el;
+ }
+
+ // Replace startNode -> [innerNodes...] -> endNode (in that order)
+ before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex));
+ after = doc.createTextNode(endNode.data.substring(range.endNodeIndex));
+ var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex);
+ var innerEls = [];
+
+ for (var i = 0, l = range.innerNodes.length; i < l; ++i) {
+ var innerNode = range.innerNodes[i];
+ var innerEl = makeReplacementNode(innerNode.data, matchIndex);
+ innerNode.parentNode.replaceChild(innerEl, innerNode);
+ innerEls.push(innerEl);
+ }
+
+ var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex);
+
+ parentNode = startNode.parentNode;
+ parentNode.insertBefore(before, startNode);
+ parentNode.insertBefore(elA, startNode);
+ parentNode.removeChild(startNode);
+
+ parentNode = endNode.parentNode;
+ parentNode.insertBefore(elB, endNode);
+ parentNode.insertBefore(after, endNode);
+ parentNode.removeChild(endNode);
+
+ return elB;
+ };
+ }
+
+ text = getText(node);
+ if (!text) {
+ return;
+ }
+
+ if (regex.global) {
+ while ((m = regex.exec(text))) {
+ matches.push(getMatchIndexes(m, captureGroup));
+ }
+ } else {
+ m = text.match(regex);
+ matches.push(getMatchIndexes(m, captureGroup));
+ }
+
+ if (matches.length) {
+ count = matches.length;
+ stepThroughMatches(node, matches, genReplacer(replacementNode));
+ }
+
+ return count;
+ }
+
+ function Plugin(editor) {
+ var self = this, currentIndex = -1;
+
+ function showDialog() {
+ var last = {}, selectedText;
+
+ selectedText = tinymce.trim(editor.selection.getContent({format: 'text'}));
+
+ function updateButtonStates() {
+ win.statusbar.find('#next').disabled(!findSpansByIndex(currentIndex + 1).length);
+ win.statusbar.find('#prev').disabled(!findSpansByIndex(currentIndex - 1).length);
+ }
+
+ function notFoundAlert() {
+ editor.windowManager.alert('Could not find the specified string.', function() {
+ win.find('#find')[0].focus();
+ });
+ }
+
+ var win = editor.windowManager.open({
+ layout: "flex",
+ pack: "center",
+ align: "center",
+ onClose: function() {
+ editor.focus();
+ self.done();
+ },
+ onSubmit: function(e) {
+ var count, caseState, text, wholeWord;
+
+ e.preventDefault();
+
+ caseState = win.find('#case').checked();
+ wholeWord = win.find('#words').checked();
+
+ text = win.find('#find').value();
+ if (!text.length) {
+ self.done(false);
+ win.statusbar.items().slice(1).disabled(true);
+ return;
+ }
+
+ if (last.text == text && last.caseState == caseState && last.wholeWord == wholeWord) {
+ if (findSpansByIndex(currentIndex + 1).length === 0) {
+ notFoundAlert();
+ return;
+ }
+
+ self.next();
+ updateButtonStates();
+ return;
+ }
+
+ count = self.find(text, caseState, wholeWord);
+ if (!count) {
+ notFoundAlert();
+ }
+
+ win.statusbar.items().slice(1).disabled(count === 0);
+ updateButtonStates();
+
+ last = {
+ text: text,
+ caseState: caseState,
+ wholeWord: wholeWord
+ };
+ },
+ buttons: [
+ {text: "Find", subtype: 'primary', onclick: function() {
+ win.submit();
+ }},
+ {text: "Replace", disabled: true, onclick: function() {
+ if (!self.replace(win.find('#replace').value())) {
+ win.statusbar.items().slice(1).disabled(true);
+ currentIndex = -1;
+ last = {};
+ }
+ }},
+ {text: "Replace all", disabled: true, onclick: function() {
+ self.replace(win.find('#replace').value(), true, true);
+ win.statusbar.items().slice(1).disabled(true);
+ last = {};
+ }},
+ {type: "spacer", flex: 1},
+ {text: "Prev", name: 'prev', disabled: true, onclick: function() {
+ self.prev();
+ updateButtonStates();
+ }},
+ {text: "Next", name: 'next', disabled: true, onclick: function() {
+ self.next();
+ updateButtonStates();
+ }}
+ ],
+ title: "Find and replace",
+ items: {
+ type: "form",
+ padding: 20,
+ labelGap: 30,
+ spacing: 10,
+ items: [
+ {type: 'textbox', name: 'find', size: 40, label: 'Find', value: selectedText},
+ {type: 'textbox', name: 'replace', size: 40, label: 'Replace with'},
+ {type: 'checkbox', name: 'case', text: 'Match case', label: ' '},
+ {type: 'checkbox', name: 'words', text: 'Whole words', label: ' '}
+ ]
+ }
+ });
+ }
+
+ self.init = function(ed) {
+ ed.addMenuItem('searchreplace', {
+ text: 'Find and replace',
+ shortcut: 'Meta+F',
+ onclick: showDialog,
+ separator: 'before',
+ context: 'edit'
+ });
+
+ ed.addButton('searchreplace', {
+ tooltip: 'Find and replace',
+ shortcut: 'Meta+F',
+ onclick: showDialog
+ });
+
+ ed.addCommand("SearchReplace", showDialog);
+ ed.shortcuts.add('Meta+F', '', showDialog);
+ };
+
+ function getElmIndex(elm) {
+ var value = elm.getAttribute('data-mce-index');
+
+ if (typeof value == "number") {
+ return "" + value;
+ }
+
+ return value;
+ }
+
+ function markAllMatches(regex) {
+ var node, marker;
+
+ marker = editor.dom.create('span', {
+ "data-mce-bogus": 1
+ });
+
+ marker.className = 'mce-match-marker'; // IE 7 adds class="mce-match-marker" and class=mce-match-marker
+ node = editor.getBody();
+
+ self.done(false);
+
+ return findAndReplaceDOMText(regex, node, marker, false, editor.schema);
+ }
+
+ function unwrap(node) {
+ var parentNode = node.parentNode;
+
+ if (node.firstChild) {
+ parentNode.insertBefore(node.firstChild, node);
+ }
+
+ node.parentNode.removeChild(node);
+ }
+
+ function findSpansByIndex(index) {
+ var nodes, spans = [];
+
+ nodes = tinymce.toArray(editor.getBody().getElementsByTagName('span'));
+ if (nodes.length) {
+ for (var i = 0; i < nodes.length; i++) {
+ var nodeIndex = getElmIndex(nodes[i]);
+
+ if (nodeIndex === null || !nodeIndex.length) {
+ continue;
+ }
+
+ if (nodeIndex === index.toString()) {
+ spans.push(nodes[i]);
+ }
+ }
+ }
+
+ return spans;
+ }
+
+ function moveSelection(forward) {
+ var testIndex = currentIndex, dom = editor.dom;
+
+ forward = forward !== false;
+
+ if (forward) {
+ testIndex++;
+ } else {
+ testIndex--;
+ }
+
+ dom.removeClass(findSpansByIndex(currentIndex), 'mce-match-marker-selected');
+
+ var spans = findSpansByIndex(testIndex);
+ if (spans.length) {
+ dom.addClass(findSpansByIndex(testIndex), 'mce-match-marker-selected');
+ editor.selection.scrollIntoView(spans[0]);
+ return testIndex;
+ }
+
+ return -1;
+ }
+
+ function removeNode(node) {
+ var dom = editor.dom, parent = node.parentNode;
+
+ dom.remove(node);
+
+ if (dom.isEmpty(parent)) {
+ dom.remove(parent);
+ }
+ }
+
+ self.find = function(text, matchCase, wholeWord) {
+ text = text.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ text = wholeWord ? '\\b' + text + '\\b' : text;
+
+ var count = markAllMatches(new RegExp(text, matchCase ? 'g' : 'gi'));
+
+ if (count) {
+ currentIndex = -1;
+ currentIndex = moveSelection(true);
+ }
+
+ return count;
+ };
+
+ self.next = function() {
+ var index = moveSelection(true);
+
+ if (index !== -1) {
+ currentIndex = index;
+ }
+ };
+
+ self.prev = function() {
+ var index = moveSelection(false);
+
+ if (index !== -1) {
+ currentIndex = index;
+ }
+ };
+
+ function isMatchSpan(node) {
+ var matchIndex = getElmIndex(node);
+
+ return matchIndex !== null && matchIndex.length > 0;
+ }
+
+ self.replace = function(text, forward, all) {
+ var i, nodes, node, matchIndex, currentMatchIndex, nextIndex = currentIndex, hasMore;
+
+ forward = forward !== false;
+
+ node = editor.getBody();
+ nodes = tinymce.grep(tinymce.toArray(node.getElementsByTagName('span')), isMatchSpan);
+ for (i = 0; i < nodes.length; i++) {
+ var nodeIndex = getElmIndex(nodes[i]);
+
+ matchIndex = currentMatchIndex = parseInt(nodeIndex, 10);
+ if (all || matchIndex === currentIndex) {
+ if (text.length) {
+ nodes[i].firstChild.nodeValue = text;
+ unwrap(nodes[i]);
+ } else {
+ removeNode(nodes[i]);
+ }
+
+ while (nodes[++i]) {
+ matchIndex = parseInt(getElmIndex(nodes[i]), 10);
+
+ if (matchIndex === currentMatchIndex) {
+ removeNode(nodes[i]);
+ } else {
+ i--;
+ break;
+ }
+ }
+
+ if (forward) {
+ nextIndex--;
+ }
+ } else if (currentMatchIndex > currentIndex) {
+ nodes[i].setAttribute('data-mce-index', currentMatchIndex - 1);
+ }
+ }
+
+ editor.undoManager.add();
+ currentIndex = nextIndex;
+
+ if (forward) {
+ hasMore = findSpansByIndex(nextIndex + 1).length > 0;
+ self.next();
+ } else {
+ hasMore = findSpansByIndex(nextIndex - 1).length > 0;
+ self.prev();
+ }
+
+ return !all && hasMore;
+ };
+
+ self.done = function(keepEditorSelection) {
+ var i, nodes, startContainer, endContainer;
+
+ nodes = tinymce.toArray(editor.getBody().getElementsByTagName('span'));
+ for (i = 0; i < nodes.length; i++) {
+ var nodeIndex = getElmIndex(nodes[i]);
+
+ if (nodeIndex !== null && nodeIndex.length) {
+ if (nodeIndex === currentIndex.toString()) {
+ if (!startContainer) {
+ startContainer = nodes[i].firstChild;
+ }
+
+ endContainer = nodes[i].firstChild;
+ }
+
+ unwrap(nodes[i]);
+ }
+ }
+
+ if (startContainer && endContainer) {
+ var rng = editor.dom.createRng();
+ rng.setStart(startContainer, 0);
+ rng.setEnd(endContainer, endContainer.data.length);
+
+ if (keepEditorSelection !== false) {
+ editor.selection.setRng(rng);
+ }
+
+ return rng;
+ }
+ };
+ }
+
+ tinymce.PluginManager.add('searchreplace', Plugin);
+})();
diff --git a/resource/tinymce/skins/lightgray/content.min.css b/resource/tinymce/skins/lightgray/content.min.css
@@ -0,0 +1 @@
+body{background-color:#FFFFFF;color:#000000;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:14px;scrollbar-3dlight-color:#F0F0EE;scrollbar-arrow-color:#676662;scrollbar-base-color:#F0F0EE;scrollbar-darkshadow-color:#DDDDDD;scrollbar-face-color:#E0E0DD;scrollbar-highlight-color:#F0F0EE;scrollbar-shadow-color:#F0F0EE;scrollbar-track-color:#F5F5F5}td,th{font-family:Verdana,Arial,Helvetica,sans-serif;font-size:14px}.mce-content-body .mce-reset{margin:0;padding:0;border:0;outline:0;vertical-align:top;background:transparent;text-decoration:none;color:black;font-family:Arial;font-size:11px;text-shadow:none;float:none;position:static;width:auto;height:auto;white-space:nowrap;cursor:inherit;line-height:normal;font-weight:normal;text-align:left;-webkit-tap-highlight-color:transparent;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;direction:ltr;max-width:none}.mce-object{border:1px dotted #3A3A3A;background:#D5D5D5 url(img/object.gif) no-repeat center}.mce-preview-object{display:inline-block;position:relative;margin:0 2px 0 2px;line-height:0;border:1px solid gray}.mce-preview-object .mce-shim{position:absolute;top:0;left:0;width:100%;height:100%;background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}figure.align-left{float:left}figure.align-right{float:right}figure.image.align-center{display:table;margin-left:auto;margin-right:auto}figure.image{display:inline-block;border:1px solid gray;margin:0 2px 0 1px;background:#f5f2f0}figure.image img{margin:8px 8px 0 8px}figure.image figcaption{margin:6px 8px 6px 8px;text-align:center}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc li{list-style-type:none}.mce-preview-object[data-mce-selected="2"] .mce-shim{display:none}.mce-pagebreak{cursor:default;display:block;border:0;width:100%;height:5px;border:1px dashed #666;margin-top:15px;page-break-before:always}@media print{.mce-pagebreak{border:0}}.mce-item-anchor{cursor:default;display:inline-block;-webkit-user-select:all;-webkit-user-modify:read-only;-moz-user-select:all;-moz-user-modify:read-only;user-select:all;user-modify:read-only;width:9px !important;height:9px !important;border:1px dotted #3A3A3A;background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-nbsp,.mce-shy{background:#AAA}.mce-shy::after{content:'-'}hr{cursor:default}.mce-match-marker{background:#AAA;color:#fff}.mce-match-marker-selected{background:#3399ff;color:#fff}.mce-spellchecker-word{border-bottom:2px solid #F00;cursor:default}.mce-spellchecker-grammar{border-bottom:2px solid #008000;cursor:default}.mce-item-table,.mce-item-table td,.mce-item-table th,.mce-item-table caption{border:1px dashed #BBB}td[data-mce-selected],th[data-mce-selected]{background-color:#3399ff !important}.mce-edit-focus{outline:1px dotted #333}.mce-content-body *[contentEditable=false] *[contentEditable=true]:focus{outline:2px solid #2d8ac7}.mce-content-body *[contentEditable=false] *[contentEditable=true]:hover{outline:2px solid #7ACAFF}.mce-content-body *[contentEditable=false][data-mce-selected]{outline:2px solid #2d8ac7}.mce-resize-bar-dragging{background-color:blue;opacity:.25;filter:alpha(opacity=25);zoom:1}
+\ No newline at end of file
diff --git a/resource/tinymce/skins/lightgray/fonts/tinymce-small.woff b/resource/tinymce/skins/lightgray/fonts/tinymce-small.woff
Binary files differ.
diff --git a/resource/tinymce/skins/lightgray/fonts/tinymce.woff b/resource/tinymce/skins/lightgray/fonts/tinymce.woff
Binary files differ.
diff --git a/resource/tinymce/skins/lightgray/img/anchor.gif b/resource/tinymce/skins/lightgray/img/anchor.gif
Binary files differ.
diff --git a/resource/tinymce/skins/lightgray/img/loader.gif b/resource/tinymce/skins/lightgray/img/loader.gif
Binary files differ.
diff --git a/resource/tinymce/skins/lightgray/img/object.gif b/resource/tinymce/skins/lightgray/img/object.gif
Binary files differ.
diff --git a/resource/tinymce/skins/lightgray/img/trans.gif b/resource/tinymce/skins/lightgray/img/trans.gif
Binary files differ.
diff --git a/resource/tinymce/skins/lightgray/skin.min.css b/resource/tinymce/skins/lightgray/skin.min.css
@@ -0,0 +1 @@
+.mce-container,.mce-container *,.mce-widget,.mce-widget *,.mce-reset{margin:0;padding:0;border:0;outline:0;vertical-align:top;background:transparent;text-decoration:none;color:#333;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;text-shadow:none;float:none;position:static;width:auto;height:auto;white-space:nowrap;cursor:inherit;-webkit-tap-highlight-color:transparent;line-height:normal;font-weight:normal;text-align:left;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;direction:ltr;max-width:none}.mce-widget button{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.mce-container *[unselectable]{-moz-user-select:none;-webkit-user-select:none;-o-user-select:none;user-select:none}.mce-fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.mce-fade.mce-in{opacity:1}.mce-tinymce{visibility:inherit !important;position:relative}.mce-fullscreen{border:0;padding:0;margin:0;overflow:hidden;height:100%;z-index:100}div.mce-fullscreen{position:fixed;top:0;left:0;width:100%;height:auto}.mce-tinymce{display:block}.mce-wordcount{position:absolute;top:0;right:0;padding:8px}div.mce-edit-area{background:#FFF;filter:none}.mce-statusbar{position:relative}.mce-statusbar .mce-container-body{position:relative}.mce-fullscreen .mce-resizehandle{display:none}.mce-charmap{border-collapse:collapse}.mce-charmap td{cursor:default;border:1px solid rgba(0,0,0,0.2);width:20px;height:20px;line-height:20px;text-align:center;vertical-align:middle;padding:2px}.mce-charmap td div{text-align:center}.mce-charmap td:hover{background:#D9D9D9}.mce-grid td.mce-grid-cell div{border:1px solid #d6d6d6;width:15px;height:15px;margin:0;cursor:pointer}.mce-grid td.mce-grid-cell div:focus{border-color:#3498db}.mce-grid td.mce-grid-cell div[disabled]{cursor:not-allowed}.mce-grid{border-spacing:2px;border-collapse:separate}.mce-grid a{display:block;border:1px solid transparent}.mce-grid a:hover,.mce-grid a:focus{border-color:#3498db}.mce-grid-border{margin:0 4px 0 4px}.mce-grid-border a{border-color:#d6d6d6;width:13px;height:13px}.mce-grid-border a:hover,.mce-grid-border a.mce-active{border-color:#3498db;background:#3498db}.mce-text-center{text-align:center}div.mce-tinymce-inline{width:100%}.mce-colorbtn-trans div{text-align:center;vertical-align:middle;font-weight:bold;font-size:20px;line-height:16px;color:#707070}.mce-monospace{font-family:"Courier New",Courier,monospace}.mce-toolbar-grp{padding:2px 0}.mce-toolbar-grp .mce-flow-layout-item{margin-bottom:0}.mce-rtl .mce-wordcount{left:0;right:auto}.mce-croprect-container{position:absolute;top:0;left:0}.mce-croprect-handle{position:absolute;top:0;left:0;width:20px;height:20px;border:2px solid white}.mce-croprect-handle-nw{border-width:2px 0 0 2px;margin:-2px 0 0 -2px;cursor:nw-resize;top:100px;left:100px}.mce-croprect-handle-ne{border-width:2px 2px 0 0;margin:-2px 0 0 -20px;cursor:ne-resize;top:100px;left:200px}.mce-croprect-handle-sw{border-width:0 0 2px 2px;margin:-20px 2px 0 -2px;cursor:sw-resize;top:200px;left:100px}.mce-croprect-handle-se{border-width:0 2px 2px 0;margin:-20px 0 0 -20px;cursor:se-resize;top:200px;left:200px}.mce-croprect-handle-move{position:absolute;cursor:move;border:0}.mce-croprect-block{opacity:.3;filter:alpha(opacity=30);zoom:1;position:absolute;background:black}.mce-croprect-handle:focus{border-color:#3498db}.mce-croprect-handle-move:focus{outline:1px solid #3498db}.mce-imagepanel{overflow:auto;background:black}.mce-imagepanel-bg{position:absolute;background:url('data:image/gif;base64,R0lGODdhDAAMAIABAMzMzP///ywAAAAADAAMAAACFoQfqYeabNyDMkBQb81Uat85nxguUAEAOw==')}.mce-imagepanel img{position:absolute}.mce-imagetool.mce-btn .mce-ico{display:block;width:20px;height:20px;text-align:center;line-height:20px;font-size:20px;padding:5px}.mce-arrow-up{margin-top:12px}.mce-arrow-down{margin-top:-12px}.mce-arrow:before,.mce-arrow:after{position:absolute;left:50%;display:block;width:0;height:0;border-style:solid;border-color:transparent;content:""}.mce-arrow.mce-arrow-up:before{top:-9px;border-bottom-color:rgba(0,0,0,0.2);border-width:0 9px 9px;margin-left:-9px}.mce-arrow.mce-arrow-down:before{bottom:-9px;border-top-color:rgba(0,0,0,0.2);border-width:9px 9px 0;margin-left:-9px}.mce-arrow.mce-arrow-up:after{top:-8px;border-bottom-color:#f0f0f0;border-width:0 8px 8px;margin-left:-8px}.mce-arrow.mce-arrow-down:after{bottom:-8px;border-top-color:#f0f0f0;border-width:8px 8px 0;margin-left:-8px}.mce-arrow.mce-arrow-left:before,.mce-arrow.mce-arrow-left:after{margin:0}.mce-arrow.mce-arrow-left:before{left:8px}.mce-arrow.mce-arrow-left:after{left:9px}.mce-arrow.mce-arrow-right:before,.mce-arrow.mce-arrow-right:after{left:auto;margin:0}.mce-arrow.mce-arrow-right:before{right:8px}.mce-arrow.mce-arrow-right:after{right:9px}.mce-arrow.mce-arrow-center.mce-arrow.mce-arrow-left:before{left:-9px;top:50%;border-right-color:rgba(0,0,0,0.2);border-width:9px 9px 9px 0;margin-top:-9px}.mce-arrow.mce-arrow-center.mce-arrow.mce-arrow-left:after{left:-8px;top:50%;border-right-color:#f0f0f0;border-width:8px 8px 8px 0;margin-top:-8px}.mce-arrow.mce-arrow-center.mce-arrow.mce-arrow-left{margin-left:12px}.mce-arrow.mce-arrow-center.mce-arrow.mce-arrow-right:before{right:-9px;top:50%;border-left-color:rgba(0,0,0,0.2);border-width:9px 0 9px 9px;margin-top:-9px}.mce-arrow.mce-arrow-center.mce-arrow.mce-arrow-right:after{right:-8px;top:50%;border-left-color:#f0f0f0;border-width:8px 0 8px 8px;margin-top:-8px}.mce-arrow.mce-arrow-center.mce-arrow.mce-arrow-right{margin-left:-14px}.mce-edit-aria-container>.mce-container-body{display:flex}.mce-edit-aria-container>.mce-container-body .mce-edit-area{flex:1}.mce-edit-aria-container>.mce-container-body .mce-sidebar>.mce-container-body{display:flex;align-items:stretch;height:100%}.mce-edit-aria-container>.mce-container-body .mce-sidebar-panel{min-width:250px;max-width:250px;position:relative}.mce-edit-aria-container>.mce-container-body .mce-sidebar-panel>.mce-container-body{position:absolute;width:100%;height:100%;overflow:auto;top:0;left:0}.mce-sidebar-toolbar{border:0 solid rgba(0,0,0,0.2);border-left-width:1px}.mce-sidebar-toolbar .mce-btn.mce-active,.mce-sidebar-toolbar .mce-btn.mce-active:hover{border:1px solid transparent;border-color:transparent;background-color:#2d8ac7}.mce-sidebar-toolbar .mce-btn.mce-active button,.mce-sidebar-toolbar .mce-btn.mce-active:hover button,.mce-sidebar-toolbar .mce-btn.mce-active button i,.mce-sidebar-toolbar .mce-btn.mce-active:hover button i{color:#fff;text-shadow:1px 1px none}.mce-sidebar-panel{border:0 solid rgba(0,0,0,0.2);border-left-width:1px}.mce-container,.mce-container-body{display:block}.mce-autoscroll{overflow:hidden}.mce-scrollbar{position:absolute;width:7px;height:100%;top:2px;right:2px;opacity:.4;filter:alpha(opacity=40);zoom:1}.mce-scrollbar-h{top:auto;right:auto;left:2px;bottom:2px;width:100%;height:7px}.mce-scrollbar-thumb{position:absolute;background-color:#000;border:1px solid #888;border-color:rgba(85,85,85,0.6);width:5px;height:100%}.mce-scrollbar-h .mce-scrollbar-thumb{width:100%;height:5px}.mce-scrollbar:hover,.mce-scrollbar.mce-active{background-color:#AAA;opacity:.6;filter:alpha(opacity=60);zoom:1}.mce-scroll{position:relative}.mce-panel{border:0 solid #cacaca;border:0 solid rgba(0,0,0,0.2);background-color:#f0f0f0}.mce-floatpanel{position:absolute}.mce-floatpanel.mce-fixed{position:fixed}.mce-floatpanel .mce-arrow,.mce-floatpanel .mce-arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.mce-floatpanel .mce-arrow{border-width:11px}.mce-floatpanel .mce-arrow:after{border-width:10px;content:""}.mce-floatpanel.mce-popover{filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background:transparent;top:0;left:0;background:#FFF;border:1px solid rgba(0,0,0,0.2);border:1px solid rgba(0,0,0,0.25)}.mce-floatpanel.mce-popover.mce-bottom{margin-top:10px;*margin-top:0}.mce-floatpanel.mce-popover.mce-bottom>.mce-arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:rgba(0,0,0,0.2);border-bottom-color:rgba(0,0,0,0.25);top:-11px}.mce-floatpanel.mce-popover.mce-bottom>.mce-arrow:after{top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#FFF}.mce-floatpanel.mce-popover.mce-bottom.mce-start{margin-left:-22px}.mce-floatpanel.mce-popover.mce-bottom.mce-start>.mce-arrow{left:20px}.mce-floatpanel.mce-popover.mce-bottom.mce-end{margin-left:22px}.mce-floatpanel.mce-popover.mce-bottom.mce-end>.mce-arrow{right:10px;left:auto}.mce-fullscreen{border:0;padding:0;margin:0;overflow:hidden;height:100%}div.mce-fullscreen{position:fixed;top:0;left:0}#mce-modal-block{opacity:0;filter:alpha(opacity=0);zoom:1;position:fixed;left:0;top:0;width:100%;height:100%;background:#000}#mce-modal-block.mce-in{opacity:.3;filter:alpha(opacity=30);zoom:1}.mce-window-move{cursor:move}.mce-window{filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background:transparent;background:#FFF;position:fixed;top:0;left:0;opacity:0;transform:scale(.1);transition:transform 100ms ease-in,opacity 150ms ease-in}.mce-window.mce-in{transform:scale(1);opacity:1}.mce-window-head{padding:9px 15px;border-bottom:1px solid #c5c5c5;position:relative}.mce-window-head .mce-close{position:absolute;right:0;top:0;height:38px;width:38px;text-align:center;cursor:pointer}.mce-window-head .mce-close i{color:#858585}.mce-close:hover i{color:#adadad}.mce-window-head .mce-title{line-height:20px;font-size:20px;font-weight:bold;text-rendering:optimizelegibility;padding-right:20px}.mce-window .mce-container-body{display:block}.mce-foot{display:block;background-color:#FFF;border-top:1px solid #c5c5c5}.mce-window-head .mce-dragh{position:absolute;top:0;left:0;cursor:move;width:90%;height:100%}.mce-window iframe{width:100%;height:100%}.mce-window-body .mce-listbox{border-color:#ccc}.mce-rtl .mce-window-head .mce-close{position:absolute;right:auto;left:15px}.mce-rtl .mce-window-head .mce-dragh{left:auto;right:0}.mce-rtl .mce-window-head .mce-title{direction:rtl;text-align:right}.mce-tooltip{position:absolute;padding:5px;opacity:.8;filter:alpha(opacity=80);zoom:1}.mce-tooltip-inner{font-size:11px;background-color:#000;color:white;max-width:200px;padding:5px 8px 4px 8px;text-align:center;white-space:normal}.mce-tooltip-arrow{position:absolute;width:0;height:0;line-height:0;border:5px dashed #000}.mce-tooltip-arrow-n{border-bottom-color:#000}.mce-tooltip-arrow-s{border-top-color:#000}.mce-tooltip-arrow-e{border-left-color:#000}.mce-tooltip-arrow-w{border-right-color:#000}.mce-tooltip-nw,.mce-tooltip-sw{margin-left:-14px}.mce-tooltip-ne,.mce-tooltip-se{margin-left:14px}.mce-tooltip-n .mce-tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-style:solid;border-top:none;border-left-color:transparent;border-right-color:transparent}.mce-tooltip-nw .mce-tooltip-arrow{top:0;left:10px;border-bottom-style:solid;border-top:none;border-left-color:transparent;border-right-color:transparent}.mce-tooltip-ne .mce-tooltip-arrow{top:0;right:10px;border-bottom-style:solid;border-top:none;border-left-color:transparent;border-right-color:transparent}.mce-tooltip-s .mce-tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-style:solid;border-bottom:none;border-left-color:transparent;border-right-color:transparent}.mce-tooltip-sw .mce-tooltip-arrow{bottom:0;left:10px;border-top-style:solid;border-bottom:none;border-left-color:transparent;border-right-color:transparent}.mce-tooltip-se .mce-tooltip-arrow{bottom:0;right:10px;border-top-style:solid;border-bottom:none;border-left-color:transparent;border-right-color:transparent}.mce-tooltip-e .mce-tooltip-arrow{right:0;top:50%;margin-top:-5px;border-left-style:solid;border-right:none;border-top-color:transparent;border-bottom-color:transparent}.mce-tooltip-w .mce-tooltip-arrow{left:0;top:50%;margin-top:-5px;border-right-style:solid;border-left:none;border-top-color:transparent;border-bottom-color:transparent}.mce-progress{display:inline-block;position:relative;height:20px}.mce-progress .mce-bar-container{display:inline-block;width:100px;height:100%;margin-right:8px;border:1px solid #ccc;overflow:hidden}.mce-progress .mce-text{display:inline-block;margin-top:auto;margin-bottom:auto;font-size:14px;width:40px;color:#333}.mce-bar{display:block;width:0;height:100%;background-color:#d7d7d7;-webkit-transition:width .2s ease;transition:width .2s ease}.mce-notification{position:absolute;background-color:#F0F0F0;padding:5px;margin-top:5px;border-width:1px;border-style:solid;border-color:#CCCCCC;transition:transform 100ms ease-in,opacity 150ms ease-in;opacity:0}.mce-notification.mce-in{opacity:1}.mce-notification-success{background-color:#dff0d8;border-color:#d6e9c6}.mce-notification-info{background-color:#d9edf7;border-color:#779ECB}.mce-notification-warning{background-color:#fcf8e3;border-color:#faebcc}.mce-notification-error{background-color:#f2dede;border-color:#ebccd1}.mce-notification.mce-has-close{padding-right:15px}.mce-notification .mce-ico{margin-top:5px}.mce-notification-inner{display:inline-block;font-size:14px;margin:5px 8px 4px 8px;text-align:center;white-space:normal;color:#31708f}.mce-notification-inner a{text-decoration:underline;cursor:pointer}.mce-notification .mce-progress{margin-right:8px}.mce-notification .mce-progress .mce-text{margin-top:5px}.mce-notification *,.mce-notification .mce-progress .mce-text{color:#333333}.mce-notification .mce-progress .mce-bar-container{border-color:#CCCCCC}.mce-notification .mce-progress .mce-bar-container .mce-bar{background-color:#333333}.mce-notification-success *,.mce-notification-success .mce-progress .mce-text{color:#3c763d}.mce-notification-success .mce-progress .mce-bar-container{border-color:#d6e9c6}.mce-notification-success .mce-progress .mce-bar-container .mce-bar{background-color:#3c763d}.mce-notification-info *,.mce-notification-info .mce-progress .mce-text{color:#31708f}.mce-notification-info .mce-progress .mce-bar-container{border-color:#779ECB}.mce-notification-info .mce-progress .mce-bar-container .mce-bar{background-color:#31708f}.mce-notification-warning *,.mce-notification-warning .mce-progress .mce-text{color:#8a6d3b}.mce-notification-warning .mce-progress .mce-bar-container{border-color:#faebcc}.mce-notification-warning .mce-progress .mce-bar-container .mce-bar{background-color:#8a6d3b}.mce-notification-error *,.mce-notification-error .mce-progress .mce-text{color:#a94442}.mce-notification-error .mce-progress .mce-bar-container{border-color:#ebccd1}.mce-notification-error .mce-progress .mce-bar-container .mce-bar{background-color:#a94442}.mce-notification .mce-close{position:absolute;top:6px;right:8px;font-size:20px;font-weight:bold;line-height:20px;color:#858585;cursor:pointer;height:20px;overflow:hidden}.mce-abs-layout{position:relative}body .mce-abs-layout-item,.mce-abs-end{position:absolute}.mce-abs-end{width:1px;height:1px}.mce-container-body.mce-abs-layout{overflow:hidden}.mce-btn{border:1px solid #b1b1b1;border-color:transparent transparent transparent transparent;position:relative;text-shadow:0 1px 1px rgba(255,255,255,0.75);display:inline-block;*display:inline;*zoom:1;background-color:#f0f0f0}.mce-btn:hover,.mce-btn:focus{color:#333;background-color:#e3e3e3;border-color:#ccc}.mce-btn.mce-disabled button,.mce-btn.mce-disabled:hover button{cursor:default;opacity:.4;filter:alpha(opacity=40);zoom:1}.mce-btn.mce-active,.mce-btn.mce-active:hover{background-color:#dbdbdb;border-color:#ccc}.mce-btn:active{background-color:#e0e0e0;border-color:#ccc}.mce-btn button{padding:4px 8px;font-size:14px;line-height:20px;*line-height:16px;cursor:pointer;color:#333;text-align:center;overflow:visible;-webkit-appearance:none}.mce-btn button::-moz-focus-inner{border:0;padding:0}.mce-btn i{text-shadow:1px 1px none}.mce-primary.mce-btn-has-text{min-width:50px}.mce-primary{color:#fff;border:1px solid transparent;border-color:transparent;background-color:#2d8ac7}.mce-primary:hover,.mce-primary:focus{background-color:#257cb6;border-color:transparent}.mce-primary.mce-disabled button,.mce-primary.mce-disabled:hover button{cursor:default;opacity:.4;filter:alpha(opacity=40);zoom:1}.mce-primary.mce-active,.mce-primary.mce-active:hover,.mce-primary:not(.mce-disabled):active{background-color:#206ea1}.mce-primary button,.mce-primary button i{color:#fff;text-shadow:1px 1px none}.mce-btn .mce-txt{font-size:inherit;line-height:inherit;color:inherit}.mce-btn-large button{padding:9px 14px;font-size:16px;line-height:normal}.mce-btn-large i{margin-top:2px}.mce-btn-small button{padding:1px 5px;font-size:12px;*padding-bottom:2px}.mce-btn-small i{line-height:20px;vertical-align:top;*line-height:18px}.mce-btn .mce-caret{margin-top:8px;margin-left:0}.mce-btn-small .mce-caret{margin-top:8px;margin-left:0}.mce-caret{display:inline-block;*display:inline;*zoom:1;width:0;height:0;vertical-align:top;border-top:4px solid #333;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.mce-disabled .mce-caret{border-top-color:#aaa}.mce-caret.mce-up{border-bottom:4px solid #333;border-top:0}.mce-btn-flat{border:0;background:transparent;filter:none}.mce-btn-flat:hover,.mce-btn-flat.mce-active,.mce-btn-flat:focus,.mce-btn-flat:active{border:0;background:#e6e6e6;filter:none}.mce-btn-has-text .mce-ico{padding-right:5px}.mce-rtl .mce-btn button{direction:rtl}.mce-btn-group .mce-btn{border-width:1px;margin:0;margin-left:2px}.mce-btn-group:not(:first-child){border-left:1px solid #d9d9d9;padding-left:3px;margin-left:3px}.mce-btn-group .mce-first{margin-left:0}.mce-btn-group .mce-btn.mce-flow-layout-item{margin:0}.mce-rtl .mce-btn-group .mce-btn{margin-left:0;margin-right:2px}.mce-rtl .mce-btn-group .mce-first{margin-right:0}.mce-rtl .mce-btn-group:not(:first-child){border-left:none;border-right:1px solid #d9d9d9;padding-right:4px;margin-right:4px}.mce-checkbox{cursor:pointer}i.mce-i-checkbox{margin:0 3px 0 0;border:1px solid #c5c5c5;background-color:#f0f0f0;text-indent:-10em;*font-size:0;*line-height:0;*text-indent:0;overflow:hidden}.mce-checked i.mce-i-checkbox{color:#333;font-size:16px;line-height:16px;text-indent:0}.mce-checkbox:focus i.mce-i-checkbox,.mce-checkbox.mce-focus i.mce-i-checkbox{border:1px solid rgba(82,168,236,0.8)}.mce-checkbox.mce-disabled .mce-label,.mce-checkbox.mce-disabled i.mce-i-checkbox{color:#acacac}.mce-checkbox .mce-label{vertical-align:middle}.mce-rtl .mce-checkbox{direction:rtl;text-align:right}.mce-rtl i.mce-i-checkbox{margin:0 0 0 3px}.mce-combobox{position:relative;display:inline-block;*display:inline;*zoom:1;*height:32px}.mce-combobox input{border:1px solid #c5c5c5;border-right-color:#c5c5c5;height:28px}.mce-combobox.mce-disabled input{color:#adadad}.mce-combobox .mce-btn{border:1px solid #c5c5c5;border-left:0;margin:0}.mce-combobox button{padding-right:8px;padding-left:8px}.mce-combobox.mce-disabled .mce-btn button{cursor:default;opacity:.4;filter:alpha(opacity=40);zoom:1}.mce-combobox .mce-status{position:absolute;right:2px;top:50%;line-height:16px;margin-top:-8px;font-size:12px;width:15px;height:15px;text-align:center;cursor:pointer}.mce-combobox.mce-has-status input{padding-right:20px}.mce-combobox.mce-has-open .mce-status{right:37px}.mce-combobox .mce-status.mce-i-warning{color:#c09853}.mce-combobox .mce-status.mce-i-checkmark{color:#468847}.mce-menu.mce-combobox-menu{border-top:0;margin-top:0;max-height:200px}.mce-menu.mce-combobox-menu .mce-menu-item{padding:4px 6px 4px 4px;font-size:11px}.mce-menu.mce-combobox-menu .mce-menu-item-sep{padding:0}.mce-menu.mce-combobox-menu .mce-text{font-size:11px}.mce-menu.mce-combobox-menu .mce-menu-item-link,.mce-menu.mce-combobox-menu .mce-menu-item-link b{font-size:11px}.mce-menu.mce-combobox-menu .mce-text b{font-size:11px}.mce-colorbox i{border:1px solid #c5c5c5;width:14px;height:14px}.mce-colorbutton .mce-ico{position:relative}.mce-colorbutton-grid{margin:4px}.mce-colorbutton button{padding-right:6px;padding-left:6px}.mce-colorbutton .mce-preview{padding-right:3px;display:block;position:absolute;left:50%;top:50%;margin-left:-17px;margin-top:7px;background:gray;width:13px;height:2px;overflow:hidden}.mce-colorbutton.mce-btn-small .mce-preview{margin-left:-16px;padding-right:0;width:16px}.mce-colorbutton .mce-open{padding-left:4px;padding-right:4px;border-left:1px solid transparent}.mce-colorbutton:hover .mce-open{border-color:#ccc}.mce-colorbutton.mce-btn-small .mce-open{padding:0 3px 0 3px}.mce-rtl .mce-colorbutton{direction:rtl}.mce-rtl .mce-colorbutton .mce-preview{margin-left:0;padding-right:0;padding-left:3px}.mce-rtl .mce-colorbutton.mce-btn-small .mce-preview{margin-left:0;padding-right:0;padding-left:2px}.mce-rtl .mce-colorbutton .mce-open{padding-left:4px;padding-right:4px;border-left:0}.mce-colorpicker{position:relative;width:250px;height:220px}.mce-colorpicker-sv{position:absolute;top:0;left:0;width:90%;height:100%;border:1px solid #c5c5c5;cursor:crosshair;overflow:hidden}.mce-colorpicker-h-chunk{width:100%}.mce-colorpicker-overlay1,.mce-colorpicker-overlay2{width:100%;height:100%;position:absolute;top:0;left:0}.mce-colorpicker-overlay1{filter:progid:DXImageTransform.Microsoft.gradient(GradientType=1, startColorstr='#ffffff', endColorstr='#00ffffff');-ms-filter:"progid:DXImageTransform.Microsoft.gradient(GradientType=1,startColorstr='#ffffff', endColorstr='#00ffffff')";background:linear-gradient(to right, #fff, rgba(255,255,255,0))}.mce-colorpicker-overlay2{filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#00000000', endColorstr='#000000');-ms-filter:"progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='#00000000', endColorstr='#000000')";background:linear-gradient(to bottom, rgba(0,0,0,0), #000)}.mce-colorpicker-selector1{background:none;position:absolute;width:12px;height:12px;margin:-8px 0 0 -8px;border:1px solid black;border-radius:50%}.mce-colorpicker-selector2{position:absolute;width:10px;height:10px;border:1px solid white;border-radius:50%}.mce-colorpicker-h{position:absolute;top:0;right:0;width:6.5%;height:100%;border:1px solid #c5c5c5;cursor:crosshair}.mce-colorpicker-h-marker{margin-top:-4px;position:absolute;top:0;left:-1px;width:100%;border:1px solid #333;background:#fff;height:4px;z-index:100}.mce-path{display:inline-block;*display:inline;*zoom:1;padding:8px;white-space:normal}.mce-path .mce-txt{display:inline-block;padding-right:3px}.mce-path .mce-path-body{display:inline-block}.mce-path-item{display:inline-block;*display:inline;*zoom:1;cursor:pointer;color:#333}.mce-path-item:hover{text-decoration:underline}.mce-path-item:focus{background:#666;color:#fff}.mce-path .mce-divider{display:inline}.mce-disabled .mce-path-item{color:#aaa}.mce-rtl .mce-path{direction:rtl}.mce-fieldset{border:0 solid #9E9E9E}.mce-fieldset>.mce-container-body{margin-top:-15px}.mce-fieldset-title{margin-left:5px;padding:0 5px 0 5px}.mce-fit-layout{display:inline-block;*display:inline;*zoom:1}.mce-fit-layout-item{position:absolute}.mce-flow-layout-item{display:inline-block;*display:inline;*zoom:1}.mce-flow-layout-item{margin:2px 0 2px 2px}.mce-flow-layout-item.mce-last{margin-right:2px}.mce-flow-layout{white-space:normal}.mce-tinymce-inline .mce-flow-layout{white-space:nowrap}.mce-rtl .mce-flow-layout{text-align:right;direction:rtl}.mce-rtl .mce-flow-layout-item{margin:2px 2px 2px 0}.mce-rtl .mce-flow-layout-item.mce-last{margin-left:2px}.mce-iframe{border:0 solid rgba(0,0,0,0.2);width:100%;height:100%}.mce-infobox{display:inline-block;*display:inline;*zoom:1;text-shadow:0 1px 1px rgba(255,255,255,0.75);overflow:hidden;border:1px solid red}.mce-infobox div{display:block;margin:5px}.mce-infobox div button{position:absolute;top:50%;right:4px;cursor:pointer;margin-top:-8px;display:none}.mce-infobox div button:focus{outline:2px solid #ccc}.mce-infobox.mce-has-help div{margin-right:25px}.mce-infobox.mce-has-help button{display:block}.mce-infobox.mce-success{background:#dff0d8;border-color:#d6e9c6}.mce-infobox.mce-success div{color:#3c763d}.mce-infobox.mce-warning{background:#fcf8e3;border-color:#faebcc}.mce-infobox.mce-warning div{color:#8a6d3b}.mce-infobox.mce-error{background:#f2dede;border-color:#ebccd1}.mce-infobox.mce-error div{color:#a94442}.mce-rtl .mce-infobox div{text-align:right;direction:rtl}.mce-label{display:inline-block;*display:inline;*zoom:1;text-shadow:0 1px 1px rgba(255,255,255,0.75);overflow:hidden}.mce-label.mce-autoscroll{overflow:auto}.mce-label.mce-disabled{color:#aaa}.mce-label.mce-multiline{white-space:pre-wrap}.mce-label.mce-success{color:#468847}.mce-label.mce-warning{color:#c09853}.mce-label.mce-error{color:#b94a48}.mce-rtl .mce-label{text-align:right;direction:rtl}.mce-menubar .mce-menubtn{border-color:transparent;background:transparent;filter:none}.mce-menubar .mce-menubtn button{color:#333}.mce-menubar{border:1px solid rgba(217,217,217,0.52)}.mce-menubar .mce-menubtn button span{color:#333}.mce-menubar .mce-caret{border-top-color:#333}.mce-menubar .mce-menubtn:hover,.mce-menubar .mce-menubtn.mce-active,.mce-menubar .mce-menubtn:focus{border-color:#ccc;background:#fff;filter:none}.mce-menubtn button{color:#333}.mce-menubtn.mce-btn-small span{font-size:12px}.mce-menubtn.mce-fixed-width span{display:inline-block;overflow-x:hidden;text-overflow:ellipsis;width:90px}.mce-menubtn.mce-fixed-width.mce-btn-small span{width:70px}.mce-menubtn .mce-caret{*margin-top:6px}.mce-rtl .mce-menubtn button{direction:rtl;text-align:right}.mce-menu-item{display:block;padding:6px 15px 6px 12px;clear:both;font-weight:normal;line-height:20px;color:#333;white-space:nowrap;cursor:pointer;line-height:normal;border-left:4px solid transparent;margin-bottom:1px}.mce-menu-item .mce-ico,.mce-menu-item .mce-text{color:#333}.mce-menu-item.mce-disabled .mce-text,.mce-menu-item.mce-disabled .mce-ico{color:#adadad}.mce-menu-item:hover .mce-text,.mce-menu-item.mce-selected .mce-text,.mce-menu-item:focus .mce-text{color:white}.mce-menu-item:hover .mce-ico,.mce-menu-item.mce-selected .mce-ico,.mce-menu-item:focus .mce-ico{color:white}.mce-menu-item.mce-disabled:hover{background:#CCC}.mce-menu-shortcut{display:inline-block;color:#adadad}.mce-menu-shortcut{display:inline-block;*display:inline;*zoom:1;padding:0 15px 0 20px}.mce-menu-item:hover .mce-menu-shortcut,.mce-menu-item.mce-selected .mce-menu-shortcut,.mce-menu-item:focus .mce-menu-shortcut{color:white}.mce-menu-item .mce-caret{margin-top:4px;*margin-top:3px;margin-right:6px;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:4px solid #333}.mce-menu-item.mce-selected .mce-caret,.mce-menu-item:focus .mce-caret,.mce-menu-item:hover .mce-caret{border-left-color:white}.mce-menu-align .mce-menu-shortcut{*margin-top:-2px}.mce-menu-align .mce-menu-shortcut,.mce-menu-align .mce-caret{position:absolute;right:0}.mce-menu-item.mce-active i{visibility:visible}.mce-menu-item-normal.mce-active{background-color:#3498db}.mce-menu-item-preview.mce-active{border-left:5px solid #aaa}.mce-menu-item-normal.mce-active .mce-text{color:white}.mce-menu-item-normal.mce-active:hover .mce-text,.mce-menu-item-normal.mce-active:hover .mce-ico{color:white}.mce-menu-item-normal.mce-active:focus .mce-text,.mce-menu-item-normal.mce-active:focus .mce-ico{color:white}.mce-menu-item:hover,.mce-menu-item.mce-selected,.mce-menu-item:focus{text-decoration:none;color:white;background-color:#2d8ac7}.mce-menu-item-link{color:#093;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mce-menu-item-link b{color:#093}.mce-menu-item-ellipsis{display:block;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.mce-menu-item:hover *,.mce-menu-item.mce-selected *,.mce-menu-item:focus *{color:white}div.mce-menu .mce-menu-item-sep,.mce-menu-item-sep:hover{border:0;padding:0;height:1px;margin:9px 1px;overflow:hidden;background:transparent;border-bottom:1px solid rgba(0,0,0,0.1);cursor:default;filter:none}div.mce-menu .mce-menu-item b{font-weight:bold}.mce-menu-item-indent-1{padding-left:20px}.mce-menu-item-indent-2{padding-left:35px}.mce-menu-item-indent-2{padding-left:35px}.mce-menu-item-indent-3{padding-left:40px}.mce-menu-item-indent-4{padding-left:45px}.mce-menu-item-indent-5{padding-left:50px}.mce-menu-item-indent-6{padding-left:55px}.mce-menu.mce-rtl{direction:rtl}.mce-rtl .mce-menu-item{text-align:right;direction:rtl;padding:6px 12px 6px 15px}.mce-menu-align.mce-rtl .mce-menu-shortcut,.mce-menu-align.mce-rtl .mce-caret{right:auto;left:0}.mce-rtl .mce-menu-item .mce-caret{margin-left:6px;margin-right:0;border-right:4px solid #333;border-left:0}.mce-rtl .mce-menu-item.mce-selected .mce-caret,.mce-rtl .mce-menu-item:focus .mce-caret,.mce-rtl .mce-menu-item:hover .mce-caret{border-left-color:transparent;border-right-color:white}.mce-throbber{position:absolute;top:0;left:0;width:100%;height:100%;opacity:.6;filter:alpha(opacity=60);zoom:1;background:#fff url('img/loader.gif') no-repeat center center}.mce-throbber-inline{position:static;height:50px}.mce-menu .mce-throbber-inline{height:25px;background-size:contain}.mce-menu{position:absolute;left:0;top:0;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background:transparent;z-index:1000;padding:5px 0 5px 0;margin:-1px 0 0;min-width:160px;background:#fff;border:1px solid #989898;border:1px solid rgba(0,0,0,0.2);z-index:1002;max-height:400px;overflow:auto;overflow-x:hidden}.mce-menu i{display:none}.mce-menu-has-icons i{display:inline-block;*display:inline}.mce-menu-sub-tr-tl{margin:-6px 0 0 -1px}.mce-menu-sub-br-bl{margin:6px 0 0 -1px}.mce-menu-sub-tl-tr{margin:-6px 0 0 1px}.mce-menu-sub-bl-br{margin:6px 0 0 1px}.mce-listbox button{text-align:left;padding-right:20px;position:relative}.mce-listbox .mce-caret{position:absolute;margin-top:-2px;right:8px;top:50%}.mce-rtl .mce-listbox .mce-caret{right:auto;left:8px}.mce-rtl .mce-listbox button{padding-right:10px;padding-left:20px}.mce-container-body .mce-resizehandle{position:absolute;right:0;bottom:0;width:16px;height:16px;visibility:visible;cursor:s-resize;margin:0}.mce-container-body .mce-resizehandle-both{cursor:se-resize}i.mce-i-resize{color:#333}.mce-selectbox{background:#fff;border:1px solid #c5c5c5}.mce-slider{border:1px solid #AAA;background:#EEE;width:100px;height:10px;position:relative;display:block}.mce-slider.mce-vertical{width:10px;height:100px}.mce-slider-handle{border:1px solid #BBB;background:#DDD;display:block;width:13px;height:13px;position:absolute;top:0;left:0;margin-left:-1px;margin-top:-2px}.mce-slider-handle:focus{background:#BBB}.mce-spacer{visibility:hidden}.mce-splitbtn .mce-open{border-left:1px solid transparent}.mce-splitbtn:hover .mce-open{border-left-color:#ccc}.mce-splitbtn button{padding-right:6px;padding-left:6px}.mce-splitbtn .mce-open{padding-right:4px;padding-left:4px}.mce-splitbtn .mce-open.mce-active{background-color:#dbdbdb;outline:1px solid #ccc}.mce-splitbtn.mce-btn-small .mce-open{padding:0 3px 0 3px}.mce-rtl .mce-splitbtn{direction:rtl;text-align:right}.mce-rtl .mce-splitbtn button{padding-right:4px;padding-left:4px}.mce-rtl .mce-splitbtn .mce-open{border-left:0}.mce-stack-layout-item{display:block}.mce-tabs{display:block;border-bottom:1px solid #c5c5c5}.mce-tabs,.mce-tabs+.mce-container-body{background:#FFF}.mce-tab{display:inline-block;*display:inline;*zoom:1;border:1px solid #c5c5c5;border-width:0 1px 0 0;background:#ffffff;padding:8px;text-shadow:0 1px 1px rgba(255,255,255,0.75);height:13px;cursor:pointer}.mce-tab:hover{background:#FDFDFD}.mce-tab.mce-active{background:#FDFDFD;border-bottom-color:transparent;margin-bottom:-1px;height:14px}.mce-rtl .mce-tabs{text-align:right;direction:rtl}.mce-rtl .mce-tab{border-width:0 0 0 1px}.mce-textbox{background:#fff;border:1px solid #c5c5c5;display:inline-block;-webkit-transition:border linear .2s, box-shadow linear .2s;transition:border linear .2s, box-shadow linear .2s;height:28px;resize:none;padding:0 4px 0 4px;white-space:pre-wrap;*white-space:pre;color:#333}.mce-textbox:focus,.mce-textbox.mce-focus{border-color:#3498db}.mce-placeholder .mce-textbox{color:#aaa}.mce-textbox.mce-multiline{padding:4px;height:auto}.mce-textbox.mce-disabled{color:#adadad}.mce-rtl .mce-textbox{text-align:right;direction:rtl}@font-face{font-family:'tinymce';src:url('fonts/tinymce.eot');src:url('fonts/tinymce.eot?#iefix') format('embedded-opentype'),url('fonts/tinymce.woff') format('woff'),url('fonts/tinymce.ttf') format('truetype'),url('fonts/tinymce.svg#tinymce') format('svg');font-weight:normal;font-style:normal}@font-face{font-family:'tinymce-small';src:url('fonts/tinymce-small.eot');src:url('fonts/tinymce-small.eot?#iefix') format('embedded-opentype'),url('fonts/tinymce-small.woff') format('woff'),url('fonts/tinymce-small.ttf') format('truetype'),url('fonts/tinymce-small.svg#tinymce') format('svg');font-weight:normal;font-style:normal}.mce-ico{font-family:'tinymce',Arial;font-style:normal;font-weight:normal;font-variant:normal;font-size:16px;line-height:16px;speak:none;vertical-align:text-top;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;display:inline-block;background:transparent center center;background-size:cover;width:16px;height:16px;color:#333}.mce-btn-small .mce-ico{font-family:'tinymce-small',Arial}.mce-i-save:before{content:"\e000"}.mce-i-newdocument:before{content:"\e001"}.mce-i-fullpage:before{content:"\e002"}.mce-i-alignleft:before{content:"\e003"}.mce-i-aligncenter:before{content:"\e004"}.mce-i-alignright:before{content:"\e005"}.mce-i-alignjustify:before{content:"\e006"}.mce-i-alignnone:before{content:"\e003"}.mce-i-cut:before{content:"\e007"}.mce-i-paste:before{content:"\e008"}.mce-i-searchreplace:before{content:"\e009"}.mce-i-bullist:before{content:"\e00a"}.mce-i-numlist:before{content:"\e00b"}.mce-i-indent:before{content:"\e00c"}.mce-i-outdent:before{content:"\e00d"}.mce-i-blockquote:before{content:"\e00e"}.mce-i-undo:before{content:"\e00f"}.mce-i-redo:before{content:"\e010"}.mce-i-link:before{content:"\e011"}.mce-i-unlink:before{content:"\e012"}.mce-i-anchor:before{content:"\e013"}.mce-i-image:before{content:"\e014"}.mce-i-media:before{content:"\e015"}.mce-i-help:before{content:"\e016"}.mce-i-code:before{content:"\e017"}.mce-i-insertdatetime:before{content:"\e018"}.mce-i-preview:before{content:"\e019"}.mce-i-forecolor:before{content:"\e01a"}.mce-i-backcolor:before{content:"\e01a"}.mce-i-table:before{content:"\e01b"}.mce-i-hr:before{content:"\e01c"}.mce-i-removeformat:before{content:"\e01d"}.mce-i-subscript:before{content:"\e01e"}.mce-i-superscript:before{content:"\e01f"}.mce-i-charmap:before{content:"\e020"}.mce-i-emoticons:before{content:"\e021"}.mce-i-print:before{content:"\e022"}.mce-i-fullscreen:before{content:"\e023"}.mce-i-spellchecker:before{content:"\e024"}.mce-i-nonbreaking:before{content:"\e025"}.mce-i-template:before{content:"\e026"}.mce-i-pagebreak:before{content:"\e027"}.mce-i-restoredraft:before{content:"\e028"}.mce-i-bold:before{content:"\e02a"}.mce-i-italic:before{content:"\e02b"}.mce-i-underline:before{content:"\e02c"}.mce-i-strikethrough:before{content:"\e02d"}.mce-i-visualchars:before{content:"\e02e"}.mce-i-visualblocks:before{content:"\e02e"}.mce-i-ltr:before{content:"\e02f"}.mce-i-rtl:before{content:"\e030"}.mce-i-copy:before{content:"\e031"}.mce-i-resize:before{content:"\e032"}.mce-i-browse:before{content:"\e034"}.mce-i-pastetext:before{content:"\e035"}.mce-i-rotateleft:before{content:"\eaa8"}.mce-i-rotateright:before{content:"\eaa9"}.mce-i-crop:before{content:"\ee78"}.mce-i-editimage:before{content:"\e915"}.mce-i-options:before{content:"\ec6a"}.mce-i-flipv:before{content:"\eaaa"}.mce-i-fliph:before{content:"\eaac"}.mce-i-zoomin:before{content:"\eb35"}.mce-i-zoomout:before{content:"\eb36"}.mce-i-sun:before{content:"\eccc"}.mce-i-moon:before{content:"\eccd"}.mce-i-arrowleft:before{content:"\edc0"}.mce-i-arrowright:before{content:"\e93c"}.mce-i-drop:before{content:"\e935"}.mce-i-contrast:before{content:"\ecd4"}.mce-i-sharpen:before{content:"\eba7"}.mce-i-resize2:before{content:"\edf9"}.mce-i-orientation:before{content:"\e601"}.mce-i-invert:before{content:"\e602"}.mce-i-gamma:before{content:"\e600"}.mce-i-remove:before{content:"\ed6a"}.mce-i-tablerowprops:before{content:"\e604"}.mce-i-tablecellprops:before{content:"\e605"}.mce-i-table2:before{content:"\e606"}.mce-i-tablemergecells:before{content:"\e607"}.mce-i-tableinsertcolbefore:before{content:"\e608"}.mce-i-tableinsertcolafter:before{content:"\e609"}.mce-i-tableinsertrowbefore:before{content:"\e60a"}.mce-i-tableinsertrowafter:before{content:"\e60b"}.mce-i-tablesplitcells:before{content:"\e60d"}.mce-i-tabledelete:before{content:"\e60e"}.mce-i-tableleftheader:before{content:"\e62a"}.mce-i-tabletopheader:before{content:"\e62b"}.mce-i-tabledeleterow:before{content:"\e800"}.mce-i-tabledeletecol:before{content:"\e801"}.mce-i-codesample:before{content:"\e603"}.mce-i-fill:before{content:"\e902"}.mce-i-borderwidth:before{content:"\e903"}.mce-i-line:before{content:"\e904"}.mce-i-count:before{content:"\e905"}.mce-i-translate:before{content:"\e907"}.mce-i-drag:before{content:"\e908"}.mce-i-home:before{content:"\e90b"}.mce-i-upload:before{content:"\e914"}.mce-i-bubble:before{content:"\e91c"}.mce-i-user:before{content:"\e91d"}.mce-i-lock:before{content:"\e926"}.mce-i-unlock:before{content:"\e927"}.mce-i-settings:before{content:"\e928"}.mce-i-remove2:before{content:"\e92a"}.mce-i-menu:before{content:"\e92d"}.mce-i-warning:before{content:"\e930"}.mce-i-question:before{content:"\e931"}.mce-i-pluscircle:before{content:"\e932"}.mce-i-info:before{content:"\e933"}.mce-i-notice:before{content:"\e934"}.mce-i-arrowup:before{content:"\e93b"}.mce-i-arrowdown:before{content:"\e93d"}.mce-i-arrowup2:before{content:"\e93f"}.mce-i-arrowdown2:before{content:"\e940"}.mce-i-menu2:before{content:"\e941"}.mce-i-newtab:before{content:"\e961"}.mce-i-a11y:before{content:"\e900"}.mce-i-plus:before{content:"\e93a"}.mce-i-insert:before{content:"\e93a"}.mce-i-minus:before{content:"\e939"}.mce-i-books:before{content:"\e911"}.mce-i-reload:before{content:"\e906"}.mce-i-toc:before{content:"\e901"}.mce-i-checkmark:before{content:"\e033"}.mce-i-checkbox:before,.mce-i-selected:before{content:"\e033"}.mce-i-insert{font-size:14px}.mce-i-selected{visibility:hidden}i.mce-i-backcolor{text-shadow:none;background:#BBB}
+\ No newline at end of file
diff --git a/resource/tinymce/themes/advanced/about.htm b/resource/tinymce/themes/advanced/about.htm
@@ -1,52 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
-<head>
- <title>{#advanced_dlg.about_title}</title>
- <script type="text/javascript" src="../../tiny_mce_popup.js"></script>
- <script type="text/javascript" src="../../utils/mctabs.js"></script>
- <script type="text/javascript" src="js/about.js"></script>
-</head>
-<body id="about" style="display: none">
- <div class="tabs">
- <ul>
- <li id="general_tab" class="current" aria-controls="general_panel"><span><a href="javascript:mcTabs.displayTab('general_tab','general_panel');" onmousedown="return false;">{#advanced_dlg.about_general}</a></span></li>
- <li id="help_tab" style="display:none" aria-hidden="true" aria-controls="help_panel"><span><a href="javascript:mcTabs.displayTab('help_tab','help_panel');" onmousedown="return false;">{#advanced_dlg.about_help}</a></span></li>
- <li id="plugins_tab" aria-controls="plugins_panel"><span><a href="javascript:mcTabs.displayTab('plugins_tab','plugins_panel');" onmousedown="return false;">{#advanced_dlg.about_plugins}</a></span></li>
- </ul>
- </div>
-
- <div class="panel_wrapper">
- <div id="general_panel" class="panel current">
- <h3>{#advanced_dlg.about_title}</h3>
- <p>Version: <span id="version"></span> (<span id="date"></span>)</p>
- <p>TinyMCE is a platform independent web based Javascript HTML WYSIWYG editor control released as Open Source under <a href="../../license.txt" target="_blank">LGPL</a>
- by Moxiecode Systems AB. It has the ability to convert HTML TEXTAREA fields or other HTML elements to editor instances.</p>
- <p>Copyright © 2003-2008, <a href="http://www.moxiecode.com" target="_blank">Moxiecode Systems AB</a>, All rights reserved.</p>
- <p>For more information about this software visit the <a href="http://tinymce.moxiecode.com" target="_blank">TinyMCE website</a>.</p>
-
- <div id="buttoncontainer">
- <a href="http://www.moxiecode.com" target="_blank"><img src="http://tinymce.moxiecode.com/images/gotmoxie.png" alt="Got Moxie?" border="0" /></a>
- </div>
- </div>
-
- <div id="plugins_panel" class="panel">
- <div id="pluginscontainer">
- <h3>{#advanced_dlg.about_loaded}</h3>
-
- <div id="plugintablecontainer">
- </div>
-
- <p> </p>
- </div>
- </div>
-
- <div id="help_panel" class="panel noscroll" style="overflow: visible;">
- <div id="iframecontainer"></div>
- </div>
- </div>
-
- <div class="mceActionPanel">
- <input type="button" id="cancel" name="cancel" value="{#close}" onclick="tinyMCEPopup.close();" />
- </div>
-</body>
-</html>
diff --git a/resource/tinymce/themes/advanced/anchor.htm b/resource/tinymce/themes/advanced/anchor.htm
@@ -1,26 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
-<head>
- <title>{#advanced_dlg.anchor_title}</title>
- <script type="text/javascript" src="../../tiny_mce_popup.js"></script>
- <script type="text/javascript" src="js/anchor.js"></script>
-</head>
-<body style="display: none" role="application" aria-labelledby="app_title">
-<form onsubmit="AnchorDialog.update();return false;" action="#">
- <table border="0" cellpadding="4" cellspacing="0" role="presentation">
- <tr>
- <td colspan="2" class="title" id="app_title">{#advanced_dlg.anchor_title}</td>
- </tr>
- <tr>
- <td class="nowrap"><label for="anchorName">{#advanced_dlg.anchor_name}:</label></td>
- <td><input name="anchorName" type="text" class="mceFocus" id="anchorName" value="" style="width: 200px" aria-required="true" /></td>
- </tr>
- </table>
-
- <div class="mceActionPanel">
- <input type="submit" id="insert" name="insert" value="{#update}" />
- <input type="button" id="cancel" name="cancel" value="{#cancel}" onclick="tinyMCEPopup.close();" />
- </div>
-</form>
-</body>
-</html>
diff --git a/resource/tinymce/themes/advanced/charmap.htm b/resource/tinymce/themes/advanced/charmap.htm
@@ -1,55 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
-<head>
- <title>{#advanced_dlg.charmap_title}</title>
- <script type="text/javascript" src="../../tiny_mce_popup.js"></script>
- <script type="text/javascript" src="js/charmap.js"></script>
-</head>
-<body id="charmap" style="display:none" role="application">
-<table align="center" border="0" cellspacing="0" cellpadding="2" role="presentation">
- <tr>
- <td colspan="2" class="title" ><label for="charmapView" id="charmap_label">{#advanced_dlg.charmap_title}</label></td>
- </tr>
- <tr>
- <td id="charmapView" rowspan="2" align="left" valign="top">
- <!-- Chars will be rendered here -->
- </td>
- <td width="100" align="center" valign="top">
- <table border="0" cellpadding="0" cellspacing="0" width="100" style="height:100px" role="presentation">
- <tr>
- <td id="codeV"> </td>
- </tr>
- <tr>
- <td id="codeN"> </td>
- </tr>
- </table>
- </td>
- </tr>
- <tr>
- <td valign="bottom" style="padding-bottom: 3px;">
- <table width="100" align="center" border="0" cellpadding="2" cellspacing="0" role="presentation">
- <tr>
- <td align="center" style="border-left: 1px solid #666699; border-top: 1px solid #666699; border-right: 1px solid #666699;"><label for="codeA">HTML-Code</label></td>
- </tr>
- <tr>
- <td style="font-size: 16px; font-weight: bold; border-left: 1px solid #666699; border-bottom: 1px solid #666699; border-right: 1px solid #666699;" id="codeA" align="center"> </td>
- </tr>
- <tr>
- <td style="font-size: 1px;"> </td>
- </tr>
- <tr>
- <td align="center" style="border-left: 1px solid #666699; border-top: 1px solid #666699; border-right: 1px solid #666699;"><label for="codeB">NUM-Code</label></td>
- </tr>
- <tr>
- <td style="font-size: 16px; font-weight: bold; border-left: 1px solid #666699; border-bottom: 1px solid #666699; border-right: 1px solid #666699;" id="codeB" align="center"> </td>
- </tr>
- </table>
- </td>
- </tr>
- <tr>
- <td colspan="2" id="charmap_usage">{#advanced_dlg.charmap_usage}</td>
- </tr>
-
-</table>
-</body>
-</html>
diff --git a/resource/tinymce/themes/advanced/color_picker.htm b/resource/tinymce/themes/advanced/color_picker.htm
@@ -1,71 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
-<head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <!-- Added by Dan S./Zotero -->
- <title>{#advanced_dlg.colorpicker_title}</title>
- <script type="text/javascript" src="../../tiny_mce_popup.js"></script>
- <script type="text/javascript" src="../../utils/mctabs.js"></script>
- <script type="text/javascript" src="js/color_picker.js"></script>
-</head>
-<body id="colorpicker" style="display: none" role="application" aria-labelledby="app_label">
- <span class="mceVoiceLabel" id="app_label" style="display:none;">{#advanced_dlg.colorpicker_title}</span>
-<form onsubmit="insertAction();return false" action="#">
- <div class="tabs">
- <ul>
- <li id="picker_tab" aria-controls="picker_panel" class="current"><span><a href="javascript:mcTabs.displayTab('picker_tab','picker_panel');" onmousedown="return false;">{#advanced_dlg.colorpicker_picker_tab}</a></span></li>
- <li id="rgb_tab" aria-controls="rgb_panel"><span><a href="javascript:;" onclick="mcTabs.displayTab('rgb_tab','rgb_panel');" onmousedown="return false;">{#advanced_dlg.colorpicker_palette_tab}</a></span></li>
- <li id="named_tab" aria-controls="named_panel"><span><a href="javascript:;" onclick="javascript:mcTabs.displayTab('named_tab','named_panel');" onmousedown="return false;">{#advanced_dlg.colorpicker_named_tab}</a></span></li>
- </ul>
- </div>
-
- <div class="panel_wrapper">
- <div id="picker_panel" class="panel current">
- <fieldset>
- <legend>{#advanced_dlg.colorpicker_picker_title}</legend>
- <div id="picker">
- <img id="colors" src="img/colorpicker.jpg" onclick="computeColor(event)" onmousedown="isMouseDown = true;return false;" onmouseup="isMouseDown = false;" onmousemove="if (isMouseDown && isMouseOver) computeColor(event); return false;" onmouseover="isMouseOver=true;" onmouseout="isMouseOver=false;" alt="" />
-
- <div id="light">
- <!-- Will be filled with divs -->
- </div>
-
- <br style="clear: both" />
- </div>
- </fieldset>
- </div>
-
- <div id="rgb_panel" class="panel">
- <fieldset>
- <legend id="webcolors_title">{#advanced_dlg.colorpicker_palette_title}</legend>
- <div id="webcolors">
- <!-- Gets filled with web safe colors-->
- </div>
-
- <br style="clear: both" />
- </fieldset>
- </div>
-
- <div id="named_panel" class="panel">
- <fieldset id="named_picker_label">
- <legend id="named_title">{#advanced_dlg.colorpicker_named_title}</legend>
- <div id="namedcolors" role="listbox" tabindex="0" aria-labelledby="named_picker_label">
- <!-- Gets filled with named colors-->
- </div>
-
- <br style="clear: both" />
-
- <div id="colornamecontainer">
- {#advanced_dlg.colorpicker_name} <span id="colorname"></span>
- </div>
- </fieldset>
- </div>
- </div>
-
- <div class="mceActionPanel">
- <input type="submit" id="insert" name="insert" value="{#apply}" />
- <input type="button" id="cancel" name="cancel" value="{#cancel}" onclick="tinyMCEPopup.close();"/>
- <div id="preview_wrapper"><div id="previewblock"><label for="color">{#advanced_dlg.colorpicker_color}</label> <input id="color" type="text" size="8" class="text mceFocus" aria-required="true" /></div><span id="preview"></span></div>
- </div>
-</form>
-</body>
-</html>
diff --git a/resource/tinymce/themes/advanced/editor_template.js b/resource/tinymce/themes/advanced/editor_template.js
@@ -1,1490 +0,0 @@
-/**
- * editor_template_src.js
- *
- * Copyright 2009, Moxiecode Systems AB
- * Released under LGPL License.
- *
- * License: http://tinymce.moxiecode.com/license
- * Contributing: http://tinymce.moxiecode.com/contributing
- */
-
-(function(tinymce) {
- var DOM = tinymce.DOM, Event = tinymce.dom.Event, extend = tinymce.extend, each = tinymce.each, Cookie = tinymce.util.Cookie, lastExtID, explode = tinymce.explode;
-
- // Generates a preview for a format
- function getPreviewCss(ed, fmt) {
- var name, previewElm, dom = ed.dom, previewCss = '', parentFontSize, previewStylesName;
-
- previewStyles = ed.settings.preview_styles;
-
- // No preview forced
- if (previewStyles === false)
- return '';
-
- // Default preview
- if (!previewStyles)
- previewStyles = 'font-family font-size font-weight text-decoration text-transform color background-color';
-
- // Removes any variables since these can't be previewed
- function removeVars(val) {
- return val.replace(/%(\w+)/g, '');
- };
-
- // Create block/inline element to use for preview
- name = fmt.block || fmt.inline || 'span';
- previewElm = dom.create(name);
-
- // Add format styles to preview element
- each(fmt.styles, function(value, name) {
- value = removeVars(value);
-
- if (value)
- dom.setStyle(previewElm, name, value);
- });
-
- // Add attributes to preview element
- each(fmt.attributes, function(value, name) {
- value = removeVars(value);
-
- if (value)
- dom.setAttrib(previewElm, name, value);
- });
-
- // Add classes to preview element
- each(fmt.classes, function(value) {
- value = removeVars(value);
-
- if (!dom.hasClass(previewElm, value))
- dom.addClass(previewElm, value);
- });
-
- // Add the previewElm outside the visual area
- dom.setStyles(previewElm, {position: 'absolute', left: -0xFFFF});
- ed.getBody().appendChild(previewElm);
-
- // Get parent container font size so we can compute px values out of em/% for older IE:s
- parentFontSize = dom.getStyle(ed.getBody(), 'fontSize', true);
- parentFontSize = /px$/.test(parentFontSize) ? parseInt(parentFontSize, 10) : 0;
-
- each(previewStyles.split(' '), function(name) {
- var value = dom.getStyle(previewElm, name, true);
-
- // If background is transparent then check if the body has a background color we can use
- if (name == 'background-color' && /transparent|rgba\s*\([^)]+,\s*0\)/.test(value)) {
- value = dom.getStyle(ed.getBody(), name, true);
-
- // Ignore white since it's the default color, not the nicest fix
- if (dom.toHex(value).toLowerCase() == '#ffffff') {
- return;
- }
- }
-
- // Old IE won't calculate the font size so we need to do that manually
- if (name == 'font-size') {
- if (/em|%$/.test(value)) {
- if (parentFontSize === 0) {
- return;
- }
-
- // Convert font size from em/% to px
- value = parseFloat(value, 10) / (/%$/.test(value) ? 100 : 1);
- value = (value * parentFontSize) + 'px';
- }
- }
-
- previewCss += name + ':' + value + ';';
- });
-
- dom.remove(previewElm);
-
- return previewCss;
- };
-
- // Tell it to load theme specific language pack(s)
- tinymce.ThemeManager.requireLangPack('advanced');
-
- tinymce.create('tinymce.themes.AdvancedTheme', {
- sizes : [8, 10, 12, 14, 18, 24, 36],
-
- // Control name lookup, format: title, command
- controls : {
- bold : ['bold_desc', 'Bold'],
- italic : ['italic_desc', 'Italic'],
- underline : ['underline_desc', 'Underline'],
- strikethrough : ['striketrough_desc', 'Strikethrough'],
- justifyleft : ['justifyleft_desc', 'JustifyLeft'],
- justifycenter : ['justifycenter_desc', 'JustifyCenter'],
- justifyright : ['justifyright_desc', 'JustifyRight'],
- justifyfull : ['justifyfull_desc', 'JustifyFull'],
- bullist : ['bullist_desc', 'InsertUnorderedList'],
- numlist : ['numlist_desc', 'InsertOrderedList'],
- outdent : ['outdent_desc', 'Outdent'],
- indent : ['indent_desc', 'Indent'],
- cut : ['cut_desc', 'Cut'],
- copy : ['copy_desc', 'Copy'],
- paste : ['paste_desc', 'Paste'],
- undo : ['undo_desc', 'Undo'],
- redo : ['redo_desc', 'Redo'],
- link : ['link_desc', 'mceLink'],
- unlink : ['unlink_desc', 'unlink'],
- image : ['image_desc', 'mceImage'],
- cleanup : ['cleanup_desc', 'mceCleanup'],
- help : ['help_desc', 'mceHelp'],
- code : ['code_desc', 'mceCodeEditor'],
- hr : ['hr_desc', 'InsertHorizontalRule'],
- removeformat : ['removeformat_desc', 'RemoveFormat'],
- sub : ['sub_desc', 'subscript'],
- sup : ['sup_desc', 'superscript'],
- forecolor : ['forecolor_desc', 'ForeColor'],
- forecolorpicker : ['forecolor_desc', 'mceForeColor'],
- backcolor : ['backcolor_desc', 'HiliteColor'],
- backcolorpicker : ['backcolor_desc', 'mceBackColor'],
- charmap : ['charmap_desc', 'mceCharMap'],
- visualaid : ['visualaid_desc', 'mceToggleVisualAid'],
- anchor : ['anchor_desc', 'mceInsertAnchor'],
- newdocument : ['newdocument_desc', 'mceNewDocument'],
- blockquote : ['blockquote_desc', 'mceBlockQuote']
- },
-
- stateControls : ['bold', 'italic', 'underline', 'strikethrough', 'bullist', 'numlist', 'justifyleft', 'justifycenter', 'justifyright', 'justifyfull', 'sub', 'sup', 'blockquote'],
-
- init : function(ed, url) {
- var t = this, s, v, o;
-
- t.editor = ed;
- t.url = url;
- t.onResolveName = new tinymce.util.Dispatcher(this);
- s = ed.settings;
-
- ed.forcedHighContrastMode = ed.settings.detect_highcontrast && t._isHighContrast();
- ed.settings.skin = ed.forcedHighContrastMode ? 'highcontrast' : ed.settings.skin;
-
- // Setup default buttons
- if (!s.theme_advanced_buttons1) {
- s = extend({
- theme_advanced_buttons1 : "bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,styleselect,formatselect",
- theme_advanced_buttons2 : "bullist,numlist,|,outdent,indent,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code",
- theme_advanced_buttons3 : "hr,removeformat,visualaid,|,sub,sup,|,charmap"
- }, s);
- }
-
- // Default settings
- t.settings = s = extend({
- theme_advanced_path : true,
- theme_advanced_toolbar_location : 'top',
- theme_advanced_blockformats : "p,address,pre,h1,h2,h3,h4,h5,h6",
- theme_advanced_toolbar_align : "left",
- theme_advanced_statusbar_location : "bottom",
- theme_advanced_fonts : "Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings,zapf dingbats",
- theme_advanced_more_colors : 1,
- theme_advanced_row_height : 23,
- theme_advanced_resize_horizontal : 1,
- theme_advanced_resizing_use_cookie : 1,
- theme_advanced_font_sizes : "1,2,3,4,5,6,7",
- theme_advanced_font_selector : "span",
- theme_advanced_show_current_color: 0,
- readonly : ed.settings.readonly
- }, s);
-
- // Setup default font_size_style_values
- if (!s.font_size_style_values)
- s.font_size_style_values = "8pt,10pt,12pt,14pt,18pt,24pt,36pt";
-
- if (tinymce.is(s.theme_advanced_font_sizes, 'string')) {
- s.font_size_style_values = tinymce.explode(s.font_size_style_values);
- s.font_size_classes = tinymce.explode(s.font_size_classes || '');
-
- // Parse string value
- o = {};
- ed.settings.theme_advanced_font_sizes = s.theme_advanced_font_sizes;
- each(ed.getParam('theme_advanced_font_sizes', '', 'hash'), function(v, k) {
- var cl;
-
- if (k == v && v >= 1 && v <= 7) {
- k = v + ' (' + t.sizes[v - 1] + 'pt)';
- cl = s.font_size_classes[v - 1];
- v = s.font_size_style_values[v - 1] || (t.sizes[v - 1] + 'pt');
- }
-
- if (/^\s*\./.test(v))
- cl = v.replace(/\./g, '');
-
- o[k] = cl ? {'class' : cl} : {fontSize : v};
- });
-
- s.theme_advanced_font_sizes = o;
- }
-
- if ((v = s.theme_advanced_path_location) && v != 'none')
- s.theme_advanced_statusbar_location = s.theme_advanced_path_location;
-
- if (s.theme_advanced_statusbar_location == 'none')
- s.theme_advanced_statusbar_location = 0;
-
- if (ed.settings.content_css !== false)
- ed.contentCSS.push(ed.baseURI.toAbsolute(url + "/skins/" + ed.settings.skin + "/content.css"));
-
- // Init editor
- ed.onInit.add(function() {
- if (!ed.settings.readonly) {
- ed.onNodeChange.add(t._nodeChanged, t);
- ed.onKeyUp.add(t._updateUndoStatus, t);
- ed.onMouseUp.add(t._updateUndoStatus, t);
- ed.dom.bind(ed.dom.getRoot(), 'dragend', function() {
- t._updateUndoStatus(ed);
- });
- }
- });
-
- ed.onSetProgressState.add(function(ed, b, ti) {
- var co, id = ed.id, tb;
-
- if (b) {
- t.progressTimer = setTimeout(function() {
- co = ed.getContainer();
- co = co.insertBefore(DOM.create('DIV', {style : 'position:relative'}), co.firstChild);
- tb = DOM.get(ed.id + '_tbl');
-
- DOM.add(co, 'div', {id : id + '_blocker', 'class' : 'mceBlocker', style : {width : tb.clientWidth + 2, height : tb.clientHeight + 2}});
- DOM.add(co, 'div', {id : id + '_progress', 'class' : 'mceProgress', style : {left : tb.clientWidth / 2, top : tb.clientHeight / 2}});
- }, ti || 0);
- } else {
- DOM.remove(id + '_blocker');
- DOM.remove(id + '_progress');
- clearTimeout(t.progressTimer);
- }
- });
-
- DOM.loadCSS(s.editor_css ? ed.documentBaseURI.toAbsolute(s.editor_css) : url + "/skins/" + ed.settings.skin + "/ui.css");
-
- if (s.skin_variant)
- DOM.loadCSS(url + "/skins/" + ed.settings.skin + "/ui_" + s.skin_variant + ".css");
- },
-
- _isHighContrast : function() {
- var actualColor, div = DOM.add(DOM.getRoot(), 'div', {'style': 'background-color: rgb(171,239,86);'});
-
- actualColor = (DOM.getStyle(div, 'background-color', true) + '').toLowerCase().replace(/ /g, '');
- DOM.remove(div);
-
- return actualColor != 'rgb(171,239,86)' && actualColor != '#abef56';
- },
-
- createControl : function(n, cf) {
- var cd, c;
-
- if (c = cf.createControl(n))
- return c;
-
- switch (n) {
- case "styleselect":
- return this._createStyleSelect();
-
- case "formatselect":
- return this._createBlockFormats();
-
- case "fontselect":
- return this._createFontSelect();
-
- case "fontsizeselect":
- return this._createFontSizeSelect();
-
- case "forecolor":
- return this._createForeColorMenu();
-
- case "backcolor":
- return this._createBackColorMenu();
- }
-
- if ((cd = this.controls[n]))
- return cf.createButton(n, {title : "advanced." + cd[0], cmd : cd[1], ui : cd[2], value : cd[3]});
- },
-
- execCommand : function(cmd, ui, val) {
- var f = this['_' + cmd];
-
- if (f) {
- f.call(this, ui, val);
- return true;
- }
-
- return false;
- },
-
- _importClasses : function(e) {
- var ed = this.editor, ctrl = ed.controlManager.get('styleselect');
-
- if (ctrl.getLength() == 0) {
- each(ed.dom.getClasses(), function(o, idx) {
- var name = 'style_' + idx, fmt;
-
- fmt = {
- inline : 'span',
- attributes : {'class' : o['class']},
- selector : '*'
- };
-
- ed.formatter.register(name, fmt);
-
- ctrl.add(o['class'], name, {
- style: function() {
- return getPreviewCss(ed, fmt);
- }
- });
- });
- }
- },
-
- _createStyleSelect : function(n) {
- var t = this, ed = t.editor, ctrlMan = ed.controlManager, ctrl;
-
- // Setup style select box
- ctrl = ctrlMan.createListBox('styleselect', {
- title : 'advanced.style_select',
- onselect : function(name) {
- var matches, formatNames = [], removedFormat;
-
- each(ctrl.items, function(item) {
- formatNames.push(item.value);
- });
-
- ed.focus();
- ed.undoManager.add();
-
- // Toggle off the current format(s)
- matches = ed.formatter.matchAll(formatNames);
- tinymce.each(matches, function(match) {
- if (!name || match == name) {
- if (match)
- ed.formatter.remove(match);
-
- removedFormat = true;
- }
- });
-
- if (!removedFormat)
- ed.formatter.apply(name);
-
- ed.undoManager.add();
- ed.nodeChanged();
-
- return false; // No auto select
- }
- });
-
- // Handle specified format
- ed.onPreInit.add(function() {
- var counter = 0, formats = ed.getParam('style_formats');
-
- if (formats) {
- each(formats, function(fmt) {
- var name, keys = 0;
-
- each(fmt, function() {keys++;});
-
- if (keys > 1) {
- name = fmt.name = fmt.name || 'style_' + (counter++);
- ed.formatter.register(name, fmt);
- ctrl.add(fmt.title, name, {
- style: function() {
- return getPreviewCss(ed, fmt);
- }
- });
- } else
- ctrl.add(fmt.title);
- });
- } else {
- each(ed.getParam('theme_advanced_styles', '', 'hash'), function(val, key) {
- var name, fmt;
-
- if (val) {
- name = 'style_' + (counter++);
- fmt = {
- inline : 'span',
- classes : val,
- selector : '*'
- };
-
- ed.formatter.register(name, fmt);
- ctrl.add(t.editor.translate(key), name, {
- style: function() {
- return getPreviewCss(ed, fmt);
- }
- });
- }
- });
- }
- });
-
- // Auto import classes if the ctrl box is empty
- if (ctrl.getLength() == 0) {
- ctrl.onPostRender.add(function(ed, n) {
- if (!ctrl.NativeListBox) {
- Event.add(n.id + '_text', 'focus', t._importClasses, t);
- Event.add(n.id + '_text', 'mousedown', t._importClasses, t);
- Event.add(n.id + '_open', 'focus', t._importClasses, t);
- Event.add(n.id + '_open', 'mousedown', t._importClasses, t);
- } else
- Event.add(n.id, 'focus', t._importClasses, t);
- });
- }
-
- return ctrl;
- },
-
- _createFontSelect : function() {
- var c, t = this, ed = t.editor;
-
- c = ed.controlManager.createListBox('fontselect', {
- title : 'advanced.fontdefault',
- onselect : function(v) {
- var cur = c.items[c.selectedIndex];
-
- if (!v && cur) {
- ed.execCommand('FontName', false, cur.value);
- return;
- }
-
- ed.execCommand('FontName', false, v);
-
- // Fake selection, execCommand will fire a nodeChange and update the selection
- c.select(function(sv) {
- return v == sv;
- });
-
- if (cur && cur.value == v) {
- c.select(null);
- }
-
- return false; // No auto select
- }
- });
-
- if (c) {
- each(ed.getParam('theme_advanced_fonts', t.settings.theme_advanced_fonts, 'hash'), function(v, k) {
- c.add(ed.translate(k), v, {style : v.indexOf('dings') == -1 ? 'font-family:' + v : ''});
- });
- }
-
- return c;
- },
-
- _createFontSizeSelect : function() {
- var t = this, ed = t.editor, c, i = 0, cl = [];
-
- c = ed.controlManager.createListBox('fontsizeselect', {title : 'advanced.font_size', onselect : function(v) {
- var cur = c.items[c.selectedIndex];
-
- if (!v && cur) {
- cur = cur.value;
-
- if (cur['class']) {
- ed.formatter.toggle('fontsize_class', {value : cur['class']});
- ed.undoManager.add();
- ed.nodeChanged();
- } else {
- ed.execCommand('FontSize', false, cur.fontSize);
- }
-
- return;
- }
-
- if (v['class']) {
- ed.focus();
- ed.undoManager.add();
- ed.formatter.toggle('fontsize_class', {value : v['class']});
- ed.undoManager.add();
- ed.nodeChanged();
- } else
- ed.execCommand('FontSize', false, v.fontSize);
-
- // Fake selection, execCommand will fire a nodeChange and update the selection
- c.select(function(sv) {
- return v == sv;
- });
-
- if (cur && (cur.value.fontSize == v.fontSize || cur.value['class'] && cur.value['class'] == v['class'])) {
- c.select(null);
- }
-
- return false; // No auto select
- }});
-
- if (c) {
- each(t.settings.theme_advanced_font_sizes, function(v, k) {
- var fz = v.fontSize;
-
- if (fz >= 1 && fz <= 7)
- fz = t.sizes[parseInt(fz) - 1] + 'pt';
-
- c.add(k, v, {'style' : 'font-size:' + fz, 'class' : 'mceFontSize' + (i++) + (' ' + (v['class'] || ''))});
- });
- }
-
- return c;
- },
-
- _createBlockFormats : function() {
- var c, fmts = {
- p : 'advanced.paragraph',
- address : 'advanced.address',
- pre : 'advanced.pre',
- h1 : 'advanced.h1',
- h2 : 'advanced.h2',
- h3 : 'advanced.h3',
- h4 : 'advanced.h4',
- h5 : 'advanced.h5',
- h6 : 'advanced.h6',
- div : 'advanced.div',
- blockquote : 'advanced.blockquote',
- code : 'advanced.code',
- dt : 'advanced.dt',
- dd : 'advanced.dd',
- samp : 'advanced.samp'
- }, t = this;
-
- c = t.editor.controlManager.createListBox('formatselect', {title : 'advanced.block', onselect : function(v) {
- t.editor.execCommand('FormatBlock', false, v);
- return false;
- }});
-
- if (c) {
- each(t.editor.getParam('theme_advanced_blockformats', t.settings.theme_advanced_blockformats, 'hash'), function(v, k) {
- c.add(t.editor.translate(k != v ? k : fmts[v]), v, {'class' : 'mce_formatPreview mce_' + v, style: function() {
- return getPreviewCss(t.editor, {block: v});
- }});
- });
- }
-
- return c;
- },
-
- _createForeColorMenu : function() {
- var c, t = this, s = t.settings, o = {}, v;
-
- if (s.theme_advanced_more_colors) {
- o.more_colors_func = function() {
- t._mceColorPicker(0, {
- color : c.value,
- func : function(co) {
- c.setColor(co);
- }
- });
- };
- }
-
- if (v = s.theme_advanced_text_colors)
- o.colors = v;
-
- if (s.theme_advanced_default_foreground_color)
- o.default_color = s.theme_advanced_default_foreground_color;
-
- o.title = 'advanced.forecolor_desc';
- o.cmd = 'ForeColor';
- o.scope = this;
-
- c = t.editor.controlManager.createColorSplitButton('forecolor', o);
-
- return c;
- },
-
- _createBackColorMenu : function() {
- var c, t = this, s = t.settings, o = {}, v;
-
- if (s.theme_advanced_more_colors) {
- o.more_colors_func = function() {
- t._mceColorPicker(0, {
- color : c.value,
- func : function(co) {
- c.setColor(co);
- }
- });
- };
- }
-
- if (v = s.theme_advanced_background_colors)
- o.colors = v;
-
- if (s.theme_advanced_default_background_color)
- o.default_color = s.theme_advanced_default_background_color;
-
- o.title = 'advanced.backcolor_desc';
- o.cmd = 'HiliteColor';
- o.scope = this;
-
- c = t.editor.controlManager.createColorSplitButton('backcolor', o);
-
- return c;
- },
-
- renderUI : function(o) {
- var n, ic, tb, t = this, ed = t.editor, s = t.settings, sc, p, nl;
-
- if (ed.settings) {
- ed.settings.aria_label = s.aria_label + ed.getLang('advanced.help_shortcut');
- }
-
- // TODO: ACC Should have an aria-describedby attribute which is user-configurable to describe what this field is actually for.
- // Maybe actually inherit it from the original textara?
- n = p = DOM.create('span', {role : 'application', 'aria-labelledby' : ed.id + '_voice', id : ed.id + '_parent', 'class' : 'mceEditor ' + ed.settings.skin + 'Skin' + (s.skin_variant ? ' ' + ed.settings.skin + 'Skin' + t._ufirst(s.skin_variant) : '') + (ed.settings.directionality == "rtl" ? ' mceRtl' : '')});
- DOM.add(n, 'span', {'class': 'mceVoiceLabel', 'style': 'display:none;', id: ed.id + '_voice'}, s.aria_label);
-
- if (!DOM.boxModel)
- n = DOM.add(n, 'div', {'class' : 'mceOldBoxModel'});
-
- n = sc = DOM.add(n, 'table', {role : "presentation", id : ed.id + '_tbl', 'class' : 'mceLayout', cellSpacing : 0, cellPadding : 0});
- n = tb = DOM.add(n, 'tbody');
-
- switch ((s.theme_advanced_layout_manager || '').toLowerCase()) {
- case "rowlayout":
- ic = t._rowLayout(s, tb, o);
- break;
-
- case "customlayout":
- ic = ed.execCallback("theme_advanced_custom_layout", s, tb, o, p);
- break;
-
- default:
- ic = t._simpleLayout(s, tb, o, p);
- }
-
- n = o.targetNode;
-
- // Add classes to first and last TRs
- nl = sc.rows;
- DOM.addClass(nl[0], 'mceFirst');
- DOM.addClass(nl[nl.length - 1], 'mceLast');
-
- // Add classes to first and last TDs
- each(DOM.select('tr', tb), function(n) {
- DOM.addClass(n.firstChild, 'mceFirst');
- DOM.addClass(n.childNodes[n.childNodes.length - 1], 'mceLast');
- });
-
- if (DOM.get(s.theme_advanced_toolbar_container))
- DOM.get(s.theme_advanced_toolbar_container).appendChild(p);
- else
- DOM.insertAfter(p, n);
-
- Event.add(ed.id + '_path_row', 'click', function(e) {
- e = e.target;
-
- if (e.nodeName == 'A') {
- t._sel(e.className.replace(/^.*mcePath_([0-9]+).*$/, '$1'));
- return false;
- }
- });
-/*
- if (DOM.get(ed.id + '_path_row')) {
- Event.add(ed.id + '_tbl', 'mouseover', function(e) {
- var re;
-
- e = e.target;
-
- if (e.nodeName == 'SPAN' && DOM.hasClass(e.parentNode, 'mceButton')) {
- re = DOM.get(ed.id + '_path_row');
- t.lastPath = re.innerHTML;
- DOM.setHTML(re, e.parentNode.title);
- }
- });
-
- Event.add(ed.id + '_tbl', 'mouseout', function(e) {
- if (t.lastPath) {
- DOM.setHTML(ed.id + '_path_row', t.lastPath);
- t.lastPath = 0;
- }
- });
- }
-*/
-
- if (!ed.getParam('accessibility_focus'))
- Event.add(DOM.add(p, 'a', {href : '#'}, '<!-- IE -->'), 'focus', function() {tinyMCE.get(ed.id).focus();});
-
- if (s.theme_advanced_toolbar_location == 'external')
- o.deltaHeight = 0;
-
- t.deltaHeight = o.deltaHeight;
- o.targetNode = null;
-
- ed.onKeyDown.add(function(ed, evt) {
- var DOM_VK_F10 = 121, DOM_VK_F11 = 122;
-
- if (evt.altKey) {
- if (evt.keyCode === DOM_VK_F10) {
- // Make sure focus is given to toolbar in Safari.
- // We can't do this in IE as it prevents giving focus to toolbar when editor is in a frame
- if (tinymce.isWebKit) {
- window.focus();
- }
- t.toolbarGroup.focus();
- return Event.cancel(evt);
- } else if (evt.keyCode === DOM_VK_F11) {
- DOM.get(ed.id + '_path_row').focus();
- return Event.cancel(evt);
- }
- }
- });
-
- // alt+0 is the UK recommended shortcut for accessing the list of access controls.
- ed.addShortcut('alt+0', '', 'mceShortcuts', t);
-
- return {
- iframeContainer : ic,
- editorContainer : ed.id + '_parent',
- sizeContainer : sc,
- deltaHeight : o.deltaHeight
- };
- },
-
- getInfo : function() {
- return {
- longname : 'Advanced theme',
- author : 'Moxiecode Systems AB',
- authorurl : 'http://tinymce.moxiecode.com',
- version : tinymce.majorVersion + "." + tinymce.minorVersion
- }
- },
-
- resizeBy : function(dw, dh) {
- var e = DOM.get(this.editor.id + '_ifr');
-
- this.resizeTo(e.clientWidth + dw, e.clientHeight + dh);
- },
-
- resizeTo : function(w, h, store) {
- var ed = this.editor, s = this.settings, e = DOM.get(ed.id + '_tbl'), ifr = DOM.get(ed.id + '_ifr');
-
- // Boundery fix box
- w = Math.max(s.theme_advanced_resizing_min_width || 100, w);
- h = Math.max(s.theme_advanced_resizing_min_height || 100, h);
- w = Math.min(s.theme_advanced_resizing_max_width || 0xFFFF, w);
- h = Math.min(s.theme_advanced_resizing_max_height || 0xFFFF, h);
-
- // Resize iframe and container
- DOM.setStyle(e, 'height', '');
- DOM.setStyle(ifr, 'height', h);
-
- if (s.theme_advanced_resize_horizontal) {
- DOM.setStyle(e, 'width', '');
- DOM.setStyle(ifr, 'width', w);
-
- // Make sure that the size is never smaller than the over all ui
- if (w < e.clientWidth) {
- w = e.clientWidth;
- DOM.setStyle(ifr, 'width', e.clientWidth);
- }
- }
-
- // Store away the size
- if (store && s.theme_advanced_resizing_use_cookie) {
- Cookie.setHash("TinyMCE_" + ed.id + "_size", {
- cw : w,
- ch : h
- });
- }
- },
-
- destroy : function() {
- var id = this.editor.id;
-
- Event.clear(id + '_resize');
- Event.clear(id + '_path_row');
- Event.clear(id + '_external_close');
- },
-
- // Internal functions
-
- _simpleLayout : function(s, tb, o, p) {
- var t = this, ed = t.editor, lo = s.theme_advanced_toolbar_location, sl = s.theme_advanced_statusbar_location, n, ic, etb, c;
-
- if (s.readonly) {
- n = DOM.add(tb, 'tr');
- n = ic = DOM.add(n, 'td', {'class' : 'mceIframeContainer'});
- return ic;
- }
-
- // Create toolbar container at top
- if (lo == 'top')
- t._addToolbars(tb, o);
-
- // Create external toolbar
- if (lo == 'external') {
- n = c = DOM.create('div', {style : 'position:relative'});
- n = DOM.add(n, 'div', {id : ed.id + '_external', 'class' : 'mceExternalToolbar'});
- DOM.add(n, 'a', {id : ed.id + '_external_close', href : 'javascript:;', 'class' : 'mceExternalClose'});
- n = DOM.add(n, 'table', {id : ed.id + '_tblext', cellSpacing : 0, cellPadding : 0});
- etb = DOM.add(n, 'tbody');
-
- if (p.firstChild.className == 'mceOldBoxModel')
- p.firstChild.appendChild(c);
- else
- p.insertBefore(c, p.firstChild);
-
- t._addToolbars(etb, o);
-
- ed.onMouseUp.add(function() {
- var e = DOM.get(ed.id + '_external');
- DOM.show(e);
-
- DOM.hide(lastExtID);
-
- var f = Event.add(ed.id + '_external_close', 'click', function() {
- DOM.hide(ed.id + '_external');
- Event.remove(ed.id + '_external_close', 'click', f);
- return false;
- });
-
- DOM.show(e);
- DOM.setStyle(e, 'top', 0 - DOM.getRect(ed.id + '_tblext').h - 1);
-
- // Fixes IE rendering bug
- DOM.hide(e);
- DOM.show(e);
- e.style.filter = '';
-
- lastExtID = ed.id + '_external';
-
- e = null;
- });
- }
-
- if (sl == 'top')
- t._addStatusBar(tb, o);
-
- // Create iframe container
- if (!s.theme_advanced_toolbar_container) {
- n = DOM.add(tb, 'tr');
- n = ic = DOM.add(n, 'td', {'class' : 'mceIframeContainer'});
- }
-
- // Create toolbar container at bottom
- if (lo == 'bottom')
- t._addToolbars(tb, o);
-
- if (sl == 'bottom')
- t._addStatusBar(tb, o);
-
- return ic;
- },
-
- _rowLayout : function(s, tb, o) {
- var t = this, ed = t.editor, dc, da, cf = ed.controlManager, n, ic, to, a;
-
- dc = s.theme_advanced_containers_default_class || '';
- da = s.theme_advanced_containers_default_align || 'center';
-
- each(explode(s.theme_advanced_containers || ''), function(c, i) {
- var v = s['theme_advanced_container_' + c] || '';
-
- switch (c.toLowerCase()) {
- case 'mceeditor':
- n = DOM.add(tb, 'tr');
- n = ic = DOM.add(n, 'td', {'class' : 'mceIframeContainer'});
- break;
-
- case 'mceelementpath':
- t._addStatusBar(tb, o);
- break;
-
- default:
- a = (s['theme_advanced_container_' + c + '_align'] || da).toLowerCase();
- a = 'mce' + t._ufirst(a);
-
- n = DOM.add(DOM.add(tb, 'tr'), 'td', {
- 'class' : 'mceToolbar ' + (s['theme_advanced_container_' + c + '_class'] || dc) + ' ' + a || da
- });
-
- to = cf.createToolbar("toolbar" + i);
- t._addControls(v, to);
- DOM.setHTML(n, to.renderHTML());
- o.deltaHeight -= s.theme_advanced_row_height;
- }
- });
-
- return ic;
- },
-
- _addControls : function(v, tb) {
- var t = this, s = t.settings, di, cf = t.editor.controlManager;
-
- if (s.theme_advanced_disable && !t._disabled) {
- di = {};
-
- each(explode(s.theme_advanced_disable), function(v) {
- di[v] = 1;
- });
-
- t._disabled = di;
- } else
- di = t._disabled;
-
- each(explode(v), function(n) {
- var c;
-
- if (di && di[n])
- return;
-
- // Compatiblity with 2.x
- if (n == 'tablecontrols') {
- each(["table","|","row_props","cell_props","|","row_before","row_after","delete_row","|","col_before","col_after","delete_col","|","split_cells","merge_cells"], function(n) {
- n = t.createControl(n, cf);
-
- if (n)
- tb.add(n);
- });
-
- return;
- }
-
- c = t.createControl(n, cf);
-
- if (c)
- tb.add(c);
- });
- },
-
- _addToolbars : function(c, o) {
- var t = this, i, tb, ed = t.editor, s = t.settings, v, cf = ed.controlManager, di, n, h = [], a, toolbarGroup, toolbarsExist = false;
-
- toolbarGroup = cf.createToolbarGroup('toolbargroup', {
- 'name': ed.getLang('advanced.toolbar'),
- 'tab_focus_toolbar':ed.getParam('theme_advanced_tab_focus_toolbar')
- });
-
- t.toolbarGroup = toolbarGroup;
-
- a = s.theme_advanced_toolbar_align.toLowerCase();
- a = 'mce' + t._ufirst(a);
-
- n = DOM.add(DOM.add(c, 'tr', {role: 'toolbar'}), 'td', {'class' : 'mceToolbar ' + a, "role":"toolbar"});
-
- // Create toolbar and add the controls
- for (i=1; (v = s['theme_advanced_buttons' + i]); i++) {
- toolbarsExist = true;
- tb = cf.createToolbar("toolbar" + i, {'class' : 'mceToolbarRow' + i});
-
- if (s['theme_advanced_buttons' + i + '_add'])
- v += ',' + s['theme_advanced_buttons' + i + '_add'];
-
- if (s['theme_advanced_buttons' + i + '_add_before'])
- v = s['theme_advanced_buttons' + i + '_add_before'] + ',' + v;
-
- t._addControls(v, tb);
- toolbarGroup.add(tb);
-
- o.deltaHeight -= s.theme_advanced_row_height;
- }
- // Handle case when there are no toolbar buttons and ensure editor height is adjusted accordingly
- if (!toolbarsExist)
- o.deltaHeight -= s.theme_advanced_row_height;
- h.push(toolbarGroup.renderHTML());
- h.push(DOM.createHTML('a', {href : '#', accesskey : 'z', title : ed.getLang("advanced.toolbar_focus"), onfocus : 'tinyMCE.getInstanceById(\'' + ed.id + '\').focus();'}, '<!-- IE -->'));
- DOM.setHTML(n, h.join(''));
- },
-
- _addStatusBar : function(tb, o) {
- var n, t = this, ed = t.editor, s = t.settings, r, mf, me, td;
-
- n = DOM.add(tb, 'tr');
- n = td = DOM.add(n, 'td', {'class' : 'mceStatusbar'});
- n = DOM.add(n, 'div', {id : ed.id + '_path_row', 'role': 'group', 'aria-labelledby': ed.id + '_path_voice'});
- if (s.theme_advanced_path) {
- DOM.add(n, 'span', {id: ed.id + '_path_voice'}, ed.translate('advanced.path'));
- DOM.add(n, 'span', {}, ': ');
- } else {
- DOM.add(n, 'span', {}, ' ');
- }
-
-
- if (s.theme_advanced_resizing) {
- DOM.add(td, 'a', {id : ed.id + '_resize', href : 'javascript:;', onclick : "return false;", 'class' : 'mceResize', tabIndex:"-1"});
-
- if (s.theme_advanced_resizing_use_cookie) {
- ed.onPostRender.add(function() {
- var o = Cookie.getHash("TinyMCE_" + ed.id + "_size"), c = DOM.get(ed.id + '_tbl');
-
- if (!o)
- return;
-
- t.resizeTo(o.cw, o.ch);
- });
- }
-
- ed.onPostRender.add(function() {
- Event.add(ed.id + '_resize', 'click', function(e) {
- e.preventDefault();
- });
-
- Event.add(ed.id + '_resize', 'mousedown', function(e) {
- var mouseMoveHandler1, mouseMoveHandler2,
- mouseUpHandler1, mouseUpHandler2,
- startX, startY, startWidth, startHeight, width, height, ifrElm;
-
- function resizeOnMove(e) {
- e.preventDefault();
-
- width = startWidth + (e.screenX - startX);
- height = startHeight + (e.screenY - startY);
-
- t.resizeTo(width, height);
- };
-
- function endResize(e) {
- // Stop listening
- Event.remove(DOM.doc, 'mousemove', mouseMoveHandler1);
- Event.remove(ed.getDoc(), 'mousemove', mouseMoveHandler2);
- Event.remove(DOM.doc, 'mouseup', mouseUpHandler1);
- Event.remove(ed.getDoc(), 'mouseup', mouseUpHandler2);
-
- width = startWidth + (e.screenX - startX);
- height = startHeight + (e.screenY - startY);
- t.resizeTo(width, height, true);
-
- ed.nodeChanged();
- };
-
- e.preventDefault();
-
- // Get the current rect size
- startX = e.screenX;
- startY = e.screenY;
- ifrElm = DOM.get(t.editor.id + '_ifr');
- startWidth = width = ifrElm.clientWidth;
- startHeight = height = ifrElm.clientHeight;
-
- // Register envent handlers
- mouseMoveHandler1 = Event.add(DOM.doc, 'mousemove', resizeOnMove);
- mouseMoveHandler2 = Event.add(ed.getDoc(), 'mousemove', resizeOnMove);
- mouseUpHandler1 = Event.add(DOM.doc, 'mouseup', endResize);
- mouseUpHandler2 = Event.add(ed.getDoc(), 'mouseup', endResize);
- });
- });
- }
-
- o.deltaHeight -= 21;
- n = tb = null;
- },
-
- _updateUndoStatus : function(ed) {
- var cm = ed.controlManager, um = ed.undoManager;
-
- cm.setDisabled('undo', !um.hasUndo() && !um.typing);
- cm.setDisabled('redo', !um.hasRedo());
- },
-
- _nodeChanged : function(ed, cm, n, co, ob) {
- var t = this, p, de = 0, v, c, s = t.settings, cl, fz, fn, fc, bc, formatNames, matches;
-
- tinymce.each(t.stateControls, function(c) {
- cm.setActive(c, ed.queryCommandState(t.controls[c][1]));
- });
-
- function getParent(name) {
- var i, parents = ob.parents, func = name;
-
- if (typeof(name) == 'string') {
- func = function(node) {
- return node.nodeName == name;
- };
- }
-
- for (i = 0; i < parents.length; i++) {
- if (func(parents[i]))
- return parents[i];
- }
- };
-
- cm.setActive('visualaid', ed.hasVisual);
- t._updateUndoStatus(ed);
- cm.setDisabled('outdent', !ed.queryCommandState('Outdent'));
-
- p = getParent('A');
- if (c = cm.get('link')) {
- c.setDisabled((!p && co) || (p && !p.href));
- c.setActive(!!p && (!p.name && !p.id));
- }
-
- if (c = cm.get('unlink')) {
- c.setDisabled(!p && co);
- c.setActive(!!p && !p.name && !p.id);
- }
-
- if (c = cm.get('anchor')) {
- c.setActive(!co && !!p && (p.name || (p.id && !p.href)));
- }
-
- p = getParent('IMG');
- if (c = cm.get('image'))
- c.setActive(!co && !!p && n.className.indexOf('mceItem') == -1);
-
- if (c = cm.get('styleselect')) {
- t._importClasses();
-
- formatNames = [];
- each(c.items, function(item) {
- formatNames.push(item.value);
- });
-
- matches = ed.formatter.matchAll(formatNames);
- c.select(matches[0]);
- tinymce.each(matches, function(match, index) {
- if (index > 0) {
- c.mark(match);
- }
- });
- }
-
- if (c = cm.get('formatselect')) {
- p = getParent(ed.dom.isBlock);
-
- if (p)
- c.select(p.nodeName.toLowerCase());
- }
-
- // Find out current fontSize, fontFamily and fontClass
- getParent(function(n) {
- if (n.nodeName === 'SPAN') {
- if (!cl && n.className)
- cl = n.className;
- }
-
- if (ed.dom.is(n, s.theme_advanced_font_selector)) {
- if (!fz && n.style.fontSize)
- fz = n.style.fontSize;
-
- if (!fn && n.style.fontFamily)
- fn = n.style.fontFamily.replace(/[\"\']+/g, '').replace(/^([^,]+).*/, '$1').toLowerCase();
-
- if (!fc && n.style.color)
- fc = n.style.color;
-
- if (!bc && n.style.backgroundColor)
- bc = n.style.backgroundColor;
- }
-
- return false;
- });
-
- if (c = cm.get('fontselect')) {
- c.select(function(v) {
- return v.replace(/^([^,]+).*/, '$1').toLowerCase() == fn;
- });
- }
-
- // Select font size
- if (c = cm.get('fontsizeselect')) {
- // Use computed style
- if (s.theme_advanced_runtime_fontsize && !fz && !cl)
- fz = ed.dom.getStyle(n, 'fontSize', true);
-
- c.select(function(v) {
- if (v.fontSize && v.fontSize === fz)
- return true;
-
- if (v['class'] && v['class'] === cl)
- return true;
- });
- }
-
- if (s.theme_advanced_show_current_color) {
- function updateColor(controlId, color) {
- if (c = cm.get(controlId)) {
- if (!color)
- color = c.settings.default_color;
- if (color !== c.value) {
- c.displayColor(color);
- }
- }
- }
- updateColor('forecolor', fc);
- updateColor('backcolor', bc);
- }
-
- if (s.theme_advanced_show_current_color) {
- function updateColor(controlId, color) {
- if (c = cm.get(controlId)) {
- if (!color)
- color = c.settings.default_color;
- if (color !== c.value) {
- c.displayColor(color);
- }
- }
- };
-
- updateColor('forecolor', fc);
- updateColor('backcolor', bc);
- }
-
- if (s.theme_advanced_path && s.theme_advanced_statusbar_location) {
- p = DOM.get(ed.id + '_path') || DOM.add(ed.id + '_path_row', 'span', {id : ed.id + '_path'});
-
- if (t.statusKeyboardNavigation) {
- t.statusKeyboardNavigation.destroy();
- t.statusKeyboardNavigation = null;
- }
-
- DOM.setHTML(p, '');
-
- getParent(function(n) {
- var na = n.nodeName.toLowerCase(), u, pi, ti = '';
-
- // Ignore non element and bogus/hidden elements
- if (n.nodeType != 1 || na === 'br' || n.getAttribute('data-mce-bogus') || DOM.hasClass(n, 'mceItemHidden') || DOM.hasClass(n, 'mceItemRemoved'))
- return;
-
- // Handle prefix
- if (tinymce.isIE && n.scopeName !== 'HTML' && n.scopeName)
- na = n.scopeName + ':' + na;
-
- // Remove internal prefix
- na = na.replace(/mce\:/g, '');
-
- // Handle node name
- switch (na) {
- case 'b':
- na = 'strong';
- break;
-
- case 'i':
- na = 'em';
- break;
-
- case 'img':
- if (v = DOM.getAttrib(n, 'src'))
- ti += 'src: ' + v + ' ';
-
- break;
-
- case 'a':
- if (v = DOM.getAttrib(n, 'name')) {
- ti += 'name: ' + v + ' ';
- na += '#' + v;
- }
-
- if (v = DOM.getAttrib(n, 'href'))
- ti += 'href: ' + v + ' ';
-
- break;
-
- case 'font':
- if (v = DOM.getAttrib(n, 'face'))
- ti += 'font: ' + v + ' ';
-
- if (v = DOM.getAttrib(n, 'size'))
- ti += 'size: ' + v + ' ';
-
- if (v = DOM.getAttrib(n, 'color'))
- ti += 'color: ' + v + ' ';
-
- break;
-
- case 'span':
- if (v = DOM.getAttrib(n, 'style'))
- ti += 'style: ' + v + ' ';
-
- break;
- }
-
- if (v = DOM.getAttrib(n, 'id'))
- ti += 'id: ' + v + ' ';
-
- if (v = n.className) {
- v = v.replace(/\b\s*(webkit|mce|Apple-)\w+\s*\b/g, '');
-
- if (v) {
- ti += 'class: ' + v + ' ';
-
- if (ed.dom.isBlock(n) || na == 'img' || na == 'span')
- na += '.' + v;
- }
- }
-
- na = na.replace(/(html:)/g, '');
- na = {name : na, node : n, title : ti};
- t.onResolveName.dispatch(t, na);
- ti = na.title;
- na = na.name;
-
- //u = "javascript:tinymce.EditorManager.get('" + ed.id + "').theme._sel('" + (de++) + "');";
- pi = DOM.create('a', {'href' : "javascript:;", role: 'button', onmousedown : "return false;", title : ti, 'class' : 'mcePath_' + (de++)}, na);
-
- if (p.hasChildNodes()) {
- p.insertBefore(DOM.create('span', {'aria-hidden': 'true'}, '\u00a0\u00bb '), p.firstChild);
- p.insertBefore(pi, p.firstChild);
- } else
- p.appendChild(pi);
- }, ed.getBody());
-
- if (DOM.select('a', p).length > 0) {
- t.statusKeyboardNavigation = new tinymce.ui.KeyboardNavigation({
- root: ed.id + "_path_row",
- items: DOM.select('a', p),
- excludeFromTabOrder: true,
- onCancel: function() {
- ed.focus();
- }
- }, DOM);
- }
- }
- },
-
- // Commands gets called by execCommand
-
- _sel : function(v) {
- this.editor.execCommand('mceSelectNodeDepth', false, v);
- },
-
- _mceInsertAnchor : function(ui, v) {
- var ed = this.editor;
-
- ed.windowManager.open({
- url : this.url + '/anchor.htm',
- width : 320 + parseInt(ed.getLang('advanced.anchor_delta_width', 0)),
- height : 90 + parseInt(ed.getLang('advanced.anchor_delta_height', 0)),
- inline : true
- }, {
- theme_url : this.url
- });
- },
-
- _mceCharMap : function() {
- var ed = this.editor;
-
- ed.windowManager.open({
- url : this.url + '/charmap.htm',
- width : 550 + parseInt(ed.getLang('advanced.charmap_delta_width', 0)),
- height : 265 + parseInt(ed.getLang('advanced.charmap_delta_height', 0)),
- inline : true
- }, {
- theme_url : this.url
- });
- },
-
- _mceHelp : function() {
- var ed = this.editor;
-
- ed.windowManager.open({
- url : this.url + '/about.htm',
- width : 480,
- height : 380,
- inline : true
- }, {
- theme_url : this.url
- });
- },
-
- _mceShortcuts : function() {
- var ed = this.editor;
- ed.windowManager.open({
- url: this.url + '/shortcuts.htm',
- width: 480,
- height: 380,
- inline: true
- }, {
- theme_url: this.url
- });
- },
-
- _mceColorPicker : function(u, v) {
- var ed = this.editor;
-
- v = v || {};
-
- ed.windowManager.open({
- url : this.url + '/color_picker.htm',
- width : 375 + parseInt(ed.getLang('advanced.colorpicker_delta_width', 0)),
- height : 250 + parseInt(ed.getLang('advanced.colorpicker_delta_height', 0)),
- close_previous : false,
- inline : true
- }, {
- input_color : v.color,
- func : v.func,
- theme_url : this.url
- });
- },
-
- _mceCodeEditor : function(ui, val) {
- var ed = this.editor;
-
- ed.windowManager.open({
- url : this.url + '/source_editor.htm',
- width : parseInt(ed.getParam("theme_advanced_source_editor_width", 720)),
- height : parseInt(ed.getParam("theme_advanced_source_editor_height", 580)),
- inline : true,
- resizable : true,
- maximizable : true
- }, {
- theme_url : this.url
- });
- },
-
- _mceImage : function(ui, val) {
- var ed = this.editor;
-
- // Internal image object like a flash placeholder
- if (ed.dom.getAttrib(ed.selection.getNode(), 'class', '').indexOf('mceItem') != -1)
- return;
-
- ed.windowManager.open({
- url : this.url + '/image.htm',
- width : 355 + parseInt(ed.getLang('advanced.image_delta_width', 0)),
- height : 275 + parseInt(ed.getLang('advanced.image_delta_height', 0)),
- inline : true
- }, {
- theme_url : this.url
- });
- },
-
- _mceLink : function(ui, val) {
- var ed = this.editor;
-
- ed.windowManager.open({
- url : this.url + '/link.htm',
- width : 310 + parseInt(ed.getLang('advanced.link_delta_width', 0)),
- height : 200 + parseInt(ed.getLang('advanced.link_delta_height', 0)),
- inline : true
- }, {
- theme_url : this.url
- });
- },
-
- _mceNewDocument : function() {
- var ed = this.editor;
-
- ed.windowManager.confirm('advanced.newdocument', function(s) {
- if (s)
- ed.execCommand('mceSetContent', false, '');
- });
- },
-
- _mceForeColor : function() {
- var t = this;
-
- this._mceColorPicker(0, {
- color: t.fgColor,
- func : function(co) {
- t.fgColor = co;
- t.editor.execCommand('ForeColor', false, co);
- }
- });
- },
-
- _mceBackColor : function() {
- var t = this;
-
- this._mceColorPicker(0, {
- color: t.bgColor,
- func : function(co) {
- t.bgColor = co;
- t.editor.execCommand('HiliteColor', false, co);
- }
- });
- },
-
- _ufirst : function(s) {
- return s.substring(0, 1).toUpperCase() + s.substring(1);
- }
- });
-
- tinymce.ThemeManager.add('advanced', tinymce.themes.AdvancedTheme);
-}(tinymce));
diff --git a/resource/tinymce/themes/advanced/image.htm b/resource/tinymce/themes/advanced/image.htm
@@ -1,80 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
-<head>
- <title>{#advanced_dlg.image_title}</title>
- <script type="text/javascript" src="../../tiny_mce_popup.js"></script>
- <script type="text/javascript" src="../../utils/mctabs.js"></script>
- <script type="text/javascript" src="../../utils/form_utils.js"></script>
- <script type="text/javascript" src="js/image.js"></script>
-</head>
-<body id="image" style="display: none">
-<form onsubmit="ImageDialog.update();return false;" action="#">
- <div class="tabs">
- <ul>
- <li id="general_tab" class="current"><span><a href="javascript:mcTabs.displayTab('general_tab','general_panel');" onmousedown="return false;">{#advanced_dlg.image_title}</a></span></li>
- </ul>
- </div>
-
- <div class="panel_wrapper">
- <div id="general_panel" class="panel current">
- <table border="0" cellpadding="4" cellspacing="0">
- <tr>
- <td class="nowrap"><label for="src">{#advanced_dlg.image_src}</label></td>
- <td><table border="0" cellspacing="0" cellpadding="0">
- <tr>
- <td><input id="src" name="src" type="text" class="mceFocus" value="" style="width: 200px" onchange="ImageDialog.getImageData();" /></td>
- <td id="srcbrowsercontainer"> </td>
- </tr>
- </table></td>
- </tr>
- <tr>
- <td><label for="image_list">{#advanced_dlg.image_list}</label></td>
- <td><select id="image_list" name="image_list" onchange="document.getElementById('src').value=this.options[this.selectedIndex].value;document.getElementById('alt').value=this.options[this.selectedIndex].text;"></select></td>
- </tr>
- <tr>
- <td class="nowrap"><label for="alt">{#advanced_dlg.image_alt}</label></td>
- <td><input id="alt" name="alt" type="text" value="" style="width: 200px" /></td>
- </tr>
- <tr>
- <td class="nowrap"><label for="align">{#advanced_dlg.image_align}</label></td>
- <td><select id="align" name="align" onchange="ImageDialog.updateStyle();">
- <option value="">{#not_set}</option>
- <option value="baseline">{#advanced_dlg.image_align_baseline}</option>
- <option value="top">{#advanced_dlg.image_align_top}</option>
- <option value="middle">{#advanced_dlg.image_align_middle}</option>
- <option value="bottom">{#advanced_dlg.image_align_bottom}</option>
- <option value="text-top">{#advanced_dlg.image_align_texttop}</option>
- <option value="text-bottom">{#advanced_dlg.image_align_textbottom}</option>
- <option value="left">{#advanced_dlg.image_align_left}</option>
- <option value="right">{#advanced_dlg.image_align_right}</option>
- </select></td>
- </tr>
- <tr>
- <td class="nowrap"><label for="width">{#advanced_dlg.image_dimensions}</label></td>
- <td><input id="width" name="width" type="text" value="" size="3" maxlength="5" />
- x
- <input id="height" name="height" type="text" value="" size="3" maxlength="5" /></td>
- </tr>
- <tr>
- <td class="nowrap"><label for="border">{#advanced_dlg.image_border}</label></td>
- <td><input id="border" name="border" type="text" value="" size="3" maxlength="3" onchange="ImageDialog.updateStyle();" /></td>
- </tr>
- <tr>
- <td class="nowrap"><label for="vspace">{#advanced_dlg.image_vspace}</label></td>
- <td><input id="vspace" name="vspace" type="text" value="" size="3" maxlength="3" onchange="ImageDialog.updateStyle();" /></td>
- </tr>
- <tr>
- <td class="nowrap"><label for="hspace">{#advanced_dlg.image_hspace}</label></td>
- <td><input id="hspace" name="hspace" type="text" value="" size="3" maxlength="3" onchange="ImageDialog.updateStyle();" /></td>
- </tr>
- </table>
- </div>
- </div>
-
- <div class="mceActionPanel">
- <input type="submit" id="insert" name="insert" value="{#insert}" />
- <input type="button" id="cancel" name="cancel" value="{#cancel}" onclick="tinyMCEPopup.close();" />
- </div>
-</form>
-</body>
-</html>
diff --git a/resource/tinymce/themes/advanced/img/colorpicker.jpg b/resource/tinymce/themes/advanced/img/colorpicker.jpg
Binary files differ.
diff --git a/resource/tinymce/themes/advanced/img/icons.gif b/resource/tinymce/themes/advanced/img/icons.gif
Binary files differ.
diff --git a/resource/tinymce/themes/advanced/js/about.js b/resource/tinymce/themes/advanced/js/about.js
@@ -1,73 +0,0 @@
-tinyMCEPopup.requireLangPack();
-
-function init() {
- var ed, tcont;
-
- tinyMCEPopup.resizeToInnerSize();
- ed = tinyMCEPopup.editor;
-
- // Give FF some time
- window.setTimeout(insertHelpIFrame, 10);
-
- tcont = document.getElementById('plugintablecontainer');
- document.getElementById('plugins_tab').style.display = 'none';
-
- var html = "";
- html += '<table id="plugintable">';
- html += '<thead>';
- html += '<tr>';
- html += '<td>' + ed.getLang('advanced_dlg.about_plugin') + '</td>';
- html += '<td>' + ed.getLang('advanced_dlg.about_author') + '</td>';
- html += '<td>' + ed.getLang('advanced_dlg.about_version') + '</td>';
- html += '</tr>';
- html += '</thead>';
- html += '<tbody>';
-
- tinymce.each(ed.plugins, function(p, n) {
- var info;
-
- if (!p.getInfo)
- return;
-
- html += '<tr>';
-
- info = p.getInfo();
-
- if (info.infourl != null && info.infourl != '')
- html += '<td width="50%" title="' + n + '"><a href="' + info.infourl + '" target="_blank">' + info.longname + '</a></td>';
- else
- html += '<td width="50%" title="' + n + '">' + info.longname + '</td>';
-
- if (info.authorurl != null && info.authorurl != '')
- html += '<td width="35%"><a href="' + info.authorurl + '" target="_blank">' + info.author + '</a></td>';
- else
- html += '<td width="35%">' + info.author + '</td>';
-
- html += '<td width="15%">' + info.version + '</td>';
- html += '</tr>';
-
- document.getElementById('plugins_tab').style.display = '';
-
- });
-
- html += '</tbody>';
- html += '</table>';
-
- tcont.innerHTML = html;
-
- tinyMCEPopup.dom.get('version').innerHTML = tinymce.majorVersion + "." + tinymce.minorVersion;
- tinyMCEPopup.dom.get('date').innerHTML = tinymce.releaseDate;
-}
-
-function insertHelpIFrame() {
- var html;
-
- if (tinyMCEPopup.getParam('docs_url')) {
- html = '<iframe width="100%" height="300" src="' + tinyMCEPopup.editor.baseURI.toAbsolute(tinyMCEPopup.getParam('docs_url')) + '"></iframe>';
- document.getElementById('iframecontainer').innerHTML = html;
- document.getElementById('help_tab').style.display = 'block';
- document.getElementById('help_tab').setAttribute("aria-hidden", "false");
- }
-}
-
-tinyMCEPopup.onInit.add(init);
diff --git a/resource/tinymce/themes/advanced/js/anchor.js b/resource/tinymce/themes/advanced/js/anchor.js
@@ -1,56 +0,0 @@
-tinyMCEPopup.requireLangPack();
-
-var AnchorDialog = {
- init : function(ed) {
- var action, elm, f = document.forms[0];
-
- this.editor = ed;
- elm = ed.dom.getParent(ed.selection.getNode(), 'A');
- v = ed.dom.getAttrib(elm, 'name') || ed.dom.getAttrib(elm, 'id');
-
- if (v) {
- this.action = 'update';
- f.anchorName.value = v;
- }
-
- f.insert.value = ed.getLang(elm ? 'update' : 'insert');
- },
-
- update : function() {
- var ed = this.editor, elm, name = document.forms[0].anchorName.value, attribName;
-
- if (!name || !/^[a-z][a-z0-9\-\_:\.]*$/i.test(name)) {
- tinyMCEPopup.alert('advanced_dlg.anchor_invalid');
- return;
- }
-
- tinyMCEPopup.restoreSelection();
-
- if (this.action != 'update')
- ed.selection.collapse(1);
-
- var aRule = ed.schema.getElementRule('a');
- if (!aRule || aRule.attributes.name) {
- attribName = 'name';
- } else {
- attribName = 'id';
- }
-
- elm = ed.dom.getParent(ed.selection.getNode(), 'A');
- if (elm) {
- elm.setAttribute(attribName, name);
- elm[attribName] = name;
- ed.undoManager.add();
- } else {
- // create with zero-sized nbsp so that in Webkit where anchor is on last line by itself caret cannot be placed after it
- var attrs = {'class' : 'mceItemAnchor'};
- attrs[attribName] = name;
- ed.execCommand('mceInsertContent', 0, ed.dom.createHTML('a', attrs, '\uFEFF'));
- ed.nodeChanged();
- }
-
- tinyMCEPopup.close();
- }
-};
-
-tinyMCEPopup.onInit.add(AnchorDialog.init, AnchorDialog);
diff --git a/resource/tinymce/themes/advanced/js/charmap.js b/resource/tinymce/themes/advanced/js/charmap.js
@@ -1,363 +0,0 @@
-/**
- * charmap.js
- *
- * Copyright 2009, Moxiecode Systems AB
- * Released under LGPL License.
- *
- * License: http://tinymce.moxiecode.com/license
- * Contributing: http://tinymce.moxiecode.com/contributing
- */
-
-tinyMCEPopup.requireLangPack();
-
-var charmap = [
- [' ', ' ', true, 'no-break space'],
- ['&', '&', true, 'ampersand'],
- ['"', '"', true, 'quotation mark'],
-// finance
- ['¢', '¢', true, 'cent sign'],
- ['€', '€', true, 'euro sign'],
- ['£', '£', true, 'pound sign'],
- ['¥', '¥', true, 'yen sign'],
-// signs
- ['©', '©', true, 'copyright sign'],
- ['®', '®', true, 'registered sign'],
- ['™', '™', true, 'trade mark sign'],
- ['‰', '‰', true, 'per mille sign'],
- ['µ', 'µ', true, 'micro sign'],
- ['·', '·', true, 'middle dot'],
- ['•', '•', true, 'bullet'],
- ['…', '…', true, 'three dot leader'],
- ['′', '′', true, 'minutes / feet'],
- ['″', '″', true, 'seconds / inches'],
- ['§', '§', true, 'section sign'],
- ['¶', '¶', true, 'paragraph sign'],
- ['ß', 'ß', true, 'sharp s / ess-zed'],
-// quotations
- ['‹', '‹', true, 'single left-pointing angle quotation mark'],
- ['›', '›', true, 'single right-pointing angle quotation mark'],
- ['«', '«', true, 'left pointing guillemet'],
- ['»', '»', true, 'right pointing guillemet'],
- ['‘', '‘', true, 'left single quotation mark'],
- ['’', '’', true, 'right single quotation mark'],
- ['“', '“', true, 'left double quotation mark'],
- ['”', '”', true, 'right double quotation mark'],
- ['‚', '‚', true, 'single low-9 quotation mark'],
- ['„', '„', true, 'double low-9 quotation mark'],
- ['<', '<', true, 'less-than sign'],
- ['>', '>', true, 'greater-than sign'],
- ['≤', '≤', true, 'less-than or equal to'],
- ['≥', '≥', true, 'greater-than or equal to'],
- ['–', '–', true, 'en dash'],
- ['—', '—', true, 'em dash'],
- ['¯', '¯', true, 'macron'],
- ['‾', '‾', true, 'overline'],
- ['¤', '¤', true, 'currency sign'],
- ['¦', '¦', true, 'broken bar'],
- ['¨', '¨', true, 'diaeresis'],
- ['¡', '¡', true, 'inverted exclamation mark'],
- ['¿', '¿', true, 'turned question mark'],
- ['ˆ', 'ˆ', true, 'circumflex accent'],
- ['˜', '˜', true, 'small tilde'],
- ['°', '°', true, 'degree sign'],
- ['−', '−', true, 'minus sign'],
- ['±', '±', true, 'plus-minus sign'],
- ['÷', '÷', true, 'division sign'],
- ['⁄', '⁄', true, 'fraction slash'],
- ['×', '×', true, 'multiplication sign'],
- ['¹', '¹', true, 'superscript one'],
- ['²', '²', true, 'superscript two'],
- ['³', '³', true, 'superscript three'],
- ['¼', '¼', true, 'fraction one quarter'],
- ['½', '½', true, 'fraction one half'],
- ['¾', '¾', true, 'fraction three quarters'],
-// math / logical
- ['ƒ', 'ƒ', true, 'function / florin'],
- ['∫', '∫', true, 'integral'],
- ['∑', '∑', true, 'n-ary sumation'],
- ['∞', '∞', true, 'infinity'],
- ['√', '√', true, 'square root'],
- ['∼', '∼', false,'similar to'],
- ['≅', '≅', false,'approximately equal to'],
- ['≈', '≈', true, 'almost equal to'],
- ['≠', '≠', true, 'not equal to'],
- ['≡', '≡', true, 'identical to'],
- ['∈', '∈', false,'element of'],
- ['∉', '∉', false,'not an element of'],
- ['∋', '∋', false,'contains as member'],
- ['∏', '∏', true, 'n-ary product'],
- ['∧', '∧', false,'logical and'],
- ['∨', '∨', false,'logical or'],
- ['¬', '¬', true, 'not sign'],
- ['∩', '∩', true, 'intersection'],
- ['∪', '∪', false,'union'],
- ['∂', '∂', true, 'partial differential'],
- ['∀', '∀', false,'for all'],
- ['∃', '∃', false,'there exists'],
- ['∅', '∅', false,'diameter'],
- ['∇', '∇', false,'backward difference'],
- ['∗', '∗', false,'asterisk operator'],
- ['∝', '∝', false,'proportional to'],
- ['∠', '∠', false,'angle'],
-// undefined
- ['´', '´', true, 'acute accent'],
- ['¸', '¸', true, 'cedilla'],
- ['ª', 'ª', true, 'feminine ordinal indicator'],
- ['º', 'º', true, 'masculine ordinal indicator'],
- ['†', '†', true, 'dagger'],
- ['‡', '‡', true, 'double dagger'],
-// alphabetical special chars
- ['À', 'À', true, 'A - grave'],
- ['Á', 'Á', true, 'A - acute'],
- ['Â', 'Â', true, 'A - circumflex'],
- ['Ã', 'Ã', true, 'A - tilde'],
- ['Ä', 'Ä', true, 'A - diaeresis'],
- ['Å', 'Å', true, 'A - ring above'],
- ['Æ', 'Æ', true, 'ligature AE'],
- ['Ç', 'Ç', true, 'C - cedilla'],
- ['È', 'È', true, 'E - grave'],
- ['É', 'É', true, 'E - acute'],
- ['Ê', 'Ê', true, 'E - circumflex'],
- ['Ë', 'Ë', true, 'E - diaeresis'],
- ['Ì', 'Ì', true, 'I - grave'],
- ['Í', 'Í', true, 'I - acute'],
- ['Î', 'Î', true, 'I - circumflex'],
- ['Ï', 'Ï', true, 'I - diaeresis'],
- ['Ð', 'Ð', true, 'ETH'],
- ['Ñ', 'Ñ', true, 'N - tilde'],
- ['Ò', 'Ò', true, 'O - grave'],
- ['Ó', 'Ó', true, 'O - acute'],
- ['Ô', 'Ô', true, 'O - circumflex'],
- ['Õ', 'Õ', true, 'O - tilde'],
- ['Ö', 'Ö', true, 'O - diaeresis'],
- ['Ø', 'Ø', true, 'O - slash'],
- ['Œ', 'Œ', true, 'ligature OE'],
- ['Š', 'Š', true, 'S - caron'],
- ['Ù', 'Ù', true, 'U - grave'],
- ['Ú', 'Ú', true, 'U - acute'],
- ['Û', 'Û', true, 'U - circumflex'],
- ['Ü', 'Ü', true, 'U - diaeresis'],
- ['Ý', 'Ý', true, 'Y - acute'],
- ['Ÿ', 'Ÿ', true, 'Y - diaeresis'],
- ['Þ', 'Þ', true, 'THORN'],
- ['à', 'à', true, 'a - grave'],
- ['á', 'á', true, 'a - acute'],
- ['â', 'â', true, 'a - circumflex'],
- ['ã', 'ã', true, 'a - tilde'],
- ['ä', 'ä', true, 'a - diaeresis'],
- ['å', 'å', true, 'a - ring above'],
- ['æ', 'æ', true, 'ligature ae'],
- ['ç', 'ç', true, 'c - cedilla'],
- ['è', 'è', true, 'e - grave'],
- ['é', 'é', true, 'e - acute'],
- ['ê', 'ê', true, 'e - circumflex'],
- ['ë', 'ë', true, 'e - diaeresis'],
- ['ì', 'ì', true, 'i - grave'],
- ['í', 'í', true, 'i - acute'],
- ['î', 'î', true, 'i - circumflex'],
- ['ï', 'ï', true, 'i - diaeresis'],
- ['ð', 'ð', true, 'eth'],
- ['ñ', 'ñ', true, 'n - tilde'],
- ['ò', 'ò', true, 'o - grave'],
- ['ó', 'ó', true, 'o - acute'],
- ['ô', 'ô', true, 'o - circumflex'],
- ['õ', 'õ', true, 'o - tilde'],
- ['ö', 'ö', true, 'o - diaeresis'],
- ['ø', 'ø', true, 'o slash'],
- ['œ', 'œ', true, 'ligature oe'],
- ['š', 'š', true, 's - caron'],
- ['ù', 'ù', true, 'u - grave'],
- ['ú', 'ú', true, 'u - acute'],
- ['û', 'û', true, 'u - circumflex'],
- ['ü', 'ü', true, 'u - diaeresis'],
- ['ý', 'ý', true, 'y - acute'],
- ['þ', 'þ', true, 'thorn'],
- ['ÿ', 'ÿ', true, 'y - diaeresis'],
- ['Α', 'Α', true, 'Alpha'],
- ['Β', 'Β', true, 'Beta'],
- ['Γ', 'Γ', true, 'Gamma'],
- ['Δ', 'Δ', true, 'Delta'],
- ['Ε', 'Ε', true, 'Epsilon'],
- ['Ζ', 'Ζ', true, 'Zeta'],
- ['Η', 'Η', true, 'Eta'],
- ['Θ', 'Θ', true, 'Theta'],
- ['Ι', 'Ι', true, 'Iota'],
- ['Κ', 'Κ', true, 'Kappa'],
- ['Λ', 'Λ', true, 'Lambda'],
- ['Μ', 'Μ', true, 'Mu'],
- ['Ν', 'Ν', true, 'Nu'],
- ['Ξ', 'Ξ', true, 'Xi'],
- ['Ο', 'Ο', true, 'Omicron'],
- ['Π', 'Π', true, 'Pi'],
- ['Ρ', 'Ρ', true, 'Rho'],
- ['Σ', 'Σ', true, 'Sigma'],
- ['Τ', 'Τ', true, 'Tau'],
- ['Υ', 'Υ', true, 'Upsilon'],
- ['Φ', 'Φ', true, 'Phi'],
- ['Χ', 'Χ', true, 'Chi'],
- ['Ψ', 'Ψ', true, 'Psi'],
- ['Ω', 'Ω', true, 'Omega'],
- ['α', 'α', true, 'alpha'],
- ['β', 'β', true, 'beta'],
- ['γ', 'γ', true, 'gamma'],
- ['δ', 'δ', true, 'delta'],
- ['ε', 'ε', true, 'epsilon'],
- ['ζ', 'ζ', true, 'zeta'],
- ['η', 'η', true, 'eta'],
- ['θ', 'θ', true, 'theta'],
- ['ι', 'ι', true, 'iota'],
- ['κ', 'κ', true, 'kappa'],
- ['λ', 'λ', true, 'lambda'],
- ['μ', 'μ', true, 'mu'],
- ['ν', 'ν', true, 'nu'],
- ['ξ', 'ξ', true, 'xi'],
- ['ο', 'ο', true, 'omicron'],
- ['π', 'π', true, 'pi'],
- ['ρ', 'ρ', true, 'rho'],
- ['ς', 'ς', true, 'final sigma'],
- ['σ', 'σ', true, 'sigma'],
- ['τ', 'τ', true, 'tau'],
- ['υ', 'υ', true, 'upsilon'],
- ['φ', 'φ', true, 'phi'],
- ['χ', 'χ', true, 'chi'],
- ['ψ', 'ψ', true, 'psi'],
- ['ω', 'ω', true, 'omega'],
-// symbols
- ['ℵ', 'ℵ', false,'alef symbol'],
- ['ϖ', 'ϖ', false,'pi symbol'],
- ['ℜ', 'ℜ', false,'real part symbol'],
- ['ϑ','ϑ', false,'theta symbol'],
- ['ϒ', 'ϒ', false,'upsilon - hook symbol'],
- ['℘', '℘', false,'Weierstrass p'],
- ['ℑ', 'ℑ', false,'imaginary part'],
-// arrows
- ['←', '←', true, 'leftwards arrow'],
- ['↑', '↑', true, 'upwards arrow'],
- ['→', '→', true, 'rightwards arrow'],
- ['↓', '↓', true, 'downwards arrow'],
- ['↔', '↔', true, 'left right arrow'],
- ['↵', '↵', false,'carriage return'],
- ['⇐', '⇐', false,'leftwards double arrow'],
- ['⇑', '⇑', false,'upwards double arrow'],
- ['⇒', '⇒', false,'rightwards double arrow'],
- ['⇓', '⇓', false,'downwards double arrow'],
- ['⇔', '⇔', false,'left right double arrow'],
- ['∴', '∴', false,'therefore'],
- ['⊂', '⊂', false,'subset of'],
- ['⊃', '⊃', false,'superset of'],
- ['⊄', '⊄', false,'not a subset of'],
- ['⊆', '⊆', false,'subset of or equal to'],
- ['⊇', '⊇', false,'superset of or equal to'],
- ['⊕', '⊕', false,'circled plus'],
- ['⊗', '⊗', false,'circled times'],
- ['⊥', '⊥', false,'perpendicular'],
- ['⋅', '⋅', false,'dot operator'],
- ['⌈', '⌈', false,'left ceiling'],
- ['⌉', '⌉', false,'right ceiling'],
- ['⌊', '⌊', false,'left floor'],
- ['⌋', '⌋', false,'right floor'],
- ['⟨', '〈', false,'left-pointing angle bracket'],
- ['⟩', '〉', false,'right-pointing angle bracket'],
- ['◊', '◊', true, 'lozenge'],
- ['♠', '♠', true, 'black spade suit'],
- ['♣', '♣', true, 'black club suit'],
- ['♥', '♥', true, 'black heart suit'],
- ['♦', '♦', true, 'black diamond suit'],
- [' ', ' ', false,'en space'],
- [' ', ' ', false,'em space'],
- [' ', ' ', false,'thin space'],
- ['‌', '‌', false,'zero width non-joiner'],
- ['‍', '‍', false,'zero width joiner'],
- ['‎', '‎', false,'left-to-right mark'],
- ['‏', '‏', false,'right-to-left mark'],
- ['­', '­', false,'soft hyphen']
-];
-
-tinyMCEPopup.onInit.add(function() {
- tinyMCEPopup.dom.setHTML('charmapView', renderCharMapHTML());
- addKeyboardNavigation();
-});
-
-function addKeyboardNavigation(){
- var tableElm, cells, settings;
-
- cells = tinyMCEPopup.dom.select("a.charmaplink", "charmapgroup");
-
- settings ={
- root: "charmapgroup",
- items: cells
- };
- cells[0].tabindex=0;
- tinyMCEPopup.dom.addClass(cells[0], "mceFocus");
- if (tinymce.isGecko) {
- cells[0].focus();
- } else {
- setTimeout(function(){
- cells[0].focus();
- }, 100);
- }
- tinyMCEPopup.editor.windowManager.createInstance('tinymce.ui.KeyboardNavigation', settings, tinyMCEPopup.dom);
-}
-
-function renderCharMapHTML() {
- var charsPerRow = 20, tdWidth=20, tdHeight=20, i;
- var html = '<div id="charmapgroup" aria-labelledby="charmap_label" tabindex="0" role="listbox">'+
- '<table role="presentation" border="0" cellspacing="1" cellpadding="0" width="' + (tdWidth*charsPerRow) +
- '"><tr height="' + tdHeight + '">';
- var cols=-1;
-
- for (i=0; i<charmap.length; i++) {
- var previewCharFn;
-
- if (charmap[i][2]==true) {
- cols++;
- previewCharFn = 'previewChar(\'' + charmap[i][1].substring(1,charmap[i][1].length) + '\',\'' + charmap[i][0].substring(1,charmap[i][0].length) + '\',\'' + charmap[i][3] + '\');';
- html += ''
- + '<td class="charmap">'
- + '<a class="charmaplink" role="button" onmouseover="'+previewCharFn+'" onfocus="'+previewCharFn+'" href="javascript:void(0)" onclick="insertChar(\'' + charmap[i][1].substring(2,charmap[i][1].length-1) + '\');" onclick="return false;" onmousedown="return false;" title="' + charmap[i][3] + ' '+ tinyMCEPopup.editor.translate("advanced_dlg.charmap_usage")+'">'
- + charmap[i][1]
- + '</a></td>';
- if ((cols+1) % charsPerRow == 0)
- html += '</tr><tr height="' + tdHeight + '">';
- }
- }
-
- if (cols % charsPerRow > 0) {
- var padd = charsPerRow - (cols % charsPerRow);
- for (var i=0; i<padd-1; i++)
- html += '<td width="' + tdWidth + '" height="' + tdHeight + '" class="charmap"> </td>';
- }
-
- html += '</tr></table></div>';
- html = html.replace(/<tr height="20"><\/tr>/g, '');
-
- return html;
-}
-
-function insertChar(chr) {
- tinyMCEPopup.execCommand('mceInsertContent', false, '&#' + chr + ';');
-
- // Refocus in window
- if (tinyMCEPopup.isWindow)
- window.focus();
-
- tinyMCEPopup.editor.focus();
- tinyMCEPopup.close();
-}
-
-function previewChar(codeA, codeB, codeN) {
- var elmA = document.getElementById('codeA');
- var elmB = document.getElementById('codeB');
- var elmV = document.getElementById('codeV');
- var elmN = document.getElementById('codeN');
-
- if (codeA=='#160;') {
- elmV.innerHTML = '__';
- } else {
- elmV.innerHTML = '&' + codeA;
- }
-
- elmB.innerHTML = '&' + codeA;
- elmA.innerHTML = '&' + codeB;
- elmN.innerHTML = codeN;
-}
diff --git a/resource/tinymce/themes/advanced/js/color_picker.js b/resource/tinymce/themes/advanced/js/color_picker.js
@@ -1,345 +0,0 @@
-tinyMCEPopup.requireLangPack();
-
-var detail = 50, strhex = "0123456789abcdef", i, isMouseDown = false, isMouseOver = false;
-
-var colors = [
- "#000000","#000033","#000066","#000099","#0000cc","#0000ff","#330000","#330033",
- "#330066","#330099","#3300cc","#3300ff","#660000","#660033","#660066","#660099",
- "#6600cc","#6600ff","#990000","#990033","#990066","#990099","#9900cc","#9900ff",
- "#cc0000","#cc0033","#cc0066","#cc0099","#cc00cc","#cc00ff","#ff0000","#ff0033",
- "#ff0066","#ff0099","#ff00cc","#ff00ff","#003300","#003333","#003366","#003399",
- "#0033cc","#0033ff","#333300","#333333","#333366","#333399","#3333cc","#3333ff",
- "#663300","#663333","#663366","#663399","#6633cc","#6633ff","#993300","#993333",
- "#993366","#993399","#9933cc","#9933ff","#cc3300","#cc3333","#cc3366","#cc3399",
- "#cc33cc","#cc33ff","#ff3300","#ff3333","#ff3366","#ff3399","#ff33cc","#ff33ff",
- "#006600","#006633","#006666","#006699","#0066cc","#0066ff","#336600","#336633",
- "#336666","#336699","#3366cc","#3366ff","#666600","#666633","#666666","#666699",
- "#6666cc","#6666ff","#996600","#996633","#996666","#996699","#9966cc","#9966ff",
- "#cc6600","#cc6633","#cc6666","#cc6699","#cc66cc","#cc66ff","#ff6600","#ff6633",
- "#ff6666","#ff6699","#ff66cc","#ff66ff","#009900","#009933","#009966","#009999",
- "#0099cc","#0099ff","#339900","#339933","#339966","#339999","#3399cc","#3399ff",
- "#669900","#669933","#669966","#669999","#6699cc","#6699ff","#999900","#999933",
- "#999966","#999999","#9999cc","#9999ff","#cc9900","#cc9933","#cc9966","#cc9999",
- "#cc99cc","#cc99ff","#ff9900","#ff9933","#ff9966","#ff9999","#ff99cc","#ff99ff",
- "#00cc00","#00cc33","#00cc66","#00cc99","#00cccc","#00ccff","#33cc00","#33cc33",
- "#33cc66","#33cc99","#33cccc","#33ccff","#66cc00","#66cc33","#66cc66","#66cc99",
- "#66cccc","#66ccff","#99cc00","#99cc33","#99cc66","#99cc99","#99cccc","#99ccff",
- "#cccc00","#cccc33","#cccc66","#cccc99","#cccccc","#ccccff","#ffcc00","#ffcc33",
- "#ffcc66","#ffcc99","#ffcccc","#ffccff","#00ff00","#00ff33","#00ff66","#00ff99",
- "#00ffcc","#00ffff","#33ff00","#33ff33","#33ff66","#33ff99","#33ffcc","#33ffff",
- "#66ff00","#66ff33","#66ff66","#66ff99","#66ffcc","#66ffff","#99ff00","#99ff33",
- "#99ff66","#99ff99","#99ffcc","#99ffff","#ccff00","#ccff33","#ccff66","#ccff99",
- "#ccffcc","#ccffff","#ffff00","#ffff33","#ffff66","#ffff99","#ffffcc","#ffffff"
-];
-
-var named = {
- '#F0F8FF':'Alice Blue','#FAEBD7':'Antique White','#00FFFF':'Aqua','#7FFFD4':'Aquamarine','#F0FFFF':'Azure','#F5F5DC':'Beige',
- '#FFE4C4':'Bisque','#000000':'Black','#FFEBCD':'Blanched Almond','#0000FF':'Blue','#8A2BE2':'Blue Violet','#A52A2A':'Brown',
- '#DEB887':'Burly Wood','#5F9EA0':'Cadet Blue','#7FFF00':'Chartreuse','#D2691E':'Chocolate','#FF7F50':'Coral','#6495ED':'Cornflower Blue',
- '#FFF8DC':'Cornsilk','#DC143C':'Crimson','#00FFFF':'Cyan','#00008B':'Dark Blue','#008B8B':'Dark Cyan','#B8860B':'Dark Golden Rod',
- '#A9A9A9':'Dark Gray','#A9A9A9':'Dark Grey','#006400':'Dark Green','#BDB76B':'Dark Khaki','#8B008B':'Dark Magenta','#556B2F':'Dark Olive Green',
- '#FF8C00':'Darkorange','#9932CC':'Dark Orchid','#8B0000':'Dark Red','#E9967A':'Dark Salmon','#8FBC8F':'Dark Sea Green','#483D8B':'Dark Slate Blue',
- '#2F4F4F':'Dark Slate Gray','#2F4F4F':'Dark Slate Grey','#00CED1':'Dark Turquoise','#9400D3':'Dark Violet','#FF1493':'Deep Pink','#00BFFF':'Deep Sky Blue',
- '#696969':'Dim Gray','#696969':'Dim Grey','#1E90FF':'Dodger Blue','#B22222':'Fire Brick','#FFFAF0':'Floral White','#228B22':'Forest Green',
- '#FF00FF':'Fuchsia','#DCDCDC':'Gainsboro','#F8F8FF':'Ghost White','#FFD700':'Gold','#DAA520':'Golden Rod','#808080':'Gray','#808080':'Grey',
- '#008000':'Green','#ADFF2F':'Green Yellow','#F0FFF0':'Honey Dew','#FF69B4':'Hot Pink','#CD5C5C':'Indian Red','#4B0082':'Indigo','#FFFFF0':'Ivory',
- '#F0E68C':'Khaki','#E6E6FA':'Lavender','#FFF0F5':'Lavender Blush','#7CFC00':'Lawn Green','#FFFACD':'Lemon Chiffon','#ADD8E6':'Light Blue',
- '#F08080':'Light Coral','#E0FFFF':'Light Cyan','#FAFAD2':'Light Golden Rod Yellow','#D3D3D3':'Light Gray','#D3D3D3':'Light Grey','#90EE90':'Light Green',
- '#FFB6C1':'Light Pink','#FFA07A':'Light Salmon','#20B2AA':'Light Sea Green','#87CEFA':'Light Sky Blue','#778899':'Light Slate Gray','#778899':'Light Slate Grey',
- '#B0C4DE':'Light Steel Blue','#FFFFE0':'Light Yellow','#00FF00':'Lime','#32CD32':'Lime Green','#FAF0E6':'Linen','#FF00FF':'Magenta','#800000':'Maroon',
- '#66CDAA':'Medium Aqua Marine','#0000CD':'Medium Blue','#BA55D3':'Medium Orchid','#9370D8':'Medium Purple','#3CB371':'Medium Sea Green','#7B68EE':'Medium Slate Blue',
- '#00FA9A':'Medium Spring Green','#48D1CC':'Medium Turquoise','#C71585':'Medium Violet Red','#191970':'Midnight Blue','#F5FFFA':'Mint Cream','#FFE4E1':'Misty Rose','#FFE4B5':'Moccasin',
- '#FFDEAD':'Navajo White','#000080':'Navy','#FDF5E6':'Old Lace','#808000':'Olive','#6B8E23':'Olive Drab','#FFA500':'Orange','#FF4500':'Orange Red','#DA70D6':'Orchid',
- '#EEE8AA':'Pale Golden Rod','#98FB98':'Pale Green','#AFEEEE':'Pale Turquoise','#D87093':'Pale Violet Red','#FFEFD5':'Papaya Whip','#FFDAB9':'Peach Puff',
- '#CD853F':'Peru','#FFC0CB':'Pink','#DDA0DD':'Plum','#B0E0E6':'Powder Blue','#800080':'Purple','#FF0000':'Red','#BC8F8F':'Rosy Brown','#4169E1':'Royal Blue',
- '#8B4513':'Saddle Brown','#FA8072':'Salmon','#F4A460':'Sandy Brown','#2E8B57':'Sea Green','#FFF5EE':'Sea Shell','#A0522D':'Sienna','#C0C0C0':'Silver',
- '#87CEEB':'Sky Blue','#6A5ACD':'Slate Blue','#708090':'Slate Gray','#708090':'Slate Grey','#FFFAFA':'Snow','#00FF7F':'Spring Green',
- '#4682B4':'Steel Blue','#D2B48C':'Tan','#008080':'Teal','#D8BFD8':'Thistle','#FF6347':'Tomato','#40E0D0':'Turquoise','#EE82EE':'Violet',
- '#F5DEB3':'Wheat','#FFFFFF':'White','#F5F5F5':'White Smoke','#FFFF00':'Yellow','#9ACD32':'Yellow Green'
-};
-
-var namedLookup = {};
-
-function init() {
- var inputColor = convertRGBToHex(tinyMCEPopup.getWindowArg('input_color')), key, value;
-
- tinyMCEPopup.resizeToInnerSize();
-
- generatePicker();
- generateWebColors();
- generateNamedColors();
-
- if (inputColor) {
- changeFinalColor(inputColor);
-
- col = convertHexToRGB(inputColor);
-
- if (col)
- updateLight(col.r, col.g, col.b);
- }
-
- for (key in named) {
- value = named[key];
- namedLookup[value.replace(/\s+/, '').toLowerCase()] = key.replace(/#/, '').toLowerCase();
- }
-}
-
-function toHexColor(color) {
- var matches, red, green, blue, toInt = parseInt;
-
- function hex(value) {
- value = parseInt(value).toString(16);
-
- return value.length > 1 ? value : '0' + value; // Padd with leading zero
- };
-
- color = tinymce.trim(color);
- color = color.replace(/^[#]/, '').toLowerCase(); // remove leading '#'
- color = namedLookup[color] || color;
-
- matches = /^rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/.exec(color);
-
- if (matches) {
- red = toInt(matches[1]);
- green = toInt(matches[2]);
- blue = toInt(matches[3]);
- } else {
- matches = /^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/.exec(color);
-
- if (matches) {
- red = toInt(matches[1], 16);
- green = toInt(matches[2], 16);
- blue = toInt(matches[3], 16);
- } else {
- matches = /^([0-9a-f])([0-9a-f])([0-9a-f])$/.exec(color);
-
- if (matches) {
- red = toInt(matches[1] + matches[1], 16);
- green = toInt(matches[2] + matches[2], 16);
- blue = toInt(matches[3] + matches[3], 16);
- } else {
- return '';
- }
- }
- }
-
- return '#' + hex(red) + hex(green) + hex(blue);
-}
-
-function insertAction() {
- var color = document.getElementById("color").value, f = tinyMCEPopup.getWindowArg('func');
-
- var hexColor = toHexColor(color);
-
- if (hexColor === '') {
- var text = tinyMCEPopup.editor.getLang('advanced_dlg.invalid_color_value');
- tinyMCEPopup.alert(text + ': ' + color);
- }
- else {
- tinyMCEPopup.restoreSelection();
-
- if (f)
- f(hexColor);
-
- tinyMCEPopup.close();
- }
-}
-
-function showColor(color, name) {
- if (name)
- document.getElementById("colorname").innerHTML = name;
-
- document.getElementById("preview").style.backgroundColor = color;
- document.getElementById("color").value = color.toUpperCase();
-}
-
-function convertRGBToHex(col) {
- var re = new RegExp("rgb\\s*\\(\\s*([0-9]+).*,\\s*([0-9]+).*,\\s*([0-9]+).*\\)", "gi");
-
- if (!col)
- return col;
-
- var rgb = col.replace(re, "$1,$2,$3").split(',');
- if (rgb.length == 3) {
- r = parseInt(rgb[0]).toString(16);
- g = parseInt(rgb[1]).toString(16);
- b = parseInt(rgb[2]).toString(16);
-
- r = r.length == 1 ? '0' + r : r;
- g = g.length == 1 ? '0' + g : g;
- b = b.length == 1 ? '0' + b : b;
-
- return "#" + r + g + b;
- }
-
- return col;
-}
-
-function convertHexToRGB(col) {
- if (col.indexOf('#') != -1) {
- col = col.replace(new RegExp('[^0-9A-F]', 'gi'), '');
-
- r = parseInt(col.substring(0, 2), 16);
- g = parseInt(col.substring(2, 4), 16);
- b = parseInt(col.substring(4, 6), 16);
-
- return {r : r, g : g, b : b};
- }
-
- return null;
-}
-
-function generatePicker() {
- var el = document.getElementById('light'), h = '', i;
-
- for (i = 0; i < detail; i++){
- h += '<div id="gs'+i+'" style="background-color:#000000; width:15px; height:3px; border-style:none; border-width:0px;"'
- + ' onclick="changeFinalColor(this.style.backgroundColor)"'
- + ' onmousedown="isMouseDown = true; return false;"'
- + ' onmouseup="isMouseDown = false;"'
- + ' onmousemove="if (isMouseDown && isMouseOver) changeFinalColor(this.style.backgroundColor); return false;"'
- + ' onmouseover="isMouseOver = true;"'
- + ' onmouseout="isMouseOver = false;"'
- + '></div>';
- }
-
- el.innerHTML = h;
-}
-
-function generateWebColors() {
- var el = document.getElementById('webcolors'), h = '', i;
-
- if (el.className == 'generated')
- return;
-
- // TODO: VoiceOver doesn't seem to support legend as a label referenced by labelledby.
- h += '<div role="listbox" aria-labelledby="webcolors_title" tabindex="0"><table role="presentation" border="0" cellspacing="1" cellpadding="0">'
- + '<tr>';
-
- for (i=0; i<colors.length; i++) {
- h += '<td bgcolor="' + colors[i] + '" width="10" height="10">'
- + '<a href="javascript:insertAction();" role="option" tabindex="-1" aria-labelledby="web_colors_' + i + '" onfocus="showColor(\'' + colors[i] + '\');" onmouseover="showColor(\'' + colors[i] + '\');" style="display:block;width:10px;height:10px;overflow:hidden;">';
- if (tinyMCEPopup.editor.forcedHighContrastMode) {
- h += '<canvas class="mceColorSwatch" height="10" width="10" data-color="' + colors[i] + '"></canvas>';
- }
- h += '<span class="mceVoiceLabel" style="display:none;" id="web_colors_' + i + '">' + colors[i].toUpperCase() + '</span>';
- h += '</a></td>';
- if ((i+1) % 18 == 0)
- h += '</tr><tr>';
- }
-
- h += '</table></div>';
-
- el.innerHTML = h;
- el.className = 'generated';
-
- paintCanvas(el);
- enableKeyboardNavigation(el.firstChild);
-}
-
-function paintCanvas(el) {
- tinyMCEPopup.getWin().tinymce.each(tinyMCEPopup.dom.select('canvas.mceColorSwatch', el), function(canvas) {
- var context;
- if (canvas.getContext && (context = canvas.getContext("2d"))) {
- context.fillStyle = canvas.getAttribute('data-color');
- context.fillRect(0, 0, 10, 10);
- }
- });
-}
-function generateNamedColors() {
- var el = document.getElementById('namedcolors'), h = '', n, v, i = 0;
-
- if (el.className == 'generated')
- return;
-
- for (n in named) {
- v = named[n];
- h += '<a href="javascript:insertAction();" role="option" tabindex="-1" aria-labelledby="named_colors_' + i + '" onfocus="showColor(\'' + n + '\',\'' + v + '\');" onmouseover="showColor(\'' + n + '\',\'' + v + '\');" style="background-color: ' + n + '">';
- if (tinyMCEPopup.editor.forcedHighContrastMode) {
- h += '<canvas class="mceColorSwatch" height="10" width="10" data-color="' + colors[i] + '"></canvas>';
- }
- h += '<span class="mceVoiceLabel" style="display:none;" id="named_colors_' + i + '">' + v + '</span>';
- h += '</a>';
- i++;
- }
-
- el.innerHTML = h;
- el.className = 'generated';
-
- paintCanvas(el);
- enableKeyboardNavigation(el);
-}
-
-function enableKeyboardNavigation(el) {
- tinyMCEPopup.editor.windowManager.createInstance('tinymce.ui.KeyboardNavigation', {
- root: el,
- items: tinyMCEPopup.dom.select('a', el)
- }, tinyMCEPopup.dom);
-}
-
-function dechex(n) {
- return strhex.charAt(Math.floor(n / 16)) + strhex.charAt(n % 16);
-}
-
-function computeColor(e) {
- var x, y, partWidth, partDetail, imHeight, r, g, b, coef, i, finalCoef, finalR, finalG, finalB, pos = tinyMCEPopup.dom.getPos(e.target);
-
- x = e.offsetX ? e.offsetX : (e.target ? e.clientX - pos.x : 0);
- y = e.offsetY ? e.offsetY : (e.target ? e.clientY - pos.y : 0);
-
- partWidth = document.getElementById('colors').width / 6;
- partDetail = detail / 2;
- imHeight = document.getElementById('colors').height;
-
- r = (x >= 0)*(x < partWidth)*255 + (x >= partWidth)*(x < 2*partWidth)*(2*255 - x * 255 / partWidth) + (x >= 4*partWidth)*(x < 5*partWidth)*(-4*255 + x * 255 / partWidth) + (x >= 5*partWidth)*(x < 6*partWidth)*255;
- g = (x >= 0)*(x < partWidth)*(x * 255 / partWidth) + (x >= partWidth)*(x < 3*partWidth)*255 + (x >= 3*partWidth)*(x < 4*partWidth)*(4*255 - x * 255 / partWidth);
- b = (x >= 2*partWidth)*(x < 3*partWidth)*(-2*255 + x * 255 / partWidth) + (x >= 3*partWidth)*(x < 5*partWidth)*255 + (x >= 5*partWidth)*(x < 6*partWidth)*(6*255 - x * 255 / partWidth);
-
- coef = (imHeight - y) / imHeight;
- r = 128 + (r - 128) * coef;
- g = 128 + (g - 128) * coef;
- b = 128 + (b - 128) * coef;
-
- changeFinalColor('#' + dechex(r) + dechex(g) + dechex(b));
- updateLight(r, g, b);
-}
-
-function updateLight(r, g, b) {
- var i, partDetail = detail / 2, finalCoef, finalR, finalG, finalB, color;
-
- for (i=0; i<detail; i++) {
- if ((i>=0) && (i<partDetail)) {
- finalCoef = i / partDetail;
- finalR = dechex(255 - (255 - r) * finalCoef);
- finalG = dechex(255 - (255 - g) * finalCoef);
- finalB = dechex(255 - (255 - b) * finalCoef);
- } else {
- finalCoef = 2 - i / partDetail;
- finalR = dechex(r * finalCoef);
- finalG = dechex(g * finalCoef);
- finalB = dechex(b * finalCoef);
- }
-
- color = finalR + finalG + finalB;
-
- setCol('gs' + i, '#'+color);
- }
-}
-
-function changeFinalColor(color) {
- if (color.indexOf('#') == -1)
- color = convertRGBToHex(color);
-
- setCol('preview', color);
- document.getElementById('color').value = color;
-}
-
-function setCol(e, c) {
- try {
- document.getElementById(e).style.backgroundColor = c;
- } catch (ex) {
- // Ignore IE warning
- }
-}
-
-tinyMCEPopup.onInit.add(init);
diff --git a/resource/tinymce/themes/advanced/js/image.js b/resource/tinymce/themes/advanced/js/image.js
@@ -1,253 +0,0 @@
-var ImageDialog = {
- preInit : function() {
- var url;
-
- tinyMCEPopup.requireLangPack();
-
- if (url = tinyMCEPopup.getParam("external_image_list_url"))
- document.write('<script language="javascript" type="text/javascript" src="' + tinyMCEPopup.editor.documentBaseURI.toAbsolute(url) + '"></script>');
- },
-
- init : function() {
- var f = document.forms[0], ed = tinyMCEPopup.editor;
-
- // Setup browse button
- document.getElementById('srcbrowsercontainer').innerHTML = getBrowserHTML('srcbrowser','src','image','theme_advanced_image');
- if (isVisible('srcbrowser'))
- document.getElementById('src').style.width = '180px';
-
- e = ed.selection.getNode();
-
- this.fillFileList('image_list', tinyMCEPopup.getParam('external_image_list', 'tinyMCEImageList'));
-
- if (e.nodeName == 'IMG') {
- f.src.value = ed.dom.getAttrib(e, 'src');
- f.alt.value = ed.dom.getAttrib(e, 'alt');
- f.border.value = this.getAttrib(e, 'border');
- f.vspace.value = this.getAttrib(e, 'vspace');
- f.hspace.value = this.getAttrib(e, 'hspace');
- f.width.value = ed.dom.getAttrib(e, 'width');
- f.height.value = ed.dom.getAttrib(e, 'height');
- f.insert.value = ed.getLang('update');
- this.styleVal = ed.dom.getAttrib(e, 'style');
- selectByValue(f, 'image_list', f.src.value);
- selectByValue(f, 'align', this.getAttrib(e, 'align'));
- this.updateStyle();
- }
- },
-
- fillFileList : function(id, l) {
- var dom = tinyMCEPopup.dom, lst = dom.get(id), v, cl;
-
- l = typeof(l) === 'function' ? l() : window[l];
-
- if (l && l.length > 0) {
- lst.options[lst.options.length] = new Option('', '');
-
- tinymce.each(l, function(o) {
- lst.options[lst.options.length] = new Option(o[0], o[1]);
- });
- } else
- dom.remove(dom.getParent(id, 'tr'));
- },
-
- update : function() {
- var f = document.forms[0], nl = f.elements, ed = tinyMCEPopup.editor, args = {}, el;
-
- tinyMCEPopup.restoreSelection();
-
- if (f.src.value === '') {
- if (ed.selection.getNode().nodeName == 'IMG') {
- ed.dom.remove(ed.selection.getNode());
- ed.execCommand('mceRepaint');
- }
-
- tinyMCEPopup.close();
- return;
- }
-
- if (!ed.settings.inline_styles) {
- args = tinymce.extend(args, {
- vspace : nl.vspace.value,
- hspace : nl.hspace.value,
- border : nl.border.value,
- align : getSelectValue(f, 'align')
- });
- } else
- args.style = this.styleVal;
-
- tinymce.extend(args, {
- src : f.src.value.replace(/ /g, '%20'),
- alt : f.alt.value,
- width : f.width.value,
- height : f.height.value
- });
-
- el = ed.selection.getNode();
-
- if (el && el.nodeName == 'IMG') {
- ed.dom.setAttribs(el, args);
- tinyMCEPopup.editor.execCommand('mceRepaint');
- tinyMCEPopup.editor.focus();
- } else {
- tinymce.each(args, function(value, name) {
- if (value === "") {
- delete args[name];
- }
- });
-
- ed.execCommand('mceInsertContent', false, tinyMCEPopup.editor.dom.createHTML('img', args), {skip_undo : 1});
- ed.undoManager.add();
- }
-
- tinyMCEPopup.close();
- },
-
- updateStyle : function() {
- var dom = tinyMCEPopup.dom, st = {}, v, f = document.forms[0];
-
- if (tinyMCEPopup.editor.settings.inline_styles) {
- tinymce.each(tinyMCEPopup.dom.parseStyle(this.styleVal), function(value, key) {
- st[key] = value;
- });
-
- // Handle align
- v = getSelectValue(f, 'align');
- if (v) {
- if (v == 'left' || v == 'right') {
- st['float'] = v;
- delete st['vertical-align'];
- } else {
- st['vertical-align'] = v;
- delete st['float'];
- }
- } else {
- delete st['float'];
- delete st['vertical-align'];
- }
-
- // Handle border
- v = f.border.value;
- if (v || v == '0') {
- if (v == '0')
- st['border'] = '0';
- else
- st['border'] = v + 'px solid black';
- } else
- delete st['border'];
-
- // Handle hspace
- v = f.hspace.value;
- if (v) {
- delete st['margin'];
- st['margin-left'] = v + 'px';
- st['margin-right'] = v + 'px';
- } else {
- delete st['margin-left'];
- delete st['margin-right'];
- }
-
- // Handle vspace
- v = f.vspace.value;
- if (v) {
- delete st['margin'];
- st['margin-top'] = v + 'px';
- st['margin-bottom'] = v + 'px';
- } else {
- delete st['margin-top'];
- delete st['margin-bottom'];
- }
-
- // Merge
- st = tinyMCEPopup.dom.parseStyle(dom.serializeStyle(st), 'img');
- this.styleVal = dom.serializeStyle(st, 'img');
- }
- },
-
- getAttrib : function(e, at) {
- var ed = tinyMCEPopup.editor, dom = ed.dom, v, v2;
-
- if (ed.settings.inline_styles) {
- switch (at) {
- case 'align':
- if (v = dom.getStyle(e, 'float'))
- return v;
-
- if (v = dom.getStyle(e, 'vertical-align'))
- return v;
-
- break;
-
- case 'hspace':
- v = dom.getStyle(e, 'margin-left')
- v2 = dom.getStyle(e, 'margin-right');
- if (v && v == v2)
- return parseInt(v.replace(/[^0-9]/g, ''));
-
- break;
-
- case 'vspace':
- v = dom.getStyle(e, 'margin-top')
- v2 = dom.getStyle(e, 'margin-bottom');
- if (v && v == v2)
- return parseInt(v.replace(/[^0-9]/g, ''));
-
- break;
-
- case 'border':
- v = 0;
-
- tinymce.each(['top', 'right', 'bottom', 'left'], function(sv) {
- sv = dom.getStyle(e, 'border-' + sv + '-width');
-
- // False or not the same as prev
- if (!sv || (sv != v && v !== 0)) {
- v = 0;
- return false;
- }
-
- if (sv)
- v = sv;
- });
-
- if (v)
- return parseInt(v.replace(/[^0-9]/g, ''));
-
- break;
- }
- }
-
- if (v = dom.getAttrib(e, at))
- return v;
-
- return '';
- },
-
- resetImageData : function() {
- var f = document.forms[0];
-
- f.width.value = f.height.value = "";
- },
-
- updateImageData : function() {
- var f = document.forms[0], t = ImageDialog;
-
- if (f.width.value == "")
- f.width.value = t.preloadImg.width;
-
- if (f.height.value == "")
- f.height.value = t.preloadImg.height;
- },
-
- getImageData : function() {
- var f = document.forms[0];
-
- this.preloadImg = new Image();
- this.preloadImg.onload = this.updateImageData;
- this.preloadImg.onerror = this.resetImageData;
- this.preloadImg.src = tinyMCEPopup.editor.documentBaseURI.toAbsolute(f.src.value);
- }
-};
-
-ImageDialog.preInit();
-tinyMCEPopup.onInit.add(ImageDialog.init, ImageDialog);
diff --git a/resource/tinymce/themes/advanced/js/link.js b/resource/tinymce/themes/advanced/js/link.js
@@ -1,159 +0,0 @@
-tinyMCEPopup.requireLangPack();
-
-var LinkDialog = {
- preInit : function() {
- var url;
-
- if (url = tinyMCEPopup.getParam("external_link_list_url"))
- document.write('<script language="javascript" type="text/javascript" src="' + tinyMCEPopup.editor.documentBaseURI.toAbsolute(url) + '"></script>');
- },
-
- init : function() {
- var f = document.forms[0], ed = tinyMCEPopup.editor;
-
- // Setup browse button
- document.getElementById('hrefbrowsercontainer').innerHTML = getBrowserHTML('hrefbrowser', 'href', 'file', 'theme_advanced_link');
- if (isVisible('hrefbrowser'))
- document.getElementById('href').style.width = '180px';
-
- this.fillClassList('class_list');
- this.fillFileList('link_list', 'tinyMCELinkList');
- this.fillTargetList('target_list');
-
- if (e = ed.dom.getParent(ed.selection.getNode(), 'A')) {
- f.href.value = ed.dom.getAttrib(e, 'href');
- f.linktitle.value = ed.dom.getAttrib(e, 'title');
- f.insert.value = ed.getLang('update');
- selectByValue(f, 'link_list', f.href.value);
- selectByValue(f, 'target_list', ed.dom.getAttrib(e, 'target'));
- selectByValue(f, 'class_list', ed.dom.getAttrib(e, 'class'));
- }
- },
-
- update : function() {
- var f = document.forms[0], ed = tinyMCEPopup.editor, e, b, href = f.href.value.replace(/ /g, '%20');
-
- tinyMCEPopup.restoreSelection();
- e = ed.dom.getParent(ed.selection.getNode(), 'A');
-
- // Remove element if there is no href
- if (!f.href.value) {
- if (e) {
- b = ed.selection.getBookmark();
- ed.dom.remove(e, 1);
- ed.selection.moveToBookmark(b);
- tinyMCEPopup.execCommand("mceEndUndoLevel");
- tinyMCEPopup.close();
- return;
- }
- }
-
- // Create new anchor elements
- if (e == null) {
- ed.getDoc().execCommand("unlink", false, null);
- tinyMCEPopup.execCommand("mceInsertLink", false, "#mce_temp_url#", {skip_undo : 1});
-
- tinymce.each(ed.dom.select("a"), function(n) {
- if (ed.dom.getAttrib(n, 'href') == '#mce_temp_url#') {
- e = n;
-
- ed.dom.setAttribs(e, {
- href : href,
- title : f.linktitle.value,
- target : f.target_list ? getSelectValue(f, "target_list") : null,
- 'class' : f.class_list ? getSelectValue(f, "class_list") : null
- });
- }
- });
- } else {
- ed.dom.setAttribs(e, {
- href : href,
- title : f.linktitle.value
- });
-
- if (f.target_list) {
- ed.dom.setAttrib(e, 'target', getSelectValue(f, "target_list"));
- }
-
- if (f.class_list) {
- ed.dom.setAttrib(e, 'class', getSelectValue(f, "class_list"));
- }
- }
-
- // Don't move caret if selection was image
- if (e.childNodes.length != 1 || e.firstChild.nodeName != 'IMG') {
- ed.focus();
- ed.selection.select(e);
- ed.selection.collapse(0);
- tinyMCEPopup.storeSelection();
- }
-
- tinyMCEPopup.execCommand("mceEndUndoLevel");
- tinyMCEPopup.close();
- },
-
- checkPrefix : function(n) {
- if (n.value && Validator.isEmail(n) && !/^\s*mailto:/i.test(n.value) && confirm(tinyMCEPopup.getLang('advanced_dlg.link_is_email')))
- n.value = 'mailto:' + n.value;
-
- if (/^\s*www\./i.test(n.value) && confirm(tinyMCEPopup.getLang('advanced_dlg.link_is_external')))
- n.value = 'http://' + n.value;
- },
-
- fillFileList : function(id, l) {
- var dom = tinyMCEPopup.dom, lst = dom.get(id), v, cl;
-
- l = window[l];
-
- if (l && l.length > 0) {
- lst.options[lst.options.length] = new Option('', '');
-
- tinymce.each(l, function(o) {
- lst.options[lst.options.length] = new Option(o[0], o[1]);
- });
- } else
- dom.remove(dom.getParent(id, 'tr'));
- },
-
- fillClassList : function(id) {
- var dom = tinyMCEPopup.dom, lst = dom.get(id), v, cl;
-
- if (v = tinyMCEPopup.getParam('theme_advanced_styles')) {
- cl = [];
-
- tinymce.each(v.split(';'), function(v) {
- var p = v.split('=');
-
- cl.push({'title' : p[0], 'class' : p[1]});
- });
- } else
- cl = tinyMCEPopup.editor.dom.getClasses();
-
- if (cl.length > 0) {
- lst.options[lst.options.length] = new Option(tinyMCEPopup.getLang('not_set'), '');
-
- tinymce.each(cl, function(o) {
- lst.options[lst.options.length] = new Option(o.title || o['class'], o['class']);
- });
- } else
- dom.remove(dom.getParent(id, 'tr'));
- },
-
- fillTargetList : function(id) {
- var dom = tinyMCEPopup.dom, lst = dom.get(id), v;
-
- lst.options[lst.options.length] = new Option(tinyMCEPopup.getLang('not_set'), '');
- lst.options[lst.options.length] = new Option(tinyMCEPopup.getLang('advanced_dlg.link_target_same'), '_self');
- lst.options[lst.options.length] = new Option(tinyMCEPopup.getLang('advanced_dlg.link_target_blank'), '_blank');
-
- if (v = tinyMCEPopup.getParam('theme_advanced_link_targets')) {
- tinymce.each(v.split(','), function(v) {
- v = v.split('=');
- lst.options[lst.options.length] = new Option(v[0], v[1]);
- });
- }
- }
-};
-
-LinkDialog.preInit();
-tinyMCEPopup.onInit.add(LinkDialog.init, LinkDialog);
diff --git a/resource/tinymce/themes/advanced/js/source_editor.js b/resource/tinymce/themes/advanced/js/source_editor.js
@@ -1,78 +0,0 @@
-tinyMCEPopup.requireLangPack();
-tinyMCEPopup.onInit.add(onLoadInit);
-
-function saveContent() {
- tinyMCEPopup.editor.setContent(document.getElementById('htmlSource').value, {source_view : true});
- tinyMCEPopup.close();
-}
-
-function onLoadInit() {
- tinyMCEPopup.resizeToInnerSize();
-
- // Remove Gecko spellchecking
- if (tinymce.isGecko)
- document.body.spellcheck = tinyMCEPopup.editor.getParam("gecko_spellcheck");
-
- document.getElementById('htmlSource').value = tinyMCEPopup.editor.getContent({source_view : true});
-
- if (tinyMCEPopup.editor.getParam("theme_advanced_source_editor_wrap", true)) {
- turnWrapOn();
- document.getElementById('wraped').checked = true;
- }
-
- resizeInputs();
-}
-
-function setWrap(val) {
- var v, n, s = document.getElementById('htmlSource');
-
- s.wrap = val;
-
- if (!tinymce.isIE) {
- v = s.value;
- n = s.cloneNode(false);
- n.setAttribute("wrap", val);
- s.parentNode.replaceChild(n, s);
- n.value = v;
- }
-}
-
-function setWhiteSpaceCss(value) {
- var el = document.getElementById('htmlSource');
- tinymce.DOM.setStyle(el, 'white-space', value);
-}
-
-function turnWrapOff() {
- if (tinymce.isWebKit) {
- setWhiteSpaceCss('pre');
- } else {
- setWrap('off');
- }
-}
-
-function turnWrapOn() {
- if (tinymce.isWebKit) {
- setWhiteSpaceCss('pre-wrap');
- } else {
- setWrap('soft');
- }
-}
-
-function toggleWordWrap(elm) {
- if (elm.checked) {
- turnWrapOn();
- } else {
- turnWrapOff();
- }
-}
-
-function resizeInputs() {
- var vp = tinyMCEPopup.dom.getViewPort(window), el;
-
- el = document.getElementById('htmlSource');
-
- if (el) {
- el.style.width = (vp.w - 20) + 'px';
- el.style.height = (vp.h - 65) + 'px';
- }
-}
diff --git a/resource/tinymce/themes/advanced/langs/en.js b/resource/tinymce/themes/advanced/langs/en.js
@@ -1 +0,0 @@
-tinyMCE.addI18n('en.advanced',{"underline_desc":"Underline (Ctrl+U)","italic_desc":"Italic (Ctrl+I)","bold_desc":"Bold (Ctrl+B)",dd:"Definition Description",dt:"Definition Term ",samp:"Code Sample",code:"Code",blockquote:"Block Quote",h6:"Heading 6",h5:"Heading 5",h4:"Heading 4",h3:"Heading 3",h2:"Heading 2",h1:"Heading 1",pre:"Preformatted",address:"Address",div:"DIV",paragraph:"Paragraph",block:"Format",fontdefault:"Font Family","font_size":"Font Size","style_select":"Styles","anchor_delta_height":"","anchor_delta_width":"","charmap_delta_height":"","charmap_delta_width":"","colorpicker_delta_height":"","colorpicker_delta_width":"","link_delta_height":"","link_delta_width":"","image_delta_height":"","image_delta_width":"","more_colors":"More Colors...","toolbar_focus":"Jump to tool buttons - Alt+Q, Jump to editor - Alt-Z, Jump to element path - Alt-X",newdocument:"Are you sure you want clear all contents?",path:"Path","clipboard_msg":"Copy/Cut/Paste is not available in Mozilla and Firefox.\nDo you want more information about this issue?","blockquote_desc":"Block Quote","help_desc":"Help","newdocument_desc":"New Document","image_props_desc":"Image Properties","paste_desc":"Paste (Ctrl+V)","copy_desc":"Copy (Ctrl+C)","cut_desc":"Cut (Ctrl+X)","anchor_desc":"Insert/Edit Anchor","visualaid_desc":"show/Hide Guidelines/Invisible Elements","charmap_desc":"Insert Special Character","backcolor_desc":"Select Background Color","forecolor_desc":"Select Text Color","custom1_desc":"Your Custom Description Here","removeformat_desc":"Remove Formatting","hr_desc":"Insert Horizontal Line","sup_desc":"Superscript","sub_desc":"Subscript","code_desc":"Edit HTML Source","cleanup_desc":"Cleanup Messy Code","image_desc":"Insert/Edit Image","unlink_desc":"Unlink","link_desc":"Insert/Edit Link","redo_desc":"Redo (Ctrl+Y)","undo_desc":"Undo (Ctrl+Z)","indent_desc":"Increase Indent","outdent_desc":"Decrease Indent","numlist_desc":"Insert/Remove Numbered List","bullist_desc":"Insert/Remove Bulleted List","justifyfull_desc":"Align Full","justifyright_desc":"Align Right","justifycenter_desc":"Align Center","justifyleft_desc":"Align Left","striketrough_desc":"Strikethrough","help_shortcut":"Press ALT-F10 for toolbar. Press ALT-0 for help","rich_text_area":"Rich Text Area","shortcuts_desc":"Accessability Help",toolbar:"Toolbar"});
-\ No newline at end of file
diff --git a/resource/tinymce/themes/advanced/langs/en_dlg.js b/resource/tinymce/themes/advanced/langs/en_dlg.js
@@ -1 +0,0 @@
-tinyMCE.addI18n('en.advanced_dlg', {"link_list":"Link List","link_is_external":"The URL you entered seems to be an external link. Do you want to add the required http:// prefix?","link_is_email":"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?","link_titlefield":"Title","link_target_blank":"Open Link in a New Window","link_target_same":"Open Link in the Same Window","link_target":"Target","link_url":"Link URL","link_title":"Insert/Edit Link","image_align_right":"Right","image_align_left":"Left","image_align_textbottom":"Text Bottom","image_align_texttop":"Text Top","image_align_bottom":"Bottom","image_align_middle":"Middle","image_align_top":"Top","image_align_baseline":"Baseline","image_align":"Alignment","image_hspace":"Horizontal Space","image_vspace":"Vertical Space","image_dimensions":"Dimensions","image_alt":"Image Description","image_list":"Image List","image_border":"Border","image_src":"Image URL","image_title":"Insert/Edit Image","charmap_title":"Select Special Character", "charmap_usage":"Use left and right arrows to navigate.","colorpicker_name":"Name:","colorpicker_color":"Color:","colorpicker_named_title":"Named Colors","colorpicker_named_tab":"Named","colorpicker_palette_title":"Palette Colors","colorpicker_palette_tab":"Palette","colorpicker_picker_title":"Color Picker","colorpicker_picker_tab":"Picker","colorpicker_title":"Select a Color","code_wordwrap":"Word Wrap","code_title":"HTML Source Editor","anchor_name":"Anchor Name","anchor_title":"Insert/Edit Anchor","about_loaded":"Loaded Plugins","about_version":"Version","about_author":"Author","about_plugin":"Plugin","about_plugins":"Plugins","about_license":"License","about_help":"Help","about_general":"About","about_title":"About TinyMCE","anchor_invalid":"Please specify a valid anchor name.","accessibility_help":"Accessibility Help","accessibility_usage_title":"General Usage","invalid_color_value":"Invalid color value","":""});
diff --git a/resource/tinymce/themes/advanced/link.htm b/resource/tinymce/themes/advanced/link.htm
@@ -1,58 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
-<head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <!-- Added by Dan S./Zotero -->
- <title>{#advanced_dlg.link_title}</title>
- <script type="text/javascript" src="../../tiny_mce_popup.js"></script>
- <script type="text/javascript" src="../../utils/mctabs.js"></script>
- <script type="text/javascript" src="../../utils/form_utils.js"></script>
- <script type="text/javascript" src="../../utils/validate.js"></script>
- <script type="text/javascript" src="js/link.js"></script>
-</head>
-<body id="link" style="display: none">
-<form onsubmit="LinkDialog.update();return false;" action="#">
- <div class="tabs">
- <ul>
- <li id="general_tab" class="current"><span><a href="javascript:mcTabs.displayTab('general_tab','general_panel');" onmousedown="return false;">{#advanced_dlg.link_title}</a></span></li>
- </ul>
- </div>
-
- <div class="panel_wrapper">
- <div id="general_panel" class="panel current">
- <table border="0" cellpadding="4" cellspacing="0">
- <tr>
- <td class="nowrap"><label for="href">{#advanced_dlg.link_url}</label></td>
- <td><table border="0" cellspacing="0" cellpadding="0">
- <tr>
- <td><input id="href" name="href" type="text" class="mceFocus" value="" style="width: 200px" onchange="LinkDialog.checkPrefix(this);" /></td>
- <td id="hrefbrowsercontainer"> </td>
- </tr>
- </table></td>
- </tr>
- <tr>
- <td><label for="link_list">{#advanced_dlg.link_list}</label></td>
- <td><select id="link_list" name="link_list" onchange="document.getElementById('href').value=this.options[this.selectedIndex].value;"></select></td>
- </tr>
- <tr>
- <td><label id="targetlistlabel" for="targetlist">{#advanced_dlg.link_target}</label></td>
- <td><select id="target_list" name="target_list"></select></td>
- </tr>
- <tr>
- <td class="nowrap"><label for="linktitle">{#advanced_dlg.link_titlefield}</label></td>
- <td><input id="linktitle" name="linktitle" type="text" value="" style="width: 200px" /></td>
- </tr>
- <tr>
- <td><label for="class_list">{#class_name}</label></td>
- <td><select id="class_list" name="class_list"></select></td>
- </tr>
- </table>
- </div>
- </div>
-
- <div class="mceActionPanel">
- <input type="submit" id="insert" name="insert" value="{#insert}" />
- <input type="button" id="cancel" name="cancel" value="{#cancel}" onclick="tinyMCEPopup.close();" />
- </div>
-</form>
-</body>
-</html>
diff --git a/resource/tinymce/themes/advanced/skins/default/content.css b/resource/tinymce/themes/advanced/skins/default/content.css
@@ -1,50 +0,0 @@
-body, td, pre {color:#000; font-family:Verdana, Arial, Helvetica, sans-serif; font-size:10px; margin:8px;}
-body {background:#FFF;}
-body.mceForceColors {background:#FFF; color:#000;}
-body.mceBrowserDefaults {background:transparent; color:inherit; font-size:inherit; font-family:inherit;}
-h1 {font-size: 2em}
-h2 {font-size: 1.5em}
-h3 {font-size: 1.17em}
-h4 {font-size: 1em}
-h5 {font-size: .83em}
-h6 {font-size: .75em}
-.mceItemTable, .mceItemTable td, .mceItemTable th, .mceItemTable caption, .mceItemVisualAid {border: 1px dashed #BBB;}
-a.mceItemAnchor {display:inline-block; -webkit-user-select:all; -webkit-user-modify:read-only; -moz-user-select:all; -moz-user-modify:read-only; width:11px !important; height:11px !important; background:url(img/items.gif) no-repeat center center}
-span.mceItemNbsp {background: #DDD}
-td.mceSelected, th.mceSelected {background-color:#3399ff !important}
-img {border:0;}
-table, img, hr, .mceItemAnchor {cursor:default}
-table td, table th {cursor:text}
-ins {border-bottom:1px solid green; text-decoration: none; color:green}
-del {color:red; text-decoration:line-through}
-cite {border-bottom:1px dashed blue}
-acronym {border-bottom:1px dotted #CCC; cursor:help}
-abbr {border-bottom:1px dashed #CCC; cursor:help}
-
-/* IE */
-* html body {
-scrollbar-3dlight-color:#F0F0EE;
-scrollbar-arrow-color:#676662;
-scrollbar-base-color:#F0F0EE;
-scrollbar-darkshadow-color:#DDD;
-scrollbar-face-color:#E0E0DD;
-scrollbar-highlight-color:#F0F0EE;
-scrollbar-shadow-color:#F0F0EE;
-scrollbar-track-color:#F5F5F5;
-}
-
-img:-moz-broken {-moz-force-broken-image-icon:1; width:24px; height:24px}
-font[face=mceinline] {font-family:inherit !important}
-*[contentEditable]:focus {outline:0}
-
-.mceItemMedia {border:1px dotted #cc0000; background-position:center; background-repeat:no-repeat; background-color:#ffffcc}
-.mceItemShockWave {background-image:url(../../img/shockwave.gif)}
-.mceItemFlash {background-image:url(../../img/flash.gif)}
-.mceItemQuickTime {background-image:url(../../img/quicktime.gif)}
-.mceItemWindowsMedia {background-image:url(../../img/windowsmedia.gif)}
-.mceItemRealMedia {background-image:url(../../img/realmedia.gif)}
-.mceItemVideo {background-image:url(../../img/video.gif)}
-.mceItemAudio {background-image:url(../../img/video.gif)}
-.mceItemEmbeddedAudio {background-image:url(../../img/video.gif)}
-.mceItemIframe {background-image:url(../../img/iframe.gif)}
-.mcePageBreak {display:block;border:0;width:100%;height:12px;border-top:1px dotted #ccc;margin-top:15px;background:#fff url(../../img/pagebreak.gif) no-repeat center top;}
diff --git a/resource/tinymce/themes/advanced/skins/default/dialog.css b/resource/tinymce/themes/advanced/skins/default/dialog.css
@@ -1,118 +0,0 @@
-/* Generic */
-body {
-font-family:Verdana, Arial, Helvetica, sans-serif; font-size:11px;
-scrollbar-3dlight-color:#F0F0EE;
-scrollbar-arrow-color:#676662;
-scrollbar-base-color:#F0F0EE;
-scrollbar-darkshadow-color:#DDDDDD;
-scrollbar-face-color:#E0E0DD;
-scrollbar-highlight-color:#F0F0EE;
-scrollbar-shadow-color:#F0F0EE;
-scrollbar-track-color:#F5F5F5;
-background:#F0F0EE;
-padding:0;
-margin:8px 8px 0 8px;
-}
-
-html {background:#F0F0EE;}
-td {font-family:Verdana, Arial, Helvetica, sans-serif; font-size:10px;}
-textarea {resize:none;outline:none;}
-a:link, a:visited {color:black;}
-a:hover {color:#2B6FB6;}
-.nowrap {white-space: nowrap}
-
-/* Forms */
-fieldset {margin:0; padding:4px; border:1px solid #919B9C; font-family:Verdana, Arial; font-size:10px;}
-legend {color:#2B6FB6; font-weight:bold;}
-label.msg {display:none;}
-label.invalid {color:#EE0000; display:inline;}
-input.invalid {border:1px solid #EE0000;}
-input {background:#FFF; border:1px solid #CCC;}
-input, select, textarea {font-family:Verdana, Arial, Helvetica, sans-serif; font-size:10px;}
-input, select, textarea {border:1px solid #808080;}
-input.radio {border:1px none #000000; background:transparent; vertical-align:middle;}
-input.checkbox {border:1px none #000000; background:transparent; vertical-align:middle;}
-.input_noborder {border:0;}
-
-/* Buttons */
-#insert, #cancel, input.button, .updateButton {
-border:0; margin:0; padding:0;
-font-weight:bold;
-width:94px; height:26px;
-background:url(img/buttons.png) 0 -26px;
-cursor:pointer;
-padding-bottom:2px;
-float:left;
-}
-
-#insert {background:url(img/buttons.png) 0 -52px}
-#cancel {background:url(img/buttons.png) 0 0; float:right}
-
-/* Browse */
-a.pickcolor, a.browse {text-decoration:none}
-a.browse span {display:block; width:20px; height:18px; background:url(../../img/icons.gif) -860px 0; border:1px solid #FFF; margin-left:1px;}
-.mceOldBoxModel a.browse span {width:22px; height:20px;}
-a.browse:hover span {border:1px solid #0A246A; background-color:#B2BBD0;}
-a.browse span.disabled {border:1px solid white; opacity:0.3; -ms-filter:'alpha(opacity=30)'; filter:alpha(opacity=30)}
-a.browse:hover span.disabled {border:1px solid white; background-color:transparent;}
-a.pickcolor span {display:block; width:20px; height:16px; background:url(../../img/icons.gif) -840px 0; margin-left:2px;}
-.mceOldBoxModel a.pickcolor span {width:21px; height:17px;}
-a.pickcolor:hover span {background-color:#B2BBD0;}
-a.pickcolor:hover span.disabled {}
-
-/* Charmap */
-table.charmap {border:1px solid #AAA; text-align:center}
-td.charmap, #charmap a {width:18px; height:18px; color:#000; border:1px solid #AAA; text-align:center; font-size:12px; vertical-align:middle; line-height: 18px;}
-#charmap a {display:block; color:#000; text-decoration:none; border:0}
-#charmap a:hover {background:#CCC;color:#2B6FB6}
-#charmap #codeN {font-size:10px; font-family:Arial,Helvetica,sans-serif; text-align:center}
-#charmap #codeV {font-size:40px; height:80px; border:1px solid #AAA; text-align:center}
-
-/* Source */
-.wordWrapCode {vertical-align:middle; border:1px none #000000; background:transparent;}
-.mceActionPanel {margin-top:5px;}
-
-/* Tabs classes */
-.tabs {width:100%; height:18px; line-height:normal; background:url(img/tabs.gif) repeat-x 0 -72px;}
-.tabs ul {margin:0; padding:0; list-style:none;}
-.tabs li {float:left; background:url(img/tabs.gif) no-repeat 0 0; margin:0 2px 0 0; padding:0 0 0 10px; line-height:17px; height:18px; display:block;}
-.tabs li.current {background:url(img/tabs.gif) no-repeat 0 -18px; margin-right:2px;}
-.tabs span {float:left; display:block; background:url(img/tabs.gif) no-repeat right -36px; padding:0px 10px 0 0;}
-.tabs .current span {background:url(img/tabs.gif) no-repeat right -54px;}
-.tabs a {text-decoration:none; font-family:Verdana, Arial; font-size:10px;}
-.tabs a:link, .tabs a:visited, .tabs a:hover {color:black;}
-
-/* Panels */
-.panel_wrapper div.panel {display:none;}
-.panel_wrapper div.current {display:block; width:100%; height:300px; overflow:visible;}
-.panel_wrapper {border:1px solid #919B9C; border-top:0px; padding:10px; padding-top:5px; clear:both; background:white;}
-
-/* Columns */
-.column {float:left;}
-.properties {width:100%;}
-.properties .column1 {}
-.properties .column2 {text-align:left;}
-
-/* Titles */
-h1, h2, h3, h4 {color:#2B6FB6; margin:0; padding:0; padding-top:5px;}
-h3 {font-size:14px;}
-.title {font-size:12px; font-weight:bold; color:#2B6FB6;}
-
-/* Dialog specific */
-#link .panel_wrapper, #link div.current {height:125px;}
-#image .panel_wrapper, #image div.current {height:200px;}
-#plugintable thead {font-weight:bold; background:#DDD;}
-#plugintable, #about #plugintable td {border:1px solid #919B9C;}
-#plugintable {width:96%; margin-top:10px;}
-#pluginscontainer {height:290px; overflow:auto;}
-#colorpicker #preview {display:inline-block; padding-left:40px; height:14px; border:1px solid black; margin-left:5px; margin-right: 5px}
-#colorpicker #previewblock {position: relative; top: -3px; padding-left:5px; padding-top: 0px; display:inline}
-#colorpicker #preview_wrapper { text-align:center; padding-top:4px; white-space: nowrap}
-#colorpicker #colors {float:left; border:1px solid gray; cursor:crosshair;}
-#colorpicker #light {border:1px solid gray; margin-left:5px; float:left;width:15px; height:150px; cursor:crosshair;}
-#colorpicker #light div {overflow:hidden;}
-#colorpicker .panel_wrapper div.current {height:175px;}
-#colorpicker #namedcolors {width:150px;}
-#colorpicker #namedcolors a {display:block; float:left; width:10px; height:10px; margin:1px 1px 0 0; overflow:hidden;}
-#colorpicker #colornamecontainer {margin-top:5px;}
-#colorpicker #picker_panel fieldset {margin:auto;width:325px;}
diff --git a/resource/tinymce/themes/advanced/skins/default/img/buttons.png b/resource/tinymce/themes/advanced/skins/default/img/buttons.png
Binary files differ.
diff --git a/resource/tinymce/themes/advanced/skins/default/img/items.gif b/resource/tinymce/themes/advanced/skins/default/img/items.gif
Binary files differ.
diff --git a/resource/tinymce/themes/advanced/skins/default/img/menu_arrow.gif b/resource/tinymce/themes/advanced/skins/default/img/menu_arrow.gif
Binary files differ.
diff --git a/resource/tinymce/themes/advanced/skins/default/img/menu_check.gif b/resource/tinymce/themes/advanced/skins/default/img/menu_check.gif
Binary files differ.
diff --git a/resource/tinymce/themes/advanced/skins/default/img/progress.gif b/resource/tinymce/themes/advanced/skins/default/img/progress.gif
Binary files differ.
diff --git a/resource/tinymce/themes/advanced/skins/default/img/tabs.gif b/resource/tinymce/themes/advanced/skins/default/img/tabs.gif
Binary files differ.
diff --git a/resource/tinymce/themes/advanced/skins/default/ui.css b/resource/tinymce/themes/advanced/skins/default/ui.css
@@ -1,219 +0,0 @@
-/* Reset */
-.defaultSkin table, .defaultSkin tbody, .defaultSkin a, .defaultSkin img, .defaultSkin tr, .defaultSkin div, .defaultSkin td, .defaultSkin iframe, .defaultSkin span, .defaultSkin *, .defaultSkin .mceText {border:0; margin:0; padding:0; background:transparent; white-space:nowrap; text-decoration:none; font-weight:normal; cursor:default; color:#000; vertical-align:baseline; width:auto; border-collapse:separate; text-align:left}
-.defaultSkin a:hover, .defaultSkin a:link, .defaultSkin a:visited, .defaultSkin a:active {text-decoration:none; font-weight:normal; cursor:default; color:#000}
-.defaultSkin table td {vertical-align:middle}
-
-/* Containers */
-.defaultSkin table {direction:ltr;background:transparent}
-.defaultSkin iframe {display:block;}
-.defaultSkin .mceToolbar {height:26px}
-.defaultSkin .mceLeft {text-align:left}
-.defaultSkin .mceRight {text-align:right}
-
-/* External */
-.defaultSkin .mceExternalToolbar {position:absolute; border:1px solid #CCC; border-bottom:0; display:none;}
-.defaultSkin .mceExternalToolbar td.mceToolbar {padding-right:13px;}
-.defaultSkin .mceExternalClose {position:absolute; top:3px; right:3px; width:7px; height:7px; background:url(../../img/icons.gif) -820px 0}
-
-/* Layout */
-.defaultSkin table.mceLayout {border:0; border-left:1px solid #CCC; border-right:1px solid #CCC}
-.defaultSkin table.mceLayout tr.mceFirst td {border-top:1px solid #CCC}
-.defaultSkin table.mceLayout tr.mceLast td {border-bottom:1px solid #CCC}
-.defaultSkin table.mceToolbar, .defaultSkin tr.mceFirst .mceToolbar tr td, .defaultSkin tr.mceLast .mceToolbar tr td {border:0; margin:0; padding:0;}
-.defaultSkin td.mceToolbar {background:#F0F0EE; padding-top:1px; vertical-align:top}
-.defaultSkin .mceIframeContainer {border-top:1px solid #CCC; border-bottom:1px solid #CCC}
-.defaultSkin .mceStatusbar {background:#F0F0EE; font-family:'MS Sans Serif',sans-serif,Verdana,Arial; font-size:9pt; line-height:16px; overflow:visible; color:#000; display:block; height:20px}
-.defaultSkin .mceStatusbar div {float:left; margin:2px}
-.defaultSkin .mceStatusbar a.mceResize {display:block; float:right; background:url(../../img/icons.gif) -800px 0; width:20px; height:20px; cursor:se-resize; outline:0}
-.defaultSkin .mceStatusbar a:hover {text-decoration:underline}
-.defaultSkin table.mceToolbar {margin-left:3px}
-.defaultSkin span.mceIcon, .defaultSkin img.mceIcon {display:block; width:20px; height:20px}
-.defaultSkin .mceIcon {background:url(../../img/icons.gif) no-repeat 20px 20px}
-.defaultSkin td.mceCenter {text-align:center;}
-.defaultSkin td.mceCenter table {margin:0 auto; text-align:left;}
-.defaultSkin td.mceRight table {margin:0 0 0 auto;}
-
-/* Button */
-.defaultSkin .mceButton {display:block; border:1px solid #F0F0EE; width:20px; height:20px; margin-right:1px}
-.defaultSkin a.mceButtonEnabled:hover {border:1px solid #0A246A; background-color:#B2BBD0}
-.defaultSkin a.mceButtonActive, .defaultSkin a.mceButtonSelected {border:1px solid #0A246A; background-color:#C2CBE0}
-.defaultSkin .mceButtonDisabled .mceIcon {opacity:0.3; -ms-filter:'alpha(opacity=30)'; filter:alpha(opacity=30)}
-.defaultSkin .mceButtonLabeled {width:auto}
-.defaultSkin .mceButtonLabeled span.mceIcon {float:left}
-.defaultSkin span.mceButtonLabel {display:block; font-size:10px; padding:4px 6px 0 22px; font-family:Tahoma,Verdana,Arial,Helvetica}
-.defaultSkin .mceButtonDisabled .mceButtonLabel {color:#888}
-
-/* Separator */
-.defaultSkin .mceSeparator {display:block; background:url(../../img/icons.gif) -180px 0; width:2px; height:20px; margin:2px 2px 0 4px}
-
-/* ListBox */
-.defaultSkin .mceListBox, .defaultSkin .mceListBox a {display:block}
-.defaultSkin .mceListBox .mceText {padding-left:4px; width:70px; text-align:left; border:1px solid #CCC; border-right:0; background:#FFF; font-family:Tahoma,Verdana,Arial,Helvetica; font-size:11px; height:20px; line-height:20px; overflow:hidden}
-.defaultSkin .mceListBox .mceOpen {width:9px; height:20px; background:url(../../img/icons.gif) -741px 0; margin-right:2px; border:1px solid #CCC;}
-.defaultSkin table.mceListBoxEnabled:hover .mceText, .defaultSkin .mceListBoxHover .mceText, .defaultSkin .mceListBoxSelected .mceText {border:1px solid #A2ABC0; border-right:0; background:#FFF}
-.defaultSkin table.mceListBoxEnabled:hover .mceOpen, .defaultSkin .mceListBoxHover .mceOpen, .defaultSkin .mceListBoxSelected .mceOpen {background-color:#FFF; border:1px solid #A2ABC0}
-.defaultSkin .mceListBoxDisabled a.mceText {color:gray; background-color:transparent;}
-.defaultSkin .mceListBoxMenu {overflow:auto; overflow-x:hidden}
-.defaultSkin .mceOldBoxModel .mceListBox .mceText {height:22px}
-.defaultSkin .mceOldBoxModel .mceListBox .mceOpen {width:11px; height:22px;}
-.defaultSkin select.mceNativeListBox {font-family:'MS Sans Serif',sans-serif,Verdana,Arial; font-size:7pt; background:#F0F0EE; border:1px solid gray; margin-right:2px;}
-
-/* SplitButton */
-.defaultSkin .mceSplitButton {width:32px; height:20px; direction:ltr}
-.defaultSkin .mceSplitButton a, .defaultSkin .mceSplitButton span {height:20px; display:block}
-.defaultSkin .mceSplitButton a.mceAction {width:20px; border:1px solid #F0F0EE; border-right:0;}
-.defaultSkin .mceSplitButton span.mceAction {width:20px; background-image:url(../../img/icons.gif);}
-.defaultSkin .mceSplitButton a.mceOpen {width:9px; background:url(../../img/icons.gif) -741px 0; border:1px solid #F0F0EE;}
-.defaultSkin .mceSplitButton span.mceOpen {display:none}
-.defaultSkin table.mceSplitButtonEnabled:hover a.mceAction, .defaultSkin .mceSplitButtonHover a.mceAction, .defaultSkin .mceSplitButtonSelected a.mceAction {border:1px solid #0A246A; border-right:0; background-color:#B2BBD0}
-.defaultSkin table.mceSplitButtonEnabled:hover a.mceOpen, .defaultSkin .mceSplitButtonHover a.mceOpen, .defaultSkin .mceSplitButtonSelected a.mceOpen {background-color:#B2BBD0; border:1px solid #0A246A;}
-.defaultSkin .mceSplitButtonDisabled .mceAction, .defaultSkin .mceSplitButtonDisabled a.mceOpen {opacity:0.3; -ms-filter:'alpha(opacity=30)'; filter:alpha(opacity=30)}
-.defaultSkin .mceSplitButtonActive a.mceAction {border:1px solid #0A246A; background-color:#C2CBE0}
-.defaultSkin .mceSplitButtonActive a.mceOpen {border-left:0;}
-
-/* ColorSplitButton */
-.defaultSkin div.mceColorSplitMenu table {background:#FFF; border:1px solid gray}
-.defaultSkin .mceColorSplitMenu td {padding:2px}
-.defaultSkin .mceColorSplitMenu a {display:block; width:9px; height:9px; overflow:hidden; border:1px solid #808080}
-.defaultSkin .mceColorSplitMenu td.mceMoreColors {padding:1px 3px 1px 1px}
-.defaultSkin .mceColorSplitMenu a.mceMoreColors {width:100%; height:auto; text-align:center; font-family:Tahoma,Verdana,Arial,Helvetica; font-size:11px; line-height:20px; border:1px solid #FFF}
-.defaultSkin .mceColorSplitMenu a.mceMoreColors:hover {border:1px solid #0A246A; background-color:#B6BDD2}
-.defaultSkin a.mceMoreColors:hover {border:1px solid #0A246A}
-.defaultSkin .mceColorPreview {margin-left:2px; width:16px; height:4px; overflow:hidden; background:#9a9b9a}
-.defaultSkin .mce_forecolor span.mceAction, .defaultSkin .mce_backcolor span.mceAction {overflow:hidden; height:16px}
-
-/* Menu */
-.defaultSkin .mceMenu {position:absolute; left:0; top:0; z-index:1000; border:1px solid #D4D0C8; direction:ltr}
-.defaultSkin .mceNoIcons span.mceIcon {width:0;}
-.defaultSkin .mceNoIcons a .mceText {padding-left:10px}
-.defaultSkin .mceMenu table {background:#FFF}
-.defaultSkin .mceMenu a, .defaultSkin .mceMenu span, .defaultSkin .mceMenu {display:block}
-.defaultSkin .mceMenu td {height:20px}
-.defaultSkin .mceMenu a {position:relative;padding:3px 0 4px 0}
-.defaultSkin .mceMenu .mceText {position:relative; display:block; font-family:Tahoma,Verdana,Arial,Helvetica; color:#000; cursor:default; margin:0; padding:0 25px 0 25px; display:block}
-.defaultSkin .mceMenu span.mceText, .defaultSkin .mceMenu .mcePreview {font-size:11px}
-.defaultSkin .mceMenu pre.mceText {font-family:Monospace}
-.defaultSkin .mceMenu .mceIcon {position:absolute; top:0; left:0; width:22px;}
-.defaultSkin .mceMenu .mceMenuItemEnabled a:hover, .defaultSkin .mceMenu .mceMenuItemActive {background-color:#dbecf3}
-.defaultSkin td.mceMenuItemSeparator {background:#DDD; height:1px}
-.defaultSkin .mceMenuItemTitle a {border:0; background:#EEE; border-bottom:1px solid #DDD}
-.defaultSkin .mceMenuItemTitle span.mceText {color:#000; font-weight:bold; padding-left:4px}
-.defaultSkin .mceMenuItemDisabled .mceText {color:#888}
-.defaultSkin .mceMenuItemSelected .mceIcon {background:url(img/menu_check.gif)}
-.defaultSkin .mceNoIcons .mceMenuItemSelected a {background:url(img/menu_arrow.gif) no-repeat -6px center}
-.defaultSkin .mceMenu span.mceMenuLine {display:none}
-.defaultSkin .mceMenuItemSub a {background:url(img/menu_arrow.gif) no-repeat top right;}
-.defaultSkin .mceMenuItem td, .defaultSkin .mceMenuItem th {line-height: normal}
-
-/* Progress,Resize */
-.defaultSkin .mceBlocker {position:absolute; left:0; top:0; z-index:1000; opacity:0.5; -ms-filter:'alpha(opacity=50)'; filter:alpha(opacity=50); background:#FFF}
-.defaultSkin .mceProgress {position:absolute; left:0; top:0; z-index:1001; background:url(img/progress.gif) no-repeat; width:32px; height:32px; margin:-16px 0 0 -16px}
-
-/* Rtl */
-.mceRtl .mceListBox .mceText {text-align: right; padding: 0 4px 0 0}
-.mceRtl .mceMenuItem .mceText {text-align: right}
-
-/* Formats */
-.defaultSkin .mce_formatPreview a {font-size:10px}
-.defaultSkin .mce_p span.mceText {}
-.defaultSkin .mce_address span.mceText {font-style:italic}
-.defaultSkin .mce_pre span.mceText {font-family:monospace}
-.defaultSkin .mce_h1 span.mceText {font-weight:bolder; font-size: 2em}
-.defaultSkin .mce_h2 span.mceText {font-weight:bolder; font-size: 1.5em}
-.defaultSkin .mce_h3 span.mceText {font-weight:bolder; font-size: 1.17em}
-.defaultSkin .mce_h4 span.mceText {font-weight:bolder; font-size: 1em}
-.defaultSkin .mce_h5 span.mceText {font-weight:bolder; font-size: .83em}
-.defaultSkin .mce_h6 span.mceText {font-weight:bolder; font-size: .75em}
-
-/* Theme */
-.defaultSkin span.mce_bold {background-position:0 0}
-.defaultSkin span.mce_italic {background-position:-60px 0}
-.defaultSkin span.mce_underline {background-position:-140px 0}
-.defaultSkin span.mce_strikethrough {background-position:-120px 0}
-.defaultSkin span.mce_undo {background-position:-160px 0}
-.defaultSkin span.mce_redo {background-position:-100px 0}
-.defaultSkin span.mce_cleanup {background-position:-40px 0}
-.defaultSkin span.mce_bullist {background-position:-20px 0}
-.defaultSkin span.mce_numlist {background-position:-80px 0}
-.defaultSkin span.mce_justifyleft {background-position:-460px 0}
-.defaultSkin span.mce_justifyright {background-position:-480px 0}
-.defaultSkin span.mce_justifycenter {background-position:-420px 0}
-.defaultSkin span.mce_justifyfull {background-position:-440px 0}
-.defaultSkin span.mce_anchor {background-position:-200px 0}
-.defaultSkin span.mce_indent {background-position:-400px 0}
-.defaultSkin span.mce_outdent {background-position:-540px 0}
-.defaultSkin span.mce_link {background-position:-500px 0}
-.defaultSkin span.mce_unlink {background-position:-640px 0}
-.defaultSkin span.mce_sub {background-position:-600px 0}
-.defaultSkin span.mce_sup {background-position:-620px 0}
-.defaultSkin span.mce_removeformat {background-position:-580px 0}
-.defaultSkin span.mce_newdocument {background-position:-520px 0}
-.defaultSkin span.mce_image {background-position:-380px 0}
-.defaultSkin span.mce_help {background-position:-340px 0}
-.defaultSkin span.mce_code {background-position:-260px 0}
-.defaultSkin span.mce_hr {background-position:-360px 0}
-.defaultSkin span.mce_visualaid {background-position:-660px 0}
-.defaultSkin span.mce_charmap {background-position:-240px 0}
-.defaultSkin span.mce_paste {background-position:-560px 0}
-.defaultSkin span.mce_copy {background-position:-700px 0}
-.defaultSkin span.mce_cut {background-position:-680px 0}
-.defaultSkin span.mce_blockquote {background-position:-220px 0}
-.defaultSkin .mce_forecolor span.mceAction {background-position:-720px 0}
-.defaultSkin .mce_backcolor span.mceAction {background-position:-760px 0}
-.defaultSkin span.mce_forecolorpicker {background-position:-720px 0}
-.defaultSkin span.mce_backcolorpicker {background-position:-760px 0}
-
-/* Plugins */
-.defaultSkin span.mce_advhr {background-position:-0px -20px}
-.defaultSkin span.mce_ltr {background-position:-20px -20px}
-.defaultSkin span.mce_rtl {background-position:-40px -20px}
-.defaultSkin span.mce_emotions {background-position:-60px -20px}
-.defaultSkin span.mce_fullpage {background-position:-80px -20px}
-.defaultSkin span.mce_fullscreen {background-position:-100px -20px}
-.defaultSkin span.mce_iespell {background-position:-120px -20px}
-.defaultSkin span.mce_insertdate {background-position:-140px -20px}
-.defaultSkin span.mce_inserttime {background-position:-160px -20px}
-.defaultSkin span.mce_absolute {background-position:-180px -20px}
-.defaultSkin span.mce_backward {background-position:-200px -20px}
-.defaultSkin span.mce_forward {background-position:-220px -20px}
-.defaultSkin span.mce_insert_layer {background-position:-240px -20px}
-.defaultSkin span.mce_insertlayer {background-position:-260px -20px}
-.defaultSkin span.mce_movebackward {background-position:-280px -20px}
-.defaultSkin span.mce_moveforward {background-position:-300px -20px}
-.defaultSkin span.mce_media {background-position:-320px -20px}
-.defaultSkin span.mce_nonbreaking {background-position:-340px -20px}
-.defaultSkin span.mce_pastetext {background-position:-360px -20px}
-.defaultSkin span.mce_pasteword {background-position:-380px -20px}
-.defaultSkin span.mce_selectall {background-position:-400px -20px}
-.defaultSkin span.mce_preview {background-position:-420px -20px}
-.defaultSkin span.mce_print {background-position:-440px -20px}
-.defaultSkin span.mce_cancel {background-position:-460px -20px}
-.defaultSkin span.mce_save {background-position:-480px -20px}
-.defaultSkin span.mce_replace {background-position:-500px -20px}
-.defaultSkin span.mce_search {background-position:-520px -20px}
-.defaultSkin span.mce_styleprops {background-position:-560px -20px}
-.defaultSkin span.mce_table {background-position:-580px -20px}
-.defaultSkin span.mce_cell_props {background-position:-600px -20px}
-.defaultSkin span.mce_delete_table {background-position:-620px -20px}
-.defaultSkin span.mce_delete_col {background-position:-640px -20px}
-.defaultSkin span.mce_delete_row {background-position:-660px -20px}
-.defaultSkin span.mce_col_after {background-position:-680px -20px}
-.defaultSkin span.mce_col_before {background-position:-700px -20px}
-.defaultSkin span.mce_row_after {background-position:-720px -20px}
-.defaultSkin span.mce_row_before {background-position:-740px -20px}
-.defaultSkin span.mce_merge_cells {background-position:-760px -20px}
-.defaultSkin span.mce_table_props {background-position:-980px -20px}
-.defaultSkin span.mce_row_props {background-position:-780px -20px}
-.defaultSkin span.mce_split_cells {background-position:-800px -20px}
-.defaultSkin span.mce_template {background-position:-820px -20px}
-.defaultSkin span.mce_visualchars {background-position:-840px -20px}
-.defaultSkin span.mce_abbr {background-position:-860px -20px}
-.defaultSkin span.mce_acronym {background-position:-880px -20px}
-.defaultSkin span.mce_attribs {background-position:-900px -20px}
-.defaultSkin span.mce_cite {background-position:-920px -20px}
-.defaultSkin span.mce_del {background-position:-940px -20px}
-.defaultSkin span.mce_ins {background-position:-960px -20px}
-.defaultSkin span.mce_pagebreak {background-position:0 -40px}
-.defaultSkin span.mce_restoredraft {background-position:-20px -40px}
-.defaultSkin span.mce_spellchecker {background-position:-540px -20px}
-.defaultSkin span.mce_visualblocks {background-position: -40px -40px}
diff --git a/resource/tinymce/themes/advanced/source_editor.htm b/resource/tinymce/themes/advanced/source_editor.htm
@@ -1,26 +0,0 @@
-<html xmlns="http://www.w3.org/1999/xhtml">
-<head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <!-- Added by Dan S./Zotero -->
- <title>{#advanced_dlg.code_title}</title>
- <script type="text/javascript" src="../../tiny_mce_popup.js"></script>
- <script type="text/javascript" src="js/source_editor.js"></script>
-</head>
-<body onresize="resizeInputs();" style="display:none; overflow:hidden;" spellcheck="false">
- <form name="source" onsubmit="saveContent();return false;" action="#">
- <div style="float: left" class="title"><label for="htmlSource">{#advanced_dlg.code_title}</label></div>
-
- <div id="wrapline" style="float: right">
- <input type="checkbox" name="wraped" id="wraped" onclick="toggleWordWrap(this);" class="wordWrapCode" /><label for="wraped">{#advanced_dlg.code_wordwrap}</label>
- </div>
-
- <br style="clear: both" />
-
- <textarea name="htmlSource" id="htmlSource" rows="15" cols="100" style="width: 100%; height: 100%; font-family: 'Courier New',Courier,monospace; font-size: 12px;" dir="ltr" wrap="off" class="mceFocus"></textarea>
-
- <div class="mceActionPanel">
- <input type="submit" role="button" name="insert" value="{#update}" id="insert" />
- <input type="button" role="button" name="cancel" value="{#cancel}" onclick="tinyMCEPopup.close();" id="cancel" />
- </div>
- </form>
-</body>
-</html>
diff --git a/resource/tinymce/themes/modern/theme.js b/resource/tinymce/themes/modern/theme.js
@@ -0,0 +1,1339 @@
+(function () {
+
+var defs = {}; // id -> {dependencies, definition, instance (possibly undefined)}
+
+// Used when there is no 'main' module.
+// The name is probably (hopefully) unique so minification removes for releases.
+var register_3795 = function (id) {
+ var module = dem(id);
+ var fragments = id.split('.');
+ var target = Function('return this;')();
+ for (var i = 0; i < fragments.length - 1; ++i) {
+ if (target[fragments[i]] === undefined)
+ target[fragments[i]] = {};
+ target = target[fragments[i]];
+ }
+ target[fragments[fragments.length - 1]] = module;
+};
+
+var instantiate = function (id) {
+ var actual = defs[id];
+ var dependencies = actual.deps;
+ var definition = actual.defn;
+ var len = dependencies.length;
+ var instances = new Array(len);
+ for (var i = 0; i < len; ++i)
+ instances[i] = dem(dependencies[i]);
+ var defResult = definition.apply(null, instances);
+ if (defResult === undefined)
+ throw 'module [' + id + '] returned undefined';
+ actual.instance = defResult;
+};
+
+var def = function (id, dependencies, definition) {
+ if (typeof id !== 'string')
+ throw 'module id must be a string';
+ else if (dependencies === undefined)
+ throw 'no dependencies for ' + id;
+ else if (definition === undefined)
+ throw 'no definition function for ' + id;
+ defs[id] = {
+ deps: dependencies,
+ defn: definition,
+ instance: undefined
+ };
+};
+
+var dem = function (id) {
+ var actual = defs[id];
+ if (actual === undefined)
+ throw 'module [' + id + '] was undefined';
+ else if (actual.instance === undefined)
+ instantiate(id);
+ return actual.instance;
+};
+
+var req = function (ids, callback) {
+ var len = ids.length;
+ var instances = new Array(len);
+ for (var i = 0; i < len; ++i)
+ instances.push(dem(ids[i]));
+ callback.apply(null, callback);
+};
+
+var ephox = {};
+
+ephox.bolt = {
+ module: {
+ api: {
+ define: def,
+ require: req,
+ demand: dem
+ }
+ }
+};
+
+var define = def;
+var require = req;
+var demand = dem;
+// this helps with minificiation when using a lot of global references
+var defineGlobal = function (id, ref) {
+ define(id, [], function () { return ref; });
+};
+/*jsc
+["tinymce.modern.Theme","global!tinymce.Env","global!tinymce.EditorManager","global!tinymce.ThemeManager","tinymce.modern.modes.Iframe","tinymce.modern.modes.Inline","tinymce.modern.ui.Resize","tinymce.modern.ui.ProgressState","global!tinymce.util.Tools","global!tinymce.ui.Factory","global!tinymce.DOM","tinymce.modern.ui.Toolbar","tinymce.modern.ui.Menubar","tinymce.modern.ui.ContextToolbars","tinymce.modern.ui.A11y","tinymce.modern.ui.Sidebar","tinymce.modern.ui.SkinLoaded","global!tinymce.ui.FloatPanel","global!tinymce.ui.Throbber","global!tinymce.util.Delay","global!tinymce.geom.Rect"]
+jsc*/
+defineGlobal("global!tinymce.Env", tinymce.Env);
+defineGlobal("global!tinymce.EditorManager", tinymce.EditorManager);
+defineGlobal("global!tinymce.ThemeManager", tinymce.ThemeManager);
+defineGlobal("global!tinymce.util.Tools", tinymce.util.Tools);
+defineGlobal("global!tinymce.ui.Factory", tinymce.ui.Factory);
+defineGlobal("global!tinymce.DOM", tinymce.DOM);
+/**
+ * Toolbar.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define('tinymce.modern.ui.Toolbar', [
+ 'global!tinymce.util.Tools',
+ 'global!tinymce.ui.Factory'
+], function (Tools, Factory) {
+ var defaultToolbar = "undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | " +
+ "bullist numlist outdent indent | link image";
+
+ var createToolbar = function (editor, items, size) {
+ var toolbarItems = [], buttonGroup;
+
+ if (!items) {
+ return;
+ }
+
+ Tools.each(items.split(/[ ,]/), function(item) {
+ var itemName;
+
+ var bindSelectorChanged = function () {
+ var selection = editor.selection;
+
+ if (item.settings.stateSelector) {
+ selection.selectorChanged(item.settings.stateSelector, function(state) {
+ item.active(state);
+ }, true);
+ }
+
+ if (item.settings.disabledStateSelector) {
+ selection.selectorChanged(item.settings.disabledStateSelector, function(state) {
+ item.disabled(state);
+ });
+ }
+ };
+
+ if (item == "|") {
+ buttonGroup = null;
+ } else {
+ if (Factory.has(item)) {
+ item = {type: item, size: size};
+ toolbarItems.push(item);
+ buttonGroup = null;
+ } else {
+ if (!buttonGroup) {
+ buttonGroup = {type: 'buttongroup', items: []};
+ toolbarItems.push(buttonGroup);
+ }
+
+ if (editor.buttons[item]) {
+ // TODO: Move control creation to some UI class
+ itemName = item;
+ item = editor.buttons[itemName];
+
+ if (typeof item == "function") {
+ item = item();
+ }
+
+ item.type = item.type || 'button';
+ item.size = size;
+
+ item = Factory.create(item);
+ buttonGroup.items.push(item);
+
+ if (editor.initialized) {
+ bindSelectorChanged();
+ } else {
+ editor.on('init', bindSelectorChanged);
+ }
+ }
+ }
+ }
+ });
+
+ return {
+ type: 'toolbar',
+ layout: 'flow',
+ items: toolbarItems
+ };
+ };
+
+ /**
+ * Creates the toolbars from config and returns a toolbar array.
+ *
+ * @param {String} size Optional toolbar item size.
+ * @return {Array} Array with toolbars.
+ */
+ var createToolbars = function (editor, size) {
+ var toolbars = [], settings = editor.settings;
+
+ var addToolbar = function (items) {
+ if (items) {
+ toolbars.push(createToolbar(editor, items, size));
+ return true;
+ }
+ };
+
+ // Convert toolbar array to multiple options
+ if (Tools.isArray(settings.toolbar)) {
+ // Empty toolbar array is the same as a disabled toolbar
+ if (settings.toolbar.length === 0) {
+ return;
+ }
+
+ Tools.each(settings.toolbar, function(toolbar, i) {
+ settings["toolbar" + (i + 1)] = toolbar;
+ });
+
+ delete settings.toolbar;
+ }
+
+ // Generate toolbar<n>
+ for (var i = 1; i < 10; i++) {
+ if (!addToolbar(settings["toolbar" + i])) {
+ break;
+ }
+ }
+
+ // Generate toolbar or default toolbar unless it's disabled
+ if (!toolbars.length && settings.toolbar !== false) {
+ addToolbar(settings.toolbar || defaultToolbar);
+ }
+
+ if (toolbars.length) {
+ return {
+ type: 'panel',
+ layout: 'stack',
+ classes: "toolbar-grp",
+ ariaRoot: true,
+ ariaRemember: true,
+ items: toolbars
+ };
+ }
+ };
+
+ return {
+ createToolbar: createToolbar,
+ createToolbars: createToolbars
+ };
+});
+
+/**
+ * Menubar.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define('tinymce.modern.ui.Menubar', [
+ 'global!tinymce.util.Tools'
+], function (Tools) {
+ var defaultMenus = {
+ file: {title: 'File', items: 'newdocument'},
+ edit: {title: 'Edit', items: 'undo redo | cut copy paste pastetext | selectall'},
+ insert: {title: 'Insert', items: '|'},
+ view: {title: 'View', items: 'visualaid |'},
+ format: {title: 'Format', items: 'bold italic underline strikethrough superscript subscript | formats | removeformat'},
+ table: {title: 'Table'},
+ tools: {title: 'Tools'}
+ };
+
+ var createMenuItem = function (menuItems, name) {
+ var menuItem;
+
+ if (name == '|') {
+ return {text: '|'};
+ }
+
+ menuItem = menuItems[name];
+
+ return menuItem;
+ };
+
+ var createMenu = function (editorMenuItems, settings, context) {
+ var menuButton, menu, menuItems, isUserDefined, removedMenuItems;
+
+ removedMenuItems = Tools.makeMap((settings.removed_menuitems || '').split(/[ ,]/));
+
+ // User defined menu
+ if (settings.menu) {
+ menu = settings.menu[context];
+ isUserDefined = true;
+ } else {
+ menu = defaultMenus[context];
+ }
+
+ if (menu) {
+ menuButton = {text: menu.title};
+ menuItems = [];
+
+ // Default/user defined items
+ Tools.each((menu.items || '').split(/[ ,]/), function(item) {
+ var menuItem = createMenuItem(editorMenuItems, item);
+
+ if (menuItem && !removedMenuItems[item]) {
+ menuItems.push(createMenuItem(editorMenuItems, item));
+ }
+ });
+
+ // Added though context
+ if (!isUserDefined) {
+ Tools.each(editorMenuItems, function(menuItem) {
+ if (menuItem.context == context) {
+ if (menuItem.separator == 'before') {
+ menuItems.push({text: '|'});
+ }
+
+ if (menuItem.prependToContext) {
+ menuItems.unshift(menuItem);
+ } else {
+ menuItems.push(menuItem);
+ }
+
+ if (menuItem.separator == 'after') {
+ menuItems.push({text: '|'});
+ }
+ }
+ });
+ }
+
+ for (var i = 0; i < menuItems.length; i++) {
+ if (menuItems[i].text == '|') {
+ if (i === 0 || i == menuItems.length - 1) {
+ menuItems.splice(i, 1);
+ }
+ }
+ }
+
+ menuButton.menu = menuItems;
+
+ if (!menuButton.menu.length) {
+ return null;
+ }
+ }
+
+ return menuButton;
+ };
+
+ var createMenuButtons = function (editor) {
+ var name, menuButtons = [], settings = editor.settings;
+
+ var defaultMenuBar = [];
+ if (settings.menu) {
+ for (name in settings.menu) {
+ defaultMenuBar.push(name);
+ }
+ } else {
+ for (name in defaultMenus) {
+ defaultMenuBar.push(name);
+ }
+ }
+
+ var enabledMenuNames = typeof settings.menubar == "string" ? settings.menubar.split(/[ ,]/) : defaultMenuBar;
+ for (var i = 0; i < enabledMenuNames.length; i++) {
+ var menu = enabledMenuNames[i];
+ menu = createMenu(editor.menuItems, editor.settings, menu);
+
+ if (menu) {
+ menuButtons.push(menu);
+ }
+ }
+
+ return menuButtons;
+ };
+
+ return {
+ createMenuButtons: createMenuButtons
+ };
+});
+
+defineGlobal("global!tinymce.util.Delay", tinymce.util.Delay);
+defineGlobal("global!tinymce.geom.Rect", tinymce.geom.Rect);
+/**
+ * ContextToolbars.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define('tinymce.modern.ui.ContextToolbars', [
+ 'global!tinymce.DOM',
+ 'global!tinymce.util.Tools',
+ 'global!tinymce.util.Delay',
+ 'tinymce.modern.ui.Toolbar',
+ 'global!tinymce.ui.Factory',
+ 'global!tinymce.geom.Rect'
+], function (DOM, Tools, Delay, Toolbar, Factory, Rect) {
+ var toClientRect = function (geomRect) {
+ return {
+ left: geomRect.x,
+ top: geomRect.y,
+ width: geomRect.w,
+ height: geomRect.h,
+ right: geomRect.x + geomRect.w,
+ bottom: geomRect.y + geomRect.h
+ };
+ };
+
+ var hideAllFloatingPanels = function (editor) {
+ Tools.each(editor.contextToolbars, function(toolbar) {
+ if (toolbar.panel) {
+ toolbar.panel.hide();
+ }
+ });
+ };
+
+ var movePanelTo = function (panel, pos) {
+ panel.moveTo(pos.left, pos.top);
+ };
+
+ var togglePositionClass = function (panel, relPos, predicate) {
+ relPos = relPos ? relPos.substr(0, 2) : '';
+
+ Tools.each({
+ t: 'down',
+ b: 'up'
+ }, function(cls, pos) {
+ panel.classes.toggle('arrow-' + cls, predicate(pos, relPos.substr(0, 1)));
+ });
+
+ Tools.each({
+ l: 'left',
+ r: 'right'
+ }, function(cls, pos) {
+ panel.classes.toggle('arrow-' + cls, predicate(pos, relPos.substr(1, 1)));
+ });
+ };
+
+ var userConstrain = function (handler, x, y, elementRect, contentAreaRect, panelRect) {
+ panelRect = toClientRect({x: x, y: y, w: panelRect.w, h: panelRect.h});
+
+ if (handler) {
+ panelRect = handler({
+ elementRect: toClientRect(elementRect),
+ contentAreaRect: toClientRect(contentAreaRect),
+ panelRect: panelRect
+ });
+ }
+
+ return panelRect;
+ };
+
+ var addContextualToolbars = function (editor) {
+ var scrollContainer, settings = editor.settings;
+
+ var getContextToolbars = function () {
+ return editor.contextToolbars || [];
+ };
+
+ var getElementRect = function (elm) {
+ var pos, targetRect, root;
+
+ pos = DOM.getPos(editor.getContentAreaContainer());
+ targetRect = editor.dom.getRect(elm);
+ root = editor.dom.getRoot();
+
+ // Adjust targetPos for scrolling in the editor
+ if (root.nodeName === 'BODY') {
+ targetRect.x -= root.ownerDocument.documentElement.scrollLeft || root.scrollLeft;
+ targetRect.y -= root.ownerDocument.documentElement.scrollTop || root.scrollTop;
+ }
+
+ targetRect.x += pos.x;
+ targetRect.y += pos.y;
+
+ return targetRect;
+ };
+
+ var reposition = function (match, shouldShow) {
+ var relPos, panelRect, elementRect, contentAreaRect, panel, relRect, testPositions, smallElementWidthThreshold;
+ var handler = settings.inline_toolbar_position_handler;
+
+ if (editor.removed) {
+ return;
+ }
+
+ if (!match || !match.toolbar.panel) {
+ hideAllFloatingPanels(editor);
+ return;
+ }
+
+ testPositions = [
+ 'bc-tc', 'tc-bc',
+ 'tl-bl', 'bl-tl',
+ 'tr-br', 'br-tr'
+ ];
+
+ panel = match.toolbar.panel;
+
+ // Only show the panel on some events not for example nodeChange since that fires when context menu is opened
+ if (shouldShow) {
+ panel.show();
+ }
+
+ elementRect = getElementRect(match.element);
+ panelRect = DOM.getRect(panel.getEl());
+ contentAreaRect = DOM.getRect(editor.getContentAreaContainer() || editor.getBody());
+ smallElementWidthThreshold = 25;
+
+ if (DOM.getStyle(match.element, 'display', true) !== 'inline') {
+ // We need to use these instead of the rect values since the style
+ // size properites might not be the same as the real size for a table
+ elementRect.w = match.element.clientWidth;
+ elementRect.h = match.element.clientHeight;
+ }
+
+ if (!editor.inline) {
+ contentAreaRect.w = editor.getDoc().documentElement.offsetWidth;
+ }
+
+ // Inflate the elementRect so it doesn't get placed above resize handles
+ if (editor.selection.controlSelection.isResizable(match.element) && elementRect.w < smallElementWidthThreshold) {
+ elementRect = Rect.inflate(elementRect, 0, 8);
+ }
+
+ relPos = Rect.findBestRelativePosition(panelRect, elementRect, contentAreaRect, testPositions);
+ elementRect = Rect.clamp(elementRect, contentAreaRect);
+
+ if (relPos) {
+ relRect = Rect.relativePosition(panelRect, elementRect, relPos);
+ movePanelTo(panel, userConstrain(handler, relRect.x, relRect.y, elementRect, contentAreaRect, panelRect));
+ } else {
+ // Allow overflow below the editor to avoid placing toolbars ontop of tables
+ contentAreaRect.h += panelRect.h;
+
+ elementRect = Rect.intersect(contentAreaRect, elementRect);
+ if (elementRect) {
+ relPos = Rect.findBestRelativePosition(panelRect, elementRect, contentAreaRect, [
+ 'bc-tc', 'bl-tl', 'br-tr'
+ ]);
+
+ if (relPos) {
+ relRect = Rect.relativePosition(panelRect, elementRect, relPos);
+ movePanelTo(panel, userConstrain(handler, relRect.x, relRect.y, elementRect, contentAreaRect, panelRect));
+ } else {
+ movePanelTo(panel, userConstrain(handler, elementRect.x, elementRect.y, elementRect, contentAreaRect, panelRect));
+ }
+ } else {
+ panel.hide();
+ }
+ }
+
+ togglePositionClass(panel, relPos, function(pos1, pos2) {
+ return pos1 === pos2;
+ });
+
+ //drawRect(contentAreaRect, 'blue');
+ //drawRect(elementRect, 'red');
+ //drawRect(panelRect, 'green');
+ };
+
+ var repositionHandler = function (show) {
+ return function () {
+ var execute = function () {
+ if (editor.selection) {
+ reposition(findFrontMostMatch(editor.selection.getNode()), show);
+ }
+ };
+
+ Delay.requestAnimationFrame(execute);
+ };
+ };
+
+ var bindScrollEvent = function () {
+ if (!scrollContainer) {
+ scrollContainer = editor.selection.getScrollContainer() || editor.getWin();
+ DOM.bind(scrollContainer, 'scroll', repositionHandler(true));
+
+ editor.on('remove', function() {
+ DOM.unbind(scrollContainer, 'scroll');
+ });
+ }
+ };
+
+ var showContextToolbar = function (match) {
+ var panel;
+
+ if (match.toolbar.panel) {
+ match.toolbar.panel.show();
+ reposition(match);
+ return;
+ }
+
+ bindScrollEvent();
+
+ panel = Factory.create({
+ type: 'floatpanel',
+ role: 'dialog',
+ classes: 'tinymce tinymce-inline arrow',
+ ariaLabel: 'Inline toolbar',
+ layout: 'flex',
+ direction: 'column',
+ align: 'stretch',
+ autohide: false,
+ autofix: true,
+ fixed: true,
+ border: 1,
+ items: Toolbar.createToolbar(editor, match.toolbar.items),
+ oncancel: function() {
+ editor.focus();
+ }
+ });
+
+ match.toolbar.panel = panel;
+ panel.renderTo(document.body).reflow();
+ reposition(match);
+ };
+
+ var hideAllContextToolbars = function () {
+ Tools.each(getContextToolbars(), function(toolbar) {
+ if (toolbar.panel) {
+ toolbar.panel.hide();
+ }
+ });
+ };
+
+ var findFrontMostMatch = function (targetElm) {
+ var i, y, parentsAndSelf, toolbars = getContextToolbars();
+
+ parentsAndSelf = editor.$(targetElm).parents().add(targetElm);
+ for (i = parentsAndSelf.length - 1; i >= 0; i--) {
+ for (y = toolbars.length - 1; y >= 0; y--) {
+ if (toolbars[y].predicate(parentsAndSelf[i])) {
+ return {
+ toolbar: toolbars[y],
+ element: parentsAndSelf[i]
+ };
+ }
+ }
+ }
+
+ return null;
+ };
+
+ editor.on('click keyup setContent ObjectResized', function(e) {
+ // Only act on partial inserts
+ if (e.type === 'setcontent' && !e.selection) {
+ return;
+ }
+
+ // Needs to be delayed to avoid Chrome img focus out bug
+ Delay.setEditorTimeout(editor, function() {
+ var match;
+
+ match = findFrontMostMatch(editor.selection.getNode());
+ if (match) {
+ hideAllContextToolbars();
+ showContextToolbar(match);
+ } else {
+ hideAllContextToolbars();
+ }
+ });
+ });
+
+ editor.on('blur hide contextmenu', hideAllContextToolbars);
+
+ editor.on('ObjectResizeStart', function() {
+ var match = findFrontMostMatch(editor.selection.getNode());
+
+ if (match && match.toolbar.panel) {
+ match.toolbar.panel.hide();
+ }
+ });
+
+ editor.on('ResizeEditor ResizeWindow', repositionHandler(true));
+ editor.on('nodeChange', repositionHandler(false));
+
+ editor.on('remove', function() {
+ Tools.each(getContextToolbars(), function(toolbar) {
+ if (toolbar.panel) {
+ toolbar.panel.remove();
+ }
+ });
+
+ editor.contextToolbars = {};
+ });
+
+ editor.shortcuts.add('ctrl+shift+e > ctrl+shift+p', '', function() {
+ var match = findFrontMostMatch(editor.selection.getNode());
+ if (match && match.toolbar.panel) {
+ match.toolbar.panel.items()[0].focus();
+ }
+ });
+ };
+
+ return {
+ addContextualToolbars: addContextualToolbars
+ };
+});
+
+/**
+ * A11y.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define('tinymce.modern.ui.A11y', [
+], function () {
+ var focus = function (panel, type) {
+ return function () {
+ var item = panel.find(type)[0];
+
+ if (item) {
+ item.focus(true);
+ }
+ };
+ };
+
+ var addKeys = function (editor, panel) {
+ editor.shortcuts.add('Alt+F9', '', focus(panel, 'menubar'));
+ editor.shortcuts.add('Alt+F10,F10', '', focus(panel, 'toolbar'));
+ editor.shortcuts.add('Alt+F11', '', focus(panel, 'elementpath'));
+ panel.on('cancel', function() {
+ editor.focus();
+ });
+ };
+
+ return {
+ addKeys: addKeys
+ };
+});
+
+/**
+ * Sidebar.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define('tinymce.modern.ui.Sidebar', [
+ 'global!tinymce.util.Tools',
+ 'global!tinymce.ui.Factory',
+ 'global!tinymce.Env'
+], function (Tools, Factory, Env) {
+ var api = function (elm) {
+ return {
+ element: function () {
+ return elm;
+ }
+ };
+ };
+
+ var trigger = function (sidebar, panel, callbackName) {
+ var callback = sidebar.settings[callbackName];
+ if (callback) {
+ callback(api(panel.getEl('body')));
+ }
+ };
+
+ var hidePanels = function (name, container, sidebars) {
+ Tools.each(sidebars, function (sidebar) {
+ var panel = container.items().filter('#' + sidebar.name)[0];
+
+ if (panel && panel.visible() && sidebar.name !== name) {
+ trigger(sidebar, panel, 'onhide');
+ panel.visible(false);
+ }
+ });
+ };
+
+ var deactivateButtons = function (toolbar) {
+ toolbar.items().each(function (ctrl) {
+ ctrl.active(false);
+ });
+ };
+
+ var findSidebar = function (sidebars, name) {
+ return Tools.grep(sidebars, function (sidebar) {
+ return sidebar.name === name;
+ })[0];
+ };
+
+ var showPanel = function (editor, name, sidebars) {
+ return function (e) {
+ var btnCtrl = e.control;
+ var container = btnCtrl.parents().filter('panel')[0];
+ var panel = container.find('#' + name)[0];
+ var sidebar = findSidebar(sidebars, name);
+
+ hidePanels(name, container, sidebars);
+ deactivateButtons(btnCtrl.parent());
+
+ if (panel && panel.visible()) {
+ trigger(sidebar, panel, 'onhide');
+ panel.hide();
+ btnCtrl.active(false);
+ } else {
+ if (panel) {
+ panel.show();
+ trigger(sidebar, panel, 'onshow');
+ } else {
+ panel = Factory.create({
+ type: 'container',
+ name: name,
+ layout: 'stack',
+ classes: 'sidebar-panel',
+ html: ''
+ });
+
+ container.prepend(panel);
+ trigger(sidebar, panel, 'onrender');
+ trigger(sidebar, panel, 'onshow');
+ }
+
+ btnCtrl.active(true);
+ }
+
+ editor.fire('ResizeEditor');
+ };
+ };
+
+ var isModernBrowser = function () {
+ return !Env.ie || Env.ie >= 11;
+ };
+
+ var hasSidebar = function (editor) {
+ return isModernBrowser() && editor.sidebars ? editor.sidebars.length > 0 : false;
+ };
+
+ var createSidebar = function (editor) {
+ var buttons = Tools.map(editor.sidebars, function (sidebar) {
+ var settings = sidebar.settings;
+
+ return {
+ type: 'button',
+ icon: settings.icon,
+ image: settings.image,
+ tooltip: settings.tooltip,
+ onclick: showPanel(editor, sidebar.name, editor.sidebars)
+ };
+ });
+
+ return {
+ type: 'panel',
+ name: 'sidebar',
+ layout: 'stack',
+ classes: 'sidebar',
+ items: [
+ {
+ type: 'toolbar',
+ layout: 'stack',
+ classes: 'sidebar-toolbar',
+ items: buttons
+ }
+ ]
+ };
+ };
+
+ return {
+ hasSidebar: hasSidebar,
+ createSidebar: createSidebar
+ };
+});
+/**
+ * SkinLoaded.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define('tinymce.modern.ui.SkinLoaded', [
+], function () {
+ var fireSkinLoaded = function (editor) {
+ return function() {
+ if (editor.initialized) {
+ editor.fire('SkinLoaded');
+ } else {
+ editor.on('init', function() {
+ editor.fire('SkinLoaded');
+ });
+ }
+ };
+ };
+
+ return {
+ fireSkinLoaded: fireSkinLoaded
+ };
+});
+
+/**
+ * Resize.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define('tinymce.modern.ui.Resize', [
+ 'global!tinymce.DOM'
+], function (DOM) {
+ var getSize = function (elm) {
+ return {
+ width: elm.clientWidth,
+ height: elm.clientHeight
+ };
+ };
+
+ var resizeTo = function (editor, width, height) {
+ var containerElm, iframeElm, containerSize, iframeSize, settings = editor.settings;
+
+ containerElm = editor.getContainer();
+ iframeElm = editor.getContentAreaContainer().firstChild;
+ containerSize = getSize(containerElm);
+ iframeSize = getSize(iframeElm);
+
+ if (width !== null) {
+ width = Math.max(settings.min_width || 100, width);
+ width = Math.min(settings.max_width || 0xFFFF, width);
+
+ DOM.setStyle(containerElm, 'width', width + (containerSize.width - iframeSize.width));
+ DOM.setStyle(iframeElm, 'width', width);
+ }
+
+ height = Math.max(settings.min_height || 100, height);
+ height = Math.min(settings.max_height || 0xFFFF, height);
+ DOM.setStyle(iframeElm, 'height', height);
+
+ editor.fire('ResizeEditor');
+ };
+
+ var resizeBy = function (editor, dw, dh) {
+ var elm = editor.getContentAreaContainer();
+ resizeTo(editor, elm.clientWidth + dw, elm.clientHeight + dh);
+ };
+
+ return {
+ resizeTo: resizeTo,
+ resizeBy: resizeBy
+ };
+});
+
+/**
+ * Iframe.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define('tinymce.modern.modes.Iframe', [
+ 'global!tinymce.util.Tools',
+ 'global!tinymce.ui.Factory',
+ 'global!tinymce.DOM',
+ 'tinymce.modern.ui.Toolbar',
+ 'tinymce.modern.ui.Menubar',
+ 'tinymce.modern.ui.ContextToolbars',
+ 'tinymce.modern.ui.A11y',
+ 'tinymce.modern.ui.Sidebar',
+ 'tinymce.modern.ui.SkinLoaded',
+ 'tinymce.modern.ui.Resize'
+], function (Tools, Factory, DOM, Toolbar, Menubar, ContextToolbars, A11y, Sidebar, SkinLoaded, Resize) {
+ var switchMode = function (panel) {
+ return function(e) {
+ panel.find('*').disabled(e.mode === 'readonly');
+ };
+ };
+
+ var editArea = function (border) {
+ return {
+ type: 'panel',
+ name: 'iframe',
+ layout: 'stack',
+ classes: 'edit-area',
+ border: border,
+ html: ''
+ };
+ };
+
+ var editAreaContainer = function (editor) {
+ return {
+ type: 'panel',
+ layout: 'stack',
+ classes: 'edit-aria-container',
+ border: '1 0 0 0',
+ items: [
+ editArea('0'),
+ Sidebar.createSidebar(editor)
+ ]
+ };
+ };
+
+ var render = function (editor, theme, args) {
+ var panel, resizeHandleCtrl, startSize, settings = editor.settings;
+
+ if (args.skinUiCss) {
+ DOM.styleSheetLoader.load(args.skinUiCss, SkinLoaded.fireSkinLoaded(editor));
+ }
+
+ panel = theme.panel = Factory.create({
+ type: 'panel',
+ role: 'application',
+ classes: 'tinymce',
+ style: 'visibility: hidden',
+ layout: 'stack',
+ border: 1,
+ items: [
+ settings.menubar === false ? null : {type: 'menubar', border: '0 0 1 0', items: Menubar.createMenuButtons(editor)},
+ Toolbar.createToolbars(editor, settings.toolbar_items_size),
+ Sidebar.hasSidebar(editor) ? editAreaContainer(editor) : editArea('1 0 0 0')
+ ]
+ });
+
+ if (settings.resize !== false) {
+ resizeHandleCtrl = {
+ type: 'resizehandle',
+ direction: settings.resize,
+
+ onResizeStart: function() {
+ var elm = editor.getContentAreaContainer().firstChild;
+
+ startSize = {
+ width: elm.clientWidth,
+ height: elm.clientHeight
+ };
+ },
+
+ onResize: function(e) {
+ if (settings.resize === 'both') {
+ Resize.resizeTo(editor, startSize.width + e.deltaX, startSize.height + e.deltaY);
+ } else {
+ Resize.resizeTo(editor, null, startSize.height + e.deltaY);
+ }
+ }
+ };
+ }
+
+ // Add statusbar if needed
+ if (settings.statusbar !== false) {
+ panel.add({type: 'panel', name: 'statusbar', classes: 'statusbar', layout: 'flow', border: '1 0 0 0', ariaRoot: true, items: [
+ {type: 'elementpath', editor: editor},
+ resizeHandleCtrl
+ ]});
+ }
+
+ editor.fire('BeforeRenderUI');
+ editor.on('SwitchMode', switchMode(panel));
+ panel.renderBefore(args.targetNode).reflow();
+
+ if (settings.readonly) {
+ editor.setMode('readonly');
+ }
+
+ if (settings.width) {
+ DOM.setStyle(panel.getEl(), 'width', settings.width);
+ }
+
+ // Remove the panel when the editor is removed
+ editor.on('remove', function() {
+ panel.remove();
+ panel = null;
+ });
+
+ // Add accesibility shortcuts
+ A11y.addKeys(editor, panel);
+ ContextToolbars.addContextualToolbars(editor);
+
+ return {
+ iframeContainer: panel.find('#iframe')[0].getEl(),
+ editorContainer: panel.getEl()
+ };
+ };
+
+ return {
+ render: render
+ };
+});
+
+defineGlobal("global!tinymce.ui.FloatPanel", tinymce.ui.FloatPanel);
+/**
+ * Inline.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define('tinymce.modern.modes.Inline', [
+ 'global!tinymce.util.Tools',
+ 'global!tinymce.ui.Factory',
+ 'global!tinymce.DOM',
+ 'global!tinymce.ui.FloatPanel',
+ 'tinymce.modern.ui.Toolbar',
+ 'tinymce.modern.ui.Menubar',
+ 'tinymce.modern.ui.ContextToolbars',
+ 'tinymce.modern.ui.A11y',
+ 'tinymce.modern.ui.SkinLoaded'
+], function (Tools, Factory, DOM, FloatPanel, Toolbar, Menubar, ContextToolbars, A11y, SkinLoaded) {
+ var render = function (editor, theme, args) {
+ var panel, inlineToolbarContainer, settings = editor.settings;
+
+ if (settings.fixed_toolbar_container) {
+ inlineToolbarContainer = DOM.select(settings.fixed_toolbar_container)[0];
+ }
+
+ var reposition = function () {
+ if (panel && panel.moveRel && panel.visible() && !panel._fixed) {
+ // TODO: This is kind of ugly and doesn't handle multiple scrollable elements
+ var scrollContainer = editor.selection.getScrollContainer(), body = editor.getBody();
+ var deltaX = 0, deltaY = 0;
+
+ if (scrollContainer) {
+ var bodyPos = DOM.getPos(body), scrollContainerPos = DOM.getPos(scrollContainer);
+
+ deltaX = Math.max(0, scrollContainerPos.x - bodyPos.x);
+ deltaY = Math.max(0, scrollContainerPos.y - bodyPos.y);
+ }
+
+ panel.fixed(false).moveRel(body, editor.rtl ? ['tr-br', 'br-tr'] : ['tl-bl', 'bl-tl', 'tr-br']).moveBy(deltaX, deltaY);
+ }
+ };
+
+ var show = function () {
+ if (panel) {
+ panel.show();
+ reposition();
+ DOM.addClass(editor.getBody(), 'mce-edit-focus');
+ }
+ };
+
+ var hide = function () {
+ if (panel) {
+ // We require two events as the inline float panel based toolbar does not have autohide=true
+ panel.hide();
+
+ // All other autohidden float panels will be closed below.
+ FloatPanel.hideAll();
+
+ DOM.removeClass(editor.getBody(), 'mce-edit-focus');
+ }
+ };
+
+ var render = function () {
+ if (panel) {
+ if (!panel.visible()) {
+ show();
+ }
+
+ return;
+ }
+
+ // Render a plain panel inside the inlineToolbarContainer if it's defined
+ panel = theme.panel = Factory.create({
+ type: inlineToolbarContainer ? 'panel' : 'floatpanel',
+ role: 'application',
+ classes: 'tinymce tinymce-inline',
+ layout: 'flex',
+ direction: 'column',
+ align: 'stretch',
+ autohide: false,
+ autofix: true,
+ fixed: !!inlineToolbarContainer,
+ border: 1,
+ items: [
+ settings.menubar === false ? null : {type: 'menubar', border: '0 0 1 0', items: Menubar.createMenuButtons(editor)},
+ Toolbar.createToolbars(editor, settings.toolbar_items_size)
+ ]
+ });
+
+ // Add statusbar
+ /*if (settings.statusbar !== false) {
+ panel.add({type: 'panel', classes: 'statusbar', layout: 'flow', border: '1 0 0 0', items: [
+ {type: 'elementpath'}
+ ]});
+ }*/
+
+ editor.fire('BeforeRenderUI');
+ panel.renderTo(inlineToolbarContainer || document.body).reflow();
+
+ A11y.addKeys(editor, panel);
+ show();
+ ContextToolbars.addContextualToolbars(editor);
+
+ editor.on('nodeChange', reposition);
+ editor.on('activate', show);
+ editor.on('deactivate', hide);
+
+ editor.nodeChanged();
+ };
+
+ settings.content_editable = true;
+
+ editor.on('focus', function() {
+ // Render only when the CSS file has been loaded
+ if (args.skinUiCss) {
+ DOM.styleSheetLoader.load(args.skinUiCss, render, render);
+ } else {
+ render();
+ }
+ });
+
+ editor.on('blur hide', hide);
+
+ // Remove the panel when the editor is removed
+ editor.on('remove', function() {
+ if (panel) {
+ panel.remove();
+ panel = null;
+ }
+ });
+
+ // Preload skin css
+ if (args.skinUiCss) {
+ DOM.styleSheetLoader.load(args.skinUiCss, SkinLoaded.fireSkinLoaded(editor));
+ }
+
+ return {};
+ };
+
+ return {
+ render: render
+ };
+});
+
+defineGlobal("global!tinymce.ui.Throbber", tinymce.ui.Throbber);
+/**
+ * ProgressState.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define('tinymce.modern.ui.ProgressState', [
+ 'global!tinymce.ui.Throbber'
+], function (Throbber) {
+ var setup = function (editor, theme) {
+ var throbber;
+
+ editor.on('ProgressState', function(e) {
+ throbber = throbber || new Throbber(theme.panel.getEl('body'));
+
+ if (e.state) {
+ throbber.show(e.time);
+ } else {
+ throbber.hide();
+ }
+ });
+ };
+
+ return {
+ setup: setup
+ };
+});
+
+/**
+ * Theme.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define('tinymce.modern.Theme', [
+ 'global!tinymce.Env',
+ 'global!tinymce.EditorManager',
+ 'global!tinymce.ThemeManager',
+ 'tinymce.modern.modes.Iframe',
+ 'tinymce.modern.modes.Inline',
+ 'tinymce.modern.ui.Resize',
+ 'tinymce.modern.ui.ProgressState'
+], function (Env, EditorManager, ThemeManager, Iframe, Inline, Resize, ProgressState) {
+ var renderUI = function(editor, theme, args) {
+ var settings = editor.settings;
+ var skin = settings.skin !== false ? settings.skin || 'lightgray' : false;
+
+ if (skin) {
+ var skinUrl = settings.skin_url;
+
+ if (skinUrl) {
+ skinUrl = editor.documentBaseURI.toAbsolute(skinUrl);
+ } else {
+ skinUrl = EditorManager.baseURL + '/skins/' + skin;
+ }
+
+ // Load special skin for IE7
+ // TODO: Remove this when we drop IE7 support
+ if (Env.documentMode <= 7) {
+ args.skinUiCss = skinUrl + '/skin.ie7.min.css';
+ } else {
+ args.skinUiCss = skinUrl + '/skin.min.css';
+ }
+
+ // Load content.min.css or content.inline.min.css
+ editor.contentCSS.push(skinUrl + '/content' + (editor.inline ? '.inline' : '') + '.min.css');
+ }
+
+ ProgressState.setup(editor, theme);
+
+ if (settings.inline) {
+ return Inline.render(editor, theme, args);
+ }
+
+ return Iframe.render(editor, theme, args);
+ };
+
+ ThemeManager.add('modern', function (editor) {
+ return {
+ renderUI: function (args) {
+ return renderUI(editor, this, args);
+ },
+ resizeTo: function (w, h) {
+ return Resize.resizeTo(editor, w, h);
+ },
+ resizeBy: function (dw, dh) {
+ return Resize.resizeBy(editor, dw, dh);
+ }
+ };
+ });
+
+ return function () {
+ };
+});
+
+dem('tinymce.modern.Theme')();
+})();
diff --git a/resource/tinymce/tiny_mce.js b/resource/tinymce/tiny_mce.js
@@ -1,19021 +0,0 @@
-// Contains modifications by Dan S./Zotero
-
-(function(win) {
- var whiteSpaceRe = /^\s*|\s*$/g,
- undef, isRegExpBroken = 'B'.replace(/A(.)|B/, '$1') === '$1';
-
- var tinymce = {
- majorVersion : '3',
-
- minorVersion : '5.7',
-
- releaseDate : '2012-09-20',
-
- _init : function() {
- // Modified by Dan S./Zotero
- //var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v;
- var t = this, d = document, na = navigator, ua = "Gecko " + na.platform, i, nl, n, base, p, v;
-
- t.isOpera = win.opera && opera.buildNumber;
-
- t.isWebKit = /WebKit/.test(ua);
-
- t.isIE = !t.isWebKit && !t.isOpera && (/MSIE/gi).test(ua) && (/Explorer/gi).test(na.appName);
-
- t.isIE6 = t.isIE && /MSIE [56]/.test(ua);
-
- t.isIE7 = t.isIE && /MSIE [7]/.test(ua);
-
- t.isIE8 = t.isIE && /MSIE [8]/.test(ua);
-
- t.isIE9 = t.isIE && /MSIE [9]/.test(ua);
-
- t.isGecko = !t.isWebKit && /Gecko/.test(ua);
-
- t.isMac = ua.indexOf('Mac') != -1;
-
- t.isAir = /adobeair/i.test(ua);
-
- t.isIDevice = /(iPad|iPhone)/.test(ua);
-
- t.isIOS5 = t.isIDevice && ua.match(/AppleWebKit\/(\d*)/)[1]>=534;
-
- // TinyMCE .NET webcontrol might be setting the values for TinyMCE
- if (win.tinyMCEPreInit) {
- t.suffix = tinyMCEPreInit.suffix;
- t.baseURL = tinyMCEPreInit.base;
- t.query = tinyMCEPreInit.query;
- return;
- }
-
- // Get suffix and base
- t.suffix = '';
-
- // If base element found, add that infront of baseURL
- nl = d.getElementsByTagName('base');
- for (i=0; i<nl.length; i++) {
- v = nl[i].href;
- if (v) {
- // Host only value like http://site.com or http://site.com:8008
- if (/^https?:\/\/[^\/]+$/.test(v))
- v += '/';
-
- base = v ? v.match(/.*\//)[0] : ''; // Get only directory
- }
- }
-
- function getBase(n) {
- if (n.src && /tiny_mce(|_gzip|_jquery|_prototype|_full)(_dev|_src)?.js/.test(n.src)) {
- if (/_(src|dev)\.js/g.test(n.src))
- t.suffix = '_src';
-
- if ((p = n.src.indexOf('?')) != -1)
- t.query = n.src.substring(p + 1);
-
- t.baseURL = n.src.substring(0, n.src.lastIndexOf('/'));
-
- // If path to script is relative and a base href was found add that one infront
- // the src property will always be an absolute one on non IE browsers and IE 8
- // so this logic will basically only be executed on older IE versions
- if (base && t.baseURL.indexOf('://') == -1 && t.baseURL.indexOf('/') !== 0)
- t.baseURL = base + t.baseURL;
-
- return t.baseURL;
- }
-
- return null;
- };
-
- // Check document
- nl = d.getElementsByTagName('script');
- for (i=0; i<nl.length; i++) {
- if (getBase(nl[i]))
- return;
- }
-
- // Check head
- n = d.getElementsByTagName('head')[0];
- if (n) {
- nl = n.getElementsByTagName('script');
- for (i=0; i<nl.length; i++) {
- if (getBase(nl[i]))
- return;
- }
- }
-
- return;
- },
-
- is : function(o, t) {
- if (!t)
- return o !== undef;
-
- if (t == 'array' && tinymce.isArray(o))
- return true;
-
- return typeof(o) == t;
- },
-
- isArray: Array.isArray || function(obj) {
- return Object.prototype.toString.call(obj) === "[object Array]";
- },
-
- makeMap : function(items, delim, map) {
- var i;
-
- items = items || [];
- delim = delim || ',';
-
- if (typeof(items) == "string")
- items = items.split(delim);
-
- map = map || {};
-
- i = items.length;
- while (i--)
- map[items[i]] = {};
-
- return map;
- },
-
- each : function(o, cb, s) {
- var n, l;
-
- if (!o)
- return 0;
-
- s = s || o;
-
- if (o.length !== undef) {
- // Indexed arrays, needed for Safari
- for (n=0, l = o.length; n < l; n++) {
- if (cb.call(s, o[n], n, o) === false)
- return 0;
- }
- } else {
- // Hashtables
- for (n in o) {
- if (o.hasOwnProperty(n)) {
- if (cb.call(s, o[n], n, o) === false)
- return 0;
- }
- }
- }
-
- return 1;
- },
-
-
- map : function(a, f) {
- var o = [];
-
- tinymce.each(a, function(v) {
- o.push(f(v));
- });
-
- return o;
- },
-
- grep : function(a, f) {
- var o = [];
-
- tinymce.each(a, function(v) {
- if (!f || f(v))
- o.push(v);
- });
-
- return o;
- },
-
- inArray : function(a, v) {
- var i, l;
-
- if (a) {
- for (i = 0, l = a.length; i < l; i++) {
- if (a[i] === v)
- return i;
- }
- }
-
- return -1;
- },
-
- extend : function(obj, ext) {
- var i, l, name, args = arguments, value;
-
- for (i = 1, l = args.length; i < l; i++) {
- ext = args[i];
- for (name in ext) {
- if (ext.hasOwnProperty(name)) {
- value = ext[name];
-
- if (value !== undef) {
- obj[name] = value;
- }
- }
- }
- }
-
- return obj;
- },
-
-
- trim : function(s) {
- return (s ? '' + s : '').replace(whiteSpaceRe, '');
- },
-
- create : function(s, p, root) {
- var t = this, sp, ns, cn, scn, c, de = 0;
-
- // Parse : <prefix> <class>:<super class>
- s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s);
- cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name
-
- // Create namespace for new class
- ns = t.createNS(s[3].replace(/\.\w+$/, ''), root);
-
- // Class already exists
- if (ns[cn])
- return;
-
- // Make pure static class
- if (s[2] == 'static') {
- ns[cn] = p;
-
- if (this.onCreate)
- this.onCreate(s[2], s[3], ns[cn]);
-
- return;
- }
-
- // Create default constructor
- if (!p[cn]) {
- p[cn] = function() {};
- de = 1;
- }
-
- // Add constructor and methods
- ns[cn] = p[cn];
- t.extend(ns[cn].prototype, p);
-
- // Extend
- if (s[5]) {
- sp = t.resolve(s[5]).prototype;
- scn = s[5].match(/\.(\w+)$/i)[1]; // Class name
-
- // Extend constructor
- c = ns[cn];
- if (de) {
- // Add passthrough constructor
- ns[cn] = function() {
- return sp[scn].apply(this, arguments);
- };
- } else {
- // Add inherit constructor
- ns[cn] = function() {
- this.parent = sp[scn];
- return c.apply(this, arguments);
- };
- }
- ns[cn].prototype[cn] = ns[cn];
-
- // Add super methods
- t.each(sp, function(f, n) {
- ns[cn].prototype[n] = sp[n];
- });
-
- // Add overridden methods
- t.each(p, function(f, n) {
- // Extend methods if needed
- if (sp[n]) {
- ns[cn].prototype[n] = function() {
- this.parent = sp[n];
- return f.apply(this, arguments);
- };
- } else {
- if (n != cn)
- ns[cn].prototype[n] = f;
- }
- });
- }
-
- // Add static methods
- t.each(p['static'], function(f, n) {
- ns[cn][n] = f;
- });
-
- if (this.onCreate)
- this.onCreate(s[2], s[3], ns[cn].prototype);
- },
-
- walk : function(o, f, n, s) {
- s = s || this;
-
- if (o) {
- if (n)
- o = o[n];
-
- tinymce.each(o, function(o, i) {
- if (f.call(s, o, i, n) === false)
- return false;
-
- tinymce.walk(o, f, n, s);
- });
- }
- },
-
- createNS : function(n, o) {
- var i, v;
-
- o = o || win;
-
- n = n.split('.');
- for (i=0; i<n.length; i++) {
- v = n[i];
-
- if (!o[v])
- o[v] = {};
-
- o = o[v];
- }
-
- return o;
- },
-
- resolve : function(n, o) {
- var i, l;
-
- o = o || win;
-
- n = n.split('.');
- for (i = 0, l = n.length; i < l; i++) {
- o = o[n[i]];
-
- if (!o)
- break;
- }
-
- return o;
- },
-
- addUnload : function(f, s) {
- var t = this, unload;
-
- unload = function() {
- var li = t.unloads, o, n;
-
- if (li) {
- // Call unload handlers
- for (n in li) {
- o = li[n];
-
- if (o && o.func)
- o.func.call(o.scope, 1); // Send in one arg to distinct unload and user destroy
- }
-
- // Detach unload function
- if (win.detachEvent) {
- win.detachEvent('onbeforeunload', fakeUnload);
- win.detachEvent('onunload', unload);
- } else if (win.removeEventListener)
- win.removeEventListener('unload', unload, false);
-
- // Destroy references
- t.unloads = o = li = w = unload = 0;
-
- // Run garbarge collector on IE
- if (win.CollectGarbage)
- CollectGarbage();
- }
- };
-
- function fakeUnload() {
- var d = document;
-
- function stop() {
- // Prevent memory leak
- d.detachEvent('onstop', stop);
-
- // Call unload handler
- if (unload)
- unload();
-
- d = 0;
- };
-
- // Is there things still loading, then do some magic
- if (d.readyState == 'interactive') {
- // Fire unload when the currently loading page is stopped
- if (d)
- d.attachEvent('onstop', stop);
-
- // Remove onstop listener after a while to prevent the unload function
- // to execute if the user presses cancel in an onbeforeunload
- // confirm dialog and then presses the browser stop button
- win.setTimeout(function() {
- if (d)
- d.detachEvent('onstop', stop);
- }, 0);
- }
- };
-
- f = {func : f, scope : s || this};
-
- if (!t.unloads) {
- // Attach unload handler
- if (win.attachEvent) {
- win.attachEvent('onunload', unload);
- win.attachEvent('onbeforeunload', fakeUnload);
- } else if (win.addEventListener)
- win.addEventListener('unload', unload, false);
-
- // Setup initial unload handler array
- t.unloads = [f];
- } else
- t.unloads.push(f);
-
- return f;
- },
-
- removeUnload : function(f) {
- var u = this.unloads, r = null;
-
- tinymce.each(u, function(o, i) {
- if (o && o.func == f) {
- u.splice(i, 1);
- r = f;
- return false;
- }
- });
-
- return r;
- },
-
- explode : function(s, d) {
- if (!s || tinymce.is(s, 'array')) {
- return s;
- }
-
- return tinymce.map(s.split(d || ','), tinymce.trim);
- },
-
- _addVer : function(u) {
- var v;
-
- if (!this.query)
- return u;
-
- v = (u.indexOf('?') == -1 ? '?' : '&') + this.query;
-
- if (u.indexOf('#') == -1)
- return u + v;
-
- return u.replace('#', v + '#');
- },
-
- // Fix function for IE 9 where regexps isn't working correctly
- // Todo: remove me once MS fixes the bug
- _replace : function(find, replace, str) {
- // On IE9 we have to fake $x replacement
- if (isRegExpBroken) {
- return str.replace(find, function() {
- var val = replace, args = arguments, i;
-
- for (i = 0; i < args.length - 2; i++) {
- if (args[i] === undef) {
- val = val.replace(new RegExp('\\$' + i, 'g'), '');
- } else {
- val = val.replace(new RegExp('\\$' + i, 'g'), args[i]);
- }
- }
-
- return val;
- });
- }
-
- return str.replace(find, replace);
- }
-
- };
-
- // Initialize the API
- tinymce._init();
-
- // Expose tinymce namespace to the global namespace (window)
- win.tinymce = win.tinyMCE = tinymce;
-
- // Describe the different namespaces
-
- })(window);
-
-
-
-tinymce.create('tinymce.util.Dispatcher', {
- scope : null,
- listeners : null,
- inDispatch: false,
-
- Dispatcher : function(scope) {
- this.scope = scope || this;
- this.listeners = [];
- },
-
- add : function(callback, scope) {
- this.listeners.push({cb : callback, scope : scope || this.scope});
-
- return callback;
- },
-
- addToTop : function(callback, scope) {
- var self = this, listener = {cb : callback, scope : scope || self.scope};
-
- // Create new listeners if addToTop is executed in a dispatch loop
- if (self.inDispatch) {
- self.listeners = [listener].concat(self.listeners);
- } else {
- self.listeners.unshift(listener);
- }
-
- return callback;
- },
-
- remove : function(callback) {
- var listeners = this.listeners, output = null;
-
- tinymce.each(listeners, function(listener, i) {
- if (callback == listener.cb) {
- output = listener;
- listeners.splice(i, 1);
- return false;
- }
- });
-
- return output;
- },
-
- dispatch : function() {
- var self = this, returnValue, args = arguments, i, listeners = self.listeners, listener;
-
- self.inDispatch = true;
-
- // Needs to be a real loop since the listener count might change while looping
- // And this is also more efficient
- for (i = 0; i < listeners.length; i++) {
- listener = listeners[i];
- returnValue = listener.cb.apply(listener.scope, args.length > 0 ? args : [listener.scope]);
-
- if (returnValue === false)
- break;
- }
-
- self.inDispatch = false;
-
- return returnValue;
- }
-
- });
-
-(function() {
- var each = tinymce.each;
-
- tinymce.create('tinymce.util.URI', {
- URI : function(u, s) {
- var t = this, o, a, b, base_url;
-
- // Trim whitespace
- u = tinymce.trim(u);
-
- // Default settings
- s = t.settings = s || {};
-
- // Strange app protocol that isn't http/https or local anchor
- // For example: mailto,skype,tel etc.
- if (/^([\w\-]+):([^\/]{2})/i.test(u) || /^\s*#/.test(u)) {
- t.source = u;
- return;
- }
-
- // Added by Dan S./Zotero
- u = u.replace("jar:file", "jarfile");
- u = u.replace("zotero@chnm.gmu.edu", "zotero.chnm.gmu.edu");
-
- // Absolute path with no host, fake host and protocol
- if (u.indexOf('/') === 0 && u.indexOf('//') !== 0)
- u = (s.base_uri ? s.base_uri.protocol || 'http' : 'http') + '://mce_host' + u;
-
- // Relative path http:// or protocol relative //path
- if (!/^[\w\-]*:?\/\//.test(u)) {
- base_url = s.base_uri ? s.base_uri.path : new tinymce.util.URI(location.href).directory;
- // Modified by Dan S./Zotero
- //u = ((s.base_uri && s.base_uri.protocol) || 'http') + '://mce_host' + t.toAbsPath(base_url, u);
- u = s.base_uri.protocol + '://' + s.base_uri.path + '/' + u;
- }
-
- // Added by Dan S./Zotero
- u = u.replace("jar:file", "jarfile");
- u = u.replace("zotero@chnm.gmu.edu", "zotero.chnm.gmu.edu");
-
- // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri)
- u = u.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something
- u = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(u);
- each(["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], function(v, i) {
- var s = u[i];
-
- // Zope 3 workaround, they use @@something
- if (s) {
- s = s.replace(/\(mce_at\)/g, '@@');
-
- // Modified by Dan S./Zotero
- s = s.replace("jarfile", "jar:file");
- s = s.replace("zotero.chnm.gmu.edu", "zotero@chnm.gmu.edu");
- }
-
- t[v] = s;
- });
-
- b = s.base_uri;
- if (b) {
- if (!t.protocol)
- t.protocol = b.protocol;
-
- if (!t.userInfo)
- t.userInfo = b.userInfo;
-
- if (!t.port && t.host === 'mce_host')
- t.port = b.port;
-
- if (!t.host || t.host === 'mce_host')
- t.host = b.host;
-
- t.source = '';
- }
-
- //t.path = t.path || '/';
- },
-
- setPath : function(p) {
- var t = this;
-
- p = /^(.*?)\/?(\w+)?$/.exec(p);
-
- // Update path parts
- t.path = p[0];
- t.directory = p[1];
- t.file = p[2];
-
- // Rebuild source
- t.source = '';
- t.getURI();
- },
-
- toRelative : function(u) {
- var t = this, o;
-
- if (u === "./")
- return u;
-
- u = new tinymce.util.URI(u, {base_uri : t});
-
- // Not on same domain/port or protocol
- if ((u.host != 'mce_host' && t.host != u.host && u.host) || t.port != u.port || t.protocol != u.protocol)
- return u.getURI();
-
- var tu = t.getURI(), uu = u.getURI();
-
- // Allow usage of the base_uri when relative_urls = true
- if(tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu))
- return tu;
-
- o = t.toRelPath(t.path, u.path);
-
- // Add query
- if (u.query)
- o += '?' + u.query;
-
- // Add anchor
- if (u.anchor)
- o += '#' + u.anchor;
-
- return o;
- },
-
- toAbsolute : function(u, nh) {
- u = new tinymce.util.URI(u, {base_uri : this});
-
- return u.getURI(this.host == u.host && this.protocol == u.protocol ? nh : 0);
- },
-
- toRelPath : function(base, path) {
- var items, bp = 0, out = '', i, l;
-
- // Split the paths
- base = base.substring(0, base.lastIndexOf('/'));
- base = base.split('/');
- items = path.split('/');
-
- if (base.length >= items.length) {
- for (i = 0, l = base.length; i < l; i++) {
- if (i >= items.length || base[i] != items[i]) {
- bp = i + 1;
- break;
- }
- }
- }
-
- if (base.length < items.length) {
- for (i = 0, l = items.length; i < l; i++) {
- if (i >= base.length || base[i] != items[i]) {
- bp = i + 1;
- break;
- }
- }
- }
-
- if (bp === 1)
- return path;
-
- for (i = 0, l = base.length - (bp - 1); i < l; i++)
- out += "../";
-
- for (i = bp - 1, l = items.length; i < l; i++) {
- if (i != bp - 1)
- out += "/" + items[i];
- else
- out += items[i];
- }
-
- return out;
- },
-
- toAbsPath : function(base, path) {
- var i, nb = 0, o = [], tr, outPath;
-
- // Split paths
- tr = /\/$/.test(path) ? '/' : '';
- base = base.split('/');
- path = path.split('/');
-
- // Remove empty chunks
- each(base, function(k) {
- if (k)
- o.push(k);
- });
-
- base = o;
-
- // Merge relURLParts chunks
- for (i = path.length - 1, o = []; i >= 0; i--) {
- // Ignore empty or .
- if (path[i].length === 0 || path[i] === ".")
- continue;
-
- // Is parent
- if (path[i] === '..') {
- nb++;
- continue;
- }
-
- // Move up
- if (nb > 0) {
- nb--;
- continue;
- }
-
- o.push(path[i]);
- }
-
- i = base.length - nb;
-
- // If /a/b/c or /
- if (i <= 0)
- outPath = o.reverse().join('/');
- else
- outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/');
-
- // Add front / if it's needed
- if (outPath.indexOf('/') !== 0)
- outPath = '/' + outPath;
-
- // Add traling / if it's needed
- if (tr && outPath.lastIndexOf('/') !== outPath.length - 1)
- outPath += tr;
-
- return outPath;
- },
-
- getURI : function(nh) {
- var s, t = this;
-
- // Rebuild source
- if (!t.source || nh) {
- s = '';
-
- if (!nh) {
- if (t.protocol)
- s += t.protocol + '://';
-
- if (t.userInfo)
- s += t.userInfo + '@';
-
- if (t.host)
- s += t.host;
-
- if (t.port)
- s += ':' + t.port;
- }
-
- if (t.path)
- s += t.path;
-
- if (t.query)
- s += '?' + t.query;
-
- if (t.anchor)
- s += '#' + t.anchor;
-
- t.source = s;
- }
-
- return t.source;
- }
- });
-})();
-
-(function() {
- var each = tinymce.each;
-
- tinymce.create('static tinymce.util.Cookie', {
- getHash : function(n) {
- var v = this.get(n), h;
-
- if (v) {
- each(v.split('&'), function(v) {
- v = v.split('=');
- h = h || {};
- h[unescape(v[0])] = unescape(v[1]);
- });
- }
-
- return h;
- },
-
- setHash : function(n, v, e, p, d, s) {
- var o = '';
-
- each(v, function(v, k) {
- o += (!o ? '' : '&') + escape(k) + '=' + escape(v);
- });
-
- this.set(n, o, e, p, d, s);
- },
-
- get : function(n) {
- var c = document.cookie, e, p = n + "=", b;
-
- // Strict mode
- if (!c)
- return;
-
- b = c.indexOf("; " + p);
-
- if (b == -1) {
- b = c.indexOf(p);
-
- if (b !== 0)
- return null;
- } else
- b += 2;
-
- e = c.indexOf(";", b);
-
- if (e == -1)
- e = c.length;
-
- return unescape(c.substring(b + p.length, e));
- },
-
- set : function(n, v, e, p, d, s) {
- document.cookie = n + "=" + escape(v) +
- ((e) ? "; expires=" + e.toGMTString() : "") +
- ((p) ? "; path=" + escape(p) : "") +
- ((d) ? "; domain=" + d : "") +
- ((s) ? "; secure" : "");
- },
-
- remove : function(name, path, domain) {
- var date = new Date();
-
- date.setTime(date.getTime() - 1000);
-
- this.set(name, '', date, path, domain);
- }
- });
-})();
-
-(function() {
- function serialize(o, quote) {
- var i, v, t, name;
-
- quote = quote || '"';
-
- if (o == null)
- return 'null';
-
- t = typeof o;
-
- if (t == 'string') {
- v = '\bb\tt\nn\ff\rr\""\'\'\\\\';
-
- return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) {
- // Make sure single quotes never get encoded inside double quotes for JSON compatibility
- if (quote === '"' && a === "'")
- return a;
-
- i = v.indexOf(b);
-
- if (i + 1)
- return '\\' + v.charAt(i + 1);
-
- a = b.charCodeAt().toString(16);
-
- return '\\u' + '0000'.substring(a.length) + a;
- }) + quote;
- }
-
- if (t == 'object') {
- if (o.hasOwnProperty && Object.prototype.toString.call(o) === '[object Array]') {
- for (i=0, v = '['; i<o.length; i++)
- v += (i > 0 ? ',' : '') + serialize(o[i], quote);
-
- return v + ']';
- }
-
- v = '{';
-
- for (name in o) {
- if (o.hasOwnProperty(name)) {
- v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name + quote +':' + serialize(o[name], quote) : '';
- }
- }
-
- return v + '}';
- }
-
- return '' + o;
- };
-
- tinymce.util.JSON = {
- serialize: serialize,
-
- parse: function(s) {
- try {
- return eval('(' + s + ')');
- } catch (ex) {
- // Ignore
- }
- }
-
- };
-})();
-
-tinymce.create('static tinymce.util.XHR', {
- send : function(o) {
- var x, t, w = window, c = 0;
-
- function ready() {
- if (!o.async || x.readyState == 4 || c++ > 10000) {
- // Modified by Dan S./Zotero
- //if (o.success && c < 10000 && x.status == 200)
- if (o.success && c < 10000 && (x.status == 200 || x.status == 0))
- o.success.call(o.success_scope, '' + x.responseText, x, o);
- else if (o.error)
- o.error.call(o.error_scope, c > 10000 ? 'TIMED_OUT' : 'GENERAL', x, o);
-
- x = null;
- } else
- w.setTimeout(ready, 10);
- };
-
- // Default settings
- o.scope = o.scope || this;
- o.success_scope = o.success_scope || o.scope;
- o.error_scope = o.error_scope || o.scope;
- o.async = o.async === false ? false : true;
- o.data = o.data || '';
-
- function get(s) {
- x = 0;
-
- try {
- x = new ActiveXObject(s);
- } catch (ex) {
- }
-
- return x;
- };
-
- x = w.XMLHttpRequest ? new XMLHttpRequest() : get('Microsoft.XMLHTTP') || get('Msxml2.XMLHTTP');
-
- if (x) {
- if (x.overrideMimeType)
- x.overrideMimeType(o.content_type);
-
- x.open(o.type || (o.data ? 'POST' : 'GET'), o.url, o.async);
-
- if (o.content_type)
- x.setRequestHeader('Content-Type', o.content_type);
-
- x.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
-
- x.send(o.data);
-
- // Syncronous request
- if (!o.async)
- return ready();
-
- // Wait for response, onReadyStateChange can not be used since it leaks memory in IE
- t = w.setTimeout(ready, 10);
- }
- }
-});
-
-(function() {
- var extend = tinymce.extend, JSON = tinymce.util.JSON, XHR = tinymce.util.XHR;
-
- tinymce.create('tinymce.util.JSONRequest', {
- JSONRequest : function(s) {
- this.settings = extend({
- }, s);
- this.count = 0;
- },
-
- send : function(o) {
- var ecb = o.error, scb = o.success;
-
- o = extend(this.settings, o);
-
- o.success = function(c, x) {
- c = JSON.parse(c);
-
- if (typeof(c) == 'undefined') {
- c = {
- error : 'JSON Parse error.'
- };
- }
-
- if (c.error)
- ecb.call(o.error_scope || o.scope, c.error, x);
- else
- scb.call(o.success_scope || o.scope, c.result);
- };
-
- o.error = function(ty, x) {
- if (ecb)
- ecb.call(o.error_scope || o.scope, ty, x);
- };
-
- o.data = JSON.serialize({
- id : o.id || 'c' + (this.count++),
- method : o.method,
- params : o.params
- });
-
- // JSON content type for Ruby on rails. Bug: #1883287
- o.content_type = 'application/json';
-
- XHR.send(o);
- },
-
- 'static' : {
- sendRPC : function(o) {
- return new tinymce.util.JSONRequest().send(o);
- }
- }
- });
-}());
-(function(tinymce){
- tinymce.VK = {
- BACKSPACE: 8,
- DELETE: 46,
- DOWN: 40,
- ENTER: 13,
- LEFT: 37,
- RIGHT: 39,
- SPACEBAR: 32,
- TAB: 9,
- UP: 38,
-
- modifierPressed: function (e) {
- return e.shiftKey || e.ctrlKey || e.altKey;
- },
-
- metaKeyPressed: function(e) {
- // Check if ctrl or meta key is pressed also check if alt is false for Polish users
- return tinymce.isMac ? e.metaKey : e.ctrlKey && !e.altKey;
- }
- };
-})(tinymce);
-
-tinymce.util.Quirks = function(editor) {
- var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection,
- settings = editor.settings, parser = editor.parser, serializer = editor.serializer;
-
- function setEditorCommandState(cmd, state) {
- try {
- editor.getDoc().execCommand(cmd, false, state);
- } catch (ex) {
- // Ignore
- }
- }
-
- function getDocumentMode() {
- var documentMode = editor.getDoc().documentMode;
-
- return documentMode ? documentMode : 6;
- };
-
- function isDefaultPrevented(e) {
- return e.isDefaultPrevented();
- };
-
- function cleanupStylesWhenDeleting() {
- function removeMergedFormatSpans(isDelete) {
- var rng, blockElm, node, clonedSpan;
-
- rng = selection.getRng();
-
- // Find root block
- blockElm = dom.getParent(rng.startContainer, dom.isBlock);
-
- // On delete clone the root span of the next block element
- if (isDelete)
- blockElm = dom.getNext(blockElm, dom.isBlock);
-
- // Locate root span element and clone it since it would otherwise get merged by the "apple-style-span" on delete/backspace
- if (blockElm) {
- node = blockElm.firstChild;
-
- // Ignore empty text nodes
- while (node && node.nodeType == 3 && node.nodeValue.length === 0)
- node = node.nextSibling;
-
- if (node && node.nodeName === 'SPAN') {
- clonedSpan = node.cloneNode(false);
- }
- }
-
- // Do the backspace/delete action
- editor.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null);
-
- // Find all odd apple-style-spans
- blockElm = dom.getParent(rng.startContainer, dom.isBlock);
- tinymce.each(dom.select('span.Apple-style-span,font.Apple-style-span', blockElm), function(span) {
- var bm = selection.getBookmark();
-
- if (clonedSpan) {
- dom.replace(clonedSpan.cloneNode(false), span, true);
- } else {
- dom.remove(span, true);
- }
-
- // Restore the selection
- selection.moveToBookmark(bm);
- });
- };
-
- editor.onKeyDown.add(function(editor, e) {
- var isDelete;
-
- isDelete = e.keyCode == DELETE;
- if (!isDefaultPrevented(e) && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) {
- e.preventDefault();
- removeMergedFormatSpans(isDelete);
- }
- });
-
- editor.addCommand('Delete', function() {removeMergedFormatSpans();});
- };
-
- function emptyEditorWhenDeleting() {
- function serializeRng(rng) {
- var body = dom.create("body");
- var contents = rng.cloneContents();
- body.appendChild(contents);
- return selection.serializer.serialize(body, {format: 'html'});
- }
-
- function allContentsSelected(rng) {
- var selection = serializeRng(rng);
-
- var allRng = dom.createRng();
- allRng.selectNode(editor.getBody());
-
- var allSelection = serializeRng(allRng);
- return selection === allSelection;
- }
-
- editor.onKeyDown.add(function(editor, e) {
- var keyCode = e.keyCode, isCollapsed;
-
- // Empty the editor if it's needed for example backspace at <p><b>|</b></p>
- if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) {
- isCollapsed = editor.selection.isCollapsed();
-
- // Selection is collapsed but the editor isn't empty
- if (isCollapsed && !dom.isEmpty(editor.getBody())) {
- return;
- }
-
- // IE deletes all contents correctly when everything is selected
- if (tinymce.isIE && !isCollapsed) {
- return;
- }
-
- // Selection isn't collapsed but not all the contents is selected
- if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) {
- return;
- }
-
- // Manually empty the editor
- editor.setContent('');
- editor.selection.setCursorLocation(editor.getBody(), 0);
- editor.nodeChanged();
- }
- });
- };
-
- function selectAll() {
- editor.onKeyDown.add(function(editor, e) {
- if (!isDefaultPrevented(e) && e.keyCode == 65 && VK.metaKeyPressed(e)) {
- e.preventDefault();
- editor.execCommand('SelectAll');
- }
- });
- };
-
- function inputMethodFocus() {
- if (!editor.settings.content_editable) {
- // Case 1 IME doesn't initialize if you focus the document
- dom.bind(editor.getDoc(), 'focusin', function(e) {
- selection.setRng(selection.getRng());
- });
-
- // Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event
- dom.bind(editor.getDoc(), 'mousedown', function(e) {
- if (e.target == editor.getDoc().documentElement) {
- editor.getWin().focus();
- selection.setRng(selection.getRng());
- }
- });
- }
- };
-
- function removeHrOnBackspace() {
- editor.onKeyDown.add(function(editor, e) {
- if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {
- if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) {
- var node = selection.getNode();
- var previousSibling = node.previousSibling;
-
- if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") {
- dom.remove(previousSibling);
- tinymce.dom.Event.cancel(e);
- }
- }
- }
- })
- }
-
- function focusBody() {
- // Fix for a focus bug in FF 3.x where the body element
- // wouldn't get proper focus if the user clicked on the HTML element
- if (!Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4
- editor.onMouseDown.add(function(editor, e) {
- if (!isDefaultPrevented(e) && e.target.nodeName === "HTML") {
- var body = editor.getBody();
-
- // Blur the body it's focused but not correctly focused
- body.blur();
-
- // Refocus the body after a little while
- setTimeout(function() {
- body.focus();
- }, 0);
- }
- });
- }
- };
-
- function selectControlElements() {
- editor.onClick.add(function(editor, e) {
- e = e.target;
-
- // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250
- // WebKit can't even do simple things like selecting an image
- // Needs tobe the setBaseAndExtend or it will fail to select floated images
- if (/^(IMG|HR)$/.test(e.nodeName)) {
- selection.getSel().setBaseAndExtent(e, 0, e, 1);
- }
-
- if (e.nodeName == 'A' && dom.hasClass(e, 'mceItemAnchor')) {
- selection.select(e);
- }
-
- editor.nodeChanged();
- });
- };
-
- function removeStylesWhenDeletingAccrossBlockElements() {
- function getAttributeApplyFunction() {
- var template = dom.getAttribs(selection.getStart().cloneNode(false));
-
- return function() {
- var target = selection.getStart();
-
- if (target !== editor.getBody()) {
- dom.setAttrib(target, "style", null);
-
- tinymce.each(template, function(attr) {
- target.setAttributeNode(attr.cloneNode(true));
- });
- }
- };
- }
-
- function isSelectionAcrossElements() {
- return !selection.isCollapsed() && dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock);
- }
-
- function blockEvent(editor, e) {
- e.preventDefault();
- return false;
- }
-
- editor.onKeyPress.add(function(editor, e) {
- var applyAttributes;
-
- if (!isDefaultPrevented(e) && (e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) {
- applyAttributes = getAttributeApplyFunction();
- editor.getDoc().execCommand('delete', false, null);
- applyAttributes();
- e.preventDefault();
- return false;
- }
- });
-
- dom.bind(editor.getDoc(), 'cut', function(e) {
- var applyAttributes;
-
- if (!isDefaultPrevented(e) && isSelectionAcrossElements()) {
- applyAttributes = getAttributeApplyFunction();
- editor.onKeyUp.addToTop(blockEvent);
-
- setTimeout(function() {
- applyAttributes();
- editor.onKeyUp.remove(blockEvent);
- }, 0);
- }
- });
- }
-
- function selectionChangeNodeChanged() {
- var lastRng, selectionTimer;
-
- dom.bind(editor.getDoc(), 'selectionchange', function() {
- if (selectionTimer) {
- clearTimeout(selectionTimer);
- selectionTimer = 0;
- }
-
- selectionTimer = window.setTimeout(function() {
- var rng = selection.getRng();
-
- // Compare the ranges to see if it was a real change or not
- if (!lastRng || !tinymce.dom.RangeUtils.compareRanges(rng, lastRng)) {
- editor.nodeChanged();
- lastRng = rng;
- }
- }, 50);
- });
- }
-
- function ensureBodyHasRoleApplication() {
- document.body.setAttribute("role", "application");
- }
-
- function disableBackspaceIntoATable() {
- editor.onKeyDown.add(function(editor, e) {
- if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {
- if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) {
- var previousSibling = selection.getNode().previousSibling;
- if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") {
- return tinymce.dom.Event.cancel(e);
- }
- }
- }
- })
- }
-
- function addNewLinesBeforeBrInPre() {
- // IE8+ rendering mode does the right thing with BR in PRE
- if (getDocumentMode() > 7) {
- return;
- }
-
- // Enable display: none in area and add a specific class that hides all BR elements in PRE to
- // avoid the caret from getting stuck at the BR elements while pressing the right arrow key
- setEditorCommandState('RespectVisibilityInDesign', true);
- editor.contentStyles.push('.mceHideBrInPre pre br {display: none}');
- dom.addClass(editor.getBody(), 'mceHideBrInPre');
-
- // Adds a \n before all BR elements in PRE to get them visual
- parser.addNodeFilter('pre', function(nodes, name) {
- var i = nodes.length, brNodes, j, brElm, sibling;
-
- while (i--) {
- brNodes = nodes[i].getAll('br');
- j = brNodes.length;
- while (j--) {
- brElm = brNodes[j];
-
- // Add \n before BR in PRE elements on older IE:s so the new lines get rendered
- sibling = brElm.prev;
- if (sibling && sibling.type === 3 && sibling.value.charAt(sibling.value - 1) != '\n') {
- sibling.value += '\n';
- } else {
- brElm.parent.insert(new tinymce.html.Node('#text', 3), brElm, true).value = '\n';
- }
- }
- }
- });
-
- // Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible
- serializer.addNodeFilter('pre', function(nodes, name) {
- var i = nodes.length, brNodes, j, brElm, sibling;
-
- while (i--) {
- brNodes = nodes[i].getAll('br');
- j = brNodes.length;
- while (j--) {
- brElm = brNodes[j];
- sibling = brElm.prev;
- if (sibling && sibling.type == 3) {
- sibling.value = sibling.value.replace(/\r?\n$/, '');
- }
- }
- }
- });
- }
-
- function removePreSerializedStylesWhenSelectingControls() {
- dom.bind(editor.getBody(), 'mouseup', function(e) {
- var value, node = selection.getNode();
-
- // Moved styles to attributes on IMG eements
- if (node.nodeName == 'IMG') {
- // Convert style width to width attribute
- if (value = dom.getStyle(node, 'width')) {
- dom.setAttrib(node, 'width', value.replace(/[^0-9%]+/g, ''));
- dom.setStyle(node, 'width', '');
- }
-
- // Convert style height to height attribute
- if (value = dom.getStyle(node, 'height')) {
- dom.setAttrib(node, 'height', value.replace(/[^0-9%]+/g, ''));
- dom.setStyle(node, 'height', '');
- }
- }
- });
- }
-
- function keepInlineElementOnDeleteBackspace() {
- editor.onKeyDown.add(function(editor, e) {
- var isDelete, rng, container, offset, brElm, sibling, collapsed;
-
- isDelete = e.keyCode == DELETE;
- if (!isDefaultPrevented(e) && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) {
- rng = selection.getRng();
- container = rng.startContainer;
- offset = rng.startOffset;
- collapsed = rng.collapsed;
-
- // Override delete if the start container is a text node and is at the beginning of text or
- // just before/after the last character to be deleted in collapsed mode
- if (container.nodeType == 3 && container.nodeValue.length > 0 && ((offset === 0 && !collapsed) || (collapsed && offset === (isDelete ? 0 : 1)))) {
- nonEmptyElements = editor.schema.getNonEmptyElements();
-
- // Prevent default logic since it's broken
- e.preventDefault();
-
- // Insert a BR before the text node this will prevent the containing element from being deleted/converted
- brElm = dom.create('br', {id: '__tmp'});
- container.parentNode.insertBefore(brElm, container);
-
- // Do the browser delete
- editor.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null);
-
- // Check if the previous sibling is empty after deleting for example: <p><b></b>|</p>
- container = selection.getRng().startContainer;
- sibling = container.previousSibling;
- if (sibling && sibling.nodeType == 1 && !dom.isBlock(sibling) && dom.isEmpty(sibling) && !nonEmptyElements[sibling.nodeName.toLowerCase()]) {
- dom.remove(sibling);
- }
-
- // Remove the temp element we inserted
- dom.remove('__tmp');
- }
- }
- });
- }
-
- function removeBlockQuoteOnBackSpace() {
- // Add block quote deletion handler
- editor.onKeyDown.add(function(editor, e) {
- var rng, container, offset, root, parent;
-
- if (isDefaultPrevented(e) || e.keyCode != VK.BACKSPACE) {
- return;
- }
-
- rng = selection.getRng();
- container = rng.startContainer;
- offset = rng.startOffset;
- root = dom.getRoot();
- parent = container;
-
- if (!rng.collapsed || offset !== 0) {
- return;
- }
-
- while (parent && parent.parentNode && parent.parentNode.firstChild == parent && parent.parentNode != root) {
- parent = parent.parentNode;
- }
-
- // Is the cursor at the beginning of a blockquote?
- if (parent.tagName === 'BLOCKQUOTE') {
- // Remove the blockquote
- editor.formatter.toggle('blockquote', null, parent);
-
- // Move the caret to the beginning of container
- rng = dom.createRng();
- rng.setStart(container, 0);
- rng.setEnd(container, 0);
- selection.setRng(rng);
- }
- });
- };
-
- function setGeckoEditingOptions() {
- function setOpts() {
- editor._refreshContentEditable();
-
- setEditorCommandState("StyleWithCSS", false);
- setEditorCommandState("enableInlineTableEditing", false);
-
- if (!settings.object_resizing) {
- setEditorCommandState("enableObjectResizing", false);
- }
- };
-
- if (!settings.readonly) {
- editor.onBeforeExecCommand.add(setOpts);
- editor.onMouseDown.add(setOpts);
- }
- };
-
- function addBrAfterLastLinks() {
- function fixLinks(editor, o) {
- tinymce.each(dom.select('a'), function(node) {
- var parentNode = node.parentNode, root = dom.getRoot();
-
- if (parentNode.lastChild === node) {
- while (parentNode && !dom.isBlock(parentNode)) {
- if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) {
- return;
- }
-
- parentNode = parentNode.parentNode;
- }
-
- dom.add(parentNode, 'br', {'data-mce-bogus' : 1});
- }
- });
- };
-
- editor.onExecCommand.add(function(editor, cmd) {
- if (cmd === 'CreateLink') {
- fixLinks(editor);
- }
- });
-
- editor.onSetContent.add(selection.onSetContent.add(fixLinks));
- };
-
- function setDefaultBlockType() {
- if (settings.forced_root_block) {
- editor.onInit.add(function() {
- setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block);
- });
- }
- }
-
- function removeGhostSelection() {
- function repaint(sender, args) {
- if (!sender || !args.initial) {
- editor.execCommand('mceRepaint');
- }
- };
-
- editor.onUndo.add(repaint);
- editor.onRedo.add(repaint);
- editor.onSetContent.add(repaint);
- };
-
- function deleteControlItemOnBackSpace() {
- editor.onKeyDown.add(function(editor, e) {
- var rng;
-
- if (!isDefaultPrevented(e) && e.keyCode == BACKSPACE) {
- rng = editor.getDoc().selection.createRange();
- if (rng && rng.item) {
- e.preventDefault();
- editor.undoManager.beforeChange();
- dom.remove(rng.item(0));
- editor.undoManager.add();
- }
- }
- });
- };
-
- function renderEmptyBlocksFix() {
- var emptyBlocksCSS;
-
- // IE10+
- if (getDocumentMode() >= 10) {
- emptyBlocksCSS = '';
- tinymce.each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) {
- emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty';
- });
-
- editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}');
- }
- };
-
- function fakeImageResize() {
- var selectedElmX, selectedElmY, selectedElm, selectedElmGhost, selectedHandle, startX, startY, startW, startH, ratio,
- resizeHandles, width, height, rootDocument = document, editableDoc = editor.getDoc();
-
- if (!settings.object_resizing || settings.webkit_fake_resize === false) {
- return;
- }
-
- // Try disabling object resizing if WebKit implements resizing in the future
- setEditorCommandState("enableObjectResizing", false);
-
- // Details about each resize handle how to scale etc
- resizeHandles = {
- // Name: x multiplier, y multiplier, delta size x, delta size y
- n: [.5, 0, 0, -1],
- e: [1, .5, 1, 0],
- s: [.5, 1, 0, 1],
- w: [0, .5, -1, 0],
- nw: [0, 0, -1, -1],
- ne: [1, 0, 1, -1],
- se: [1, 1, 1, 1],
- sw : [0, 1, -1, 1]
- };
-
- function resizeElement(e) {
- var deltaX, deltaY;
-
- // Calc new width/height
- deltaX = e.screenX - startX;
- deltaY = e.screenY - startY;
-
- // Calc new size
- width = deltaX * selectedHandle[2] + startW;
- height = deltaY * selectedHandle[3] + startH;
-
- // Never scale down lower than 5 pixels
- width = width < 5 ? 5 : width;
- height = height < 5 ? 5 : height;
-
- // Constrain proportions when modifier key is pressed or if the nw, ne, sw, se corners are moved on an image
- if (VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0)) {
- width = Math.round(height / ratio);
- height = Math.round(width * ratio);
- }
-
- // Update ghost size
- dom.setStyles(selectedElmGhost, {
- width: width,
- height: height
- });
-
- // Update ghost X position if needed
- if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) {
- dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width));
- }
-
- // Update ghost Y position if needed
- if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) {
- dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height));
- }
- }
-
- function endResize() {
- function setSizeProp(name, value) {
- if (value) {
- // Resize by using style or attribute
- if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) {
- dom.setStyle(selectedElm, name, value);
- } else {
- dom.setAttrib(selectedElm, name, value);
- }
- }
- }
-
- // Set width/height properties
- setSizeProp('width', width);
- setSizeProp('height', height);
-
- dom.unbind(editableDoc, 'mousemove', resizeElement);
- dom.unbind(editableDoc, 'mouseup', endResize);
-
- if (rootDocument != editableDoc) {
- dom.unbind(rootDocument, 'mousemove', resizeElement);
- dom.unbind(rootDocument, 'mouseup', endResize);
- }
-
- // Remove ghost and update resize handle positions
- dom.remove(selectedElmGhost);
- showResizeRect(selectedElm);
- }
-
- function showResizeRect(targetElm) {
- var position, targetWidth, targetHeight;
-
- hideResizeRect();
-
- // Get position and size of target
- position = dom.getPos(targetElm);
- selectedElmX = position.x;
- selectedElmY = position.y;
- targetWidth = targetElm.offsetWidth;
- targetHeight = targetElm.offsetHeight;
-
- // Reset width/height if user selects a new image/table
- if (selectedElm != targetElm) {
- selectedElm = targetElm;
- width = height = 0;
- }
-
- tinymce.each(resizeHandles, function(handle, name) {
- var handleElm;
-
- // Get existing or render resize handle
- handleElm = dom.get('mceResizeHandle' + name);
- if (!handleElm) {
- handleElm = dom.add(editableDoc.documentElement, 'div', {
- id: 'mceResizeHandle' + name,
- 'class': 'mceResizeHandle',
- style: 'cursor:' + name + '-resize; margin:0; padding:0'
- });
-
- dom.bind(handleElm, 'mousedown', function(e) {
- e.preventDefault();
-
- endResize();
-
- startX = e.screenX;
- startY = e.screenY;
- startW = selectedElm.clientWidth;
- startH = selectedElm.clientHeight;
- ratio = startH / startW;
- selectedHandle = handle;
-
- selectedElmGhost = selectedElm.cloneNode(true);
- dom.addClass(selectedElmGhost, 'mceClonedResizable');
- dom.setStyles(selectedElmGhost, {
- left: selectedElmX,
- top: selectedElmY,
- margin: 0
- });
-
- editableDoc.documentElement.appendChild(selectedElmGhost);
-
- dom.bind(editableDoc, 'mousemove', resizeElement);
- dom.bind(editableDoc, 'mouseup', endResize);
-
- if (rootDocument != editableDoc) {
- dom.bind(rootDocument, 'mousemove', resizeElement);
- dom.bind(rootDocument, 'mouseup', endResize);
- }
- });
- } else {
- dom.show(handleElm);
- }
-
- // Position element
- dom.setStyles(handleElm, {
- left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2),
- top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2)
- });
- });
-
- // Only add resize rectangle on WebKit and only on images
- if (!tinymce.isOpera && selectedElm.nodeName == "IMG") {
- selectedElm.setAttribute('data-mce-selected', '1');
- }
- }
-
- function hideResizeRect() {
- if (selectedElm) {
- selectedElm.removeAttribute('data-mce-selected');
- }
-
- for (var name in resizeHandles) {
- dom.hide('mceResizeHandle' + name);
- }
- }
-
- // Add CSS for resize handles, cloned element and selected
- editor.contentStyles.push(
- '.mceResizeHandle {' +
- 'position: absolute;' +
- 'border: 1px solid black;' +
- 'background: #FFF;' +
- 'width: 5px;' +
- 'height: 5px;' +
- 'z-index: 10000' +
- '}' +
- '.mceResizeHandle:hover {' +
- 'background: #000' +
- '}' +
- 'img[data-mce-selected] {' +
- 'outline: 1px solid black' +
- '}' +
- 'img.mceClonedResizable, table.mceClonedResizable {' +
- 'position: absolute;' +
- 'outline: 1px dashed black;' +
- 'opacity: .5;' +
- 'z-index: 10000' +
- '}'
- );
-
- function updateResizeRect() {
- var controlElm = dom.getParent(selection.getNode(), 'table,img');
-
- // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v
- tinymce.each(dom.select('img[data-mce-selected]'), function(img) {
- img.removeAttribute('data-mce-selected');
- });
-
- if (controlElm) {
- showResizeRect(controlElm);
- } else {
- hideResizeRect();
- }
- }
-
- // Show/hide resize rect when image is selected
- editor.onNodeChange.add(updateResizeRect);
-
- // Fixes WebKit quirk where it returns IMG on getNode if caret is after last image in container
- dom.bind(editableDoc, 'selectionchange', updateResizeRect);
-
- // Remove the internal attribute when serializing the DOM
- editor.serializer.addAttributeFilter('data-mce-selected', function(nodes, name) {
- var i = nodes.length;
-
- while (i--) {
- nodes[i].attr(name, null);
- }
- });
- }
-
- function keepNoScriptContents() {
- if (getDocumentMode() < 9) {
- parser.addNodeFilter('noscript', function(nodes) {
- var i = nodes.length, node, textNode;
-
- while (i--) {
- node = nodes[i];
- textNode = node.firstChild;
-
- if (textNode) {
- node.attr('data-mce-innertext', textNode.value);
- }
- }
- });
-
- serializer.addNodeFilter('noscript', function(nodes) {
- var i = nodes.length, node, textNode, value;
-
- while (i--) {
- node = nodes[i];
- textNode = nodes[i].firstChild;
-
- if (textNode) {
- textNode.value = tinymce.html.Entities.decode(textNode.value);
- } else {
- // Old IE can't retain noscript value so an attribute is used to store it
- value = node.attributes.map['data-mce-innertext'];
- if (value) {
- node.attr('data-mce-innertext', null);
- textNode = new tinymce.html.Node('#text', 3);
- textNode.value = value;
- textNode.raw = true;
- node.append(textNode);
- }
- }
- }
- });
- }
- }
-
- // All browsers
- disableBackspaceIntoATable();
- removeBlockQuoteOnBackSpace();
- emptyEditorWhenDeleting();
-
- // WebKit
- if (tinymce.isWebKit) {
- keepInlineElementOnDeleteBackspace();
- cleanupStylesWhenDeleting();
- inputMethodFocus();
- selectControlElements();
- setDefaultBlockType();
-
- // iOS
- if (tinymce.isIDevice) {
- selectionChangeNodeChanged();
- } else {
- fakeImageResize();
- selectAll();
- }
- }
-
- // IE
- if (tinymce.isIE) {
- removeHrOnBackspace();
- ensureBodyHasRoleApplication();
- addNewLinesBeforeBrInPre();
- removePreSerializedStylesWhenSelectingControls();
- deleteControlItemOnBackSpace();
- renderEmptyBlocksFix();
- keepNoScriptContents();
- }
-
- // Gecko
- if (tinymce.isGecko) {
- removeHrOnBackspace();
- focusBody();
- removeStylesWhenDeletingAccrossBlockElements();
- setGeckoEditingOptions();
- addBrAfterLastLinks();
- removeGhostSelection();
- }
-
- // Opera
- if (tinymce.isOpera) {
- fakeImageResize();
- }
-};
-(function(tinymce) {
- var namedEntities, baseEntities, reverseEntities,
- attrsCharsRegExp = /[&<>\"\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
- textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
- rawCharsRegExp = /[<>&\"\']/g,
- entityRegExp = /&(#x|#)?([\w]+);/g,
- asciiMap = {
- 128 : "\u20AC", 130 : "\u201A", 131 : "\u0192", 132 : "\u201E", 133 : "\u2026", 134 : "\u2020",
- 135 : "\u2021", 136 : "\u02C6", 137 : "\u2030", 138 : "\u0160", 139 : "\u2039", 140 : "\u0152",
- 142 : "\u017D", 145 : "\u2018", 146 : "\u2019", 147 : "\u201C", 148 : "\u201D", 149 : "\u2022",
- 150 : "\u2013", 151 : "\u2014", 152 : "\u02DC", 153 : "\u2122", 154 : "\u0161", 155 : "\u203A",
- 156 : "\u0153", 158 : "\u017E", 159 : "\u0178"
- };
-
- // Raw entities
- baseEntities = {
- '\"' : '"', // Needs to be escaped since the YUI compressor would otherwise break the code
- "'" : ''',
- '<' : '<',
- '>' : '>',
- '&' : '&'
- };
-
- // Reverse lookup table for raw entities
- reverseEntities = {
- '<' : '<',
- '>' : '>',
- '&' : '&',
- '"' : '"',
- ''' : "'"
- };
-
- // Decodes text by using the browser
- function nativeDecode(text) {
- var elm;
-
- elm = document.createElement("div");
- elm.innerHTML = text;
-
- return elm.textContent || elm.innerText || text;
- };
-
- // Build a two way lookup table for the entities
- function buildEntitiesLookup(items, radix) {
- var i, chr, entity, lookup = {};
-
- if (items) {
- items = items.split(',');
- radix = radix || 10;
-
- // Build entities lookup table
- for (i = 0; i < items.length; i += 2) {
- chr = String.fromCharCode(parseInt(items[i], radix));
-
- // Only add non base entities
- if (!baseEntities[chr]) {
- entity = '&' + items[i + 1] + ';';
- lookup[chr] = entity;
- lookup[entity] = chr;
- }
- }
-
- return lookup;
- }
- };
-
- // Unpack entities lookup where the numbers are in radix 32 to reduce the size
- namedEntities = buildEntitiesLookup(
- '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' +
- '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' +
- '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' +
- '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' +
- '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' +
- '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' +
- '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' +
- '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' +
- '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' +
- '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' +
- 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' +
- 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' +
- 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' +
- 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' +
- 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' +
- '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' +
- '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' +
- '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' +
- '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' +
- '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' +
- 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' +
- 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' +
- 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' +
- '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' +
- '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32);
-
- tinymce.html = tinymce.html || {};
-
- tinymce.html.Entities = {
- encodeRaw : function(text, attr) {
- return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
- return baseEntities[chr] || chr;
- });
- },
-
- encodeAllRaw : function(text) {
- return ('' + text).replace(rawCharsRegExp, function(chr) {
- return baseEntities[chr] || chr;
- });
- },
-
- encodeNumeric : function(text, attr) {
- return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
- // Multi byte sequence convert it to a single entity
- if (chr.length > 1)
- return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';';
-
- return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';';
- });
- },
-
- encodeNamed : function(text, attr, entities) {
- entities = entities || namedEntities;
-
- return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
- return baseEntities[chr] || entities[chr] || chr;
- });
- },
-
- getEncodeFunc : function(name, entities) {
- var Entities = tinymce.html.Entities;
-
- entities = buildEntitiesLookup(entities) || namedEntities;
-
- function encodeNamedAndNumeric(text, attr) {
- return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
- return baseEntities[chr] || entities[chr] || '&#' + chr.charCodeAt(0) + ';' || chr;
- });
- };
-
- function encodeCustomNamed(text, attr) {
- return Entities.encodeNamed(text, attr, entities);
- };
-
- // Replace + with , to be compatible with previous TinyMCE versions
- name = tinymce.makeMap(name.replace(/\+/g, ','));
-
- // Named and numeric encoder
- if (name.named && name.numeric)
- return encodeNamedAndNumeric;
-
- // Named encoder
- if (name.named) {
- // Custom names
- if (entities)
- return encodeCustomNamed;
-
- return Entities.encodeNamed;
- }
-
- // Numeric
- if (name.numeric)
- return Entities.encodeNumeric;
-
- // Raw encoder
- return Entities.encodeRaw;
- },
-
- decode : function(text) {
- return text.replace(entityRegExp, function(all, numeric, value) {
- if (numeric) {
- value = parseInt(value, numeric.length === 2 ? 16 : 10);
-
- // Support upper UTF
- if (value > 0xFFFF) {
- value -= 0x10000;
-
- return String.fromCharCode(0xD800 + (value >> 10), 0xDC00 + (value & 0x3FF));
- } else
- return asciiMap[value] || String.fromCharCode(value);
- }
-
- return reverseEntities[all] || namedEntities[all] || nativeDecode(all);
- });
- }
- };
-})(tinymce);
-
-tinymce.html.Styles = function(settings, schema) {
- var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi,
- urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi,
- styleRegExp = /\s*([^:]+):\s*([^;]+);?/g,
- trimRightRegExp = /\s+$/,
- urlColorRegExp = /rgb/,
- undef, i, encodingLookup = {}, encodingItems;
-
- settings = settings || {};
-
- encodingItems = '\\" \\\' \\; \\: ; : \uFEFF'.split(' ');
- for (i = 0; i < encodingItems.length; i++) {
- encodingLookup[encodingItems[i]] = '\uFEFF' + i;
- encodingLookup['\uFEFF' + i] = encodingItems[i];
- }
-
- function toHex(match, r, g, b) {
- function hex(val) {
- val = parseInt(val).toString(16);
-
- return val.length > 1 ? val : '0' + val; // 0 -> 00
- };
-
- return '#' + hex(r) + hex(g) + hex(b);
- };
-
- return {
- toHex : function(color) {
- return color.replace(rgbRegExp, toHex);
- },
-
- parse : function(css) {
- var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope || this;
-
- function compress(prefix, suffix) {
- var top, right, bottom, left;
-
- // Get values and check it it needs compressing
- top = styles[prefix + '-top' + suffix];
- if (!top)
- return;
-
- right = styles[prefix + '-right' + suffix];
- if (top != right)
- return;
-
- bottom = styles[prefix + '-bottom' + suffix];
- if (right != bottom)
- return;
-
- left = styles[prefix + '-left' + suffix];
- if (bottom != left)
- return;
-
- // Compress
- styles[prefix + suffix] = left;
- delete styles[prefix + '-top' + suffix];
- delete styles[prefix + '-right' + suffix];
- delete styles[prefix + '-bottom' + suffix];
- delete styles[prefix + '-left' + suffix];
- };
-
- function canCompress(key) {
- var value = styles[key], i;
-
- if (!value || value.indexOf(' ') < 0)
- return;
-
- value = value.split(' ');
- i = value.length;
- while (i--) {
- if (value[i] !== value[0])
- return false;
- }
-
- styles[key] = value[0];
-
- return true;
- };
-
- function compress2(target, a, b, c) {
- if (!canCompress(a))
- return;
-
- if (!canCompress(b))
- return;
-
- if (!canCompress(c))
- return;
-
- // Compress
- styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c];
- delete styles[a];
- delete styles[b];
- delete styles[c];
- };
-
- // Encodes the specified string by replacing all \" \' ; : with _<num>
- function encode(str) {
- isEncoded = true;
-
- return encodingLookup[str];
- };
-
- // Decodes the specified string by replacing all _<num> with it's original value \" \' etc
- // It will also decode the \" \' if keep_slashes is set to fale or omitted
- function decode(str, keep_slashes) {
- if (isEncoded) {
- str = str.replace(/\uFEFF[0-9]/g, function(str) {
- return encodingLookup[str];
- });
- }
-
- if (!keep_slashes)
- str = str.replace(/\\([\'\";:])/g, "$1");
-
- return str;
- };
-
- function processUrl(match, url, url2, url3, str, str2) {
- str = str || str2;
-
- if (str) {
- str = decode(str);
-
- // Force strings into single quote format
- return "'" + str.replace(/\'/g, "\\'") + "'";
- }
-
- url = decode(url || url2 || url3);
-
- // Convert the URL to relative/absolute depending on config
- if (urlConverter)
- url = urlConverter.call(urlConverterScope, url, 'style');
-
- // Output new URL format
- return "url('" + url.replace(/\'/g, "\\'") + "')";
- };
-
- if (css) {
- // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing
- css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) {
- return str.replace(/[;:]/g, encode);
- });
-
- // Parse styles
- while (matches = styleRegExp.exec(css)) {
- name = matches[1].replace(trimRightRegExp, '').toLowerCase();
- value = matches[2].replace(trimRightRegExp, '');
-
- if (name && value.length > 0) {
- // Opera will produce 700 instead of bold in their style values
- if (name === 'font-weight' && value === '700')
- value = 'bold';
- else if (name === 'color' || name === 'background-color') // Lowercase colors like RED
- value = value.toLowerCase();
-
- // Convert RGB colors to HEX
- value = value.replace(rgbRegExp, toHex);
-
- // Convert URLs and force them into url('value') format
- value = value.replace(urlOrStrRegExp, processUrl);
- styles[name] = isEncoded ? decode(value, true) : value;
- }
-
- styleRegExp.lastIndex = matches.index + matches[0].length;
- }
-
- // Compress the styles to reduce it's size for example IE will expand styles
- compress("border", "");
- compress("border", "-width");
- compress("border", "-color");
- compress("border", "-style");
- compress("padding", "");
- compress("margin", "");
- compress2('border', 'border-width', 'border-style', 'border-color');
-
- // Remove pointless border, IE produces these
- if (styles.border === 'medium none')
- delete styles.border;
- }
-
- return styles;
- },
-
- serialize : function(styles, element_name) {
- var css = '', name, value;
-
- function serializeStyles(name) {
- var styleList, i, l, value;
-
- styleList = schema.styles[name];
- if (styleList) {
- for (i = 0, l = styleList.length; i < l; i++) {
- name = styleList[i];
- value = styles[name];
-
- if (value !== undef && value.length > 0)
- css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
- }
- }
- };
-
- // Serialize styles according to schema
- if (element_name && schema && schema.styles) {
- // Serialize global styles and element specific styles
- serializeStyles('*');
- serializeStyles(element_name);
- } else {
- // Output the styles in the order they are inside the object
- for (name in styles) {
- value = styles[name];
-
- if (value !== undef && value.length > 0)
- css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
- }
- }
-
- return css;
- }
- };
-};
-
-(function(tinymce) {
- var mapCache = {}, makeMap = tinymce.makeMap, each = tinymce.each;
-
- function split(str, delim) {
- return str.split(delim || ',');
- };
-
- function unpack(lookup, data) {
- var key, elements = {};
-
- function replace(value) {
- return value.replace(/[A-Z]+/g, function(key) {
- return replace(lookup[key]);
- });
- };
-
- // Unpack lookup
- for (key in lookup) {
- if (lookup.hasOwnProperty(key))
- lookup[key] = replace(lookup[key]);
- }
-
- // Unpack and parse data into object map
- replace(data).replace(/#/g, '#text').replace(/(\w+)\[([^\]]+)\]\[([^\]]*)\]/g, function(str, name, attributes, children) {
- attributes = split(attributes, '|');
-
- elements[name] = {
- attributes : makeMap(attributes),
- attributesOrder : attributes,
- children : makeMap(children, '|', {'#comment' : {}})
- }
- });
-
- return elements;
- };
-
- function getHTML5() {
- var html5 = mapCache.html5;
-
- if (!html5) {
- html5 = mapCache.html5 = unpack({
- A : 'id|accesskey|class|dir|draggable|item|hidden|itemprop|role|spellcheck|style|subject|title|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup',
- B : '#|a|abbr|area|audio|b|bdo|br|button|canvas|cite|code|command|datalist|del|dfn|em|embed|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|meta|' +
- 'meter|noscript|object|output|progress|q|ruby|samp|script|select|small|span|strong|sub|sup|svg|textarea|time|var|video|wbr',
- C : '#|a|abbr|area|address|article|aside|audio|b|bdo|blockquote|br|button|canvas|cite|code|command|datalist|del|details|dfn|dialog|div|dl|em|embed|fieldset|' +
- 'figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|menu|meta|meter|nav|noscript|ol|object|output|' +
- 'p|pre|progress|q|ruby|samp|script|section|select|small|span|strong|style|sub|sup|svg|table|textarea|time|ul|var|video'
- }, 'html[A|manifest][body|head]' +
- 'head[A][base|command|link|meta|noscript|script|style|title]' +
- 'title[A][#]' +
- 'base[A|href|target][]' +
- 'link[A|href|rel|media|type|sizes][]' +
- 'meta[A|http-equiv|name|content|charset][]' +
- 'style[A|type|media|scoped][#]' +
- 'script[A|charset|type|src|defer|async][#]' +
- 'noscript[A][C]' +
- 'body[A][C]' +
- 'section[A][C]' +
- 'nav[A][C]' +
- 'article[A][C]' +
- 'aside[A][C]' +
- 'h1[A][B]' +
- 'h2[A][B]' +
- 'h3[A][B]' +
- 'h4[A][B]' +
- 'h5[A][B]' +
- 'h6[A][B]' +
- 'hgroup[A][h1|h2|h3|h4|h5|h6]' +
- 'header[A][C]' +
- 'footer[A][C]' +
- 'address[A][C]' +
- 'p[A][B]' +
- 'br[A][]' +
- 'pre[A][B]' +
- 'dialog[A][dd|dt]' +
- 'blockquote[A|cite][C]' +
- 'ol[A|start|reversed][li]' +
- 'ul[A][li]' +
- 'li[A|value][C]' +
- 'dl[A][dd|dt]' +
- 'dt[A][B]' +
- 'dd[A][C]' +
- 'a[A|href|target|ping|rel|media|type][B]' +
- 'em[A][B]' +
- 'strong[A][B]' +
- 'small[A][B]' +
- 'cite[A][B]' +
- 'q[A|cite][B]' +
- 'dfn[A][B]' +
- 'abbr[A][B]' +
- 'code[A][B]' +
- 'var[A][B]' +
- 'samp[A][B]' +
- 'kbd[A][B]' +
- 'sub[A][B]' +
- 'sup[A][B]' +
- 'i[A][B]' +
- 'b[A][B]' +
- 'mark[A][B]' +
- 'progress[A|value|max][B]' +
- 'meter[A|value|min|max|low|high|optimum][B]' +
- 'time[A|datetime][B]' +
- 'ruby[A][B|rt|rp]' +
- 'rt[A][B]' +
- 'rp[A][B]' +
- 'bdo[A][B]' +
- 'span[A][B]' +
- 'ins[A|cite|datetime][B]' +
- 'del[A|cite|datetime][B]' +
- 'figure[A][C|legend|figcaption]' +
- 'figcaption[A][C]' +
- 'img[A|alt|src|height|width|usemap|ismap][]' +
- 'iframe[A|name|src|height|width|sandbox|seamless][]' +
- 'embed[A|src|height|width|type][]' +
- 'object[A|data|type|height|width|usemap|name|form|classid][param]' +
- 'param[A|name|value][]' +
- 'details[A|open][C|legend]' +
- 'command[A|type|label|icon|disabled|checked|radiogroup][]' +
- 'menu[A|type|label][C|li]' +
- 'legend[A][C|B]' +
- 'div[A][C]' +
- 'source[A|src|type|media][]' +
- 'audio[A|src|autobuffer|autoplay|loop|controls][source]' +
- 'video[A|src|autobuffer|autoplay|loop|controls|width|height|poster][source]' +
- 'hr[A][]' +
- 'form[A|accept-charset|action|autocomplete|enctype|method|name|novalidate|target][C]' +
- 'fieldset[A|disabled|form|name][C|legend]' +
- 'label[A|form|for][B]' +
- 'input[A|type|accept|alt|autocomplete|autofocus|checked|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|height|list|max|maxlength|min|' +
- 'multiple|pattern|placeholder|readonly|required|size|src|step|width|files|value|name][]' +
- 'button[A|autofocus|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|name|value|type][B]' +
- 'select[A|autofocus|disabled|form|multiple|name|size][option|optgroup]' +
- 'datalist[A][B|option]' +
- 'optgroup[A|disabled|label][option]' +
- 'option[A|disabled|selected|label|value][]' +
- 'textarea[A|autofocus|disabled|form|maxlength|name|placeholder|readonly|required|rows|cols|wrap][]' +
- 'keygen[A|autofocus|challenge|disabled|form|keytype|name][]' +
- 'output[A|for|form|name][B]' +
- 'canvas[A|width|height][]' +
- 'map[A|name][B|C]' +
- 'area[A|shape|coords|href|alt|target|media|rel|ping|type][]' +
- 'mathml[A][]' +
- 'svg[A][]' +
- 'table[A|border][caption|colgroup|thead|tfoot|tbody|tr]' +
- 'caption[A][C]' +
- 'colgroup[A|span][col]' +
- 'col[A|span][]' +
- 'thead[A][tr]' +
- 'tfoot[A][tr]' +
- 'tbody[A][tr]' +
- 'tr[A][th|td]' +
- 'th[A|headers|rowspan|colspan|scope][B]' +
- 'td[A|headers|rowspan|colspan][C]' +
- 'wbr[A][]'
- );
- }
-
- return html5;
- };
-
- function getHTML4() {
- var html4 = mapCache.html4;
-
- if (!html4) {
- // This is the XHTML 1.0 transitional elements with it's attributes and children packed to reduce it's size
- html4 = mapCache.html4 = unpack({
- Z : 'H|K|N|O|P',
- Y : 'X|form|R|Q',
- ZG : 'E|span|width|align|char|charoff|valign',
- X : 'p|T|div|U|W|isindex|fieldset|table',
- ZF : 'E|align|char|charoff|valign',
- W : 'pre|hr|blockquote|address|center|noframes',
- ZE : 'abbr|axis|headers|scope|rowspan|colspan|align|char|charoff|valign|nowrap|bgcolor|width|height',
- ZD : '[E][S]',
- U : 'ul|ol|dl|menu|dir',
- ZC : 'p|Y|div|U|W|table|br|span|bdo|object|applet|img|map|K|N|Q',
- T : 'h1|h2|h3|h4|h5|h6',
- ZB : 'X|S|Q',
- S : 'R|P',
- ZA : 'a|G|J|M|O|P',
- R : 'a|H|K|N|O',
- Q : 'noscript|P',
- P : 'ins|del|script',
- O : 'input|select|textarea|label|button',
- N : 'M|L',
- M : 'em|strong|dfn|code|q|samp|kbd|var|cite|abbr|acronym',
- L : 'sub|sup',
- K : 'J|I',
- J : 'tt|i|b|u|s|strike',
- I : 'big|small|font|basefont',
- H : 'G|F',
- G : 'br|span|bdo',
- F : 'object|applet|img|map|iframe',
- E : 'A|B|C',
- D : 'accesskey|tabindex|onfocus|onblur',
- C : 'onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup',
- B : 'lang|xml:lang|dir',
- A : 'id|class|style|title'
- }, 'script[id|charset|type|language|src|defer|xml:space][]' +
- 'style[B|id|type|media|title|xml:space][]' +
- 'object[E|declare|classid|codebase|data|type|codetype|archive|standby|width|height|usemap|name|tabindex|align|border|hspace|vspace][#|param|Y]' +
- 'param[id|name|value|valuetype|type][]' +
- 'p[E|align][#|S]' +
- 'a[E|D|charset|type|name|href|hreflang|rel|rev|shape|coords|target][#|Z]' +
- 'br[A|clear][]' +
- 'span[E][#|S]' +
- 'bdo[A|C|B][#|S]' +
- 'applet[A|codebase|archive|code|object|alt|name|width|height|align|hspace|vspace][#|param|Y]' +
- 'h1[E|align][#|S]' +
- 'img[E|src|alt|name|longdesc|width|height|usemap|ismap|align|border|hspace|vspace][]' +
- 'map[B|C|A|name][X|form|Q|area]' +
- 'h2[E|align][#|S]' +
- 'iframe[A|longdesc|name|src|frameborder|marginwidth|marginheight|scrolling|align|width|height][#|Y]' +
- 'h3[E|align][#|S]' +
- 'tt[E][#|S]' +
- 'i[E][#|S]' +
- 'b[E][#|S]' +
- 'u[E][#|S]' +
- 's[E][#|S]' +
- 'strike[E][#|S]' +
- 'big[E][#|S]' +
- 'small[E][#|S]' +
- 'font[A|B|size|color|face][#|S]' +
- 'basefont[id|size|color|face][]' +
- 'em[E][#|S]' +
- 'strong[E][#|S]' +
- 'dfn[E][#|S]' +
- 'code[E][#|S]' +
- 'q[E|cite][#|S]' +
- 'samp[E][#|S]' +
- 'kbd[E][#|S]' +
- 'var[E][#|S]' +
- 'cite[E][#|S]' +
- 'abbr[E][#|S]' +
- 'acronym[E][#|S]' +
- 'sub[E][#|S]' +
- 'sup[E][#|S]' +
- 'input[E|D|type|name|value|checked|disabled|readonly|size|maxlength|src|alt|usemap|onselect|onchange|accept|align][]' +
- 'select[E|name|size|multiple|disabled|tabindex|onfocus|onblur|onchange][optgroup|option]' +
- 'optgroup[E|disabled|label][option]' +
- 'option[E|selected|disabled|label|value][]' +
- 'textarea[E|D|name|rows|cols|disabled|readonly|onselect|onchange][]' +
- 'label[E|for|accesskey|onfocus|onblur][#|S]' +
- 'button[E|D|name|value|type|disabled][#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' +
- 'h4[E|align][#|S]' +
- 'ins[E|cite|datetime][#|Y]' +
- 'h5[E|align][#|S]' +
- 'del[E|cite|datetime][#|Y]' +
- 'h6[E|align][#|S]' +
- 'div[E|align][#|Y]' +
- 'ul[E|type|compact][li]' +
- 'li[E|type|value][#|Y]' +
- 'ol[E|type|compact|start][li]' +
- 'dl[E|compact][dt|dd]' +
- 'dt[E][#|S]' +
- 'dd[E][#|Y]' +
- 'menu[E|compact][li]' +
- 'dir[E|compact][li]' +
- 'pre[E|width|xml:space][#|ZA]' +
- 'hr[E|align|noshade|size|width][]' +
- 'blockquote[E|cite][#|Y]' +
- 'address[E][#|S|p]' +
- 'center[E][#|Y]' +
- 'noframes[E][#|Y]' +
- 'isindex[A|B|prompt][]' +
- 'fieldset[E][#|legend|Y]' +
- 'legend[E|accesskey|align][#|S]' +
- 'table[E|summary|width|border|frame|rules|cellspacing|cellpadding|align|bgcolor][caption|col|colgroup|thead|tfoot|tbody|tr]' +
- 'caption[E|align][#|S]' +
- 'col[ZG][]' +
- 'colgroup[ZG][col]' +
- 'thead[ZF][tr]' +
- 'tr[ZF|bgcolor][th|td]' +
- 'th[E|ZE][#|Y]' +
- 'form[E|action|method|name|enctype|onsubmit|onreset|accept|accept-charset|target][#|X|R|Q]' +
- 'noscript[E][#|Y]' +
- 'td[E|ZE][#|Y]' +
- 'tfoot[ZF][tr]' +
- 'tbody[ZF][tr]' +
- 'area[E|D|shape|coords|href|nohref|alt|target][]' +
- 'base[id|href|target][]' +
- 'body[E|onload|onunload|background|bgcolor|text|link|vlink|alink][#|Y]'
- );
- }
-
- return html4;
- };
-
- tinymce.html.Schema = function(settings) {
- var self = this, elements = {}, children = {}, patternElements = [], validStyles, schemaItems;
- var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, blockElementsMap, nonEmptyElementsMap, customElementsMap = {};
-
- // Creates an lookup table map object for the specified option or the default value
- function createLookupTable(option, default_value, extend) {
- var value = settings[option];
-
- if (!value) {
- // Get cached default map or make it if needed
- value = mapCache[option];
-
- if (!value) {
- value = makeMap(default_value, ' ', makeMap(default_value.toUpperCase(), ' '));
- value = tinymce.extend(value, extend);
-
- mapCache[option] = value;
- }
- } else {
- // Create custom map
- value = makeMap(value, ',', makeMap(value.toUpperCase(), ' '));
- }
-
- return value;
- };
-
- settings = settings || {};
- schemaItems = settings.schema == "html5" ? getHTML5() : getHTML4();
-
- // Allow all elements and attributes if verify_html is set to false
- if (settings.verify_html === false)
- settings.valid_elements = '*[*]';
-
- // Build styles list
- if (settings.valid_styles) {
- validStyles = {};
-
- // Convert styles into a rule list
- each(settings.valid_styles, function(value, key) {
- validStyles[key] = tinymce.explode(value);
- });
- }
-
- // Setup map objects
- whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script noscript style textarea');
- selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr');
- shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link meta param embed source wbr');
- boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize noshade nowrap readonly selected autoplay loop controls');
- nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object', shortEndedElementsMap);
- textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' +
- 'blockquote center dir fieldset header footer article section hgroup aside nav figure');
- blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' +
- 'th tr td li ol ul caption dl dt dd noscript menu isindex samp option datalist select optgroup', textBlockElementsMap);
-
- // Converts a wildcard expression string to a regexp for example *a will become /.*a/.
- function patternToRegExp(str) {
- return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$');
- };
-
- // Parses the specified valid_elements string and adds to the current rules
- // This function is a bit hard to read since it's heavily optimized for speed
- function addValidElements(valid_elements) {
- var ei, el, ai, al, yl, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder,
- prefix, outputName, globalAttributes, globalAttributesOrder, transElement, key, childKey, value,
- elementRuleRegExp = /^([#+\-])?([^\[\/]+)(?:\/([^\[]+))?(?:\[([^\]]+)\])?$/,
- attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/,
- hasPatternsRegExp = /[*?+]/;
-
- if (valid_elements) {
- // Split valid elements into an array with rules
- valid_elements = split(valid_elements);
-
- if (elements['@']) {
- globalAttributes = elements['@'].attributes;
- globalAttributesOrder = elements['@'].attributesOrder;
- }
-
- // Loop all rules
- for (ei = 0, el = valid_elements.length; ei < el; ei++) {
- // Parse element rule
- matches = elementRuleRegExp.exec(valid_elements[ei]);
- if (matches) {
- // Setup local names for matches
- prefix = matches[1];
- elementName = matches[2];
- outputName = matches[3];
- attrData = matches[4];
-
- // Create new attributes and attributesOrder
- attributes = {};
- attributesOrder = [];
-
- // Create the new element
- element = {
- attributes : attributes,
- attributesOrder : attributesOrder
- };
-
- // Padd empty elements prefix
- if (prefix === '#')
- element.paddEmpty = true;
-
- // Remove empty elements prefix
- if (prefix === '-')
- element.removeEmpty = true;
-
- // Copy attributes from global rule into current rule
- if (globalAttributes) {
- for (key in globalAttributes)
- attributes[key] = globalAttributes[key];
-
- attributesOrder.push.apply(attributesOrder, globalAttributesOrder);
- }
-
- // Attributes defined
- if (attrData) {
- attrData = split(attrData, '|');
- for (ai = 0, al = attrData.length; ai < al; ai++) {
- matches = attrRuleRegExp.exec(attrData[ai]);
- if (matches) {
- attr = {};
- attrType = matches[1];
- attrName = matches[2].replace(/::/g, ':');
- prefix = matches[3];
- value = matches[4];
-
- // Required
- if (attrType === '!') {
- element.attributesRequired = element.attributesRequired || [];
- element.attributesRequired.push(attrName);
- attr.required = true;
- }
-
- // Denied from global
- if (attrType === '-') {
- delete attributes[attrName];
- attributesOrder.splice(tinymce.inArray(attributesOrder, attrName), 1);
- continue;
- }
-
- // Default value
- if (prefix) {
- // Default value
- if (prefix === '=') {
- element.attributesDefault = element.attributesDefault || [];
- element.attributesDefault.push({name: attrName, value: value});
- attr.defaultValue = value;
- }
-
- // Forced value
- if (prefix === ':') {
- element.attributesForced = element.attributesForced || [];
- element.attributesForced.push({name: attrName, value: value});
- attr.forcedValue = value;
- }
-
- // Required values
- if (prefix === '<')
- attr.validValues = makeMap(value, '?');
- }
-
- // Check for attribute patterns
- if (hasPatternsRegExp.test(attrName)) {
- element.attributePatterns = element.attributePatterns || [];
- attr.pattern = patternToRegExp(attrName);
- element.attributePatterns.push(attr);
- } else {
- // Add attribute to order list if it doesn't already exist
- if (!attributes[attrName])
- attributesOrder.push(attrName);
-
- attributes[attrName] = attr;
- }
- }
- }
- }
-
- // Global rule, store away these for later usage
- if (!globalAttributes && elementName == '@') {
- globalAttributes = attributes;
- globalAttributesOrder = attributesOrder;
- }
-
- // Handle substitute elements such as b/strong
- if (outputName) {
- element.outputName = elementName;
- elements[outputName] = element;
- }
-
- // Add pattern or exact element
- if (hasPatternsRegExp.test(elementName)) {
- element.pattern = patternToRegExp(elementName);
- patternElements.push(element);
- } else
- elements[elementName] = element;
- }
- }
- }
- };
-
- function setValidElements(valid_elements) {
- elements = {};
- patternElements = [];
-
- addValidElements(valid_elements);
-
- each(schemaItems, function(element, name) {
- children[name] = element.children;
- });
- };
-
- // Adds custom non HTML elements to the schema
- function addCustomElements(custom_elements) {
- var customElementRegExp = /^(~)?(.+)$/;
-
- if (custom_elements) {
- each(split(custom_elements), function(rule) {
- var matches = customElementRegExp.exec(rule),
- inline = matches[1] === '~',
- cloneName = inline ? 'span' : 'div',
- name = matches[2];
-
- children[name] = children[cloneName];
- customElementsMap[name] = cloneName;
-
- // If it's not marked as inline then add it to valid block elements
- if (!inline) {
- blockElementsMap[name.toUpperCase()] = {};
- blockElementsMap[name] = {};
- }
-
- // Add elements clone if needed
- if (!elements[name]) {
- elements[name] = elements[cloneName];
- }
-
- // Add custom elements at span/div positions
- each(children, function(element, child) {
- if (element[cloneName])
- element[name] = element[cloneName];
- });
- });
- }
- };
-
- // Adds valid children to the schema object
- function addValidChildren(valid_children) {
- var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/;
-
- if (valid_children) {
- each(split(valid_children), function(rule) {
- var matches = childRuleRegExp.exec(rule), parent, prefix;
-
- if (matches) {
- prefix = matches[1];
-
- // Add/remove items from default
- if (prefix)
- parent = children[matches[2]];
- else
- parent = children[matches[2]] = {'#comment' : {}};
-
- parent = children[matches[2]];
-
- each(split(matches[3], '|'), function(child) {
- if (prefix === '-')
- delete parent[child];
- else
- parent[child] = {};
- });
- }
- });
- }
- };
-
- function getElementRule(name) {
- var element = elements[name], i;
-
- // Exact match found
- if (element)
- return element;
-
- // No exact match then try the patterns
- i = patternElements.length;
- while (i--) {
- element = patternElements[i];
-
- if (element.pattern.test(name))
- return element;
- }
- };
-
- if (!settings.valid_elements) {
- // No valid elements defined then clone the elements from the schema spec
- each(schemaItems, function(element, name) {
- elements[name] = {
- attributes : element.attributes,
- attributesOrder : element.attributesOrder
- };
-
- children[name] = element.children;
- });
-
- // Switch these on HTML4
- if (settings.schema != "html5") {
- each(split('strong/b,em/i'), function(item) {
- item = split(item, '/');
- elements[item[1]].outputName = item[0];
- });
- }
-
- // Add default alt attribute for images
- elements.img.attributesDefault = [{name: 'alt', value: ''}];
-
- // Remove these if they are empty by default
- each(split('ol,ul,sub,sup,blockquote,span,font,a,table,tbody,tr,strong,em,b,i'), function(name) {
- if (elements[name]) {
- elements[name].removeEmpty = true;
- }
- });
-
- // Padd these by default
- each(split('p,h1,h2,h3,h4,h5,h6,th,td,pre,div,address,caption'), function(name) {
- elements[name].paddEmpty = true;
- });
- } else
- setValidElements(settings.valid_elements);
-
- addCustomElements(settings.custom_elements);
- addValidChildren(settings.valid_children);
- addValidElements(settings.extended_valid_elements);
-
- // Todo: Remove this when we fix list handling to be valid
- addValidChildren('+ol[ul|ol],+ul[ul|ol]');
-
- // Delete invalid elements
- if (settings.invalid_elements) {
- tinymce.each(tinymce.explode(settings.invalid_elements), function(item) {
- if (elements[item])
- delete elements[item];
- });
- }
-
- // If the user didn't allow span only allow internal spans
- if (!getElementRule('span'))
- addValidElements('span[!data-mce-type|*]');
-
- self.children = children;
-
- self.styles = validStyles;
-
- self.getBoolAttrs = function() {
- return boolAttrMap;
- };
-
- self.getBlockElements = function() {
- return blockElementsMap;
- };
-
- self.getTextBlockElements = function() {
- return textBlockElementsMap;
- };
-
- self.getShortEndedElements = function() {
- return shortEndedElementsMap;
- };
-
- self.getSelfClosingElements = function() {
- return selfClosingElementsMap;
- };
-
- self.getNonEmptyElements = function() {
- return nonEmptyElementsMap;
- };
-
- self.getWhiteSpaceElements = function() {
- return whiteSpaceElementsMap;
- };
-
- self.isValidChild = function(name, child) {
- var parent = children[name];
-
- return !!(parent && parent[child]);
- };
-
- self.isValid = function(name, attr) {
- var attrPatterns, i, rule = getElementRule(name);
-
- // Check if it's a valid element
- if (rule) {
- if (attr) {
- // Check if attribute name exists
- if (rule.attributes[attr]) {
- return true;
- }
-
- // Check if attribute matches a regexp pattern
- attrPatterns = rule.attributePatterns;
- if (attrPatterns) {
- i = attrPatterns.length;
- while (i--) {
- if (attrPatterns[i].pattern.test(name)) {
- return true;
- }
- }
- }
- } else {
- return true;
- }
- }
-
- // No match
- return false;
- };
-
- self.getElementRule = getElementRule;
-
- self.getCustomElements = function() {
- return customElementsMap;
- };
-
- self.addValidElements = addValidElements;
-
- self.setValidElements = setValidElements;
-
- self.addCustomElements = addCustomElements;
-
- self.addValidChildren = addValidChildren;
-
- self.elements = elements;
- };
-})(tinymce);
-
-(function(tinymce) {
- tinymce.html.SaxParser = function(settings, schema) {
- var self = this, noop = function() {};
-
- settings = settings || {};
- self.schema = schema = schema || new tinymce.html.Schema();
-
- if (settings.fix_self_closing !== false)
- settings.fix_self_closing = true;
-
- // Add handler functions from settings and setup default handlers
- tinymce.each('comment cdata text start end pi doctype'.split(' '), function(name) {
- if (name)
- self[name] = settings[name] || noop;
- });
-
- self.parse = function(html) {
- var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name, isInternalElement, removeInternalElements,
- shortEndedElements, fillAttrsMap, isShortEnded, validate, elementRule, isValidElement, attr, attribsValue, invalidPrefixRegExp,
- validAttributesMap, validAttributePatterns, attributesRequired, attributesDefault, attributesForced, selfClosing,
- tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0, decode = tinymce.html.Entities.decode, fixSelfClosing, isIE;
-
- function processEndTag(name) {
- var pos, i;
-
- // Find position of parent of the same type
- pos = stack.length;
- while (pos--) {
- if (stack[pos].name === name)
- break;
- }
-
- // Found parent
- if (pos >= 0) {
- // Close all the open elements
- for (i = stack.length - 1; i >= pos; i--) {
- name = stack[i];
-
- if (name.valid)
- self.end(name.name);
- }
-
- // Remove the open elements from the stack
- stack.length = pos;
- }
- };
-
- function parseAttribute(match, name, value, val2, val3) {
- var attrRule, i;
-
- name = name.toLowerCase();
- value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute
-
- // Validate name and value
- if (validate && !isInternalElement && name.indexOf('data-mce-') !== 0) {
- attrRule = validAttributesMap[name];
-
- // Find rule by pattern matching
- if (!attrRule && validAttributePatterns) {
- i = validAttributePatterns.length;
- while (i--) {
- attrRule = validAttributePatterns[i];
- if (attrRule.pattern.test(name))
- break;
- }
-
- // No rule matched
- if (i === -1)
- attrRule = null;
- }
-
- // No attribute rule found
- if (!attrRule)
- return;
-
- // Validate value
- if (attrRule.validValues && !(value in attrRule.validValues))
- return;
- }
-
- // Add attribute to list and map
- attrList.map[name] = value;
- attrList.push({
- name: name,
- value: value
- });
- };
-
- // Precompile RegExps and map objects
- tokenRegExp = new RegExp('<(?:' +
- '(?:!--([\\w\\W]*?)-->)|' + // Comment
- '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA
- '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE
- '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI
- '(?:\\/([^>]+)>)|' + // End element
- '(?:([A-Za-z0-9\\-\\:\\.]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element
- ')', 'g');
-
- attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g;
- specialElements = {
- 'script' : /<\/script[^>]*>/gi,
- 'style' : /<\/style[^>]*>/gi,
- 'noscript' : /<\/noscript[^>]*>/gi
- };
-
- // Setup lookup tables for empty elements and boolean attributes
- shortEndedElements = schema.getShortEndedElements();
- selfClosing = settings.self_closing_elements || schema.getSelfClosingElements();
- fillAttrsMap = schema.getBoolAttrs();
- validate = settings.validate;
- removeInternalElements = settings.remove_internals;
- fixSelfClosing = settings.fix_self_closing;
- isIE = tinymce.isIE;
- invalidPrefixRegExp = /^:/;
-
- while (matches = tokenRegExp.exec(html)) {
- // Text
- if (index < matches.index)
- self.text(decode(html.substr(index, matches.index - index)));
-
- if (value = matches[6]) { // End element
- value = value.toLowerCase();
-
- // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
- if (isIE && invalidPrefixRegExp.test(value))
- value = value.substr(1);
-
- processEndTag(value);
- } else if (value = matches[7]) { // Start element
- value = value.toLowerCase();
-
- // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
- if (isIE && invalidPrefixRegExp.test(value))
- value = value.substr(1);
-
- isShortEnded = value in shortEndedElements;
-
- // Is self closing tag for example an <li> after an open <li>
- if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value)
- processEndTag(value);
-
- // Validate element
- if (!validate || (elementRule = schema.getElementRule(value))) {
- isValidElement = true;
-
- // Grab attributes map and patters when validation is enabled
- if (validate) {
- validAttributesMap = elementRule.attributes;
- validAttributePatterns = elementRule.attributePatterns;
- }
-
- // Parse attributes
- if (attribsValue = matches[8]) {
- isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element
-
- // If the element has internal attributes then remove it if we are told to do so
- if (isInternalElement && removeInternalElements)
- isValidElement = false;
-
- attrList = [];
- attrList.map = {};
-
- attribsValue.replace(attrRegExp, parseAttribute);
- } else {
- attrList = [];
- attrList.map = {};
- }
-
- // Process attributes if validation is enabled
- if (validate && !isInternalElement) {
- attributesRequired = elementRule.attributesRequired;
- attributesDefault = elementRule.attributesDefault;
- attributesForced = elementRule.attributesForced;
-
- // Handle forced attributes
- if (attributesForced) {
- i = attributesForced.length;
- while (i--) {
- attr = attributesForced[i];
- name = attr.name;
- attrValue = attr.value;
-
- if (attrValue === '{$uid}')
- attrValue = 'mce_' + idCount++;
-
- attrList.map[name] = attrValue;
- attrList.push({name: name, value: attrValue});
- }
- }
-
- // Handle default attributes
- if (attributesDefault) {
- i = attributesDefault.length;
- while (i--) {
- attr = attributesDefault[i];
- name = attr.name;
-
- if (!(name in attrList.map)) {
- attrValue = attr.value;
-
- if (attrValue === '{$uid}')
- attrValue = 'mce_' + idCount++;
-
- attrList.map[name] = attrValue;
- attrList.push({name: name, value: attrValue});
- }
- }
- }
-
- // Handle required attributes
- if (attributesRequired) {
- i = attributesRequired.length;
- while (i--) {
- if (attributesRequired[i] in attrList.map)
- break;
- }
-
- // None of the required attributes where found
- if (i === -1)
- isValidElement = false;
- }
-
- // Invalidate element if it's marked as bogus
- if (attrList.map['data-mce-bogus'])
- isValidElement = false;
- }
-
- if (isValidElement)
- self.start(value, attrList, isShortEnded);
- } else
- isValidElement = false;
-
- // Treat script, noscript and style a bit different since they may include code that looks like elements
- if (endRegExp = specialElements[value]) {
- endRegExp.lastIndex = index = matches.index + matches[0].length;
-
- if (matches = endRegExp.exec(html)) {
- if (isValidElement)
- text = html.substr(index, matches.index - index);
-
- index = matches.index + matches[0].length;
- } else {
- text = html.substr(index);
- index = html.length;
- }
-
- if (isValidElement && text.length > 0)
- self.text(text, true);
-
- if (isValidElement)
- self.end(value);
-
- tokenRegExp.lastIndex = index;
- continue;
- }
-
- // Push value on to stack
- if (!isShortEnded) {
- if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1)
- stack.push({name: value, valid: isValidElement});
- else if (isValidElement)
- self.end(value);
- }
- } else if (value = matches[1]) { // Comment
- self.comment(value);
- } else if (value = matches[2]) { // CDATA
- self.cdata(value);
- } else if (value = matches[3]) { // DOCTYPE
- self.doctype(value);
- } else if (value = matches[4]) { // PI
- self.pi(value, matches[5]);
- }
-
- index = matches.index + matches[0].length;
- }
-
- // Text
- if (index < html.length)
- self.text(decode(html.substr(index)));
-
- // Close any open elements
- for (i = stack.length - 1; i >= 0; i--) {
- value = stack[i];
-
- if (value.valid)
- self.end(value.name);
- }
- };
- }
-})(tinymce);
-
-(function(tinymce) {
- var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = {
- '#text' : 3,
- '#comment' : 8,
- '#cdata' : 4,
- '#pi' : 7,
- '#doctype' : 10,
- '#document-fragment' : 11
- };
-
- // Walks the tree left/right
- function walk(node, root_node, prev) {
- var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next';
-
- // Walk into nodes if it has a start
- if (node[startName])
- return node[startName];
-
- // Return the sibling if it has one
- if (node !== root_node) {
- sibling = node[siblingName];
-
- if (sibling)
- return sibling;
-
- // Walk up the parents to look for siblings
- for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) {
- sibling = parent[siblingName];
-
- if (sibling)
- return sibling;
- }
- }
- };
-
- function Node(name, type) {
- this.name = name;
- this.type = type;
-
- if (type === 1) {
- this.attributes = [];
- this.attributes.map = {};
- }
- }
-
- tinymce.extend(Node.prototype, {
- replace : function(node) {
- var self = this;
-
- if (node.parent)
- node.remove();
-
- self.insert(node, self);
- self.remove();
-
- return self;
- },
-
- attr : function(name, value) {
- var self = this, attrs, i, undef;
-
- if (typeof name !== "string") {
- for (i in name)
- self.attr(i, name[i]);
-
- return self;
- }
-
- if (attrs = self.attributes) {
- if (value !== undef) {
- // Remove attribute
- if (value === null) {
- if (name in attrs.map) {
- delete attrs.map[name];
-
- i = attrs.length;
- while (i--) {
- if (attrs[i].name === name) {
- attrs = attrs.splice(i, 1);
- return self;
- }
- }
- }
-
- return self;
- }
-
- // Set attribute
- if (name in attrs.map) {
- // Set attribute
- i = attrs.length;
- while (i--) {
- if (attrs[i].name === name) {
- attrs[i].value = value;
- break;
- }
- }
- } else
- attrs.push({name: name, value: value});
-
- attrs.map[name] = value;
-
- return self;
- } else {
- return attrs.map[name];
- }
- }
- },
-
- clone : function() {
- var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs;
-
- // Clone element attributes
- if (selfAttrs = self.attributes) {
- cloneAttrs = [];
- cloneAttrs.map = {};
-
- for (i = 0, l = selfAttrs.length; i < l; i++) {
- selfAttr = selfAttrs[i];
-
- // Clone everything except id
- if (selfAttr.name !== 'id') {
- cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value};
- cloneAttrs.map[selfAttr.name] = selfAttr.value;
- }
- }
-
- clone.attributes = cloneAttrs;
- }
-
- clone.value = self.value;
- clone.shortEnded = self.shortEnded;
-
- return clone;
- },
-
- wrap : function(wrapper) {
- var self = this;
-
- self.parent.insert(wrapper, self);
- wrapper.append(self);
-
- return self;
- },
-
- unwrap : function() {
- var self = this, node, next;
-
- for (node = self.firstChild; node; ) {
- next = node.next;
- self.insert(node, self, true);
- node = next;
- }
-
- self.remove();
- },
-
- remove : function() {
- var self = this, parent = self.parent, next = self.next, prev = self.prev;
-
- if (parent) {
- if (parent.firstChild === self) {
- parent.firstChild = next;
-
- if (next)
- next.prev = null;
- } else {
- prev.next = next;
- }
-
- if (parent.lastChild === self) {
- parent.lastChild = prev;
-
- if (prev)
- prev.next = null;
- } else {
- next.prev = prev;
- }
-
- self.parent = self.next = self.prev = null;
- }
-
- return self;
- },
-
- append : function(node) {
- var self = this, last;
-
- if (node.parent)
- node.remove();
-
- last = self.lastChild;
- if (last) {
- last.next = node;
- node.prev = last;
- self.lastChild = node;
- } else
- self.lastChild = self.firstChild = node;
-
- node.parent = self;
-
- return node;
- },
-
- insert : function(node, ref_node, before) {
- var parent;
-
- if (node.parent)
- node.remove();
-
- parent = ref_node.parent || this;
-
- if (before) {
- if (ref_node === parent.firstChild)
- parent.firstChild = node;
- else
- ref_node.prev.next = node;
-
- node.prev = ref_node.prev;
- node.next = ref_node;
- ref_node.prev = node;
- } else {
- if (ref_node === parent.lastChild)
- parent.lastChild = node;
- else
- ref_node.next.prev = node;
-
- node.next = ref_node.next;
- node.prev = ref_node;
- ref_node.next = node;
- }
-
- node.parent = parent;
-
- return node;
- },
-
- getAll : function(name) {
- var self = this, node, collection = [];
-
- for (node = self.firstChild; node; node = walk(node, self)) {
- if (node.name === name)
- collection.push(node);
- }
-
- return collection;
- },
-
- empty : function() {
- var self = this, nodes, i, node;
-
- // Remove all children
- if (self.firstChild) {
- nodes = [];
-
- // Collect the children
- for (node = self.firstChild; node; node = walk(node, self))
- nodes.push(node);
-
- // Remove the children
- i = nodes.length;
- while (i--) {
- node = nodes[i];
- node.parent = node.firstChild = node.lastChild = node.next = node.prev = null;
- }
- }
-
- self.firstChild = self.lastChild = null;
-
- return self;
- },
-
- isEmpty : function(elements) {
- var self = this, node = self.firstChild, i, name;
-
- if (node) {
- do {
- if (node.type === 1) {
- // Ignore bogus elements
- if (node.attributes.map['data-mce-bogus'])
- continue;
-
- // Keep empty elements like <img />
- if (elements[node.name])
- return false;
-
- // Keep elements with data attributes or name attribute like <a name="1"></a>
- i = node.attributes.length;
- while (i--) {
- name = node.attributes[i].name;
- if (name === "name" || name.indexOf('data-mce-') === 0)
- return false;
- }
- }
-
- // Keep comments
- if (node.type === 8)
- return false;
-
- // Keep non whitespace text nodes
- if ((node.type === 3 && !whiteSpaceRegExp.test(node.value)))
- return false;
- } while (node = walk(node, self));
- }
-
- return true;
- },
-
- walk : function(prev) {
- return walk(this, null, prev);
- }
- });
-
- tinymce.extend(Node, {
- create : function(name, attrs) {
- var node, attrName;
-
- // Create node
- node = new Node(name, typeLookup[name] || 1);
-
- // Add attributes if needed
- if (attrs) {
- for (attrName in attrs)
- node.attr(attrName, attrs[attrName]);
- }
-
- return node;
- }
- });
-
- tinymce.html.Node = Node;
-})(tinymce);
-
-(function(tinymce) {
- var Node = tinymce.html.Node;
-
- tinymce.html.DomParser = function(settings, schema) {
- var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {};
-
- settings = settings || {};
- settings.validate = "validate" in settings ? settings.validate : true;
- settings.root_name = settings.root_name || 'body';
- self.schema = schema = schema || new tinymce.html.Schema();
-
- function fixInvalidChildren(nodes) {
- var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i,
- childClone, nonEmptyElements, nonSplitableElements, textBlockElements, sibling, nextNode;
-
- nonSplitableElements = tinymce.makeMap('tr,td,th,tbody,thead,tfoot,table');
- nonEmptyElements = schema.getNonEmptyElements();
- textBlockElements = schema.getTextBlockElements();
-
- for (ni = 0; ni < nodes.length; ni++) {
- node = nodes[ni];
-
- // Already removed or fixed
- if (!node.parent || node.fixed)
- continue;
-
- // If the invalid element is a text block and the text block is within a parent LI element
- // Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office
- if (textBlockElements[node.name] && node.parent.name == 'li') {
- // Move sibling text blocks after LI element
- sibling = node.next;
- while (sibling) {
- if (textBlockElements[sibling.name]) {
- sibling.name = 'li';
- sibling.fixed = true;
- node.parent.insert(sibling, node.parent);
- } else {
- break;
- }
-
- sibling = sibling.next;
- }
-
- // Unwrap current text block
- node.unwrap(node);
- continue;
- }
-
- // Get list of all parent nodes until we find a valid parent to stick the child into
- parents = [node];
- for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && !nonSplitableElements[parent.name]; parent = parent.parent)
- parents.push(parent);
-
- // Found a suitable parent
- if (parent && parents.length > 1) {
- // Reverse the array since it makes looping easier
- parents.reverse();
-
- // Clone the related parent and insert that after the moved node
- newParent = currentNode = self.filterNode(parents[0].clone());
-
- // Start cloning and moving children on the left side of the target node
- for (i = 0; i < parents.length - 1; i++) {
- if (schema.isValidChild(currentNode.name, parents[i].name)) {
- tempNode = self.filterNode(parents[i].clone());
- currentNode.append(tempNode);
- } else
- tempNode = currentNode;
-
- for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1]; ) {
- nextNode = childNode.next;
- tempNode.append(childNode);
- childNode = nextNode;
- }
-
- currentNode = tempNode;
- }
-
- if (!newParent.isEmpty(nonEmptyElements)) {
- parent.insert(newParent, parents[0], true);
- parent.insert(node, newParent);
- } else {
- parent.insert(node, parents[0], true);
- }
-
- // Check if the element is empty by looking through it's contents and special treatment for <p><br /></p>
- parent = parents[0];
- if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') {
- parent.empty().remove();
- }
- } else if (node.parent) {
- // If it's an LI try to find a UL/OL for it or wrap it
- if (node.name === 'li') {
- sibling = node.prev;
- if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
- sibling.append(node);
- continue;
- }
-
- sibling = node.next;
- if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
- sibling.insert(node, sibling.firstChild, true);
- continue;
- }
-
- node.wrap(self.filterNode(new Node('ul', 1)));
- continue;
- }
-
- // Try wrapping the element in a DIV
- if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) {
- node.wrap(self.filterNode(new Node('div', 1)));
- } else {
- // We failed wrapping it, then remove or unwrap it
- if (node.name === 'style' || node.name === 'script')
- node.empty().remove();
- else
- node.unwrap();
- }
- }
- }
- };
-
- self.filterNode = function(node) {
- var i, name, list;
-
- // Run element filters
- if (name in nodeFilters) {
- list = matchedNodes[name];
-
- if (list)
- list.push(node);
- else
- matchedNodes[name] = [node];
- }
-
- // Run attribute filters
- i = attributeFilters.length;
- while (i--) {
- name = attributeFilters[i].name;
-
- if (name in node.attributes.map) {
- list = matchedAttributes[name];
-
- if (list)
- list.push(node);
- else
- matchedAttributes[name] = [node];
- }
- }
-
- return node;
- };
-
- self.addNodeFilter = function(name, callback) {
- tinymce.each(tinymce.explode(name), function(name) {
- var list = nodeFilters[name];
-
- if (!list)
- nodeFilters[name] = list = [];
-
- list.push(callback);
- });
- };
-
- self.addAttributeFilter = function(name, callback) {
- tinymce.each(tinymce.explode(name), function(name) {
- var i;
-
- for (i = 0; i < attributeFilters.length; i++) {
- if (attributeFilters[i].name === name) {
- attributeFilters[i].callbacks.push(callback);
- return;
- }
- }
-
- attributeFilters.push({name: name, callbacks: [callback]});
- });
- };
-
- self.parse = function(html, args) {
- var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate,
- blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement,
- endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements, children, nonEmptyElements, rootBlockName;
-
- args = args || {};
- matchedNodes = {};
- matchedAttributes = {};
- blockElements = tinymce.extend(tinymce.makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements());
- nonEmptyElements = schema.getNonEmptyElements();
- children = schema.children;
- validate = settings.validate;
- rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block;
-
- whiteSpaceElements = schema.getWhiteSpaceElements();
- startWhiteSpaceRegExp = /^[ \t\r\n]+/;
- endWhiteSpaceRegExp = /[ \t\r\n]+$/;
- allWhiteSpaceRegExp = /[ \t\r\n]+/g;
- isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/;
-
- function addRootBlocks() {
- var node = rootNode.firstChild, next, rootBlockNode;
-
- while (node) {
- next = node.next;
-
- if (node.type == 3 || (node.type == 1 && node.name !== 'p' && !blockElements[node.name] && !node.attr('data-mce-type'))) {
- if (!rootBlockNode) {
- // Create a new root block element
- rootBlockNode = createNode(rootBlockName, 1);
- rootNode.insert(rootBlockNode, node);
- rootBlockNode.append(node);
- } else
- rootBlockNode.append(node);
- } else {
- rootBlockNode = null;
- }
-
- node = next;
- };
- };
-
- function createNode(name, type) {
- var node = new Node(name, type), list;
-
- if (name in nodeFilters) {
- list = matchedNodes[name];
-
- if (list)
- list.push(node);
- else
- matchedNodes[name] = [node];
- }
-
- return node;
- };
-
- function removeWhitespaceBefore(node) {
- var textNode, textVal, sibling;
-
- for (textNode = node.prev; textNode && textNode.type === 3; ) {
- textVal = textNode.value.replace(endWhiteSpaceRegExp, '');
-
- if (textVal.length > 0) {
- textNode.value = textVal;
- textNode = textNode.prev;
- } else {
- sibling = textNode.prev;
- textNode.remove();
- textNode = sibling;
- }
- }
- };
-
- function cloneAndExcludeBlocks(input) {
- var name, output = {};
-
- for (name in input) {
- if (name !== 'li' && name != 'p') {
- output[name] = input[name];
- }
- }
-
- return output;
- };
-
- parser = new tinymce.html.SaxParser({
- validate : validate,
-
- // Exclude P and LI from DOM parsing since it's treated better by the DOM parser
- self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()),
-
- cdata: function(text) {
- node.append(createNode('#cdata', 4)).value = text;
- },
-
- text: function(text, raw) {
- var textNode;
-
- // Trim all redundant whitespace on non white space elements
- if (!isInWhiteSpacePreservedElement) {
- text = text.replace(allWhiteSpaceRegExp, ' ');
-
- if (node.lastChild && blockElements[node.lastChild.name])
- text = text.replace(startWhiteSpaceRegExp, '');
- }
-
- // Do we need to create the node
- if (text.length !== 0) {
- textNode = createNode('#text', 3);
- textNode.raw = !!raw;
- node.append(textNode).value = text;
- }
- },
-
- comment: function(text) {
- node.append(createNode('#comment', 8)).value = text;
- },
-
- pi: function(name, text) {
- node.append(createNode(name, 7)).value = text;
- removeWhitespaceBefore(node);
- },
-
- doctype: function(text) {
- var newNode;
-
- newNode = node.append(createNode('#doctype', 10));
- newNode.value = text;
- removeWhitespaceBefore(node);
- },
-
- start: function(name, attrs, empty) {
- var newNode, attrFiltersLen, elementRule, textNode, attrName, text, sibling, parent;
-
- elementRule = validate ? schema.getElementRule(name) : {};
- if (elementRule) {
- newNode = createNode(elementRule.outputName || name, 1);
- newNode.attributes = attrs;
- newNode.shortEnded = empty;
-
- node.append(newNode);
-
- // Check if node is valid child of the parent node is the child is
- // unknown we don't collect it since it's probably a custom element
- parent = children[node.name];
- if (parent && children[newNode.name] && !parent[newNode.name])
- invalidChildren.push(newNode);
-
- attrFiltersLen = attributeFilters.length;
- while (attrFiltersLen--) {
- attrName = attributeFilters[attrFiltersLen].name;
-
- if (attrName in attrs.map) {
- list = matchedAttributes[attrName];
-
- if (list)
- list.push(newNode);
- else
- matchedAttributes[attrName] = [newNode];
- }
- }
-
- // Trim whitespace before block
- if (blockElements[name])
- removeWhitespaceBefore(newNode);
-
- // Change current node if the element wasn't empty i.e not <br /> or <img />
- if (!empty)
- node = newNode;
-
- // Check if we are inside a whitespace preserved element
- if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
- isInWhiteSpacePreservedElement = true;
- }
- }
- },
-
- end: function(name) {
- var textNode, elementRule, text, sibling, tempNode;
-
- elementRule = validate ? schema.getElementRule(name) : {};
- if (elementRule) {
- if (blockElements[name]) {
- if (!isInWhiteSpacePreservedElement) {
- // Trim whitespace of the first node in a block
- textNode = node.firstChild;
- if (textNode && textNode.type === 3) {
- text = textNode.value.replace(startWhiteSpaceRegExp, '');
-
- // Any characters left after trim or should we remove it
- if (text.length > 0) {
- textNode.value = text;
- textNode = textNode.next;
- } else {
- sibling = textNode.next;
- textNode.remove();
- textNode = sibling;
- }
-
- // Remove any pure whitespace siblings
- while (textNode && textNode.type === 3) {
- text = textNode.value;
- sibling = textNode.next;
-
- if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
- textNode.remove();
- textNode = sibling;
- }
-
- textNode = sibling;
- }
- }
-
- // Trim whitespace of the last node in a block
- textNode = node.lastChild;
- if (textNode && textNode.type === 3) {
- text = textNode.value.replace(endWhiteSpaceRegExp, '');
-
- // Any characters left after trim or should we remove it
- if (text.length > 0) {
- textNode.value = text;
- textNode = textNode.prev;
- } else {
- sibling = textNode.prev;
- textNode.remove();
- textNode = sibling;
- }
-
- // Remove any pure whitespace siblings
- while (textNode && textNode.type === 3) {
- text = textNode.value;
- sibling = textNode.prev;
-
- if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
- textNode.remove();
- textNode = sibling;
- }
-
- textNode = sibling;
- }
- }
- }
-
- // Trim start white space
- // Removed due to: #5424
- /*textNode = node.prev;
- if (textNode && textNode.type === 3) {
- text = textNode.value.replace(startWhiteSpaceRegExp, '');
-
- if (text.length > 0)
- textNode.value = text;
- else
- textNode.remove();
- }*/
- }
-
- // Check if we exited a whitespace preserved element
- if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
- isInWhiteSpacePreservedElement = false;
- }
-
- // Handle empty nodes
- if (elementRule.removeEmpty || elementRule.paddEmpty) {
- if (node.isEmpty(nonEmptyElements)) {
- if (elementRule.paddEmpty)
- node.empty().append(new Node('#text', '3')).value = '\u00a0';
- else {
- // Leave nodes that have a name like <a name="name">
- if (!node.attributes.map.name && !node.attributes.map.id) {
- tempNode = node.parent;
- node.empty().remove();
- node = tempNode;
- return;
- }
- }
- }
- }
-
- node = node.parent;
- }
- }
- }, schema);
-
- rootNode = node = new Node(args.context || settings.root_name, 11);
-
- parser.parse(html);
-
- // Fix invalid children or report invalid children in a contextual parsing
- if (validate && invalidChildren.length) {
- if (!args.context)
- fixInvalidChildren(invalidChildren);
- else
- args.invalid = true;
- }
-
- // Wrap nodes in the root into block elements if the root is body
- if (rootBlockName && rootNode.name == 'body')
- addRootBlocks();
-
- // Run filters only when the contents is valid
- if (!args.invalid) {
- // Run node filters
- for (name in matchedNodes) {
- list = nodeFilters[name];
- nodes = matchedNodes[name];
-
- // Remove already removed children
- fi = nodes.length;
- while (fi--) {
- if (!nodes[fi].parent)
- nodes.splice(fi, 1);
- }
-
- for (i = 0, l = list.length; i < l; i++)
- list[i](nodes, name, args);
- }
-
- // Run attribute filters
- for (i = 0, l = attributeFilters.length; i < l; i++) {
- list = attributeFilters[i];
-
- if (list.name in matchedAttributes) {
- nodes = matchedAttributes[list.name];
-
- // Remove already removed children
- fi = nodes.length;
- while (fi--) {
- if (!nodes[fi].parent)
- nodes.splice(fi, 1);
- }
-
- for (fi = 0, fl = list.callbacks.length; fi < fl; fi++)
- list.callbacks[fi](nodes, list.name, args);
- }
- }
- }
-
- return rootNode;
- };
-
- // Remove <br> at end of block elements Gecko and WebKit injects BR elements to
- // make it possible to place the caret inside empty blocks. This logic tries to remove
- // these elements and keep br elements that where intended to be there intact
- if (settings.remove_trailing_brs) {
- self.addNodeFilter('br', function(nodes, name) {
- var i, l = nodes.length, node, blockElements = tinymce.extend({}, schema.getBlockElements()),
- nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName;
-
- // Remove brs from body element as well
- blockElements.body = 1;
-
- // Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p>
- for (i = 0; i < l; i++) {
- node = nodes[i];
- parent = node.parent;
-
- if (blockElements[node.parent.name] && node === parent.lastChild) {
- // Loop all nodes to the left of the current node and check for other BR elements
- // excluding bookmarks since they are invisible
- prev = node.prev;
- while (prev) {
- prevName = prev.name;
-
- // Ignore bookmarks
- if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') {
- // Found a non BR element
- if (prevName !== "br")
- break;
-
- // Found another br it's a <br><br> structure then don't remove anything
- if (prevName === 'br') {
- node = null;
- break;
- }
- }
-
- prev = prev.prev;
- }
-
- if (node) {
- node.remove();
-
- // Is the parent to be considered empty after we removed the BR
- if (parent.isEmpty(nonEmptyElements)) {
- elementRule = schema.getElementRule(parent.name);
-
- // Remove or padd the element depending on schema rule
- if (elementRule) {
- if (elementRule.removeEmpty)
- parent.remove();
- else if (elementRule.paddEmpty)
- parent.empty().append(new tinymce.html.Node('#text', 3)).value = '\u00a0';
- }
- }
- }
- } else {
- // Replaces BR elements inside inline elements like <p><b><i><br></i></b></p> so they become <p><b><i> </i></b></p>
- lastParent = node;
- while (parent.firstChild === lastParent && parent.lastChild === lastParent) {
- lastParent = parent;
-
- if (blockElements[parent.name]) {
- break;
- }
-
- parent = parent.parent;
- }
-
- if (lastParent === parent) {
- textNode = new tinymce.html.Node('#text', 3);
- textNode.value = '\u00a0';
- node.replace(textNode);
- }
- }
- }
- });
- }
-
- // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included.
- if (!settings.allow_html_in_named_anchor) {
- self.addAttributeFilter('id,name', function(nodes, name) {
- var i = nodes.length, sibling, prevSibling, parent, node;
-
- while (i--) {
- node = nodes[i];
- if (node.name === 'a' && node.firstChild && !node.attr('href')) {
- parent = node.parent;
-
- // Move children after current node
- sibling = node.lastChild;
- do {
- prevSibling = sibling.prev;
- parent.insert(sibling, node);
- sibling = prevSibling;
- } while (sibling);
- }
- }
- });
- }
- }
-})(tinymce);
-
-tinymce.html.Writer = function(settings) {
- var html = [], indent, indentBefore, indentAfter, encode, htmlOutput;
-
- settings = settings || {};
- indent = settings.indent;
- indentBefore = tinymce.makeMap(settings.indent_before || '');
- indentAfter = tinymce.makeMap(settings.indent_after || '');
- encode = tinymce.html.Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities);
- htmlOutput = settings.element_format == "html";
-
- return {
- start: function(name, attrs, empty) {
- var i, l, attr, value;
-
- if (indent && indentBefore[name] && html.length > 0) {
- value = html[html.length - 1];
-
- if (value.length > 0 && value !== '\n')
- html.push('\n');
- }
-
- html.push('<', name);
-
- if (attrs) {
- for (i = 0, l = attrs.length; i < l; i++) {
- attr = attrs[i];
- html.push(' ', attr.name, '="', encode(attr.value, true), '"');
- }
- }
-
- if (!empty || htmlOutput)
- html[html.length] = '>';
- else
- html[html.length] = ' />';
-
- if (empty && indent && indentAfter[name] && html.length > 0) {
- value = html[html.length - 1];
-
- if (value.length > 0 && value !== '\n')
- html.push('\n');
- }
- },
-
- end: function(name) {
- var value;
-
- /*if (indent && indentBefore[name] && html.length > 0) {
- value = html[html.length - 1];
-
- if (value.length > 0 && value !== '\n')
- html.push('\n');
- }*/
-
- html.push('</', name, '>');
-
- if (indent && indentAfter[name] && html.length > 0) {
- value = html[html.length - 1];
-
- if (value.length > 0 && value !== '\n')
- html.push('\n');
- }
- },
-
- text: function(text, raw) {
- if (text.length > 0)
- html[html.length] = raw ? text : encode(text);
- },
-
- cdata: function(text) {
- html.push('<![CDATA[', text, ']]>');
- },
-
- comment: function(text) {
- html.push('<!--', text, '-->');
- },
-
- pi: function(name, text) {
- if (text)
- html.push('<?', name, ' ', text, '?>');
- else
- html.push('<?', name, '?>');
-
- if (indent)
- html.push('\n');
- },
-
- doctype: function(text) {
- html.push('<!DOCTYPE', text, '>', indent ? '\n' : '');
- },
-
- reset: function() {
- html.length = 0;
- },
-
- getContent: function() {
- return html.join('').replace(/\n$/, '');
- }
- };
-};
-
-(function(tinymce) {
- tinymce.html.Serializer = function(settings, schema) {
- var self = this, writer = new tinymce.html.Writer(settings);
-
- settings = settings || {};
- settings.validate = "validate" in settings ? settings.validate : true;
-
- self.schema = schema = schema || new tinymce.html.Schema();
- self.writer = writer;
-
- self.serialize = function(node) {
- var handlers, validate;
-
- validate = settings.validate;
-
- handlers = {
- // #text
- 3: function(node, raw) {
- writer.text(node.value, node.raw);
- },
-
- // #comment
- 8: function(node) {
- writer.comment(node.value);
- },
-
- // Processing instruction
- 7: function(node) {
- writer.pi(node.name, node.value);
- },
-
- // Doctype
- 10: function(node) {
- writer.doctype(node.value);
- },
-
- // CDATA
- 4: function(node) {
- writer.cdata(node.value);
- },
-
- // Document fragment
- 11: function(node) {
- if ((node = node.firstChild)) {
- do {
- walk(node);
- } while (node = node.next);
- }
- }
- };
-
- writer.reset();
-
- function walk(node) {
- var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule;
-
- if (!handler) {
- name = node.name;
- isEmpty = node.shortEnded;
- attrs = node.attributes;
-
- // Sort attributes
- if (validate && attrs && attrs.length > 1) {
- sortedAttrs = [];
- sortedAttrs.map = {};
-
- elementRule = schema.getElementRule(node.name);
- for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) {
- attrName = elementRule.attributesOrder[i];
-
- if (attrName in attrs.map) {
- attrValue = attrs.map[attrName];
- sortedAttrs.map[attrName] = attrValue;
- sortedAttrs.push({name: attrName, value: attrValue});
- }
- }
-
- for (i = 0, l = attrs.length; i < l; i++) {
- attrName = attrs[i].name;
-
- if (!(attrName in sortedAttrs.map)) {
- attrValue = attrs.map[attrName];
- sortedAttrs.map[attrName] = attrValue;
- sortedAttrs.push({name: attrName, value: attrValue});
- }
- }
-
- attrs = sortedAttrs;
- }
-
- writer.start(node.name, attrs, isEmpty);
-
- if (!isEmpty) {
- if ((node = node.firstChild)) {
- do {
- walk(node);
- } while (node = node.next);
- }
-
- writer.end(name);
- }
- } else
- handler(node);
- }
-
- // Serialize element and treat all non elements as fragments
- if (node.type == 1 && !settings.inner)
- walk(node);
- else
- handlers[11](node);
-
- return writer.getContent();
- };
- }
-})(tinymce);
-
-// JSLint defined globals
-/*global tinymce:false, window:false */
-
-tinymce.dom = {};
-
-(function(namespace, expando) {
- var w3cEventModel = !!document.addEventListener;
-
- function addEvent(target, name, callback, capture) {
- if (target.addEventListener) {
- target.addEventListener(name, callback, capture || false);
- } else if (target.attachEvent) {
- target.attachEvent('on' + name, callback);
- }
- }
-
- function removeEvent(target, name, callback, capture) {
- if (target.removeEventListener) {
- target.removeEventListener(name, callback, capture || false);
- } else if (target.detachEvent) {
- target.detachEvent('on' + name, callback);
- }
- }
-
- function fix(original_event, data) {
- var name, event = data || {};
-
- // Dummy function that gets replaced on the delegation state functions
- function returnFalse() {
- return false;
- }
-
- // Dummy function that gets replaced on the delegation state functions
- function returnTrue() {
- return true;
- }
-
- // Copy all properties from the original event
- for (name in original_event) {
- // layerX/layerY is deprecated in Chrome and produces a warning
- if (name !== "layerX" && name !== "layerY") {
- event[name] = original_event[name];
- }
- }
-
- // Normalize target IE uses srcElement
- if (!event.target) {
- event.target = event.srcElement || document;
- }
-
- // Add preventDefault method
- event.preventDefault = function() {
- event.isDefaultPrevented = returnTrue;
-
- // Execute preventDefault on the original event object
- if (original_event) {
- if (original_event.preventDefault) {
- original_event.preventDefault();
- } else {
- original_event.returnValue = false; // IE
- }
- }
- };
-
- // Add stopPropagation
- event.stopPropagation = function() {
- event.isPropagationStopped = returnTrue;
-
- // Execute stopPropagation on the original event object
- if (original_event) {
- if (original_event.stopPropagation) {
- original_event.stopPropagation();
- } else {
- original_event.cancelBubble = true; // IE
- }
- }
- };
-
- // Add stopImmediatePropagation
- event.stopImmediatePropagation = function() {
- event.isImmediatePropagationStopped = returnTrue;
- event.stopPropagation();
- };
-
- // Add event delegation states
- if (!event.isDefaultPrevented) {
- event.isDefaultPrevented = returnFalse;
- event.isPropagationStopped = returnFalse;
- event.isImmediatePropagationStopped = returnFalse;
- }
-
- return event;
- }
-
- function bindOnReady(win, callback, event_utils) {
- var doc = win.document, event = {type: 'ready'};
-
- // Gets called when the DOM is ready
- function readyHandler() {
- if (!event_utils.domLoaded) {
- event_utils.domLoaded = true;
- callback(event);
- }
- }
-
- // Page already loaded then fire it directly
- if (doc.readyState == "complete") {
- readyHandler();
- return;
- }
-
- // Use W3C method
- if (w3cEventModel) {
- addEvent(win, 'DOMContentLoaded', readyHandler);
- } else {
- // Use IE method
- addEvent(doc, "readystatechange", function() {
- if (doc.readyState === "complete") {
- removeEvent(doc, "readystatechange", arguments.callee);
- readyHandler();
- }
- });
-
- // Wait until we can scroll, when we can the DOM is initialized
- if (doc.documentElement.doScroll && win === win.top) {
- (function() {
- try {
- // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author.
- // http://javascript.nwbox.com/IEContentLoaded/
- doc.documentElement.doScroll("left");
- } catch (ex) {
- setTimeout(arguments.callee, 0);
- return;
- }
-
- readyHandler();
- })();
- }
- }
-
- // Fallback if any of the above methods should fail for some odd reason
- addEvent(win, 'load', readyHandler);
- }
-
- function EventUtils(proxy) {
- var self = this, events = {}, count, isFocusBlurBound, hasFocusIn, hasMouseEnterLeave, mouseEnterLeave;
-
- hasMouseEnterLeave = "onmouseenter" in document.documentElement;
- hasFocusIn = "onfocusin" in document.documentElement;
- mouseEnterLeave = {mouseenter: 'mouseover', mouseleave: 'mouseout'};
- count = 1;
-
- // State if the DOMContentLoaded was executed or not
- self.domLoaded = false;
- self.events = events;
-
- function executeHandlers(evt, id) {
- var callbackList, i, l, callback;
-
- callbackList = events[id][evt.type];
- if (callbackList) {
- for (i = 0, l = callbackList.length; i < l; i++) {
- callback = callbackList[i];
-
- // Check if callback exists might be removed if a unbind is called inside the callback
- if (callback && callback.func.call(callback.scope, evt) === false) {
- evt.preventDefault();
- }
-
- // Should we stop propagation to immediate listeners
- if (evt.isImmediatePropagationStopped()) {
- return;
- }
- }
- }
- }
-
- self.bind = function(target, names, callback, scope) {
- var id, callbackList, i, name, fakeName, nativeHandler, capture, win = window;
-
- // Native event handler function patches the event and executes the callbacks for the expando
- function defaultNativeHandler(evt) {
- executeHandlers(fix(evt || win.event), id);
- }
-
- // Don't bind to text nodes or comments
- if (!target || target.nodeType === 3 || target.nodeType === 8) {
- return;
- }
-
- // Create or get events id for the target
- if (!target[expando]) {
- id = count++;
- target[expando] = id;
- events[id] = {};
- } else {
- id = target[expando];
-
- if (!events[id]) {
- events[id] = {};
- }
- }
-
- // Setup the specified scope or use the target as a default
- scope = scope || target;
-
- // Split names and bind each event, enables you to bind multiple events with one call
- names = names.split(' ');
- i = names.length;
- while (i--) {
- name = names[i];
- nativeHandler = defaultNativeHandler;
- fakeName = capture = false;
-
- // Use ready instead of DOMContentLoaded
- if (name === "DOMContentLoaded") {
- name = "ready";
- }
-
- // DOM is already ready
- if ((self.domLoaded || target.readyState == 'complete') && name === "ready") {
- self.domLoaded = true;
- callback.call(scope, fix({type: name}));
- continue;
- }
-
- // Handle mouseenter/mouseleaver
- if (!hasMouseEnterLeave) {
- fakeName = mouseEnterLeave[name];
-
- if (fakeName) {
- nativeHandler = function(evt) {
- var current, related;
-
- current = evt.currentTarget;
- related = evt.relatedTarget;
-
- // Check if related is inside the current target if it's not then the event should be ignored since it's a mouseover/mouseout inside the element
- if (related && current.contains) {
- // Use contains for performance
- related = current.contains(related);
- } else {
- while (related && related !== current) {
- related = related.parentNode;
- }
- }
-
- // Fire fake event
- if (!related) {
- evt = fix(evt || win.event);
- evt.type = evt.type === 'mouseout' ? 'mouseleave' : 'mouseenter';
- evt.target = current;
- executeHandlers(evt, id);
- }
- };
- }
- }
-
- // Fake bubbeling of focusin/focusout
- if (!hasFocusIn && (name === "focusin" || name === "focusout")) {
- capture = true;
- fakeName = name === "focusin" ? "focus" : "blur";
- nativeHandler = function(evt) {
- evt = fix(evt || win.event);
- evt.type = evt.type === 'focus' ? 'focusin' : 'focusout';
- executeHandlers(evt, id);
- };
- }
-
- // Setup callback list and bind native event
- callbackList = events[id][name];
- if (!callbackList) {
- events[id][name] = callbackList = [{func: callback, scope: scope}];
- callbackList.fakeName = fakeName;
- callbackList.capture = capture;
-
- // Add the nativeHandler to the callback list so that we can later unbind it
- callbackList.nativeHandler = nativeHandler;
- if (!w3cEventModel) {
- callbackList.proxyHandler = proxy(id);
- }
-
- // Check if the target has native events support
- if (name === "ready") {
- bindOnReady(target, nativeHandler, self);
- } else {
- addEvent(target, fakeName || name, w3cEventModel ? nativeHandler : callbackList.proxyHandler, capture);
- }
- } else {
- // If it already has an native handler then just push the callback
- callbackList.push({func: callback, scope: scope});
- }
- }
-
- target = callbackList = 0; // Clean memory for IE
-
- return callback;
- };
-
- self.unbind = function(target, names, callback) {
- var id, callbackList, i, ci, name, eventMap;
-
- // Don't bind to text nodes or comments
- if (!target || target.nodeType === 3 || target.nodeType === 8) {
- return self;
- }
-
- // Unbind event or events if the target has the expando
- id = target[expando];
- if (id) {
- eventMap = events[id];
-
- // Specific callback
- if (names) {
- names = names.split(' ');
- i = names.length;
- while (i--) {
- name = names[i];
- callbackList = eventMap[name];
-
- // Unbind the event if it exists in the map
- if (callbackList) {
- // Remove specified callback
- if (callback) {
- ci = callbackList.length;
- while (ci--) {
- if (callbackList[ci].func === callback) {
- callbackList.splice(ci, 1);
- }
- }
- }
-
- // Remove all callbacks if there isn't a specified callback or there is no callbacks left
- if (!callback || callbackList.length === 0) {
- delete eventMap[name];
- removeEvent(target, callbackList.fakeName || name, w3cEventModel ? callbackList.nativeHandler : callbackList.proxyHandler, callbackList.capture);
- }
- }
- }
- } else {
- // All events for a specific element
- for (name in eventMap) {
- callbackList = eventMap[name];
- removeEvent(target, callbackList.fakeName || name, w3cEventModel ? callbackList.nativeHandler : callbackList.proxyHandler, callbackList.capture);
- }
-
- eventMap = {};
- }
-
- // Check if object is empty, if it isn't then we won't remove the expando map
- for (name in eventMap) {
- return self;
- }
-
- // Delete event object
- delete events[id];
-
- // Remove expando from target
- try {
- // IE will fail here since it can't delete properties from window
- delete target[expando];
- } catch (ex) {
- // IE will set it to null
- target[expando] = null;
- }
- }
-
- return self;
- };
-
- self.fire = function(target, name, args) {
- var id, event;
-
- // Don't bind to text nodes or comments
- if (!target || target.nodeType === 3 || target.nodeType === 8) {
- return self;
- }
-
- // Build event object by patching the args
- event = fix(null, args);
- event.type = name;
-
- do {
- // Found an expando that means there is listeners to execute
- id = target[expando];
- if (id) {
- executeHandlers(event, id);
- }
-
- // Walk up the DOM
- target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow;
- } while (target && !event.isPropagationStopped());
-
- return self;
- };
-
- self.clean = function(target) {
- var i, children, unbind = self.unbind;
-
- // Don't bind to text nodes or comments
- if (!target || target.nodeType === 3 || target.nodeType === 8) {
- return self;
- }
-
- // Unbind any element on the specificed target
- if (target[expando]) {
- unbind(target);
- }
-
- // Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children
- if (!target.getElementsByTagName) {
- target = target.document;
- }
-
- // Remove events from each child element
- if (target && target.getElementsByTagName) {
- unbind(target);
-
- children = target.getElementsByTagName('*');
- i = children.length;
- while (i--) {
- target = children[i];
-
- if (target[expando]) {
- unbind(target);
- }
- }
- }
-
- return self;
- };
-
- self.callNativeHandler = function(id, evt) {
- if (events) {
- events[id][evt.type].nativeHandler(evt);
- }
- };
-
- self.destory = function() {
- events = {};
- };
-
- // Legacy function calls
-
- self.add = function(target, events, func, scope) {
- // Old API supported direct ID assignment
- if (typeof(target) === "string") {
- target = document.getElementById(target);
- }
-
- // Old API supported multiple targets
- if (target && target instanceof Array) {
- var i = target.length;
-
- while (i--) {
- self.add(target[i], events, func, scope);
- }
-
- return;
- }
-
- // Old API called ready init
- if (events === "init") {
- events = "ready";
- }
-
- return self.bind(target, events instanceof Array ? events.join(' ') : events, func, scope);
- };
-
- self.remove = function(target, events, func, scope) {
- if (!target) {
- return self;
- }
-
- // Old API supported direct ID assignment
- if (typeof(target) === "string") {
- target = document.getElementById(target);
- }
-
- // Old API supported multiple targets
- if (target instanceof Array) {
- var i = target.length;
-
- while (i--) {
- self.remove(target[i], events, func, scope);
- }
-
- return self;
- }
-
- return self.unbind(target, events instanceof Array ? events.join(' ') : events, func);
- };
-
- self.clear = function(target) {
- // Old API supported direct ID assignment
- if (typeof(target) === "string") {
- target = document.getElementById(target);
- }
-
- return self.clean(target);
- };
-
- self.cancel = function(e) {
- if (e) {
- self.prevent(e);
- self.stop(e);
- }
-
- return false;
- };
-
- self.prevent = function(e) {
- if (!e.preventDefault) {
- e = fix(e);
- }
-
- e.preventDefault();
-
- return false;
- };
-
- self.stop = function(e) {
- if (!e.stopPropagation) {
- e = fix(e);
- }
-
- e.stopPropagation();
-
- return false;
- };
- }
-
- namespace.EventUtils = EventUtils;
-
- namespace.Event = new EventUtils(function(id) {
- return function(evt) {
- tinymce.dom.Event.callNativeHandler(id, evt);
- };
- });
-
- // Bind ready event when tinymce script is loaded
- namespace.Event.bind(window, 'ready', function() {});
-
- namespace = 0;
-})(tinymce.dom, 'data-mce-expando'); // Namespace and expando
-
-tinymce.dom.TreeWalker = function(start_node, root_node) {
- var node = start_node;
-
- function findSibling(node, start_name, sibling_name, shallow) {
- var sibling, parent;
-
- if (node) {
- // Walk into nodes if it has a start
- if (!shallow && node[start_name])
- return node[start_name];
-
- // Return the sibling if it has one
- if (node != root_node) {
- sibling = node[sibling_name];
- if (sibling)
- return sibling;
-
- // Walk up the parents to look for siblings
- for (parent = node.parentNode; parent && parent != root_node; parent = parent.parentNode) {
- sibling = parent[sibling_name];
- if (sibling)
- return sibling;
- }
- }
- }
- };
-
- this.current = function() {
- return node;
- };
-
- this.next = function(shallow) {
- return (node = findSibling(node, 'firstChild', 'nextSibling', shallow));
- };
-
- this.prev = function(shallow) {
- return (node = findSibling(node, 'lastChild', 'previousSibling', shallow));
- };
-};
-
-(function(tinymce) {
- // Shorten names
- var each = tinymce.each,
- is = tinymce.is,
- isWebKit = tinymce.isWebKit,
- isIE = tinymce.isIE,
- Entities = tinymce.html.Entities,
- simpleSelectorRe = /^([a-z0-9],?)+$/i,
- whiteSpaceRegExp = /^[ \t\r\n]*$/;
-
- tinymce.create('tinymce.dom.DOMUtils', {
- doc : null,
- root : null,
- files : null,
- pixelStyles : /^(top|left|bottom|right|width|height|borderWidth)$/,
- props : {
- "for" : "htmlFor",
- "class" : "className",
- className : "className",
- checked : "checked",
- disabled : "disabled",
- maxlength : "maxLength",
- readonly : "readOnly",
- selected : "selected",
- value : "value",
- id : "id",
- name : "name",
- type : "type"
- },
-
- DOMUtils : function(d, s) {
- var t = this, globalStyle, name, blockElementsMap;
-
- t.doc = d;
- t.win = window;
- t.files = {};
- t.cssFlicker = false;
- t.counter = 0;
- t.stdMode = !tinymce.isIE || d.documentMode >= 8;
- t.boxModel = !tinymce.isIE || d.compatMode == "CSS1Compat" || t.stdMode;
- t.hasOuterHTML = "outerHTML" in d.createElement("a");
-
- t.settings = s = tinymce.extend({
- keep_values : false,
- hex_colors : 1
- }, s);
-
- t.schema = s.schema;
- t.styles = new tinymce.html.Styles({
- url_converter : s.url_converter,
- url_converter_scope : s.url_converter_scope
- }, s.schema);
-
- // Fix IE6SP2 flicker and check it failed for pre SP2
- if (tinymce.isIE6) {
- try {
- d.execCommand('BackgroundImageCache', false, true);
- } catch (e) {
- t.cssFlicker = true;
- }
- }
-
- t.fixDoc(d);
- t.events = s.ownEvents ? new tinymce.dom.EventUtils(s.proxy) : tinymce.dom.Event;
- tinymce.addUnload(t.destroy, t);
- blockElementsMap = s.schema ? s.schema.getBlockElements() : {};
-
- t.isBlock = function(node) {
- // This function is called in module pattern style since it might be executed with the wrong this scope
- var type = node.nodeType;
-
- // If it's a node then check the type and use the nodeName
- if (type)
- return !!(type === 1 && blockElementsMap[node.nodeName]);
-
- return !!blockElementsMap[node];
- };
- },
-
- fixDoc: function(doc) {
- var settings = this.settings, name;
-
- if (isIE && settings.schema) {
- // Add missing HTML 4/5 elements to IE
- ('abbr article aside audio canvas ' +
- 'details figcaption figure footer ' +
- 'header hgroup mark menu meter nav ' +
- 'output progress section summary ' +
- 'time video').replace(/\w+/g, function(name) {
- doc.createElement(name);
- });
-
- // Create all custom elements
- for (name in settings.schema.getCustomElements()) {
- doc.createElement(name);
- }
- }
- },
-
- clone: function(node, deep) {
- var self = this, clone, doc;
-
- // TODO: Add feature detection here in the future
- if (!isIE || node.nodeType !== 1 || deep) {
- return node.cloneNode(deep);
- }
-
- doc = self.doc;
-
- // Make a HTML5 safe shallow copy
- if (!deep) {
- clone = doc.createElement(node.nodeName);
-
- // Copy attribs
- each(self.getAttribs(node), function(attr) {
- self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName));
- });
-
- return clone;
- }
-/*
- // Setup HTML5 patched document fragment
- if (!self.frag) {
- self.frag = doc.createDocumentFragment();
- self.fixDoc(self.frag);
- }
-
- // Make a deep copy by adding it to the document fragment then removing it this removed the :section
- clone = doc.createElement('div');
- self.frag.appendChild(clone);
- clone.innerHTML = node.outerHTML;
- self.frag.removeChild(clone);
-*/
- return clone.firstChild;
- },
-
- getRoot : function() {
- var t = this, s = t.settings;
-
- return (s && t.get(s.root_element)) || t.doc.body;
- },
-
- getViewPort : function(w) {
- var d, b;
-
- w = !w ? this.win : w;
- d = w.document;
- b = this.boxModel ? d.documentElement : d.body;
-
- // Returns viewport size excluding scrollbars
- return {
- x : w.pageXOffset || b.scrollLeft,
- y : w.pageYOffset || b.scrollTop,
- w : w.innerWidth || b.clientWidth,
- h : w.innerHeight || b.clientHeight
- };
- },
-
- getRect : function(e) {
- var p, t = this, sr;
-
- e = t.get(e);
- p = t.getPos(e);
- sr = t.getSize(e);
-
- return {
- x : p.x,
- y : p.y,
- w : sr.w,
- h : sr.h
- };
- },
-
- getSize : function(e) {
- var t = this, w, h;
-
- e = t.get(e);
- w = t.getStyle(e, 'width');
- h = t.getStyle(e, 'height');
-
- // Non pixel value, then force offset/clientWidth
- if (w.indexOf('px') === -1)
- w = 0;
-
- // Non pixel value, then force offset/clientWidth
- if (h.indexOf('px') === -1)
- h = 0;
-
- return {
- w : parseInt(w, 10) || e.offsetWidth || e.clientWidth,
- h : parseInt(h, 10) || e.offsetHeight || e.clientHeight
- };
- },
-
- getParent : function(n, f, r) {
- return this.getParents(n, f, r, false);
- },
-
- getParents : function(n, f, r, c) {
- var t = this, na, se = t.settings, o = [];
-
- n = t.get(n);
- c = c === undefined;
-
- if (se.strict_root)
- r = r || t.getRoot();
-
- // Wrap node name as func
- if (is(f, 'string')) {
- na = f;
-
- if (f === '*') {
- f = function(n) {return n.nodeType == 1;};
- } else {
- f = function(n) {
- return t.is(n, na);
- };
- }
- }
-
- while (n) {
- if (n == r || !n.nodeType || n.nodeType === 9)
- break;
-
- if (!f || f(n)) {
- if (c)
- o.push(n);
- else
- return n;
- }
-
- n = n.parentNode;
- }
-
- return c ? o : null;
- },
-
- get : function(e) {
- var n;
-
- if (e && this.doc && typeof(e) == 'string') {
- n = e;
- e = this.doc.getElementById(e);
-
- // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick
- if (e && e.id !== n)
- return this.doc.getElementsByName(n)[1];
- }
-
- return e;
- },
-
- getNext : function(node, selector) {
- return this._findSib(node, selector, 'nextSibling');
- },
-
- getPrev : function(node, selector) {
- return this._findSib(node, selector, 'previousSibling');
- },
-
-
- select : function(pa, s) {
- var t = this;
-
- return tinymce.dom.Sizzle(pa, t.get(s) || t.get(t.settings.root_element) || t.doc, []);
- },
-
- is : function(n, selector) {
- var i;
-
- // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance
- if (n.length === undefined) {
- // Simple all selector
- if (selector === '*')
- return n.nodeType == 1;
-
- // Simple selector just elements
- if (simpleSelectorRe.test(selector)) {
- selector = selector.toLowerCase().split(/,/);
- n = n.nodeName.toLowerCase();
-
- for (i = selector.length - 1; i >= 0; i--) {
- if (selector[i] == n)
- return true;
- }
-
- return false;
- }
- }
-
- return tinymce.dom.Sizzle.matches(selector, n.nodeType ? [n] : n).length > 0;
- },
-
-
- add : function(p, n, a, h, c) {
- var t = this;
-
- return this.run(p, function(p) {
- var e, k;
-
- e = is(n, 'string') ? t.doc.createElement(n) : n;
- t.setAttribs(e, a);
-
- if (h) {
- if (h.nodeType)
- e.appendChild(h);
- else
- t.setHTML(e, h);
- }
-
- return !c ? p.appendChild(e) : e;
- });
- },
-
- create : function(n, a, h) {
- return this.add(this.doc.createElement(n), n, a, h, 1);
- },
-
- createHTML : function(n, a, h) {
- var o = '', t = this, k;
-
- o += '<' + n;
-
- for (k in a) {
- if (a.hasOwnProperty(k))
- o += ' ' + k + '="' + t.encode(a[k]) + '"';
- }
-
- // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime
- if (typeof(h) != "undefined")
- return o + '>' + h + '</' + n + '>';
-
- return o + ' />';
- },
-
- remove : function(node, keep_children) {
- return this.run(node, function(node) {
- var child, parent = node.parentNode;
-
- if (!parent)
- return null;
-
- if (keep_children) {
- while (child = node.firstChild) {
- // IE 8 will crash if you don't remove completely empty text nodes
- if (!tinymce.isIE || child.nodeType !== 3 || child.nodeValue)
- parent.insertBefore(child, node);
- else
- node.removeChild(child);
- }
- }
-
- return parent.removeChild(node);
- });
- },
-
- setStyle : function(n, na, v) {
- var t = this;
-
- return t.run(n, function(e) {
- var s, i;
-
- s = e.style;
-
- // Camelcase it, if needed
- na = na.replace(/-(\D)/g, function(a, b){
- return b.toUpperCase();
- });
-
- // Default px suffix on these
- if (t.pixelStyles.test(na) && (tinymce.is(v, 'number') || /^[\-0-9\.]+$/.test(v)))
- v += 'px';
-
- switch (na) {
- case 'opacity':
- // IE specific opacity
- if (isIE) {
- s.filter = v === '' ? '' : "alpha(opacity=" + (v * 100) + ")";
-
- if (!n.currentStyle || !n.currentStyle.hasLayout)
- s.display = 'inline-block';
- }
-
- // Fix for older browsers
- s[na] = s['-moz-opacity'] = s['-khtml-opacity'] = v || '';
- break;
-
- case 'float':
- isIE ? s.styleFloat = v : s.cssFloat = v;
- break;
-
- default:
- s[na] = v || '';
- }
-
- // Force update of the style data
- if (t.settings.update_styles)
- t.setAttrib(e, 'data-mce-style');
- });
- },
-
- getStyle : function(n, na, c) {
- n = this.get(n);
-
- if (!n)
- return;
-
- // Gecko
- if (this.doc.defaultView && c) {
- // Remove camelcase
- na = na.replace(/[A-Z]/g, function(a){
- return '-' + a;
- });
-
- try {
- return this.doc.defaultView.getComputedStyle(n, null).getPropertyValue(na);
- } catch (ex) {
- // Old safari might fail
- return null;
- }
- }
-
- // Camelcase it, if needed
- na = na.replace(/-(\D)/g, function(a, b){
- return b.toUpperCase();
- });
-
- if (na == 'float')
- na = isIE ? 'styleFloat' : 'cssFloat';
-
- // IE & Opera
- if (n.currentStyle && c)
- return n.currentStyle[na];
-
- return n.style ? n.style[na] : undefined;
- },
-
- setStyles : function(e, o) {
- var t = this, s = t.settings, ol;
-
- ol = s.update_styles;
- s.update_styles = 0;
-
- each(o, function(v, n) {
- t.setStyle(e, n, v);
- });
-
- // Update style info
- s.update_styles = ol;
- if (s.update_styles)
- t.setAttrib(e, s.cssText);
- },
-
- removeAllAttribs: function(e) {
- return this.run(e, function(e) {
- var i, attrs = e.attributes;
- for (i = attrs.length - 1; i >= 0; i--) {
- e.removeAttributeNode(attrs.item(i));
- }
- });
- },
-
- setAttrib : function(e, n, v) {
- var t = this;
-
- // Whats the point
- if (!e || !n)
- return;
-
- // Strict XML mode
- if (t.settings.strict)
- n = n.toLowerCase();
-
- return this.run(e, function(e) {
- var s = t.settings;
- var originalValue = e.getAttribute(n);
- if (v !== null) {
- switch (n) {
- case "style":
- if (!is(v, 'string')) {
- each(v, function(v, n) {
- t.setStyle(e, n, v);
- });
-
- return;
- }
-
- // No mce_style for elements with these since they might get resized by the user
- if (s.keep_values) {
- if (v && !t._isRes(v))
- e.setAttribute('data-mce-style', v, 2);
- else
- e.removeAttribute('data-mce-style', 2);
- }
-
- e.style.cssText = v;
- break;
-
- case "class":
- e.className = v || ''; // Fix IE null bug
- break;
-
- case "src":
- case "href":
- if (s.keep_values) {
- if (s.url_converter)
- v = s.url_converter.call(s.url_converter_scope || t, v, n, e);
-
- t.setAttrib(e, 'data-mce-' + n, v, 2);
- }
-
- break;
-
- case "shape":
- e.setAttribute('data-mce-style', v);
- break;
- }
- }
- if (is(v) && v !== null && v.length !== 0)
- e.setAttribute(n, '' + v, 2);
- else
- e.removeAttribute(n, 2);
-
- // fire onChangeAttrib event for attributes that have changed
- if (tinyMCE.activeEditor && originalValue != v) {
- var ed = tinyMCE.activeEditor;
- ed.onSetAttrib.dispatch(ed, e, n, v);
- }
- });
- },
-
- setAttribs : function(e, o) {
- var t = this;
-
- return this.run(e, function(e) {
- each(o, function(v, n) {
- t.setAttrib(e, n, v);
- });
- });
- },
-
- getAttrib : function(e, n, dv) {
- var v, t = this, undef;
-
- e = t.get(e);
-
- if (!e || e.nodeType !== 1)
- return dv === undef ? false : dv;
-
- if (!is(dv))
- dv = '';
-
- // Try the mce variant for these
- if (/^(src|href|style|coords|shape)$/.test(n)) {
- v = e.getAttribute("data-mce-" + n);
-
- if (v)
- return v;
- }
-
- if (isIE && t.props[n]) {
- v = e[t.props[n]];
- v = v && v.nodeValue ? v.nodeValue : v;
- }
-
- if (!v)
- v = e.getAttribute(n, 2);
-
- // Check boolean attribs
- if (/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(n)) {
- if (e[t.props[n]] === true && v === '')
- return n;
-
- return v ? n : '';
- }
-
- // Inner input elements will override attributes on form elements
- if (e.nodeName === "FORM" && e.getAttributeNode(n))
- return e.getAttributeNode(n).nodeValue;
-
- if (n === 'style') {
- v = v || e.style.cssText;
-
- if (v) {
- v = t.serializeStyle(t.parseStyle(v), e.nodeName);
-
- if (t.settings.keep_values && !t._isRes(v))
- e.setAttribute('data-mce-style', v);
- }
- }
-
- // Remove Apple and WebKit stuff
- if (isWebKit && n === "class" && v)
- v = v.replace(/(apple|webkit)\-[a-z\-]+/gi, '');
-
- // Handle IE issues
- if (isIE) {
- switch (n) {
- case 'rowspan':
- case 'colspan':
- // IE returns 1 as default value
- if (v === 1)
- v = '';
-
- break;
-
- case 'size':
- // IE returns +0 as default value for size
- if (v === '+0' || v === 20 || v === 0)
- v = '';
-
- break;
-
- case 'width':
- case 'height':
- case 'vspace':
- case 'checked':
- case 'disabled':
- case 'readonly':
- if (v === 0)
- v = '';
-
- break;
-
- case 'hspace':
- // IE returns -1 as default value
- if (v === -1)
- v = '';
-
- break;
-
- case 'maxlength':
- case 'tabindex':
- // IE returns default value
- if (v === 32768 || v === 2147483647 || v === '32768')
- v = '';
-
- break;
-
- case 'multiple':
- case 'compact':
- case 'noshade':
- case 'nowrap':
- if (v === 65535)
- return n;
-
- return dv;
-
- case 'shape':
- v = v.toLowerCase();
- break;
-
- default:
- // IE has odd anonymous function for event attributes
- if (n.indexOf('on') === 0 && v)
- v = tinymce._replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/, '$1', '' + v);
- }
- }
-
- return (v !== undef && v !== null && v !== '') ? '' + v : dv;
- },
-
- getPos : function(n, ro) {
- var t = this, x = 0, y = 0, e, d = t.doc, r;
-
- n = t.get(n);
- ro = ro || d.body;
-
- if (n) {
- // Use getBoundingClientRect if it exists since it's faster than looping offset nodes
- if (n.getBoundingClientRect) {
- n = n.getBoundingClientRect();
- e = t.boxModel ? d.documentElement : d.body;
-
- // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit
- // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position
- x = n.left + (d.documentElement.scrollLeft || d.body.scrollLeft) - e.clientTop;
- y = n.top + (d.documentElement.scrollTop || d.body.scrollTop) - e.clientLeft;
-
- return {x : x, y : y};
- }
-
- r = n;
- while (r && r != ro && r.nodeType) {
- x += r.offsetLeft || 0;
- y += r.offsetTop || 0;
- r = r.offsetParent;
- }
-
- r = n.parentNode;
- while (r && r != ro && r.nodeType) {
- x -= r.scrollLeft || 0;
- y -= r.scrollTop || 0;
- r = r.parentNode;
- }
- }
-
- return {x : x, y : y};
- },
-
- parseStyle : function(st) {
- return this.styles.parse(st);
- },
-
- serializeStyle : function(o, name) {
- return this.styles.serialize(o, name);
- },
-
- addStyle: function(cssText) {
- var doc = this.doc, head;
-
- // Create style element if needed
- styleElm = doc.getElementById('mceDefaultStyles');
- if (!styleElm) {
- styleElm = doc.createElement('style'),
- styleElm.id = 'mceDefaultStyles';
- styleElm.type = 'text/css';
-
- head = doc.getElementsByTagName('head')[0];
- if (head.firstChild) {
- head.insertBefore(styleElm, head.firstChild);
- } else {
- head.appendChild(styleElm);
- }
- }
-
- // Append style data to old or new style element
- if (styleElm.styleSheet) {
- styleElm.styleSheet.cssText += cssText;
- } else {
- styleElm.appendChild(doc.createTextNode(cssText));
- }
- },
-
- loadCSS : function(u) {
- var t = this, d = t.doc, head;
-
- if (!u)
- u = '';
-
- head = d.getElementsByTagName('head')[0];
-
- each(u.split(','), function(u) {
- var link;
-
- if (t.files[u])
- return;
-
- t.files[u] = true;
- link = t.create('link', {rel : 'stylesheet', href : tinymce._addVer(u)});
-
- // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug
- // This fix seems to resolve that issue by realcing the document ones a stylesheet finishes loading
- // It's ugly but it seems to work fine.
- if (isIE && d.documentMode && d.recalc) {
- link.onload = function() {
- if (d.recalc)
- d.recalc();
-
- link.onload = null;
- };
- }
-
- head.appendChild(link);
- });
- },
-
- addClass : function(e, c) {
- return this.run(e, function(e) {
- var o;
-
- if (!c)
- return 0;
-
- if (this.hasClass(e, c))
- return e.className;
-
- o = this.removeClass(e, c);
-
- return e.className = (o != '' ? (o + ' ') : '') + c;
- });
- },
-
- removeClass : function(e, c) {
- var t = this, re;
-
- return t.run(e, function(e) {
- var v;
-
- if (t.hasClass(e, c)) {
- if (!re)
- re = new RegExp("(^|\\s+)" + c + "(\\s+|$)", "g");
-
- v = e.className.replace(re, ' ');
- v = tinymce.trim(v != ' ' ? v : '');
-
- e.className = v;
-
- // Empty class attr
- if (!v) {
- e.removeAttribute('class');
- e.removeAttribute('className');
- }
-
- return v;
- }
-
- return e.className;
- });
- },
-
- hasClass : function(n, c) {
- n = this.get(n);
-
- if (!n || !c)
- return false;
-
- return (' ' + n.className + ' ').indexOf(' ' + c + ' ') !== -1;
- },
-
- show : function(e) {
- return this.setStyle(e, 'display', 'block');
- },
-
- hide : function(e) {
- return this.setStyle(e, 'display', 'none');
- },
-
- isHidden : function(e) {
- e = this.get(e);
-
- return !e || e.style.display == 'none' || this.getStyle(e, 'display') == 'none';
- },
-
- uniqueId : function(p) {
- return (!p ? 'mce_' : p) + (this.counter++);
- },
-
- setHTML : function(element, html) {
- var self = this;
-
- return self.run(element, function(element) {
- if (isIE) {
- // Remove all child nodes, IE keeps empty text nodes in DOM
- while (element.firstChild)
- element.removeChild(element.firstChild);
-
- try {
- // IE will remove comments from the beginning
- // unless you padd the contents with something
- element.innerHTML = '<br />' + html;
- element.removeChild(element.firstChild);
- } catch (ex) {
- // IE sometimes produces an unknown runtime error on innerHTML if it's an block element within a block element for example a div inside a p
- // This seems to fix this problem
-
- // Create new div with HTML contents and a BR infront to keep comments
- var newElement = self.create('div');
- newElement.innerHTML = '<br />' + html;
-
- // Add all children from div to target
- each (tinymce.grep(newElement.childNodes), function(node, i) {
- // Skip br element
- if (i && element.canHaveHTML)
- element.appendChild(node);
- });
- }
- } else
- element.innerHTML = html;
-
- return html;
- });
- },
-
- getOuterHTML : function(elm) {
- var doc, self = this;
-
- elm = self.get(elm);
-
- if (!elm)
- return null;
-
- if (elm.nodeType === 1 && self.hasOuterHTML)
- return elm.outerHTML;
-
- doc = (elm.ownerDocument || self.doc).createElement("body");
- doc.appendChild(elm.cloneNode(true));
-
- return doc.innerHTML;
- },
-
- setOuterHTML : function(e, h, d) {
- var t = this;
-
- function setHTML(e, h, d) {
- var n, tp;
-
- tp = d.createElement("body");
- tp.innerHTML = h;
-
- n = tp.lastChild;
- while (n) {
- t.insertAfter(n.cloneNode(true), e);
- n = n.previousSibling;
- }
-
- t.remove(e);
- };
-
- return this.run(e, function(e) {
- e = t.get(e);
-
- // Only set HTML on elements
- if (e.nodeType == 1) {
- d = d || e.ownerDocument || t.doc;
-
- if (isIE) {
- try {
- // Try outerHTML for IE it sometimes produces an unknown runtime error
- if (isIE && e.nodeType == 1)
- e.outerHTML = h;
- else
- setHTML(e, h, d);
- } catch (ex) {
- // Fix for unknown runtime error
- setHTML(e, h, d);
- }
- } else
- setHTML(e, h, d);
- }
- });
- },
-
- decode : Entities.decode,
-
- encode : Entities.encodeAllRaw,
-
- insertAfter : function(node, reference_node) {
- reference_node = this.get(reference_node);
-
- return this.run(node, function(node) {
- var parent, nextSibling;
-
- parent = reference_node.parentNode;
- nextSibling = reference_node.nextSibling;
-
- if (nextSibling)
- parent.insertBefore(node, nextSibling);
- else
- parent.appendChild(node);
-
- return node;
- });
- },
-
- replace : function(n, o, k) {
- var t = this;
-
- if (is(o, 'array'))
- n = n.cloneNode(true);
-
- return t.run(o, function(o) {
- if (k) {
- each(tinymce.grep(o.childNodes), function(c) {
- n.appendChild(c);
- });
- }
-
- return o.parentNode.replaceChild(n, o);
- });
- },
-
- rename : function(elm, name) {
- var t = this, newElm;
-
- if (elm.nodeName != name.toUpperCase()) {
- // Rename block element
- newElm = t.create(name);
-
- // Copy attribs to new block
- each(t.getAttribs(elm), function(attr_node) {
- t.setAttrib(newElm, attr_node.nodeName, t.getAttrib(elm, attr_node.nodeName));
- });
-
- // Replace block
- t.replace(newElm, elm, 1);
- }
-
- return newElm || elm;
- },
-
- findCommonAncestor : function(a, b) {
- var ps = a, pe;
-
- while (ps) {
- pe = b;
-
- while (pe && ps != pe)
- pe = pe.parentNode;
-
- if (ps == pe)
- break;
-
- ps = ps.parentNode;
- }
-
- if (!ps && a.ownerDocument)
- return a.ownerDocument.documentElement;
-
- return ps;
- },
-
- toHex : function(s) {
- var c = /^\s*rgb\s*?\(\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?\)\s*$/i.exec(s);
-
- function hex(s) {
- s = parseInt(s, 10).toString(16);
-
- return s.length > 1 ? s : '0' + s; // 0 -> 00
- };
-
- if (c) {
- s = '#' + hex(c[1]) + hex(c[2]) + hex(c[3]);
-
- return s;
- }
-
- return s;
- },
-
- getClasses : function() {
- var t = this, cl = [], i, lo = {}, f = t.settings.class_filter, ov;
-
- if (t.classes)
- return t.classes;
-
- function addClasses(s) {
- // IE style imports
- each(s.imports, function(r) {
- addClasses(r);
- });
-
- each(s.cssRules || s.rules, function(r) {
- // Real type or fake it on IE
- switch (r.type || 1) {
- // Rule
- case 1:
- if (r.selectorText) {
- each(r.selectorText.split(','), function(v) {
- v = v.replace(/^\s*|\s*$|^\s\./g, "");
-
- // Is internal or it doesn't contain a class
- if (/\.mce/.test(v) || !/\.[\w\-]+$/.test(v))
- return;
-
- // Remove everything but class name
- ov = v;
- v = tinymce._replace(/.*\.([a-z0-9_\-]+).*/i, '$1', v);
-
- // Filter classes
- if (f && !(v = f(v, ov)))
- return;
-
- if (!lo[v]) {
- cl.push({'class' : v});
- lo[v] = 1;
- }
- });
- }
- break;
-
- // Import
- case 3:
- addClasses(r.styleSheet);
- break;
- }
- });
- };
-
- try {
- each(t.doc.styleSheets, addClasses);
- } catch (ex) {
- // Ignore
- }
-
- if (cl.length > 0)
- t.classes = cl;
-
- return cl;
- },
-
- run : function(e, f, s) {
- var t = this, o;
-
- if (t.doc && typeof(e) === 'string')
- e = t.get(e);
-
- if (!e)
- return false;
-
- s = s || this;
- if (!e.nodeType && (e.length || e.length === 0)) {
- o = [];
-
- each(e, function(e, i) {
- if (e) {
- if (typeof(e) == 'string')
- e = t.doc.getElementById(e);
-
- o.push(f.call(s, e, i));
- }
- });
-
- return o;
- }
-
- return f.call(s, e);
- },
-
- getAttribs : function(n) {
- var o;
-
- n = this.get(n);
-
- if (!n)
- return [];
-
- if (isIE) {
- o = [];
-
- // Object will throw exception in IE
- if (n.nodeName == 'OBJECT')
- return n.attributes;
-
- // IE doesn't keep the selected attribute if you clone option elements
- if (n.nodeName === 'OPTION' && this.getAttrib(n, 'selected'))
- o.push({specified : 1, nodeName : 'selected'});
-
- // It's crazy that this is faster in IE but it's because it returns all attributes all the time
- n.cloneNode(false).outerHTML.replace(/<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi, '').replace(/[\w:\-]+/gi, function(a) {
- o.push({specified : 1, nodeName : a});
- });
-
- return o;
- }
-
- return n.attributes;
- },
-
- isEmpty : function(node, elements) {
- var self = this, i, attributes, type, walker, name, brCount = 0;
-
- node = node.firstChild;
- if (node) {
- walker = new tinymce.dom.TreeWalker(node, node.parentNode);
- elements = elements || self.schema ? self.schema.getNonEmptyElements() : null;
-
- do {
- type = node.nodeType;
-
- if (type === 1) {
- // Ignore bogus elements
- if (node.getAttribute('data-mce-bogus'))
- continue;
-
- // Keep empty elements like <img />
- name = node.nodeName.toLowerCase();
- if (elements && elements[name]) {
- // Ignore single BR elements in blocks like <p><br /></p> or <p><span><br /></span></p>
- if (name === 'br') {
- brCount++;
- continue;
- }
-
- return false;
- }
-
- // Keep elements with data-bookmark attributes or name attribute like <a name="1"></a>
- attributes = self.getAttribs(node);
- i = node.attributes.length;
- while (i--) {
- name = node.attributes[i].nodeName;
- if (name === "name" || name === 'data-mce-bookmark')
- return false;
- }
- }
-
- // Keep comment nodes
- if (type == 8)
- return false;
-
- // Keep non whitespace text nodes
- if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue)))
- return false;
- } while (node = walker.next());
- }
-
- return brCount <= 1;
- },
-
- destroy : function(s) {
- var t = this;
-
- t.win = t.doc = t.root = t.events = t.frag = null;
-
- // Manual destroy then remove unload handler
- if (!s)
- tinymce.removeUnload(t.destroy);
- },
-
- createRng : function() {
- var d = this.doc;
-
- return d.createRange ? d.createRange() : new tinymce.dom.Range(this);
- },
-
- nodeIndex : function(node, normalized) {
- var idx = 0, lastNodeType, lastNode, nodeType;
-
- if (node) {
- for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) {
- nodeType = node.nodeType;
-
- // Normalize text nodes
- if (normalized && nodeType == 3) {
- if (nodeType == lastNodeType || !node.nodeValue.length)
- continue;
- }
- idx++;
- lastNodeType = nodeType;
- }
- }
-
- return idx;
- },
-
- split : function(pe, e, re) {
- var t = this, r = t.createRng(), bef, aft, pa;
-
- // W3C valid browsers tend to leave empty nodes to the left/right side of the contents, this makes sense
- // but we don't want that in our code since it serves no purpose for the end user
- // For example if this is chopped:
- // <p>text 1<span><b>CHOP</b></span>text 2</p>
- // would produce:
- // <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p>
- // this function will then trim of empty edges and produce:
- // <p>text 1</p><b>CHOP</b><p>text 2</p>
- function trim(node) {
- var i, children = node.childNodes, type = node.nodeType;
-
- function surroundedBySpans(node) {
- var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN';
- var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN';
- return previousIsSpan && nextIsSpan;
- }
-
- if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark')
- return;
-
- for (i = children.length - 1; i >= 0; i--)
- trim(children[i]);
-
- if (type != 9) {
- // Keep non whitespace text nodes
- if (type == 3 && node.nodeValue.length > 0) {
- // If parent element isn't a block or there isn't any useful contents for example "<p> </p>"
- // Also keep text nodes with only spaces if surrounded by spans.
- // eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b
- var trimmedLength = tinymce.trim(node.nodeValue).length;
- if (!t.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node))
- return;
- } else if (type == 1) {
- // If the only child is a bookmark then move it up
- children = node.childNodes;
- if (children.length == 1 && children[0] && children[0].nodeType == 1 && children[0].getAttribute('data-mce-type') == 'bookmark')
- node.parentNode.insertBefore(children[0], node);
-
- // Keep non empty elements or img, hr etc
- if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName))
- return;
- }
-
- t.remove(node);
- }
-
- return node;
- };
-
- if (pe && e) {
- // Get before chunk
- r.setStart(pe.parentNode, t.nodeIndex(pe));
- r.setEnd(e.parentNode, t.nodeIndex(e));
- bef = r.extractContents();
-
- // Get after chunk
- r = t.createRng();
- r.setStart(e.parentNode, t.nodeIndex(e) + 1);
- r.setEnd(pe.parentNode, t.nodeIndex(pe) + 1);
- aft = r.extractContents();
-
- // Insert before chunk
- pa = pe.parentNode;
- pa.insertBefore(trim(bef), pe);
-
- // Insert middle chunk
- if (re)
- pa.replaceChild(re, e);
- else
- pa.insertBefore(e, pe);
-
- // Insert after chunk
- pa.insertBefore(trim(aft), pe);
- t.remove(pe);
-
- return re || e;
- }
- },
-
- bind : function(target, name, func, scope) {
- return this.events.add(target, name, func, scope || this);
- },
-
- unbind : function(target, name, func) {
- return this.events.remove(target, name, func);
- },
-
- fire : function(target, name, evt) {
- return this.events.fire(target, name, evt);
- },
-
- // Returns the content editable state of a node
- getContentEditable: function(node) {
- var contentEditable;
-
- // Check type
- if (node.nodeType != 1) {
- return null;
- }
-
- // Check for fake content editable
- contentEditable = node.getAttribute("data-mce-contenteditable");
- if (contentEditable && contentEditable !== "inherit") {
- return contentEditable;
- }
-
- // Check for real content editable
- return node.contentEditable !== "inherit" ? node.contentEditable : null;
- },
-
-
- _findSib : function(node, selector, name) {
- var t = this, f = selector;
-
- if (node) {
- // If expression make a function of it using is
- if (is(f, 'string')) {
- f = function(node) {
- return t.is(node, selector);
- };
- }
-
- // Loop all siblings
- for (node = node[name]; node; node = node[name]) {
- if (f(node))
- return node;
- }
- }
-
- return null;
- },
-
- _isRes : function(c) {
- // Is live resizble element
- return /^(top|left|bottom|right|width|height)/i.test(c) || /;\s*(top|left|bottom|right|width|height)/i.test(c);
- }
-
- /*
- walk : function(n, f, s) {
- var d = this.doc, w;
-
- if (d.createTreeWalker) {
- w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false);
-
- while ((n = w.nextNode()) != null)
- f.call(s || this, n);
- } else
- tinymce.walk(n, f, 'childNodes', s);
- }
- */
-
- /*
- toRGB : function(s) {
- var c = /^\s*?#([0-9A-F]{2})([0-9A-F]{1,2})([0-9A-F]{2})?\s*?$/.exec(s);
-
- if (c) {
- // #FFF -> #FFFFFF
- if (!is(c[3]))
- c[3] = c[2] = c[1];
-
- return "rgb(" + parseInt(c[1], 16) + "," + parseInt(c[2], 16) + "," + parseInt(c[3], 16) + ")";
- }
-
- return s;
- }
- */
- });
-
- tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0});
-})(tinymce);
-
-(function(ns) {
- // Range constructor
- function Range(dom) {
- var t = this,
- doc = dom.doc,
- EXTRACT = 0,
- CLONE = 1,
- DELETE = 2,
- TRUE = true,
- FALSE = false,
- START_OFFSET = 'startOffset',
- START_CONTAINER = 'startContainer',
- END_CONTAINER = 'endContainer',
- END_OFFSET = 'endOffset',
- extend = tinymce.extend,
- nodeIndex = dom.nodeIndex;
-
- extend(t, {
- // Inital states
- startContainer : doc,
- startOffset : 0,
- endContainer : doc,
- endOffset : 0,
- collapsed : TRUE,
- commonAncestorContainer : doc,
-
- // Range constants
- START_TO_START : 0,
- START_TO_END : 1,
- END_TO_END : 2,
- END_TO_START : 3,
-
- // Public methods
- setStart : setStart,
- setEnd : setEnd,
- setStartBefore : setStartBefore,
- setStartAfter : setStartAfter,
- setEndBefore : setEndBefore,
- setEndAfter : setEndAfter,
- collapse : collapse,
- selectNode : selectNode,
- selectNodeContents : selectNodeContents,
- compareBoundaryPoints : compareBoundaryPoints,
- deleteContents : deleteContents,
- extractContents : extractContents,
- cloneContents : cloneContents,
- insertNode : insertNode,
- surroundContents : surroundContents,
- cloneRange : cloneRange,
- toStringIE : toStringIE
- });
-
- function createDocumentFragment() {
- return doc.createDocumentFragment();
- };
-
- function setStart(n, o) {
- _setEndPoint(TRUE, n, o);
- };
-
- function setEnd(n, o) {
- _setEndPoint(FALSE, n, o);
- };
-
- function setStartBefore(n) {
- setStart(n.parentNode, nodeIndex(n));
- };
-
- function setStartAfter(n) {
- setStart(n.parentNode, nodeIndex(n) + 1);
- };
-
- function setEndBefore(n) {
- setEnd(n.parentNode, nodeIndex(n));
- };
-
- function setEndAfter(n) {
- setEnd(n.parentNode, nodeIndex(n) + 1);
- };
-
- function collapse(ts) {
- if (ts) {
- t[END_CONTAINER] = t[START_CONTAINER];
- t[END_OFFSET] = t[START_OFFSET];
- } else {
- t[START_CONTAINER] = t[END_CONTAINER];
- t[START_OFFSET] = t[END_OFFSET];
- }
-
- t.collapsed = TRUE;
- };
-
- function selectNode(n) {
- setStartBefore(n);
- setEndAfter(n);
- };
-
- function selectNodeContents(n) {
- setStart(n, 0);
- setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length);
- };
-
- function compareBoundaryPoints(h, r) {
- var sc = t[START_CONTAINER], so = t[START_OFFSET], ec = t[END_CONTAINER], eo = t[END_OFFSET],
- rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset;
-
- // Check START_TO_START
- if (h === 0)
- return _compareBoundaryPoints(sc, so, rsc, rso);
-
- // Check START_TO_END
- if (h === 1)
- return _compareBoundaryPoints(ec, eo, rsc, rso);
-
- // Check END_TO_END
- if (h === 2)
- return _compareBoundaryPoints(ec, eo, rec, reo);
-
- // Check END_TO_START
- if (h === 3)
- return _compareBoundaryPoints(sc, so, rec, reo);
- };
-
- function deleteContents() {
- _traverse(DELETE);
- };
-
- function extractContents() {
- return _traverse(EXTRACT);
- };
-
- function cloneContents() {
- return _traverse(CLONE);
- };
-
- function insertNode(n) {
- var startContainer = this[START_CONTAINER],
- startOffset = this[START_OFFSET], nn, o;
-
- // Node is TEXT_NODE or CDATA
- if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) {
- if (!startOffset) {
- // At the start of text
- startContainer.parentNode.insertBefore(n, startContainer);
- } else if (startOffset >= startContainer.nodeValue.length) {
- // At the end of text
- dom.insertAfter(n, startContainer);
- } else {
- // Middle, need to split
- nn = startContainer.splitText(startOffset);
- startContainer.parentNode.insertBefore(n, nn);
- }
- } else {
- // Insert element node
- if (startContainer.childNodes.length > 0)
- o = startContainer.childNodes[startOffset];
-
- if (o)
- startContainer.insertBefore(n, o);
- else
- startContainer.appendChild(n);
- }
- };
-
- function surroundContents(n) {
- var f = t.extractContents();
-
- t.insertNode(n);
- n.appendChild(f);
- t.selectNode(n);
- };
-
- function cloneRange() {
- return extend(new Range(dom), {
- startContainer : t[START_CONTAINER],
- startOffset : t[START_OFFSET],
- endContainer : t[END_CONTAINER],
- endOffset : t[END_OFFSET],
- collapsed : t.collapsed,
- commonAncestorContainer : t.commonAncestorContainer
- });
- };
-
- // Private methods
-
- function _getSelectedNode(container, offset) {
- var child;
-
- if (container.nodeType == 3 /* TEXT_NODE */)
- return container;
-
- if (offset < 0)
- return container;
-
- child = container.firstChild;
- while (child && offset > 0) {
- --offset;
- child = child.nextSibling;
- }
-
- if (child)
- return child;
-
- return container;
- };
-
- function _isCollapsed() {
- return (t[START_CONTAINER] == t[END_CONTAINER] && t[START_OFFSET] == t[END_OFFSET]);
- };
-
- function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) {
- var c, offsetC, n, cmnRoot, childA, childB;
-
- // In the first case the boundary-points have the same container. A is before B
- // if its offset is less than the offset of B, A is equal to B if its offset is
- // equal to the offset of B, and A is after B if its offset is greater than the
- // offset of B.
- if (containerA == containerB) {
- if (offsetA == offsetB)
- return 0; // equal
-
- if (offsetA < offsetB)
- return -1; // before
-
- return 1; // after
- }
-
- // In the second case a child node C of the container of A is an ancestor
- // container of B. In this case, A is before B if the offset of A is less than or
- // equal to the index of the child node C and A is after B otherwise.
- c = containerB;
- while (c && c.parentNode != containerA)
- c = c.parentNode;
-
- if (c) {
- offsetC = 0;
- n = containerA.firstChild;
-
- while (n != c && offsetC < offsetA) {
- offsetC++;
- n = n.nextSibling;
- }
-
- if (offsetA <= offsetC)
- return -1; // before
-
- return 1; // after
- }
-
- // In the third case a child node C of the container of B is an ancestor container
- // of A. In this case, A is before B if the index of the child node C is less than
- // the offset of B and A is after B otherwise.
- c = containerA;
- while (c && c.parentNode != containerB) {
- c = c.parentNode;
- }
-
- if (c) {
- offsetC = 0;
- n = containerB.firstChild;
-
- while (n != c && offsetC < offsetB) {
- offsetC++;
- n = n.nextSibling;
- }
-
- if (offsetC < offsetB)
- return -1; // before
-
- return 1; // after
- }
-
- // In the fourth case, none of three other cases hold: the containers of A and B
- // are siblings or descendants of sibling nodes. In this case, A is before B if
- // the container of A is before the container of B in a pre-order traversal of the
- // Ranges' context tree and A is after B otherwise.
- cmnRoot = dom.findCommonAncestor(containerA, containerB);
- childA = containerA;
-
- while (childA && childA.parentNode != cmnRoot)
- childA = childA.parentNode;
-
- if (!childA)
- childA = cmnRoot;
-
- childB = containerB;
- while (childB && childB.parentNode != cmnRoot)
- childB = childB.parentNode;
-
- if (!childB)
- childB = cmnRoot;
-
- if (childA == childB)
- return 0; // equal
-
- n = cmnRoot.firstChild;
- while (n) {
- if (n == childA)
- return -1; // before
-
- if (n == childB)
- return 1; // after
-
- n = n.nextSibling;
- }
- };
-
- function _setEndPoint(st, n, o) {
- var ec, sc;
-
- if (st) {
- t[START_CONTAINER] = n;
- t[START_OFFSET] = o;
- } else {
- t[END_CONTAINER] = n;
- t[END_OFFSET] = o;
- }
-
- // If one boundary-point of a Range is set to have a root container
- // other than the current one for the Range, the Range is collapsed to
- // the new position. This enforces the restriction that both boundary-
- // points of a Range must have the same root container.
- ec = t[END_CONTAINER];
- while (ec.parentNode)
- ec = ec.parentNode;
-
- sc = t[START_CONTAINER];
- while (sc.parentNode)
- sc = sc.parentNode;
-
- if (sc == ec) {
- // The start position of a Range is guaranteed to never be after the
- // end position. To enforce this restriction, if the start is set to
- // be at a position after the end, the Range is collapsed to that
- // position.
- if (_compareBoundaryPoints(t[START_CONTAINER], t[START_OFFSET], t[END_CONTAINER], t[END_OFFSET]) > 0)
- t.collapse(st);
- } else
- t.collapse(st);
-
- t.collapsed = _isCollapsed();
- t.commonAncestorContainer = dom.findCommonAncestor(t[START_CONTAINER], t[END_CONTAINER]);
- };
-
- function _traverse(how) {
- var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep;
-
- if (t[START_CONTAINER] == t[END_CONTAINER])
- return _traverseSameContainer(how);
-
- for (c = t[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
- if (p == t[START_CONTAINER])
- return _traverseCommonStartContainer(c, how);
-
- ++endContainerDepth;
- }
-
- for (c = t[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
- if (p == t[END_CONTAINER])
- return _traverseCommonEndContainer(c, how);
-
- ++startContainerDepth;
- }
-
- depthDiff = startContainerDepth - endContainerDepth;
-
- startNode = t[START_CONTAINER];
- while (depthDiff > 0) {
- startNode = startNode.parentNode;
- depthDiff--;
- }
-
- endNode = t[END_CONTAINER];
- while (depthDiff < 0) {
- endNode = endNode.parentNode;
- depthDiff++;
- }
-
- // ascend the ancestor hierarchy until we have a common parent.
- for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) {
- startNode = sp;
- endNode = ep;
- }
-
- return _traverseCommonAncestors(startNode, endNode, how);
- };
-
- function _traverseSameContainer(how) {
- var frag, s, sub, n, cnt, sibling, xferNode, start, len;
-
- if (how != DELETE)
- frag = createDocumentFragment();
-
- // If selection is empty, just return the fragment
- if (t[START_OFFSET] == t[END_OFFSET])
- return frag;
-
- // Text node needs special case handling
- if (t[START_CONTAINER].nodeType == 3 /* TEXT_NODE */) {
- // get the substring
- s = t[START_CONTAINER].nodeValue;
- sub = s.substring(t[START_OFFSET], t[END_OFFSET]);
-
- // set the original text node to its new value
- if (how != CLONE) {
- n = t[START_CONTAINER];
- start = t[START_OFFSET];
- len = t[END_OFFSET] - t[START_OFFSET];
-
- if (start === 0 && len >= n.nodeValue.length - 1) {
- n.parentNode.removeChild(n);
- } else {
- n.deleteData(start, len);
- }
-
- // Nothing is partially selected, so collapse to start point
- t.collapse(TRUE);
- }
-
- if (how == DELETE)
- return;
-
- if (sub.length > 0) {
- frag.appendChild(doc.createTextNode(sub));
- }
-
- return frag;
- }
-
- // Copy nodes between the start/end offsets.
- n = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]);
- cnt = t[END_OFFSET] - t[START_OFFSET];
-
- while (n && cnt > 0) {
- sibling = n.nextSibling;
- xferNode = _traverseFullySelected(n, how);
-
- if (frag)
- frag.appendChild( xferNode );
-
- --cnt;
- n = sibling;
- }
-
- // Nothing is partially selected, so collapse to start point
- if (how != CLONE)
- t.collapse(TRUE);
-
- return frag;
- };
-
- function _traverseCommonStartContainer(endAncestor, how) {
- var frag, n, endIdx, cnt, sibling, xferNode;
-
- if (how != DELETE)
- frag = createDocumentFragment();
-
- n = _traverseRightBoundary(endAncestor, how);
-
- if (frag)
- frag.appendChild(n);
-
- endIdx = nodeIndex(endAncestor);
- cnt = endIdx - t[START_OFFSET];
-
- if (cnt <= 0) {
- // Collapse to just before the endAncestor, which
- // is partially selected.
- if (how != CLONE) {
- t.setEndBefore(endAncestor);
- t.collapse(FALSE);
- }
-
- return frag;
- }
-
- n = endAncestor.previousSibling;
- while (cnt > 0) {
- sibling = n.previousSibling;
- xferNode = _traverseFullySelected(n, how);
-
- if (frag)
- frag.insertBefore(xferNode, frag.firstChild);
-
- --cnt;
- n = sibling;
- }
-
- // Collapse to just before the endAncestor, which
- // is partially selected.
- if (how != CLONE) {
- t.setEndBefore(endAncestor);
- t.collapse(FALSE);
- }
-
- return frag;
- };
-
- function _traverseCommonEndContainer(startAncestor, how) {
- var frag, startIdx, n, cnt, sibling, xferNode;
-
- if (how != DELETE)
- frag = createDocumentFragment();
-
- n = _traverseLeftBoundary(startAncestor, how);
- if (frag)
- frag.appendChild(n);
-
- startIdx = nodeIndex(startAncestor);
- ++startIdx; // Because we already traversed it
-
- cnt = t[END_OFFSET] - startIdx;
- n = startAncestor.nextSibling;
- while (n && cnt > 0) {
- sibling = n.nextSibling;
- xferNode = _traverseFullySelected(n, how);
-
- if (frag)
- frag.appendChild(xferNode);
-
- --cnt;
- n = sibling;
- }
-
- if (how != CLONE) {
- t.setStartAfter(startAncestor);
- t.collapse(TRUE);
- }
-
- return frag;
- };
-
- function _traverseCommonAncestors(startAncestor, endAncestor, how) {
- var n, frag, commonParent, startOffset, endOffset, cnt, sibling, nextSibling;
-
- if (how != DELETE)
- frag = createDocumentFragment();
-
- n = _traverseLeftBoundary(startAncestor, how);
- if (frag)
- frag.appendChild(n);
-
- commonParent = startAncestor.parentNode;
- startOffset = nodeIndex(startAncestor);
- endOffset = nodeIndex(endAncestor);
- ++startOffset;
-
- cnt = endOffset - startOffset;
- sibling = startAncestor.nextSibling;
-
- while (cnt > 0) {
- nextSibling = sibling.nextSibling;
- n = _traverseFullySelected(sibling, how);
-
- if (frag)
- frag.appendChild(n);
-
- sibling = nextSibling;
- --cnt;
- }
-
- n = _traverseRightBoundary(endAncestor, how);
-
- if (frag)
- frag.appendChild(n);
-
- if (how != CLONE) {
- t.setStartAfter(startAncestor);
- t.collapse(TRUE);
- }
-
- return frag;
- };
-
- function _traverseRightBoundary(root, how) {
- var next = _getSelectedNode(t[END_CONTAINER], t[END_OFFSET] - 1), parent, clonedParent, prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != t[END_CONTAINER];
-
- if (next == root)
- return _traverseNode(next, isFullySelected, FALSE, how);
-
- parent = next.parentNode;
- clonedParent = _traverseNode(parent, FALSE, FALSE, how);
-
- while (parent) {
- while (next) {
- prevSibling = next.previousSibling;
- clonedChild = _traverseNode(next, isFullySelected, FALSE, how);
-
- if (how != DELETE)
- clonedParent.insertBefore(clonedChild, clonedParent.firstChild);
-
- isFullySelected = TRUE;
- next = prevSibling;
- }
-
- if (parent == root)
- return clonedParent;
-
- next = parent.previousSibling;
- parent = parent.parentNode;
-
- clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how);
-
- if (how != DELETE)
- clonedGrandParent.appendChild(clonedParent);
-
- clonedParent = clonedGrandParent;
- }
- };
-
- function _traverseLeftBoundary(root, how) {
- var next = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]), isFullySelected = next != t[START_CONTAINER], parent, clonedParent, nextSibling, clonedChild, clonedGrandParent;
-
- if (next == root)
- return _traverseNode(next, isFullySelected, TRUE, how);
-
- parent = next.parentNode;
- clonedParent = _traverseNode(parent, FALSE, TRUE, how);
-
- while (parent) {
- while (next) {
- nextSibling = next.nextSibling;
- clonedChild = _traverseNode(next, isFullySelected, TRUE, how);
-
- if (how != DELETE)
- clonedParent.appendChild(clonedChild);
-
- isFullySelected = TRUE;
- next = nextSibling;
- }
-
- if (parent == root)
- return clonedParent;
-
- next = parent.nextSibling;
- parent = parent.parentNode;
-
- clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how);
-
- if (how != DELETE)
- clonedGrandParent.appendChild(clonedParent);
-
- clonedParent = clonedGrandParent;
- }
- };
-
- function _traverseNode(n, isFullySelected, isLeft, how) {
- var txtValue, newNodeValue, oldNodeValue, offset, newNode;
-
- if (isFullySelected)
- return _traverseFullySelected(n, how);
-
- if (n.nodeType == 3 /* TEXT_NODE */) {
- txtValue = n.nodeValue;
-
- if (isLeft) {
- offset = t[START_OFFSET];
- newNodeValue = txtValue.substring(offset);
- oldNodeValue = txtValue.substring(0, offset);
- } else {
- offset = t[END_OFFSET];
- newNodeValue = txtValue.substring(0, offset);
- oldNodeValue = txtValue.substring(offset);
- }
-
- if (how != CLONE)
- n.nodeValue = oldNodeValue;
-
- if (how == DELETE)
- return;
-
- newNode = dom.clone(n, FALSE);
- newNode.nodeValue = newNodeValue;
-
- return newNode;
- }
-
- if (how == DELETE)
- return;
-
- return dom.clone(n, FALSE);
- };
-
- function _traverseFullySelected(n, how) {
- if (how != DELETE)
- return how == CLONE ? dom.clone(n, TRUE) : n;
-
- n.parentNode.removeChild(n);
- };
-
- function toStringIE() {
- return dom.create('body', null, cloneContents()).outerText;
- }
-
- return t;
- };
-
- ns.Range = Range;
-
- // Older IE versions doesn't let you override toString by it's constructor so we have to stick it in the prototype
- Range.prototype.toString = function() {
- return this.toStringIE();
- };
-})(tinymce.dom);
-
-(function() {
- function Selection(selection) {
- var self = this, dom = selection.dom, TRUE = true, FALSE = false;
-
- function getPosition(rng, start) {
- var checkRng, startIndex = 0, endIndex, inside,
- children, child, offset, index, position = -1, parent;
-
- // Setup test range, collapse it and get the parent
- checkRng = rng.duplicate();
- checkRng.collapse(start);
- parent = checkRng.parentElement();
-
- // Check if the selection is within the right document
- if (parent.ownerDocument !== selection.dom.doc)
- return;
-
- // IE will report non editable elements as it's parent so look for an editable one
- while (parent.contentEditable === "false") {
- parent = parent.parentNode;
- }
-
- // If parent doesn't have any children then return that we are inside the element
- if (!parent.hasChildNodes()) {
- return {node : parent, inside : 1};
- }
-
- // Setup node list and endIndex
- children = parent.children;
- endIndex = children.length - 1;
-
- // Perform a binary search for the position
- while (startIndex <= endIndex) {
- index = Math.floor((startIndex + endIndex) / 2);
-
- // Move selection to node and compare the ranges
- child = children[index];
- checkRng.moveToElementText(child);
- position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng);
-
- // Before/after or an exact match
- if (position > 0) {
- endIndex = index - 1;
- } else if (position < 0) {
- startIndex = index + 1;
- } else {
- return {node : child};
- }
- }
-
- // Check if child position is before or we didn't find a position
- if (position < 0) {
- // No element child was found use the parent element and the offset inside that
- if (!child) {
- checkRng.moveToElementText(parent);
- checkRng.collapse(true);
- child = parent;
- inside = true;
- } else
- checkRng.collapse(false);
-
- // Walk character by character in text node until we hit the selected range endpoint, hit the end of document or parent isn't the right one
- // We need to walk char by char since rng.text or rng.htmlText will trim line endings
- offset = 0;
- while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) {
- if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) {
- break;
- }
-
- offset++;
- }
- } else {
- // Child position is after the selection endpoint
- checkRng.collapse(true);
-
- // Walk character by character in text node until we hit the selected range endpoint, hit the end of document or parent isn't the right one
- offset = 0;
- while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) {
- if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) {
- break;
- }
-
- offset++;
- }
- }
-
- return {node : child, position : position, offset : offset, inside : inside};
- };
-
- // Returns a W3C DOM compatible range object by using the IE Range API
- function getRange() {
- var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark, fail;
-
- // If selection is outside the current document just return an empty range
- element = ieRange.item ? ieRange.item(0) : ieRange.parentElement();
- if (element.ownerDocument != dom.doc)
- return domRange;
-
- collapsed = selection.isCollapsed();
-
- // Handle control selection
- if (ieRange.item) {
- domRange.setStart(element.parentNode, dom.nodeIndex(element));
- domRange.setEnd(domRange.startContainer, domRange.startOffset + 1);
-
- return domRange;
- }
-
- function findEndPoint(start) {
- var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue;
-
- container = endPoint.node;
- offset = endPoint.offset;
-
- if (endPoint.inside && !container.hasChildNodes()) {
- domRange[start ? 'setStart' : 'setEnd'](container, 0);
- return;
- }
-
- if (offset === undef) {
- domRange[start ? 'setStartBefore' : 'setEndAfter'](container);
- return;
- }
-
- if (endPoint.position < 0) {
- sibling = endPoint.inside ? container.firstChild : container.nextSibling;
-
- if (!sibling) {
- domRange[start ? 'setStartAfter' : 'setEndAfter'](container);
- return;
- }
-
- if (!offset) {
- if (sibling.nodeType == 3)
- domRange[start ? 'setStart' : 'setEnd'](sibling, 0);
- else
- domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling);
-
- return;
- }
-
- // Find the text node and offset
- while (sibling) {
- nodeValue = sibling.nodeValue;
- textNodeOffset += nodeValue.length;
-
- // We are at or passed the position we where looking for
- if (textNodeOffset >= offset) {
- container = sibling;
- textNodeOffset -= offset;
- textNodeOffset = nodeValue.length - textNodeOffset;
- break;
- }
-
- sibling = sibling.nextSibling;
- }
- } else {
- // Find the text node and offset
- sibling = container.previousSibling;
-
- if (!sibling)
- return domRange[start ? 'setStartBefore' : 'setEndBefore'](container);
-
- // If there isn't any text to loop then use the first position
- if (!offset) {
- if (container.nodeType == 3)
- domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length);
- else
- domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling);
-
- return;
- }
-
- while (sibling) {
- textNodeOffset += sibling.nodeValue.length;
-
- // We are at or passed the position we where looking for
- if (textNodeOffset >= offset) {
- container = sibling;
- textNodeOffset -= offset;
- break;
- }
-
- sibling = sibling.previousSibling;
- }
- }
-
- domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset);
- };
-
- try {
- // Find start point
- findEndPoint(true);
-
- // Find end point if needed
- if (!collapsed)
- findEndPoint();
- } catch (ex) {
- // IE has a nasty bug where text nodes might throw "invalid argument" when you
- // access the nodeValue or other properties of text nodes. This seems to happend when
- // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it.
- if (ex.number == -2147024809) {
- // Get the current selection
- bookmark = self.getBookmark(2);
-
- // Get start element
- tmpRange = ieRange.duplicate();
- tmpRange.collapse(true);
- element = tmpRange.parentElement();
-
- // Get end element
- if (!collapsed) {
- tmpRange = ieRange.duplicate();
- tmpRange.collapse(false);
- element2 = tmpRange.parentElement();
- element2.innerHTML = element2.innerHTML;
- }
-
- // Remove the broken elements
- element.innerHTML = element.innerHTML;
-
- // Restore the selection
- self.moveToBookmark(bookmark);
-
- // Since the range has moved we need to re-get it
- ieRange = selection.getRng();
-
- // Find start point
- findEndPoint(true);
-
- // Find end point if needed
- if (!collapsed)
- findEndPoint();
- } else
- throw ex; // Throw other errors
- }
-
- return domRange;
- };
-
- this.getBookmark = function(type) {
- var rng = selection.getRng(), start, end, bookmark = {};
-
- function getIndexes(node) {
- var parent, root, children, i, indexes = [];
-
- parent = node.parentNode;
- root = dom.getRoot().parentNode;
-
- while (parent != root && parent.nodeType !== 9) {
- children = parent.children;
-
- i = children.length;
- while (i--) {
- if (node === children[i]) {
- indexes.push(i);
- break;
- }
- }
-
- node = parent;
- parent = parent.parentNode;
- }
-
- return indexes;
- };
-
- function getBookmarkEndPoint(start) {
- var position;
-
- position = getPosition(rng, start);
- if (position) {
- return {
- position : position.position,
- offset : position.offset,
- indexes : getIndexes(position.node),
- inside : position.inside
- };
- }
- };
-
- // Non ubstructive bookmark
- if (type === 2) {
- // Handle text selection
- if (!rng.item) {
- bookmark.start = getBookmarkEndPoint(true);
-
- if (!selection.isCollapsed())
- bookmark.end = getBookmarkEndPoint();
- } else
- bookmark.start = {ctrl : true, indexes : getIndexes(rng.item(0))};
- }
-
- return bookmark;
- };
-
- this.moveToBookmark = function(bookmark) {
- var rng, body = dom.doc.body;
-
- function resolveIndexes(indexes) {
- var node, i, idx, children;
-
- node = dom.getRoot();
- for (i = indexes.length - 1; i >= 0; i--) {
- children = node.children;
- idx = indexes[i];
-
- if (idx <= children.length - 1) {
- node = children[idx];
- }
- }
-
- return node;
- };
-
- function setBookmarkEndPoint(start) {
- var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef;
-
- if (endPoint) {
- moveLeft = endPoint.position > 0;
-
- moveRng = body.createTextRange();
- moveRng.moveToElementText(resolveIndexes(endPoint.indexes));
-
- offset = endPoint.offset;
- if (offset !== undef) {
- moveRng.collapse(endPoint.inside || moveLeft);
- moveRng.moveStart('character', moveLeft ? -offset : offset);
- } else
- moveRng.collapse(start);
-
- rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng);
-
- if (start)
- rng.collapse(true);
- }
- };
-
- if (bookmark.start) {
- if (bookmark.start.ctrl) {
- rng = body.createControlRange();
- rng.addElement(resolveIndexes(bookmark.start.indexes));
- rng.select();
- } else {
- rng = body.createTextRange();
- setBookmarkEndPoint(true);
- setBookmarkEndPoint();
- rng.select();
- }
- }
- };
-
- this.addRange = function(rng) {
- var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling,
- doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm;
-
- function setEndPoint(start) {
- var container, offset, marker, tmpRng, nodes;
-
- marker = dom.create('a');
- container = start ? startContainer : endContainer;
- offset = start ? startOffset : endOffset;
- tmpRng = ieRng.duplicate();
-
- if (container == doc || container == doc.documentElement) {
- container = body;
- offset = 0;
- }
-
- if (container.nodeType == 3) {
- container.parentNode.insertBefore(marker, container);
- tmpRng.moveToElementText(marker);
- tmpRng.moveStart('character', offset);
- dom.remove(marker);
- ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
- } else {
- nodes = container.childNodes;
-
- if (nodes.length) {
- if (offset >= nodes.length) {
- dom.insertAfter(marker, nodes[nodes.length - 1]);
- } else {
- container.insertBefore(marker, nodes[offset]);
- }
-
- tmpRng.moveToElementText(marker);
- } else if (container.canHaveHTML) {
- // Empty node selection for example <div>|</div>
- // Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open
- container.innerHTML = '<span>\uFEFF</span>';
- marker = container.firstChild;
- tmpRng.moveToElementText(marker);
- tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason
- }
-
- ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
- dom.remove(marker);
- }
- }
-
- // Setup some shorter versions
- startContainer = rng.startContainer;
- startOffset = rng.startOffset;
- endContainer = rng.endContainer;
- endOffset = rng.endOffset;
- ieRng = body.createTextRange();
-
- // If single element selection then try making a control selection out of it
- if (startContainer == endContainer && startContainer.nodeType == 1) {
- // Trick to place the caret inside an empty block element like <p></p>
- if (startOffset == endOffset && !startContainer.hasChildNodes()) {
- if (startContainer.canHaveHTML) {
- // Check if previous sibling is an empty block if it is then we need to render it
- // IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236
- // Example this: <p></p><p>|</p> would become this: <p>|</p><p></p>
- sibling = startContainer.previousSibling;
- if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) {
- sibling.innerHTML = '\uFEFF';
- } else {
- sibling = null;
- }
-
- startContainer.innerHTML = '<span>\uFEFF</span><span>\uFEFF</span>';
- ieRng.moveToElementText(startContainer.lastChild);
- ieRng.select();
- dom.doc.selection.clear();
- startContainer.innerHTML = '';
-
- if (sibling) {
- sibling.innerHTML = '';
- }
- return;
- } else {
- startOffset = dom.nodeIndex(startContainer);
- startContainer = startContainer.parentNode;
- }
- }
-
- if (startOffset == endOffset - 1) {
- try {
- ctrlElm = startContainer.childNodes[startOffset];
- ctrlRng = body.createControlRange();
- ctrlRng.addElement(ctrlElm);
- ctrlRng.select();
-
- // Check if the range produced is on the correct element and is a control range
- // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398
- nativeRng = selection.getRng();
- if (nativeRng.item && ctrlElm === nativeRng.item(0)) {
- return;
- }
- } catch (ex) {
- // Ignore
- }
- }
- }
-
- // Set start/end point of selection
- setEndPoint(true);
- setEndPoint();
-
- // Select the new range and scroll it into view
- ieRng.select();
- };
-
- // Expose range method
- this.getRangeAt = getRange;
- };
-
- // Expose the selection object
- tinymce.dom.TridentSelection = Selection;
-})();
-
-
-/*
- * Sizzle CSS Selector Engine
- * Copyright, The Dojo Foundation
- * Released under the MIT, BSD, and GPL Licenses.
- * More information: http://sizzlejs.com/
- */
-(function(){
-
-var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
- expando = "sizcache",
- done = 0,
- toString = Object.prototype.toString,
- hasDuplicate = false,
- baseHasDuplicate = true,
- rBackslash = /\\/g,
- rReturn = /\r\n/g,
- rNonWord = /\W/;
-
-// Here we check if the JavaScript engine is using some sort of
-// optimization where it does not always call our comparision
-// function. If that is the case, discard the hasDuplicate value.
-// Thus far that includes Google Chrome.
-[0, 0].sort(function() {
- baseHasDuplicate = false;
- return 0;
-});
-
-var Sizzle = function( selector, context, results, seed ) {
- results = results || [];
- context = context || document;
-
- var origContext = context;
-
- if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
- return [];
- }
-
- if ( !selector || typeof selector !== "string" ) {
- return results;
- }
-
- var m, set, checkSet, extra, ret, cur, pop, i,
- prune = true,
- contextXML = Sizzle.isXML( context ),
- parts = [],
- soFar = selector;
-
- // Reset the position of the chunker regexp (start from head)
- do {
- chunker.exec( "" );
- m = chunker.exec( soFar );
-
- if ( m ) {
- soFar = m[3];
-
- parts.push( m[1] );
-
- if ( m[2] ) {
- extra = m[3];
- break;
- }
- }
- } while ( m );
-
- if ( parts.length > 1 && origPOS.exec( selector ) ) {
-
- if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
- set = posProcess( parts[0] + parts[1], context, seed );
-
- } else {
- set = Expr.relative[ parts[0] ] ?
- [ context ] :
- Sizzle( parts.shift(), context );
-
- while ( parts.length ) {
- selector = parts.shift();
-
- if ( Expr.relative[ selector ] ) {
- selector += parts.shift();
- }
-
- set = posProcess( selector, set, seed );
- }
- }
-
- } else {
- // Take a shortcut and set the context if the root selector is an ID
- // (but not if it'll be faster if the inner selector is an ID)
- if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&
- Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {
-
- ret = Sizzle.find( parts.shift(), context, contextXML );
- context = ret.expr ?
- Sizzle.filter( ret.expr, ret.set )[0] :
- ret.set[0];
- }
-
- if ( context ) {
- ret = seed ?
- { expr: parts.pop(), set: makeArray(seed) } :
- Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );
-
- set = ret.expr ?
- Sizzle.filter( ret.expr, ret.set ) :
- ret.set;
-
- if ( parts.length > 0 ) {
- checkSet = makeArray( set );
-
- } else {
- prune = false;
- }
-
- while ( parts.length ) {
- cur = parts.pop();
- pop = cur;
-
- if ( !Expr.relative[ cur ] ) {
- cur = "";
- } else {
- pop = parts.pop();
- }
-
- if ( pop == null ) {
- pop = context;
- }
-
- Expr.relative[ cur ]( checkSet, pop, contextXML );
- }
-
- } else {
- checkSet = parts = [];
- }
- }
-
- if ( !checkSet ) {
- checkSet = set;
- }
-
- if ( !checkSet ) {
- Sizzle.error( cur || selector );
- }
-
- if ( toString.call(checkSet) === "[object Array]" ) {
- if ( !prune ) {
- results.push.apply( results, checkSet );
-
- } else if ( context && context.nodeType === 1 ) {
- for ( i = 0; checkSet[i] != null; i++ ) {
- if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) {
- results.push( set[i] );
- }
- }
-
- } else {
- for ( i = 0; checkSet[i] != null; i++ ) {
- if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
- results.push( set[i] );
- }
- }
- }
-
- } else {
- makeArray( checkSet, results );
- }
-
- if ( extra ) {
- Sizzle( extra, origContext, results, seed );
- Sizzle.uniqueSort( results );
- }
-
- return results;
-};
-
-Sizzle.uniqueSort = function( results ) {
- if ( sortOrder ) {
- hasDuplicate = baseHasDuplicate;
- results.sort( sortOrder );
-
- if ( hasDuplicate ) {
- for ( var i = 1; i < results.length; i++ ) {
- if ( results[i] === results[ i - 1 ] ) {
- results.splice( i--, 1 );
- }
- }
- }
- }
-
- return results;
-};
-
-Sizzle.matches = function( expr, set ) {
- return Sizzle( expr, null, null, set );
-};
-
-Sizzle.matchesSelector = function( node, expr ) {
- return Sizzle( expr, null, null, [node] ).length > 0;
-};
-
-Sizzle.find = function( expr, context, isXML ) {
- var set, i, len, match, type, left;
-
- if ( !expr ) {
- return [];
- }
-
- for ( i = 0, len = Expr.order.length; i < len; i++ ) {
- type = Expr.order[i];
-
- if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
- left = match[1];
- match.splice( 1, 1 );
-
- if ( left.substr( left.length - 1 ) !== "\\" ) {
- match[1] = (match[1] || "").replace( rBackslash, "" );
- set = Expr.find[ type ]( match, context, isXML );
-
- if ( set != null ) {
- expr = expr.replace( Expr.match[ type ], "" );
- break;
- }
- }
- }
- }
-
- if ( !set ) {
- set = typeof context.getElementsByTagName !== "undefined" ?
- context.getElementsByTagName( "*" ) :
- [];
- }
-
- return { set: set, expr: expr };
-};
-
-Sizzle.filter = function( expr, set, inplace, not ) {
- var match, anyFound,
- type, found, item, filter, left,
- i, pass,
- old = expr,
- result = [],
- curLoop = set,
- isXMLFilter = set && set[0] && Sizzle.isXML( set[0] );
-
- while ( expr && set.length ) {
- for ( type in Expr.filter ) {
- if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {
- filter = Expr.filter[ type ];
- left = match[1];
-
- anyFound = false;
-
- match.splice(1,1);
-
- if ( left.substr( left.length - 1 ) === "\\" ) {
- continue;
- }
-
- if ( curLoop === result ) {
- result = [];
- }
-
- if ( Expr.preFilter[ type ] ) {
- match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
-
- if ( !match ) {
- anyFound = found = true;
-
- } else if ( match === true ) {
- continue;
- }
- }
-
- if ( match ) {
- for ( i = 0; (item = curLoop[i]) != null; i++ ) {
- if ( item ) {
- found = filter( item, match, i, curLoop );
- pass = not ^ found;
-
- if ( inplace && found != null ) {
- if ( pass ) {
- anyFound = true;
-
- } else {
- curLoop[i] = false;
- }
-
- } else if ( pass ) {
- result.push( item );
- anyFound = true;
- }
- }
- }
- }
-
- if ( found !== undefined ) {
- if ( !inplace ) {
- curLoop = result;
- }
-
- expr = expr.replace( Expr.match[ type ], "" );
-
- if ( !anyFound ) {
- return [];
- }
-
- break;
- }
- }
- }
-
- // Improper expression
- if ( expr === old ) {
- if ( anyFound == null ) {
- Sizzle.error( expr );
-
- } else {
- break;
- }
- }
-
- old = expr;
- }
-
- return curLoop;
-};
-
-Sizzle.error = function( msg ) {
- throw new Error( "Syntax error, unrecognized expression: " + msg );
-};
-
-var getText = Sizzle.getText = function( elem ) {
- var i, node,
- nodeType = elem.nodeType,
- ret = "";
-
- if ( nodeType ) {
- if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
- // Use textContent || innerText for elements
- if ( typeof elem.textContent === 'string' ) {
- return elem.textContent;
- } else if ( typeof elem.innerText === 'string' ) {
- // Replace IE's carriage returns
- return elem.innerText.replace( rReturn, '' );
- } else {
- // Traverse it's children
- for ( elem = elem.firstChild; elem; elem = elem.nextSibling) {
- ret += getText( elem );
- }
- }
- } else if ( nodeType === 3 || nodeType === 4 ) {
- return elem.nodeValue;
- }
- } else {
-
- // If no nodeType, this is expected to be an array
- for ( i = 0; (node = elem[i]); i++ ) {
- // Do not traverse comment nodes
- if ( node.nodeType !== 8 ) {
- ret += getText( node );
- }
- }
- }
- return ret;
-};
-
-var Expr = Sizzle.selectors = {
- order: [ "ID", "NAME", "TAG" ],
-
- match: {
- ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
- CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
- NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,
- ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,
- TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,
- CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,
- POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,
- PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/
- },
-
- leftMatch: {},
-
- attrMap: {
- "class": "className",
- "for": "htmlFor"
- },
-
- attrHandle: {
- href: function( elem ) {
- return elem.getAttribute( "href" );
- },
- type: function( elem ) {
- return elem.getAttribute( "type" );
- }
- },
-
- relative: {
- "+": function(checkSet, part){
- var isPartStr = typeof part === "string",
- isTag = isPartStr && !rNonWord.test( part ),
- isPartStrNotTag = isPartStr && !isTag;
-
- if ( isTag ) {
- part = part.toLowerCase();
- }
-
- for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
- if ( (elem = checkSet[i]) ) {
- while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
-
- checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?
- elem || false :
- elem === part;
- }
- }
-
- if ( isPartStrNotTag ) {
- Sizzle.filter( part, checkSet, true );
- }
- },
-
- ">": function( checkSet, part ) {
- var elem,
- isPartStr = typeof part === "string",
- i = 0,
- l = checkSet.length;
-
- if ( isPartStr && !rNonWord.test( part ) ) {
- part = part.toLowerCase();
-
- for ( ; i < l; i++ ) {
- elem = checkSet[i];
-
- if ( elem ) {
- var parent = elem.parentNode;
- checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false;
- }
- }
-
- } else {
- for ( ; i < l; i++ ) {
- elem = checkSet[i];
-
- if ( elem ) {
- checkSet[i] = isPartStr ?
- elem.parentNode :
- elem.parentNode === part;
- }
- }
-
- if ( isPartStr ) {
- Sizzle.filter( part, checkSet, true );
- }
- }
- },
-
- "": function(checkSet, part, isXML){
- var nodeCheck,
- doneName = done++,
- checkFn = dirCheck;
-
- if ( typeof part === "string" && !rNonWord.test( part ) ) {
- part = part.toLowerCase();
- nodeCheck = part;
- checkFn = dirNodeCheck;
- }
-
- checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML );
- },
-
- "~": function( checkSet, part, isXML ) {
- var nodeCheck,
- doneName = done++,
- checkFn = dirCheck;
-
- if ( typeof part === "string" && !rNonWord.test( part ) ) {
- part = part.toLowerCase();
- nodeCheck = part;
- checkFn = dirNodeCheck;
- }
-
- checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML );
- }
- },
-
- find: {
- ID: function( match, context, isXML ) {
- if ( typeof context.getElementById !== "undefined" && !isXML ) {
- var m = context.getElementById(match[1]);
- // Check parentNode to catch when Blackberry 4.6 returns
- // nodes that are no longer in the document #6963
- return m && m.parentNode ? [m] : [];
- }
- },
-
- NAME: function( match, context ) {
- if ( typeof context.getElementsByName !== "undefined" ) {
- var ret = [],
- results = context.getElementsByName( match[1] );
-
- for ( var i = 0, l = results.length; i < l; i++ ) {
- if ( results[i].getAttribute("name") === match[1] ) {
- ret.push( results[i] );
- }
- }
-
- return ret.length === 0 ? null : ret;
- }
- },
-
- TAG: function( match, context ) {
- if ( typeof context.getElementsByTagName !== "undefined" ) {
- return context.getElementsByTagName( match[1] );
- }
- }
- },
- preFilter: {
- CLASS: function( match, curLoop, inplace, result, not, isXML ) {
- match = " " + match[1].replace( rBackslash, "" ) + " ";
-
- if ( isXML ) {
- return match;
- }
-
- for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
- if ( elem ) {
- if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) {
- if ( !inplace ) {
- result.push( elem );
- }
-
- } else if ( inplace ) {
- curLoop[i] = false;
- }
- }
- }
-
- return false;
- },
-
- ID: function( match ) {
- return match[1].replace( rBackslash, "" );
- },
-
- TAG: function( match, curLoop ) {
- return match[1].replace( rBackslash, "" ).toLowerCase();
- },
-
- CHILD: function( match ) {
- if ( match[1] === "nth" ) {
- if ( !match[2] ) {
- Sizzle.error( match[0] );
- }
-
- match[2] = match[2].replace(/^\+|\s*/g, '');
-
- // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
- var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec(
- match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" ||
- !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);
-
- // calculate the numbers (first)n+(last) including if they are negative
- match[2] = (test[1] + (test[2] || 1)) - 0;
- match[3] = test[3] - 0;
- }
- else if ( match[2] ) {
- Sizzle.error( match[0] );
- }
-
- // TODO: Move to normal caching system
- match[0] = done++;
-
- return match;
- },
-
- ATTR: function( match, curLoop, inplace, result, not, isXML ) {
- var name = match[1] = match[1].replace( rBackslash, "" );
-
- if ( !isXML && Expr.attrMap[name] ) {
- match[1] = Expr.attrMap[name];
- }
-
- // Handle if an un-quoted value was used
- match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" );
-
- if ( match[2] === "~=" ) {
- match[4] = " " + match[4] + " ";
- }
-
- return match;
- },
-
- PSEUDO: function( match, curLoop, inplace, result, not ) {
- if ( match[1] === "not" ) {
- // If we're dealing with a complex expression, or a simple one
- if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) {
- match[3] = Sizzle(match[3], null, null, curLoop);
-
- } else {
- var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
-
- if ( !inplace ) {
- result.push.apply( result, ret );
- }
-
- return false;
- }
-
- } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) {
- return true;
- }
-
- return match;
- },
-
- POS: function( match ) {
- match.unshift( true );
-
- return match;
- }
- },
-
- filters: {
- enabled: function( elem ) {
- return elem.disabled === false && elem.type !== "hidden";
- },
-
- disabled: function( elem ) {
- return elem.disabled === true;
- },
-
- checked: function( elem ) {
- return elem.checked === true;
- },
-
- selected: function( elem ) {
- // Accessing this property makes selected-by-default
- // options in Safari work properly
- if ( elem.parentNode ) {
- elem.parentNode.selectedIndex;
- }
-
- return elem.selected === true;
- },
-
- parent: function( elem ) {
- return !!elem.firstChild;
- },
-
- empty: function( elem ) {
- return !elem.firstChild;
- },
-
- has: function( elem, i, match ) {
- return !!Sizzle( match[3], elem ).length;
- },
-
- header: function( elem ) {
- return (/h\d/i).test( elem.nodeName );
- },
-
- text: function( elem ) {
- var attr = elem.getAttribute( "type" ), type = elem.type;
- // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc)
- // use getAttribute instead to test this case
- return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null );
- },
-
- radio: function( elem ) {
- return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type;
- },
-
- checkbox: function( elem ) {
- return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type;
- },
-
- file: function( elem ) {
- return elem.nodeName.toLowerCase() === "input" && "file" === elem.type;
- },
-
- password: function( elem ) {
- return elem.nodeName.toLowerCase() === "input" && "password" === elem.type;
- },
-
- submit: function( elem ) {
- var name = elem.nodeName.toLowerCase();
- return (name === "input" || name === "button") && "submit" === elem.type;
- },
-
- image: function( elem ) {
- return elem.nodeName.toLowerCase() === "input" && "image" === elem.type;
- },
-
- reset: function( elem ) {
- var name = elem.nodeName.toLowerCase();
- return (name === "input" || name === "button") && "reset" === elem.type;
- },
-
- button: function( elem ) {
- var name = elem.nodeName.toLowerCase();
- return name === "input" && "button" === elem.type || name === "button";
- },
-
- input: function( elem ) {
- return (/input|select|textarea|button/i).test( elem.nodeName );
- },
-
- focus: function( elem ) {
- return elem === elem.ownerDocument.activeElement;
- }
- },
- setFilters: {
- first: function( elem, i ) {
- return i === 0;
- },
-
- last: function( elem, i, match, array ) {
- return i === array.length - 1;
- },
-
- even: function( elem, i ) {
- return i % 2 === 0;
- },
-
- odd: function( elem, i ) {
- return i % 2 === 1;
- },
-
- lt: function( elem, i, match ) {
- return i < match[3] - 0;
- },
-
- gt: function( elem, i, match ) {
- return i > match[3] - 0;
- },
-
- nth: function( elem, i, match ) {
- return match[3] - 0 === i;
- },
-
- eq: function( elem, i, match ) {
- return match[3] - 0 === i;
- }
- },
- filter: {
- PSEUDO: function( elem, match, i, array ) {
- var name = match[1],
- filter = Expr.filters[ name ];
-
- if ( filter ) {
- return filter( elem, i, match, array );
-
- } else if ( name === "contains" ) {
- return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0;
-
- } else if ( name === "not" ) {
- var not = match[3];
-
- for ( var j = 0, l = not.length; j < l; j++ ) {
- if ( not[j] === elem ) {
- return false;
- }
- }
-
- return true;
-
- } else {
- Sizzle.error( name );
- }
- },
-
- CHILD: function( elem, match ) {
- var first, last,
- doneName, parent, cache,
- count, diff,
- type = match[1],
- node = elem;
-
- switch ( type ) {
- case "only":
- case "first":
- while ( (node = node.previousSibling) ) {
- if ( node.nodeType === 1 ) {
- return false;
- }
- }
-
- if ( type === "first" ) {
- return true;
- }
-
- node = elem;
-
- /* falls through */
- case "last":
- while ( (node = node.nextSibling) ) {
- if ( node.nodeType === 1 ) {
- return false;
- }
- }
-
- return true;
-
- case "nth":
- first = match[2];
- last = match[3];
-
- if ( first === 1 && last === 0 ) {
- return true;
- }
-
- doneName = match[0];
- parent = elem.parentNode;
-
- if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) {
- count = 0;
-
- for ( node = parent.firstChild; node; node = node.nextSibling ) {
- if ( node.nodeType === 1 ) {
- node.nodeIndex = ++count;
- }
- }
-
- parent[ expando ] = doneName;
- }
-
- diff = elem.nodeIndex - last;
-
- if ( first === 0 ) {
- return diff === 0;
-
- } else {
- return ( diff % first === 0 && diff / first >= 0 );
- }
- }
- },
-
- ID: function( elem, match ) {
- return elem.nodeType === 1 && elem.getAttribute("id") === match;
- },
-
- TAG: function( elem, match ) {
- return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match;
- },
-
- CLASS: function( elem, match ) {
- return (" " + (elem.className || elem.getAttribute("class")) + " ")
- .indexOf( match ) > -1;
- },
-
- ATTR: function( elem, match ) {
- var name = match[1],
- result = Sizzle.attr ?
- Sizzle.attr( elem, name ) :
- Expr.attrHandle[ name ] ?
- Expr.attrHandle[ name ]( elem ) :
- elem[ name ] != null ?
- elem[ name ] :
- elem.getAttribute( name ),
- value = result + "",
- type = match[2],
- check = match[4];
-
- return result == null ?
- type === "!=" :
- !type && Sizzle.attr ?
- result != null :
- type === "=" ?
- value === check :
- type === "*=" ?
- value.indexOf(check) >= 0 :
- type === "~=" ?
- (" " + value + " ").indexOf(check) >= 0 :
- !check ?
- value && result !== false :
- type === "!=" ?
- value !== check :
- type === "^=" ?
- value.indexOf(check) === 0 :
- type === "$=" ?
- value.substr(value.length - check.length) === check :
- type === "|=" ?
- value === check || value.substr(0, check.length + 1) === check + "-" :
- false;
- },
-
- POS: function( elem, match, i, array ) {
- var name = match[2],
- filter = Expr.setFilters[ name ];
-
- if ( filter ) {
- return filter( elem, i, match, array );
- }
- }
- }
-};
-
-var origPOS = Expr.match.POS,
- fescape = function(all, num){
- return "\\" + (num - 0 + 1);
- };
-
-for ( var type in Expr.match ) {
- Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) );
- Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) );
-}
-// Expose origPOS
-// "global" as in regardless of relation to brackets/parens
-Expr.match.globalPOS = origPOS;
-
-var makeArray = function( array, results ) {
- array = Array.prototype.slice.call( array, 0 );
-
- if ( results ) {
- results.push.apply( results, array );
- return results;
- }
-
- return array;
-};
-
-// Perform a simple check to determine if the browser is capable of
-// converting a NodeList to an array using builtin methods.
-// Also verifies that the returned array holds DOM nodes
-// (which is not the case in the Blackberry browser)
-try {
- Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType;
-
-// Provide a fallback method if it does not work
-} catch( e ) {
- makeArray = function( array, results ) {
- var i = 0,
- ret = results || [];
-
- if ( toString.call(array) === "[object Array]" ) {
- Array.prototype.push.apply( ret, array );
-
- } else {
- if ( typeof array.length === "number" ) {
- for ( var l = array.length; i < l; i++ ) {
- ret.push( array[i] );
- }
-
- } else {
- for ( ; array[i]; i++ ) {
- ret.push( array[i] );
- }
- }
- }
-
- return ret;
- };
-}
-
-var sortOrder, siblingCheck;
-
-if ( document.documentElement.compareDocumentPosition ) {
- sortOrder = function( a, b ) {
- if ( a === b ) {
- hasDuplicate = true;
- return 0;
- }
-
- if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) {
- return a.compareDocumentPosition ? -1 : 1;
- }
-
- return a.compareDocumentPosition(b) & 4 ? -1 : 1;
- };
-
-} else {
- sortOrder = function( a, b ) {
- // The nodes are identical, we can exit early
- if ( a === b ) {
- hasDuplicate = true;
- return 0;
-
- // Fallback to using sourceIndex (in IE) if it's available on both nodes
- } else if ( a.sourceIndex && b.sourceIndex ) {
- return a.sourceIndex - b.sourceIndex;
- }
-
- var al, bl,
- ap = [],
- bp = [],
- aup = a.parentNode,
- bup = b.parentNode,
- cur = aup;
-
- // If the nodes are siblings (or identical) we can do a quick check
- if ( aup === bup ) {
- return siblingCheck( a, b );
-
- // If no parents were found then the nodes are disconnected
- } else if ( !aup ) {
- return -1;
-
- } else if ( !bup ) {
- return 1;
- }
-
- // Otherwise they're somewhere else in the tree so we need
- // to build up a full list of the parentNodes for comparison
- while ( cur ) {
- ap.unshift( cur );
- cur = cur.parentNode;
- }
-
- cur = bup;
-
- while ( cur ) {
- bp.unshift( cur );
- cur = cur.parentNode;
- }
-
- al = ap.length;
- bl = bp.length;
-
- // Start walking down the tree looking for a discrepancy
- for ( var i = 0; i < al && i < bl; i++ ) {
- if ( ap[i] !== bp[i] ) {
- return siblingCheck( ap[i], bp[i] );
- }
- }
-
- // We ended someplace up the tree so do a sibling check
- return i === al ?
- siblingCheck( a, bp[i], -1 ) :
- siblingCheck( ap[i], b, 1 );
- };
-
- siblingCheck = function( a, b, ret ) {
- if ( a === b ) {
- return ret;
- }
-
- var cur = a.nextSibling;
-
- while ( cur ) {
- if ( cur === b ) {
- return -1;
- }
-
- cur = cur.nextSibling;
- }
-
- return 1;
- };
-}
-
-// Check to see if the browser returns elements by name when
-// querying by getElementById (and provide a workaround)
-(function(){
- // We're going to inject a fake input element with a specified name
- var form = document.createElement("div"),
- id = "script" + (new Date()).getTime(),
- root = document.documentElement;
-
- form.innerHTML = "<a name='" + id + "'/>";
-
- // Inject it into the root element, check its status, and remove it quickly
- root.insertBefore( form, root.firstChild );
-
- // The workaround has to do additional checks after a getElementById
- // Which slows things down for other browsers (hence the branching)
- if ( document.getElementById( id ) ) {
- Expr.find.ID = function( match, context, isXML ) {
- if ( typeof context.getElementById !== "undefined" && !isXML ) {
- var m = context.getElementById(match[1]);
-
- return m ?
- m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ?
- [m] :
- undefined :
- [];
- }
- };
-
- Expr.filter.ID = function( elem, match ) {
- var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
-
- return elem.nodeType === 1 && node && node.nodeValue === match;
- };
- }
-
- root.removeChild( form );
-
- // release memory in IE
- root = form = null;
-})();
-
-(function(){
- // Check to see if the browser returns only elements
- // when doing getElementsByTagName("*")
-
- // Create a fake element
- var div = document.createElement("div");
- div.appendChild( document.createComment("") );
-
- // Make sure no comments are found
- if ( div.getElementsByTagName("*").length > 0 ) {
- Expr.find.TAG = function( match, context ) {
- var results = context.getElementsByTagName( match[1] );
-
- // Filter out possible comments
- if ( match[1] === "*" ) {
- var tmp = [];
-
- for ( var i = 0; results[i]; i++ ) {
- if ( results[i].nodeType === 1 ) {
- tmp.push( results[i] );
- }
- }
-
- results = tmp;
- }
-
- return results;
- };
- }
-
- // Check to see if an attribute returns normalized href attributes
- div.innerHTML = "<a href='#'></a>";
-
- if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" &&
- div.firstChild.getAttribute("href") !== "#" ) {
-
- Expr.attrHandle.href = function( elem ) {
- return elem.getAttribute( "href", 2 );
- };
- }
-
- // release memory in IE
- div = null;
-})();
-
-if ( document.querySelectorAll ) {
- (function(){
- var oldSizzle = Sizzle,
- div = document.createElement("div"),
- id = "__sizzle__";
-
- div.innerHTML = "<p class='TEST'></p>";
-
- // Safari can't handle uppercase or unicode characters when
- // in quirks mode.
- if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
- return;
- }
-
- Sizzle = function( query, context, extra, seed ) {
- context = context || document;
-
- // Only use querySelectorAll on non-XML documents
- // (ID selectors don't work in non-HTML documents)
- if ( !seed && !Sizzle.isXML(context) ) {
- // See if we find a selector to speed up
- var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query );
-
- if ( match && (context.nodeType === 1 || context.nodeType === 9) ) {
- // Speed-up: Sizzle("TAG")
- if ( match[1] ) {
- return makeArray( context.getElementsByTagName( query ), extra );
-
- // Speed-up: Sizzle(".CLASS")
- } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) {
- return makeArray( context.getElementsByClassName( match[2] ), extra );
- }
- }
-
- if ( context.nodeType === 9 ) {
- // Speed-up: Sizzle("body")
- // The body element only exists once, optimize finding it
- if ( query === "body" && context.body ) {
- return makeArray( [ context.body ], extra );
-
- // Speed-up: Sizzle("#ID")
- } else if ( match && match[3] ) {
- var elem = context.getElementById( match[3] );
-
- // Check parentNode to catch when Blackberry 4.6 returns
- // nodes that are no longer in the document #6963
- if ( elem && elem.parentNode ) {
- // Handle the case where IE and Opera return items
- // by name instead of ID
- if ( elem.id === match[3] ) {
- return makeArray( [ elem ], extra );
- }
-
- } else {
- return makeArray( [], extra );
- }
- }
-
- try {
- return makeArray( context.querySelectorAll(query), extra );
- } catch(qsaError) {}
-
- // qSA works strangely on Element-rooted queries
- // We can work around this by specifying an extra ID on the root
- // and working up from there (Thanks to Andrew Dupont for the technique)
- // IE 8 doesn't work on object elements
- } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
- var oldContext = context,
- old = context.getAttribute( "id" ),
- nid = old || id,
- hasParent = context.parentNode,
- relativeHierarchySelector = /^\s*[+~]/.test( query );
-
- if ( !old ) {
- context.setAttribute( "id", nid );
- } else {
- nid = nid.replace( /'/g, "\\$&" );
- }
- if ( relativeHierarchySelector && hasParent ) {
- context = context.parentNode;
- }
-
- try {
- if ( !relativeHierarchySelector || hasParent ) {
- return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra );
- }
-
- } catch(pseudoError) {
- } finally {
- if ( !old ) {
- oldContext.removeAttribute( "id" );
- }
- }
- }
- }
-
- return oldSizzle(query, context, extra, seed);
- };
-
- for ( var prop in oldSizzle ) {
- Sizzle[ prop ] = oldSizzle[ prop ];
- }
-
- // release memory in IE
- div = null;
- })();
-}
-
-(function(){
- var html = document.documentElement,
- matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector;
-
- if ( matches ) {
- // Check to see if it's possible to do matchesSelector
- // on a disconnected node (IE 9 fails this)
- var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ),
- pseudoWorks = false;
-
- try {
- // This should fail with an exception
- // Gecko does not error, returns false instead
- matches.call( document.documentElement, "[test!='']:sizzle" );
-
- } catch( pseudoError ) {
- pseudoWorks = true;
- }
-
- Sizzle.matchesSelector = function( node, expr ) {
- // Make sure that attribute selectors are quoted
- expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']");
-
- if ( !Sizzle.isXML( node ) ) {
- try {
- if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) {
- var ret = matches.call( node, expr );
-
- // IE 9's matchesSelector returns false on disconnected nodes
- if ( ret || !disconnectedMatch ||
- // As well, disconnected nodes are said to be in a document
- // fragment in IE 9, so check for that
- node.document && node.document.nodeType !== 11 ) {
- return ret;
- }
- }
- } catch(e) {}
- }
-
- return Sizzle(expr, null, null, [node]).length > 0;
- };
- }
-})();
-
-(function(){
- var div = document.createElement("div");
-
- div.innerHTML = "<div class='test e'></div><div class='test'></div>";
-
- // Opera can't find a second classname (in 9.6)
- // Also, make sure that getElementsByClassName actually exists
- if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) {
- return;
- }
-
- // Safari caches class attributes, doesn't catch changes (in 3.2)
- div.lastChild.className = "e";
-
- if ( div.getElementsByClassName("e").length === 1 ) {
- return;
- }
-
- Expr.order.splice(1, 0, "CLASS");
- Expr.find.CLASS = function( match, context, isXML ) {
- if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
- return context.getElementsByClassName(match[1]);
- }
- };
-
- // release memory in IE
- div = null;
-})();
-
-function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
- for ( var i = 0, l = checkSet.length; i < l; i++ ) {
- var elem = checkSet[i];
-
- if ( elem ) {
- var match = false;
-
- elem = elem[dir];
-
- while ( elem ) {
- if ( elem[ expando ] === doneName ) {
- match = checkSet[elem.sizset];
- break;
- }
-
- if ( elem.nodeType === 1 && !isXML ){
- elem[ expando ] = doneName;
- elem.sizset = i;
- }
-
- if ( elem.nodeName.toLowerCase() === cur ) {
- match = elem;
- break;
- }
-
- elem = elem[dir];
- }
-
- checkSet[i] = match;
- }
- }
-}
-
-function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
- for ( var i = 0, l = checkSet.length; i < l; i++ ) {
- var elem = checkSet[i];
-
- if ( elem ) {
- var match = false;
-
- elem = elem[dir];
-
- while ( elem ) {
- if ( elem[ expando ] === doneName ) {
- match = checkSet[elem.sizset];
- break;
- }
-
- if ( elem.nodeType === 1 ) {
- if ( !isXML ) {
- elem[ expando ] = doneName;
- elem.sizset = i;
- }
-
- if ( typeof cur !== "string" ) {
- if ( elem === cur ) {
- match = true;
- break;
- }
-
- } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) {
- match = elem;
- break;
- }
- }
-
- elem = elem[dir];
- }
-
- checkSet[i] = match;
- }
- }
-}
-
-if ( document.documentElement.contains ) {
- Sizzle.contains = function( a, b ) {
- return a !== b && (a.contains ? a.contains(b) : true);
- };
-
-} else if ( document.documentElement.compareDocumentPosition ) {
- Sizzle.contains = function( a, b ) {
- return !!(a.compareDocumentPosition(b) & 16);
- };
-
-} else {
- Sizzle.contains = function() {
- return false;
- };
-}
-
-Sizzle.isXML = function( elem ) {
- // documentElement is verified for cases where it doesn't yet exist
- // (such as loading iframes in IE - #4833)
- var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement;
-
- return documentElement ? documentElement.nodeName !== "HTML" : false;
-};
-
-var posProcess = function( selector, context, seed ) {
- var match,
- tmpSet = [],
- later = "",
- root = context.nodeType ? [context] : context;
-
- // Position selectors must be done after the filter
- // And so must :not(positional) so we move all PSEUDOs to the end
- while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
- later += match[0];
- selector = selector.replace( Expr.match.PSEUDO, "" );
- }
-
- selector = Expr.relative[selector] ? selector + "*" : selector;
-
- for ( var i = 0, l = root.length; i < l; i++ ) {
- Sizzle( selector, root[i], tmpSet, seed );
- }
-
- return Sizzle.filter( later, tmpSet );
-};
-
-// EXPOSE
-
-window.tinymce.dom.Sizzle = Sizzle;
-
-})();
-
-
-(function(tinymce) {
- tinymce.dom.Element = function(id, settings) {
- var t = this, dom, el;
-
- t.settings = settings = settings || {};
- t.id = id;
- t.dom = dom = settings.dom || tinymce.DOM;
-
- // Only IE leaks DOM references, this is a lot faster
- if (!tinymce.isIE)
- el = dom.get(t.id);
-
- tinymce.each(
- ('getPos,getRect,getParent,add,setStyle,getStyle,setStyles,' +
- 'setAttrib,setAttribs,getAttrib,addClass,removeClass,' +
- 'hasClass,getOuterHTML,setOuterHTML,remove,show,hide,' +
- 'isHidden,setHTML,get').split(/,/), function(k) {
- t[k] = function() {
- var a = [id], i;
-
- for (i = 0; i < arguments.length; i++)
- a.push(arguments[i]);
-
- a = dom[k].apply(dom, a);
- t.update(k);
-
- return a;
- };
- }
- );
-
- tinymce.extend(t, {
- on : function(n, f, s) {
- return tinymce.dom.Event.add(t.id, n, f, s);
- },
-
- getXY : function() {
- return {
- x : parseInt(t.getStyle('left')),
- y : parseInt(t.getStyle('top'))
- };
- },
-
- getSize : function() {
- var n = dom.get(t.id);
-
- return {
- w : parseInt(t.getStyle('width') || n.clientWidth),
- h : parseInt(t.getStyle('height') || n.clientHeight)
- };
- },
-
- moveTo : function(x, y) {
- t.setStyles({left : x, top : y});
- },
-
- moveBy : function(x, y) {
- var p = t.getXY();
-
- t.moveTo(p.x + x, p.y + y);
- },
-
- resizeTo : function(w, h) {
- t.setStyles({width : w, height : h});
- },
-
- resizeBy : function(w, h) {
- var s = t.getSize();
-
- t.resizeTo(s.w + w, s.h + h);
- },
-
- update : function(k) {
- var b;
-
- if (tinymce.isIE6 && settings.blocker) {
- k = k || '';
-
- // Ignore getters
- if (k.indexOf('get') === 0 || k.indexOf('has') === 0 || k.indexOf('is') === 0)
- return;
-
- // Remove blocker on remove
- if (k == 'remove') {
- dom.remove(t.blocker);
- return;
- }
-
- if (!t.blocker) {
- t.blocker = dom.uniqueId();
- b = dom.add(settings.container || dom.getRoot(), 'iframe', {id : t.blocker, style : 'position:absolute;', frameBorder : 0, src : 'javascript:""'});
- dom.setStyle(b, 'opacity', 0);
- } else
- b = dom.get(t.blocker);
-
- dom.setStyles(b, {
- left : t.getStyle('left', 1),
- top : t.getStyle('top', 1),
- width : t.getStyle('width', 1),
- height : t.getStyle('height', 1),
- display : t.getStyle('display', 1),
- zIndex : parseInt(t.getStyle('zIndex', 1) || 0) - 1
- });
- }
- }
- });
- };
-})(tinymce);
-
-(function(tinymce) {
- function trimNl(s) {
- return s.replace(/[\n\r]+/g, '');
- };
-
- // Shorten names
- var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each, TreeWalker = tinymce.dom.TreeWalker;
-
- tinymce.create('tinymce.dom.Selection', {
- Selection : function(dom, win, serializer, editor) {
- var t = this;
-
- t.dom = dom;
- t.win = win;
- t.serializer = serializer;
- t.editor = editor;
-
- // Add events
- each([
- 'onBeforeSetContent',
-
- 'onBeforeGetContent',
-
- 'onSetContent',
-
- 'onGetContent'
- ], function(e) {
- t[e] = new tinymce.util.Dispatcher(t);
- });
-
- // No W3C Range support
- if (!t.win.getSelection)
- t.tridentSel = new tinymce.dom.TridentSelection(t);
-
- if (tinymce.isIE && dom.boxModel)
- this._fixIESelection();
-
- // Prevent leaks
- tinymce.addUnload(t.destroy, t);
- },
-
- setCursorLocation: function(node, offset) {
- var t = this; var r = t.dom.createRng();
- r.setStart(node, offset);
- r.setEnd(node, offset);
- t.setRng(r);
- t.collapse(false);
- },
- getContent : function(s) {
- var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n;
-
- s = s || {};
- wb = wa = '';
- s.get = true;
- s.format = s.format || 'html';
- s.forced_root_block = '';
- t.onBeforeGetContent.dispatch(t, s);
-
- if (s.format == 'text')
- return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : ''));
-
- if (r.cloneContents) {
- n = r.cloneContents();
-
- if (n)
- e.appendChild(n);
- } else if (is(r.item) || is(r.htmlText)) {
- // IE will produce invalid markup if elements are present that
- // it doesn't understand like custom elements or HTML5 elements.
- // Adding a BR in front of the contents and then remoiving it seems to fix it though.
- e.innerHTML = '<br>' + (r.item ? r.item(0).outerHTML : r.htmlText);
- e.removeChild(e.firstChild);
- } else
- e.innerHTML = r.toString();
-
- // Keep whitespace before and after
- if (/^\s/.test(e.innerHTML))
- wb = ' ';
-
- if (/\s+$/.test(e.innerHTML))
- wa = ' ';
-
- s.getInner = true;
-
- s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa;
- t.onGetContent.dispatch(t, s);
-
- return s.content;
- },
-
- setContent : function(content, args) {
- var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp;
-
- args = args || {format : 'html'};
- args.set = true;
- content = args.content = content;
-
- // Dispatch before set content event
- if (!args.no_events)
- self.onBeforeSetContent.dispatch(self, args);
-
- content = args.content;
-
- if (rng.insertNode) {
- // Make caret marker since insertNode places the caret in the beginning of text after insert
- content += '<span id="__caret">_</span>';
-
- // Delete and insert new node
- if (rng.startContainer == doc && rng.endContainer == doc) {
- // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents
- doc.body.innerHTML = content;
- } else {
- rng.deleteContents();
-
- if (doc.body.childNodes.length === 0) {
- doc.body.innerHTML = content;
- } else {
- // createContextualFragment doesn't exists in IE 9 DOMRanges
- if (rng.createContextualFragment) {
- rng.insertNode(rng.createContextualFragment(content));
- } else {
- // Fake createContextualFragment call in IE 9
- frag = doc.createDocumentFragment();
- temp = doc.createElement('div');
-
- frag.appendChild(temp);
- temp.outerHTML = content;
-
- rng.insertNode(frag);
- }
- }
- }
-
- // Move to caret marker
- caretNode = self.dom.get('__caret');
-
- // Make sure we wrap it compleatly, Opera fails with a simple select call
- rng = doc.createRange();
- rng.setStartBefore(caretNode);
- rng.setEndBefore(caretNode);
- self.setRng(rng);
-
- // Remove the caret position
- self.dom.remove('__caret');
-
- try {
- self.setRng(rng);
- } catch (ex) {
- // Might fail on Opera for some odd reason
- }
- } else {
- if (rng.item) {
- // Delete content and get caret text selection
- doc.execCommand('Delete', false, null);
- rng = self.getRng();
- }
-
- // Explorer removes spaces from the beginning of pasted contents
- if (/^\s+/.test(content)) {
- rng.pasteHTML('<span id="__mce_tmp">_</span>' + content);
- self.dom.remove('__mce_tmp');
- } else
- rng.pasteHTML(content);
- }
-
- // Dispatch set content event
- if (!args.no_events)
- self.onSetContent.dispatch(self, args);
- },
-
- getStart : function() {
- var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node;
-
- if (rng.duplicate || rng.item) {
- // Control selection, return first item
- if (rng.item)
- return rng.item(0);
-
- // Get start element
- checkRng = rng.duplicate();
- checkRng.collapse(1);
- startElement = checkRng.parentElement();
- if (startElement.ownerDocument !== self.dom.doc) {
- startElement = self.dom.getRoot();
- }
-
- // Check if range parent is inside the start element, then return the inner parent element
- // This will fix issues when a single element is selected, IE would otherwise return the wrong start element
- parentElement = node = rng.parentElement();
- while (node = node.parentNode) {
- if (node == startElement) {
- startElement = parentElement;
- break;
- }
- }
-
- return startElement;
- } else {
- startElement = rng.startContainer;
-
- if (startElement.nodeType == 1 && startElement.hasChildNodes())
- startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)];
-
- if (startElement && startElement.nodeType == 3)
- return startElement.parentNode;
-
- return startElement;
- }
- },
-
- getEnd : function() {
- var self = this, rng = self.getRng(), endElement, endOffset;
-
- if (rng.duplicate || rng.item) {
- if (rng.item)
- return rng.item(0);
-
- rng = rng.duplicate();
- rng.collapse(0);
- endElement = rng.parentElement();
- if (endElement.ownerDocument !== self.dom.doc) {
- endElement = self.dom.getRoot();
- }
-
- if (endElement && endElement.nodeName == 'BODY')
- return endElement.lastChild || endElement;
-
- return endElement;
- } else {
- endElement = rng.endContainer;
- endOffset = rng.endOffset;
-
- if (endElement.nodeType == 1 && endElement.hasChildNodes())
- endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset];
-
- if (endElement && endElement.nodeType == 3)
- return endElement.parentNode;
-
- return endElement;
- }
- },
-
- getBookmark : function(type, normalized) {
- var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles;
-
- function findIndex(name, element) {
- var index = 0;
-
- each(dom.select(name), function(node, i) {
- if (node == element)
- index = i;
- });
-
- return index;
- };
-
- function normalizeTableCellSelection(rng) {
- function moveEndPoint(start) {
- var container, offset, childNodes, prefix = start ? 'start' : 'end';
-
- container = rng[prefix + 'Container'];
- offset = rng[prefix + 'Offset'];
-
- if (container.nodeType == 1 && container.nodeName == "TR") {
- childNodes = container.childNodes;
- container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)];
- if (container) {
- offset = start ? 0 : container.childNodes.length;
- rng['set' + (start ? 'Start' : 'End')](container, offset);
- }
- }
- };
-
- moveEndPoint(true);
- moveEndPoint();
-
- return rng;
- };
-
- function getLocation() {
- var rng = t.getRng(true), root = dom.getRoot(), bookmark = {};
-
- function getPoint(rng, start) {
- var container = rng[start ? 'startContainer' : 'endContainer'],
- offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0;
-
- if (container.nodeType == 3) {
- if (normalized) {
- for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling)
- offset += node.nodeValue.length;
- }
-
- point.push(offset);
- } else {
- childNodes = container.childNodes;
-
- if (offset >= childNodes.length && childNodes.length) {
- after = 1;
- offset = Math.max(0, childNodes.length - 1);
- }
-
- point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after);
- }
-
- for (; container && container != root; container = container.parentNode)
- point.push(t.dom.nodeIndex(container, normalized));
-
- return point;
- };
-
- bookmark.start = getPoint(rng, true);
-
- if (!t.isCollapsed())
- bookmark.end = getPoint(rng);
-
- return bookmark;
- };
-
- if (type == 2) {
- if (t.tridentSel)
- return t.tridentSel.getBookmark(type);
-
- return getLocation();
- }
-
- // Handle simple range
- if (type)
- return {rng : t.getRng()};
-
- rng = t.getRng();
- id = dom.uniqueId();
- collapsed = tinyMCE.activeEditor.selection.isCollapsed();
- styles = 'overflow:hidden;line-height:0px';
-
- // Explorer method
- if (rng.duplicate || rng.item) {
- // Text selection
- if (!rng.item) {
- rng2 = rng.duplicate();
-
- try {
- // Insert start marker
- rng.collapse();
- rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>');
-
- // Insert end marker
- if (!collapsed) {
- rng2.collapse(false);
-
- // Detect the empty space after block elements in IE and move the end back one character <p></p>] becomes <p>]</p>
- rng.moveToElementText(rng2.parentElement());
- if (rng.compareEndPoints('StartToEnd', rng2) === 0)
- rng2.move('character', -1);
-
- rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>');
- }
- } catch (ex) {
- // IE might throw unspecified error so lets ignore it
- return null;
- }
- } else {
- // Control selection
- element = rng.item(0);
- name = element.nodeName;
-
- return {name : name, index : findIndex(name, element)};
- }
- } else {
- element = t.getNode();
- name = element.nodeName;
- if (name == 'IMG')
- return {name : name, index : findIndex(name, element)};
-
- // W3C method
- rng2 = normalizeTableCellSelection(rng.cloneRange());
-
- // Insert end marker
- if (!collapsed) {
- rng2.collapse(false);
- rng2.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_end', style : styles}, chr));
- }
-
- rng = normalizeTableCellSelection(rng);
- rng.collapse(true);
- rng.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_start', style : styles}, chr));
- }
-
- t.moveToBookmark({id : id, keep : 1});
-
- return {id : id};
- },
-
- moveToBookmark : function(bookmark) {
- var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset;
-
- function setEndPoint(start) {
- var point = bookmark[start ? 'start' : 'end'], i, node, offset, children;
-
- if (point) {
- offset = point[0];
-
- // Find container node
- for (node = root, i = point.length - 1; i >= 1; i--) {
- children = node.childNodes;
-
- if (point[i] > children.length - 1)
- return;
-
- node = children[point[i]];
- }
-
- // Move text offset to best suitable location
- if (node.nodeType === 3)
- offset = Math.min(point[0], node.nodeValue.length);
-
- // Move element offset to best suitable location
- if (node.nodeType === 1)
- offset = Math.min(point[0], node.childNodes.length);
-
- // Set offset within container node
- if (start)
- rng.setStart(node, offset);
- else
- rng.setEnd(node, offset);
- }
-
- return true;
- };
-
- function restoreEndPoint(suffix) {
- var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep;
-
- if (marker) {
- node = marker.parentNode;
-
- if (suffix == 'start') {
- if (!keep) {
- idx = dom.nodeIndex(marker);
- } else {
- node = marker.firstChild;
- idx = 1;
- }
-
- startContainer = endContainer = node;
- startOffset = endOffset = idx;
- } else {
- if (!keep) {
- idx = dom.nodeIndex(marker);
- } else {
- node = marker.firstChild;
- idx = 1;
- }
-
- endContainer = node;
- endOffset = idx;
- }
-
- if (!keep) {
- prev = marker.previousSibling;
- next = marker.nextSibling;
-
- // Remove all marker text nodes
- each(tinymce.grep(marker.childNodes), function(node) {
- if (node.nodeType == 3)
- node.nodeValue = node.nodeValue.replace(/\uFEFF/g, '');
- });
-
- // Remove marker but keep children if for example contents where inserted into the marker
- // Also remove duplicated instances of the marker for example by a split operation or by WebKit auto split on paste feature
- while (marker = dom.get(bookmark.id + '_' + suffix))
- dom.remove(marker, 1);
-
- // If siblings are text nodes then merge them unless it's Opera since it some how removes the node
- // and we are sniffing since adding a lot of detection code for a browser with 3% of the market isn't worth the effort. Sorry, Opera but it's just a fact
- if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !tinymce.isOpera) {
- idx = prev.nodeValue.length;
- prev.appendData(next.nodeValue);
- dom.remove(next);
-
- if (suffix == 'start') {
- startContainer = endContainer = prev;
- startOffset = endOffset = idx;
- } else {
- endContainer = prev;
- endOffset = idx;
- }
- }
- }
- }
- };
-
- function addBogus(node) {
- // Adds a bogus BR element for empty block elements
- if (dom.isBlock(node) && !node.innerHTML && !isIE)
- node.innerHTML = '<br data-mce-bogus="1" />';
-
- return node;
- };
-
- if (bookmark) {
- if (bookmark.start) {
- rng = dom.createRng();
- root = dom.getRoot();
-
- if (t.tridentSel)
- return t.tridentSel.moveToBookmark(bookmark);
-
- if (setEndPoint(true) && setEndPoint()) {
- t.setRng(rng);
- }
- } else if (bookmark.id) {
- // Restore start/end points
- restoreEndPoint('start');
- restoreEndPoint('end');
-
- if (startContainer) {
- rng = dom.createRng();
- rng.setStart(addBogus(startContainer), startOffset);
- rng.setEnd(addBogus(endContainer), endOffset);
- t.setRng(rng);
- }
- } else if (bookmark.name) {
- t.select(dom.select(bookmark.name)[bookmark.index]);
- } else if (bookmark.rng)
- t.setRng(bookmark.rng);
- }
- },
-
- select : function(node, content) {
- var t = this, dom = t.dom, rng = dom.createRng(), idx;
-
- function setPoint(node, start) {
- var walker = new TreeWalker(node, node);
-
- do {
- // Text node
- if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length !== 0) {
- if (start)
- rng.setStart(node, 0);
- else
- rng.setEnd(node, node.nodeValue.length);
-
- return;
- }
-
- // BR element
- if (node.nodeName == 'BR') {
- if (start)
- rng.setStartBefore(node);
- else
- rng.setEndBefore(node);
-
- return;
- }
- } while (node = (start ? walker.next() : walker.prev()));
- };
-
- if (node) {
- idx = dom.nodeIndex(node);
- rng.setStart(node.parentNode, idx);
- rng.setEnd(node.parentNode, idx + 1);
-
- // Find first/last text node or BR element
- if (content) {
- setPoint(node, 1);
- setPoint(node);
- }
-
- t.setRng(rng);
- }
-
- return node;
- },
-
- isCollapsed : function() {
- var t = this, r = t.getRng(), s = t.getSel();
-
- if (!r || r.item)
- return false;
-
- if (r.compareEndPoints)
- return r.compareEndPoints('StartToEnd', r) === 0;
-
- return !s || r.collapsed;
- },
-
- collapse : function(to_start) {
- var self = this, rng = self.getRng(), node;
-
- // Control range on IE
- if (rng.item) {
- node = rng.item(0);
- rng = self.win.document.body.createTextRange();
- rng.moveToElementText(node);
- }
-
- rng.collapse(!!to_start);
- self.setRng(rng);
- },
-
- getSel : function() {
- var t = this, w = this.win;
-
- return w.getSelection ? w.getSelection() : w.document.selection;
- },
-
- getRng : function(w3c) {
- var self = this, selection, rng, elm, doc = self.win.document;
-
- // Found tridentSel object then we need to use that one
- if (w3c && self.tridentSel) {
- return self.tridentSel.getRangeAt(0);
- }
-
- try {
- if (selection = self.getSel()) {
- rng = selection.rangeCount > 0 ? selection.getRangeAt(0) : (selection.createRange ? selection.createRange() : doc.createRange());
- }
- } catch (ex) {
- // IE throws unspecified error here if TinyMCE is placed in a frame/iframe
- }
-
- // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet
- if (tinymce.isIE && rng && rng.setStart && doc.selection.createRange().item) {
- elm = doc.selection.createRange().item(0);
- rng = doc.createRange();
- rng.setStartBefore(elm);
- rng.setEndAfter(elm);
- }
-
- // No range found then create an empty one
- // This can occur when the editor is placed in a hidden container element on Gecko
- // Or on IE when there was an exception
- if (!rng) {
- rng = doc.createRange ? doc.createRange() : doc.body.createTextRange();
- }
-
- // If range is at start of document then move it to start of body
- if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) {
- elm = self.dom.getRoot();
- rng.setStart(elm, 0);
- rng.setEnd(elm, 0);
- }
-
- if (self.selectedRange && self.explicitRange) {
- if (rng.compareBoundaryPoints(rng.START_TO_START, self.selectedRange) === 0 && rng.compareBoundaryPoints(rng.END_TO_END, self.selectedRange) === 0) {
- // Safari, Opera and Chrome only ever select text which causes the range to change.
- // This lets us use the originally set range if the selection hasn't been changed by the user.
- rng = self.explicitRange;
- } else {
- self.selectedRange = null;
- self.explicitRange = null;
- }
- }
-
- return rng;
- },
-
- setRng : function(r, forward) {
- var s, t = this;
-
- if (!t.tridentSel) {
- s = t.getSel();
-
- if (s) {
- t.explicitRange = r;
-
- try {
- s.removeAllRanges();
- } catch (ex) {
- // IE9 might throw errors here don't know why
- }
-
- s.addRange(r);
-
- // Forward is set to false and we have an extend function
- if (forward === false && s.extend) {
- s.collapse(r.endContainer, r.endOffset);
- s.extend(r.startContainer, r.startOffset);
- }
-
- // adding range isn't always successful so we need to check range count otherwise an exception can occur
- t.selectedRange = s.rangeCount > 0 ? s.getRangeAt(0) : null;
- }
- } else {
- // Is W3C Range
- if (r.cloneRange) {
- try {
- t.tridentSel.addRange(r);
- return;
- } catch (ex) {
- //IE9 throws an error here if called before selection is placed in the editor
- }
- }
-
- // Is IE specific range
- try {
- r.select();
- } catch (ex) {
- // Needed for some odd IE bug #1843306
- }
- }
- },
-
- setNode : function(n) {
- var t = this;
-
- t.setContent(t.dom.getOuterHTML(n));
-
- return n;
- },
-
- getNode : function() {
- var t = this, rng = t.getRng(), sel = t.getSel(), elm, start = rng.startContainer, end = rng.endContainer;
-
- function skipEmptyTextNodes(n, forwards) {
- var orig = n;
- while (n && n.nodeType === 3 && n.length === 0) {
- n = forwards ? n.nextSibling : n.previousSibling;
- }
- return n || orig;
- };
-
- // Range maybe lost after the editor is made visible again
- if (!rng)
- return t.dom.getRoot();
-
- if (rng.setStart) {
- elm = rng.commonAncestorContainer;
-
- // Handle selection a image or other control like element such as anchors
- if (!rng.collapsed) {
- if (rng.startContainer == rng.endContainer) {
- if (rng.endOffset - rng.startOffset < 2) {
- if (rng.startContainer.hasChildNodes())
- elm = rng.startContainer.childNodes[rng.startOffset];
- }
- }
-
- // If the anchor node is a element instead of a text node then return this element
- //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1)
- // return sel.anchorNode.childNodes[sel.anchorOffset];
-
- // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent.
- // This happens when you double click an underlined word in FireFox.
- if (start.nodeType === 3 && end.nodeType === 3) {
- if (start.length === rng.startOffset) {
- start = skipEmptyTextNodes(start.nextSibling, true);
- } else {
- start = start.parentNode;
- }
- if (rng.endOffset === 0) {
- end = skipEmptyTextNodes(end.previousSibling, false);
- } else {
- end = end.parentNode;
- }
-
- if (start && start === end)
- return start;
- }
- }
-
- if (elm && elm.nodeType == 3)
- return elm.parentNode;
-
- return elm;
- }
-
- return rng.item ? rng.item(0) : rng.parentElement();
- },
-
- getSelectedBlocks : function(st, en) {
- var t = this, dom = t.dom, sb, eb, n, bl = [];
-
- sb = dom.getParent(st || t.getStart(), dom.isBlock);
- eb = dom.getParent(en || t.getEnd(), dom.isBlock);
-
- if (sb)
- bl.push(sb);
-
- if (sb && eb && sb != eb) {
- n = sb;
-
- var walker = new TreeWalker(sb, dom.getRoot());
- while ((n = walker.next()) && n != eb) {
- if (dom.isBlock(n))
- bl.push(n);
- }
- }
-
- if (eb && sb != eb)
- bl.push(eb);
-
- return bl;
- },
-
- isForward: function(){
- var dom = this.dom, sel = this.getSel(), anchorRange, focusRange;
-
- // No support for selection direction then always return true
- if (!sel || sel.anchorNode == null || sel.focusNode == null) {
- return true;
- }
-
- anchorRange = dom.createRng();
- anchorRange.setStart(sel.anchorNode, sel.anchorOffset);
- anchorRange.collapse(true);
-
- focusRange = dom.createRng();
- focusRange.setStart(sel.focusNode, sel.focusOffset);
- focusRange.collapse(true);
-
- return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0;
- },
-
- normalize : function() {
- var self = this, rng, normalized, collapsed, node, sibling;
-
- function normalizeEndPoint(start) {
- var container, offset, walker, dom = self.dom, body = dom.getRoot(), node, nonEmptyElementsMap, nodeName;
-
- function hasBrBeforeAfter(node, left) {
- var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body);
-
- while (node = walker[left ? 'prev' : 'next']()) {
- if (node.nodeName === "BR") {
- return true;
- }
- }
- };
-
- // Walks the dom left/right to find a suitable text node to move the endpoint into
- // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG
- function findTextNodeRelative(left, startNode) {
- var walker, lastInlineElement;
-
- startNode = startNode || container;
- walker = new TreeWalker(startNode, dom.getParent(startNode.parentNode, dom.isBlock) || body);
-
- // Walk left until we hit a text node we can move to or a block/br/img
- while (node = walker[left ? 'prev' : 'next']()) {
- // Found text node that has a length
- if (node.nodeType === 3 && node.nodeValue.length > 0) {
- container = node;
- offset = left ? node.nodeValue.length : 0;
- normalized = true;
- return;
- }
-
- // Break if we find a block or a BR/IMG/INPUT etc
- if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
- return;
- }
-
- lastInlineElement = node;
- }
-
- // Only fetch the last inline element when in caret mode for now
- if (collapsed && lastInlineElement) {
- container = lastInlineElement;
- normalized = true;
- offset = 0;
- }
- };
-
- container = rng[(start ? 'start' : 'end') + 'Container'];
- offset = rng[(start ? 'start' : 'end') + 'Offset'];
- nonEmptyElementsMap = dom.schema.getNonEmptyElements();
-
- // If the container is a document move it to the body element
- if (container.nodeType === 9) {
- container = dom.getRoot();
- offset = 0;
- }
-
- // If the container is body try move it into the closest text node or position
- if (container === body) {
- // If start is before/after a image, table etc
- if (start) {
- node = container.childNodes[offset > 0 ? offset - 1 : 0];
- if (node) {
- nodeName = node.nodeName.toLowerCase();
- if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") {
- return;
- }
- }
- }
-
- // Resolve the index
- if (container.hasChildNodes()) {
- container = container.childNodes[Math.min(!start && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1)];
- offset = 0;
-
- // Don't walk into elements that doesn't have any child nodes like a IMG
- if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) {
- // Walk the DOM to find a text node to place the caret at or a BR
- node = container;
- walker = new TreeWalker(container, body);
-
- do {
- // Found a text node use that position
- if (node.nodeType === 3 && node.nodeValue.length > 0) {
- offset = start ? 0 : node.nodeValue.length;
- container = node;
- normalized = true;
- break;
- }
-
- // Found a BR/IMG element that we can place the caret before
- if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
- offset = dom.nodeIndex(node);
- container = node.parentNode;
-
- // Put caret after image when moving the end point
- if (node.nodeName == "IMG" && !start) {
- offset++;
- }
-
- normalized = true;
- break;
- }
- } while (node = (start ? walker.next() : walker.prev()));
- }
- }
- }
-
- // Lean the caret to the left if possible
- if (collapsed) {
- // So this: <b>x</b><i>|x</i>
- // Becomes: <b>x|</b><i>x</i>
- // Seems that only gecko has issues with this
- if (container.nodeType === 3 && offset === 0) {
- findTextNodeRelative(true);
- }
-
- // Lean left into empty inline elements when the caret is before a BR
- // So this: <i><b></b><i>|<br></i>
- // Becomes: <i><b>|</b><i><br></i>
- // Seems that only gecko has issues with this
- if (container.nodeType === 1) {
- node = container.childNodes[offset];
- if(node && node.nodeName === 'BR' && !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) {
- findTextNodeRelative(true, container.childNodes[offset]);
- }
- }
- }
-
- // Lean the start of the selection right if possible
- // So this: x[<b>x]</b>
- // Becomes: x<b>[x]</b>
- if (start && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) {
- findTextNodeRelative(false);
- }
-
- // Set endpoint if it was normalized
- if (normalized)
- rng['set' + (start ? 'Start' : 'End')](container, offset);
- };
-
- // Normalize only on non IE browsers for now
- if (tinymce.isIE)
- return;
-
- rng = self.getRng();
- collapsed = rng.collapsed;
-
- // Normalize the end points
- normalizeEndPoint(true);
-
- if (!collapsed)
- normalizeEndPoint();
-
- // Set the selection if it was normalized
- if (normalized) {
- // If it was collapsed then make sure it still is
- if (collapsed) {
- rng.collapse(true);
- }
-
- //console.log(self.dom.dumpRng(rng));
- self.setRng(rng, self.isForward());
- }
- },
-
- selectorChanged: function(selector, callback) {
- var self = this, currentSelectors;
-
- if (!self.selectorChangedData) {
- self.selectorChangedData = {};
- currentSelectors = {};
-
- self.editor.onNodeChange.addToTop(function(ed, cm, node) {
- var dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {};
-
- // Check for new matching selectors
- each(self.selectorChangedData, function(callbacks, selector) {
- each(parents, function(node) {
- if (dom.is(node, selector)) {
- if (!currentSelectors[selector]) {
- // Execute callbacks
- each(callbacks, function(callback) {
- callback(true, {node: node, selector: selector, parents: parents});
- });
-
- currentSelectors[selector] = callbacks;
- }
-
- matchedSelectors[selector] = callbacks;
- return false;
- }
- });
- });
-
- // Check if current selectors still match
- each(currentSelectors, function(callbacks, selector) {
- if (!matchedSelectors[selector]) {
- delete currentSelectors[selector];
-
- each(callbacks, function(callback) {
- callback(false, {node: node, selector: selector, parents: parents});
- });
- }
- });
- });
- }
-
- // Add selector listeners
- if (!self.selectorChangedData[selector]) {
- self.selectorChangedData[selector] = [];
- }
-
- self.selectorChangedData[selector].push(callback);
-
- return self;
- },
-
- destroy : function(manual) {
- var self = this;
-
- self.win = null;
-
- // Manual destroy then remove unload handler
- if (!manual)
- tinymce.removeUnload(self.destroy);
- },
-
- // IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode
- _fixIESelection : function() {
- var dom = this.dom, doc = dom.doc, body = doc.body, started, startRng, htmlElm;
-
- // Return range from point or null if it failed
- function rngFromPoint(x, y) {
- var rng = body.createTextRange();
-
- try {
- rng.moveToPoint(x, y);
- } catch (ex) {
- // IE sometimes throws and exception, so lets just ignore it
- rng = null;
- }
-
- return rng;
- };
-
- // Fires while the selection is changing
- function selectionChange(e) {
- var pointRng;
-
- // Check if the button is down or not
- if (e.button) {
- // Create range from mouse position
- pointRng = rngFromPoint(e.x, e.y);
-
- if (pointRng) {
- // Check if pointRange is before/after selection then change the endPoint
- if (pointRng.compareEndPoints('StartToStart', startRng) > 0)
- pointRng.setEndPoint('StartToStart', startRng);
- else
- pointRng.setEndPoint('EndToEnd', startRng);
-
- pointRng.select();
- }
- } else
- endSelection();
- }
-
- // Removes listeners
- function endSelection() {
- var rng = doc.selection.createRange();
-
- // If the range is collapsed then use the last start range
- if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0)
- startRng.select();
-
- dom.unbind(doc, 'mouseup', endSelection);
- dom.unbind(doc, 'mousemove', selectionChange);
- startRng = started = 0;
- };
-
- // Make HTML element unselectable since we are going to handle selection by hand
- doc.documentElement.unselectable = true;
-
- // Detect when user selects outside BODY
- dom.bind(doc, ['mousedown', 'contextmenu'], function(e) {
- if (e.target.nodeName === 'HTML') {
- if (started)
- endSelection();
-
- // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML
- htmlElm = doc.documentElement;
- if (htmlElm.scrollHeight > htmlElm.clientHeight)
- return;
-
- started = 1;
- // Setup start position
- startRng = rngFromPoint(e.x, e.y);
- if (startRng) {
- // Listen for selection change events
- dom.bind(doc, 'mouseup', endSelection);
- dom.bind(doc, 'mousemove', selectionChange);
-
- dom.win.focus();
- startRng.select();
- }
- }
- });
- }
- });
-})(tinymce);
-
-(function(tinymce) {
- tinymce.dom.Serializer = function(settings, dom, schema) {
- var onPreProcess, onPostProcess, isIE = tinymce.isIE, each = tinymce.each, htmlParser;
-
- // Support the old apply_source_formatting option
- if (!settings.apply_source_formatting)
- settings.indent = false;
-
- // Default DOM and Schema if they are undefined
- dom = dom || tinymce.DOM;
- schema = schema || new tinymce.html.Schema(settings);
- settings.entity_encoding = settings.entity_encoding || 'named';
- settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true;
-
- onPreProcess = new tinymce.util.Dispatcher(self);
-
- onPostProcess = new tinymce.util.Dispatcher(self);
-
- htmlParser = new tinymce.html.DomParser(settings, schema);
-
- // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed
- htmlParser.addAttributeFilter('src,href,style', function(nodes, name) {
- var i = nodes.length, node, value, internalName = 'data-mce-' + name, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef;
-
- while (i--) {
- node = nodes[i];
-
- value = node.attributes.map[internalName];
- if (value !== undef) {
- // Set external name to internal value and remove internal
- node.attr(name, value.length > 0 ? value : null);
- node.attr(internalName, null);
- } else {
- // No internal attribute found then convert the value we have in the DOM
- value = node.attributes.map[name];
-
- if (name === "style")
- value = dom.serializeStyle(dom.parseStyle(value), node.name);
- else if (urlConverter)
- value = urlConverter.call(urlConverterScope, value, name, node.name);
-
- node.attr(name, value.length > 0 ? value : null);
- }
- }
- });
-
- // Remove internal classes mceItem<..> or mceSelected
- htmlParser.addAttributeFilter('class', function(nodes, name) {
- var i = nodes.length, node, value;
-
- while (i--) {
- node = nodes[i];
- value = node.attr('class').replace(/(?:^|\s)mce(Item\w+|Selected)(?!\S)/g, '');
- node.attr('class', value.length > 0 ? value : null);
- }
- });
-
- // Remove bookmark elements
- htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) {
- var i = nodes.length, node;
-
- while (i--) {
- node = nodes[i];
-
- if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup)
- node.remove();
- }
- });
-
- // Remove expando attributes
- htmlParser.addAttributeFilter('data-mce-expando', function(nodes, name, args) {
- var i = nodes.length;
-
- while (i--) {
- nodes[i].attr(name, null);
- }
- });
-
- htmlParser.addNodeFilter('noscript', function(nodes) {
- var i = nodes.length, node;
-
- while (i--) {
- node = nodes[i].firstChild;
-
- if (node) {
- node.value = tinymce.html.Entities.decode(node.value);
- }
- }
- });
-
- // Force script into CDATA sections and remove the mce- prefix also add comments around styles
- htmlParser.addNodeFilter('script,style', function(nodes, name) {
- var i = nodes.length, node, value;
-
- function trim(value) {
- return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n')
- .replace(/^[\r\n]*|[\r\n]*$/g, '')
- .replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '')
- .replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, '');
- };
-
- while (i--) {
- node = nodes[i];
- value = node.firstChild ? node.firstChild.value : '';
-
- if (name === "script") {
- // Remove mce- prefix from script elements
- node.attr('type', (node.attr('type') || 'text/javascript').replace(/^mce\-/, ''));
-
- if (value.length > 0)
- node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>';
- } else {
- if (value.length > 0)
- node.firstChild.value = '<!--\n' + trim(value) + '\n-->';
- }
- }
- });
-
- // Convert comments to cdata and handle protected comments
- htmlParser.addNodeFilter('#comment', function(nodes, name) {
- var i = nodes.length, node;
-
- while (i--) {
- node = nodes[i];
-
- if (node.value.indexOf('[CDATA[') === 0) {
- node.name = '#cdata';
- node.type = 4;
- node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, '');
- } else if (node.value.indexOf('mce:protected ') === 0) {
- node.name = "#text";
- node.type = 3;
- node.raw = true;
- node.value = unescape(node.value).substr(14);
- }
- }
- });
-
- htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) {
- var i = nodes.length, node;
-
- while (i--) {
- node = nodes[i];
- if (node.type === 7)
- node.remove();
- else if (node.type === 1) {
- if (name === "input" && !("type" in node.attributes.map))
- node.attr('type', 'text');
- }
- }
- });
-
- // Fix list elements, TODO: Replace this later
- if (settings.fix_list_elements) {
- htmlParser.addNodeFilter('ul,ol', function(nodes, name) {
- var i = nodes.length, node, parentNode;
-
- while (i--) {
- node = nodes[i];
- parentNode = node.parent;
-
- if (parentNode.name === 'ul' || parentNode.name === 'ol') {
- if (node.prev && node.prev.name === 'li') {
- node.prev.append(node);
- }
- }
- }
- });
- }
-
- // Remove internal data attributes
- htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style', function(nodes, name) {
- var i = nodes.length;
-
- while (i--) {
- nodes[i].attr(name, null);
- }
- });
-
- // Return public methods
- return {
- schema : schema,
-
- addNodeFilter : htmlParser.addNodeFilter,
-
- addAttributeFilter : htmlParser.addAttributeFilter,
-
- onPreProcess : onPreProcess,
-
- onPostProcess : onPostProcess,
-
- serialize : function(node, args) {
- var impl, doc, oldDoc, htmlSerializer, content;
-
- // Explorer won't clone contents of script and style and the
- // selected index of select elements are cleared on a clone operation.
- if (isIE && dom.select('script,style,select,map').length > 0) {
- content = node.innerHTML;
- node = node.cloneNode(false);
- dom.setHTML(node, content);
- } else
- node = node.cloneNode(true);
-
- // Nodes needs to be attached to something in WebKit/Opera
- // Older builds of Opera crashes if you attach the node to an document created dynamically
- // and since we can't feature detect a crash we need to sniff the acutal build number
- // This fix will make DOM ranges and make Sizzle happy!
- impl = node.ownerDocument.implementation;
- if (impl.createHTMLDocument) {
- // Create an empty HTML document
- doc = impl.createHTMLDocument("");
-
- // Add the element or it's children if it's a body element to the new document
- each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) {
- doc.body.appendChild(doc.importNode(node, true));
- });
-
- // Grab first child or body element for serialization
- if (node.nodeName != 'BODY')
- node = doc.body.firstChild;
- else
- node = doc.body;
-
- // set the new document in DOMUtils so createElement etc works
- oldDoc = dom.doc;
- dom.doc = doc;
- }
-
- args = args || {};
- args.format = args.format || 'html';
-
- // Pre process
- if (!args.no_events) {
- args.node = node;
- onPreProcess.dispatch(self, args);
- }
-
- // Setup serializer
- htmlSerializer = new tinymce.html.Serializer(settings, schema);
-
- // Parse and serialize HTML
- args.content = htmlSerializer.serialize(
- htmlParser.parse(tinymce.trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)), args)
- );
-
- // Replace all BOM characters for now until we can find a better solution
- if (!args.cleanup)
- args.content = args.content.replace(/\uFEFF/g, '');
-
- // Post process
- if (!args.no_events)
- onPostProcess.dispatch(self, args);
-
- // Restore the old document if it was changed
- if (oldDoc)
- dom.doc = oldDoc;
-
- args.node = null;
-
- return args.content;
- },
-
- addRules : function(rules) {
- schema.addValidElements(rules);
- },
-
- setRules : function(rules) {
- schema.setValidElements(rules);
- }
- };
- };
-})(tinymce);
-(function(tinymce) {
- tinymce.dom.ScriptLoader = function(settings) {
- var QUEUED = 0,
- LOADING = 1,
- LOADED = 2,
- states = {},
- queue = [],
- scriptLoadedCallbacks = {},
- queueLoadedCallbacks = [],
- loading = 0,
- undef;
-
- function loadScript(url, callback) {
- var t = this, dom = tinymce.DOM, elm, uri, loc, id;
-
- // Execute callback when script is loaded
- function done() {
- dom.remove(id);
-
- if (elm)
- elm.onreadystatechange = elm.onload = elm = null;
-
- callback();
- };
-
- function error() {
- // Report the error so it's easier for people to spot loading errors
- if (typeof(console) !== "undefined" && console.log)
- console.log("Failed to load: " + url);
-
- // We can't mark it as done if there is a load error since
- // A) We don't want to produce 404 errors on the server and
- // B) the onerror event won't fire on all browsers.
- // done();
- };
-
- id = dom.uniqueId();
-
- if (tinymce.isIE6) {
- uri = new tinymce.util.URI(url);
- loc = location;
-
- // If script is from same domain and we
- // use IE 6 then use XHR since it's more reliable
- if (uri.host == loc.hostname && uri.port == loc.port && (uri.protocol + ':') == loc.protocol && uri.protocol.toLowerCase() != 'file') {
- tinymce.util.XHR.send({
- url : tinymce._addVer(uri.getURI()),
- success : function(content) {
- // Create new temp script element
- var script = dom.create('script', {
- type : 'text/javascript'
- });
-
- // Evaluate script in global scope
- script.text = content;
- document.getElementsByTagName('head')[0].appendChild(script);
- dom.remove(script);
-
- done();
- },
-
- error : error
- });
-
- return;
- }
- }
-
- // Create new script element
- elm = document.createElement('script');
- elm.id = id;
- elm.type = 'text/javascript';
- elm.src = tinymce._addVer(url);
-
- // Add onload listener for non IE browsers since IE9
- // fires onload event before the script is parsed and executed
- if (!tinymce.isIE)
- elm.onload = done;
-
- // Add onerror event will get fired on some browsers but not all of them
- elm.onerror = error;
-
- // Opera 9.60 doesn't seem to fire the onreadystate event at correctly
- if (!tinymce.isOpera) {
- elm.onreadystatechange = function() {
- var state = elm.readyState;
-
- // Loaded state is passed on IE 6 however there
- // are known issues with this method but we can't use
- // XHR in a cross domain loading
- if (state == 'complete' || state == 'loaded')
- done();
- };
- }
-
- // Most browsers support this feature so we report errors
- // for those at least to help users track their missing plugins etc
- // todo: Removed since it produced error if the document is unloaded by navigating away, re-add it as an option
- /*elm.onerror = function() {
- alert('Failed to load: ' + url);
- };*/
-
- // Add script to document
- (document.getElementsByTagName('head')[0] || document.body).appendChild(elm);
- };
-
- this.isDone = function(url) {
- return states[url] == LOADED;
- };
-
- this.markDone = function(url) {
- states[url] = LOADED;
- };
-
- this.add = this.load = function(url, callback, scope) {
- var item, state = states[url];
-
- // Add url to load queue
- if (state == undef) {
- queue.push(url);
- states[url] = QUEUED;
- }
-
- if (callback) {
- // Store away callback for later execution
- if (!scriptLoadedCallbacks[url])
- scriptLoadedCallbacks[url] = [];
-
- scriptLoadedCallbacks[url].push({
- func : callback,
- scope : scope || this
- });
- }
- };
-
- this.loadQueue = function(callback, scope) {
- this.loadScripts(queue, callback, scope);
- };
-
- this.loadScripts = function(scripts, callback, scope) {
- var loadScripts;
-
- function execScriptLoadedCallbacks(url) {
- // Execute URL callback functions
- tinymce.each(scriptLoadedCallbacks[url], function(callback) {
- callback.func.call(callback.scope);
- });
-
- scriptLoadedCallbacks[url] = undef;
- };
-
- queueLoadedCallbacks.push({
- func : callback,
- scope : scope || this
- });
-
- loadScripts = function() {
- var loadingScripts = tinymce.grep(scripts);
-
- // Current scripts has been handled
- scripts.length = 0;
-
- // Load scripts that needs to be loaded
- tinymce.each(loadingScripts, function(url) {
- // Script is already loaded then execute script callbacks directly
- if (states[url] == LOADED) {
- execScriptLoadedCallbacks(url);
- return;
- }
-
- // Is script not loading then start loading it
- if (states[url] != LOADING) {
- states[url] = LOADING;
- loading++;
-
- loadScript(url, function() {
- states[url] = LOADED;
- loading--;
-
- execScriptLoadedCallbacks(url);
-
- // Load more scripts if they where added by the recently loaded script
- loadScripts();
- });
- }
- });
-
- // No scripts are currently loading then execute all pending queue loaded callbacks
- if (!loading) {
- tinymce.each(queueLoadedCallbacks, function(callback) {
- callback.func.call(callback.scope);
- });
-
- queueLoadedCallbacks.length = 0;
- }
- };
-
- loadScripts();
- };
- };
-
- // Global script loader
- tinymce.ScriptLoader = new tinymce.dom.ScriptLoader();
-})(tinymce);
-
-(function(tinymce) {
- tinymce.dom.RangeUtils = function(dom) {
- var INVISIBLE_CHAR = '\uFEFF';
-
- this.walk = function(rng, callback) {
- var startContainer = rng.startContainer,
- startOffset = rng.startOffset,
- endContainer = rng.endContainer,
- endOffset = rng.endOffset,
- ancestor, startPoint,
- endPoint, node, parent, siblings, nodes;
-
- // Handle table cell selection the table plugin enables
- // you to fake select table cells and perform formatting actions on them
- nodes = dom.select('td.mceSelected,th.mceSelected');
- if (nodes.length > 0) {
- tinymce.each(nodes, function(node) {
- callback([node]);
- });
-
- return;
- }
-
- function exclude(nodes) {
- var node;
-
- // First node is excluded
- node = nodes[0];
- if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) {
- nodes.splice(0, 1);
- }
-
- // Last node is excluded
- node = nodes[nodes.length - 1];
- if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) {
- nodes.splice(nodes.length - 1, 1);
- }
-
- return nodes;
- };
-
- function collectSiblings(node, name, end_node) {
- var siblings = [];
-
- for (; node && node != end_node; node = node[name])
- siblings.push(node);
-
- return siblings;
- };
-
- function findEndPoint(node, root) {
- do {
- if (node.parentNode == root)
- return node;
-
- node = node.parentNode;
- } while(node);
- };
-
- function walkBoundary(start_node, end_node, next) {
- var siblingName = next ? 'nextSibling' : 'previousSibling';
-
- for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) {
- parent = node.parentNode;
- siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName);
-
- if (siblings.length) {
- if (!next)
- siblings.reverse();
-
- callback(exclude(siblings));
- }
- }
- };
-
- // If index based start position then resolve it
- if (startContainer.nodeType == 1 && startContainer.hasChildNodes())
- startContainer = startContainer.childNodes[startOffset];
-
- // If index based end position then resolve it
- if (endContainer.nodeType == 1 && endContainer.hasChildNodes())
- endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)];
-
- // Same container
- if (startContainer == endContainer)
- return callback(exclude([startContainer]));
-
- // Find common ancestor and end points
- ancestor = dom.findCommonAncestor(startContainer, endContainer);
-
- // Process left side
- for (node = startContainer; node; node = node.parentNode) {
- if (node === endContainer)
- return walkBoundary(startContainer, ancestor, true);
-
- if (node === ancestor)
- break;
- }
-
- // Process right side
- for (node = endContainer; node; node = node.parentNode) {
- if (node === startContainer)
- return walkBoundary(endContainer, ancestor);
-
- if (node === ancestor)
- break;
- }
-
- // Find start/end point
- startPoint = findEndPoint(startContainer, ancestor) || startContainer;
- endPoint = findEndPoint(endContainer, ancestor) || endContainer;
-
- // Walk left leaf
- walkBoundary(startContainer, startPoint, true);
-
- // Walk the middle from start to end point
- siblings = collectSiblings(
- startPoint == startContainer ? startPoint : startPoint.nextSibling,
- 'nextSibling',
- endPoint == endContainer ? endPoint.nextSibling : endPoint
- );
-
- if (siblings.length)
- callback(exclude(siblings));
-
- // Walk right leaf
- walkBoundary(endContainer, endPoint);
- };
-
- this.split = function(rng) {
- var startContainer = rng.startContainer,
- startOffset = rng.startOffset,
- endContainer = rng.endContainer,
- endOffset = rng.endOffset;
-
- function splitText(node, offset) {
- return node.splitText(offset);
- };
-
- // Handle single text node
- if (startContainer == endContainer && startContainer.nodeType == 3) {
- if (startOffset > 0 && startOffset < startContainer.nodeValue.length) {
- endContainer = splitText(startContainer, startOffset);
- startContainer = endContainer.previousSibling;
-
- if (endOffset > startOffset) {
- endOffset = endOffset - startOffset;
- startContainer = endContainer = splitText(endContainer, endOffset).previousSibling;
- endOffset = endContainer.nodeValue.length;
- startOffset = 0;
- } else {
- endOffset = 0;
- }
- }
- } else {
- // Split startContainer text node if needed
- if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) {
- startContainer = splitText(startContainer, startOffset);
- startOffset = 0;
- }
-
- // Split endContainer text node if needed
- if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) {
- endContainer = splitText(endContainer, endOffset).previousSibling;
- endOffset = endContainer.nodeValue.length;
- }
- }
-
- return {
- startContainer : startContainer,
- startOffset : startOffset,
- endContainer : endContainer,
- endOffset : endOffset
- };
- };
-
- };
-
- tinymce.dom.RangeUtils.compareRanges = function(rng1, rng2) {
- if (rng1 && rng2) {
- // Compare native IE ranges
- if (rng1.item || rng1.duplicate) {
- // Both are control ranges and the selected element matches
- if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0))
- return true;
-
- // Both are text ranges and the range matches
- if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1))
- return true;
- } else {
- // Compare w3c ranges
- return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;
- }
- }
-
- return false;
- };
-})(tinymce);
-
-(function(tinymce) {
- var Event = tinymce.dom.Event, each = tinymce.each;
-
- tinymce.create('tinymce.ui.KeyboardNavigation', {
- KeyboardNavigation: function(settings, dom) {
- var t = this, root = settings.root, items = settings.items,
- enableUpDown = settings.enableUpDown, enableLeftRight = settings.enableLeftRight || !settings.enableUpDown,
- excludeFromTabOrder = settings.excludeFromTabOrder,
- itemFocussed, itemBlurred, rootKeydown, rootFocussed, focussedId;
-
- dom = dom || tinymce.DOM;
-
- itemFocussed = function(evt) {
- focussedId = evt.target.id;
- };
-
- itemBlurred = function(evt) {
- dom.setAttrib(evt.target.id, 'tabindex', '-1');
- };
-
- rootFocussed = function(evt) {
- var item = dom.get(focussedId);
- dom.setAttrib(item, 'tabindex', '0');
- item.focus();
- };
-
- t.focus = function() {
- dom.get(focussedId).focus();
- };
-
- t.destroy = function() {
- each(items, function(item) {
- var elm = dom.get(item.id);
-
- dom.unbind(elm, 'focus', itemFocussed);
- dom.unbind(elm, 'blur', itemBlurred);
- });
-
- var rootElm = dom.get(root);
- dom.unbind(rootElm, 'focus', rootFocussed);
- dom.unbind(rootElm, 'keydown', rootKeydown);
-
- items = dom = root = t.focus = itemFocussed = itemBlurred = rootKeydown = rootFocussed = null;
- t.destroy = function() {};
- };
-
- t.moveFocus = function(dir, evt) {
- var idx = -1, controls = t.controls, newFocus;
-
- if (!focussedId)
- return;
-
- each(items, function(item, index) {
- if (item.id === focussedId) {
- idx = index;
- return false;
- }
- });
-
- idx += dir;
- if (idx < 0) {
- idx = items.length - 1;
- } else if (idx >= items.length) {
- idx = 0;
- }
-
- newFocus = items[idx];
- dom.setAttrib(focussedId, 'tabindex', '-1');
- dom.setAttrib(newFocus.id, 'tabindex', '0');
- dom.get(newFocus.id).focus();
-
- if (settings.actOnFocus) {
- settings.onAction(newFocus.id);
- }
-
- if (evt)
- Event.cancel(evt);
- };
-
- rootKeydown = function(evt) {
- var DOM_VK_LEFT = 37, DOM_VK_RIGHT = 39, DOM_VK_UP = 38, DOM_VK_DOWN = 40, DOM_VK_ESCAPE = 27, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_SPACE = 32;
-
- switch (evt.keyCode) {
- case DOM_VK_LEFT:
- if (enableLeftRight) t.moveFocus(-1);
- break;
-
- case DOM_VK_RIGHT:
- if (enableLeftRight) t.moveFocus(1);
- break;
-
- case DOM_VK_UP:
- if (enableUpDown) t.moveFocus(-1);
- break;
-
- case DOM_VK_DOWN:
- if (enableUpDown) t.moveFocus(1);
- break;
-
- case DOM_VK_ESCAPE:
- if (settings.onCancel) {
- settings.onCancel();
- Event.cancel(evt);
- }
- break;
-
- case DOM_VK_ENTER:
- case DOM_VK_RETURN:
- case DOM_VK_SPACE:
- if (settings.onAction) {
- settings.onAction(focussedId);
- Event.cancel(evt);
- }
- break;
- }
- };
-
- // Set up state and listeners for each item.
- each(items, function(item, idx) {
- var tabindex, elm;
-
- if (!item.id) {
- item.id = dom.uniqueId('_mce_item_');
- }
-
- elm = dom.get(item.id);
-
- if (excludeFromTabOrder) {
- dom.bind(elm, 'blur', itemBlurred);
- tabindex = '-1';
- } else {
- tabindex = (idx === 0 ? '0' : '-1');
- }
-
- elm.setAttribute('tabindex', tabindex);
- dom.bind(elm, 'focus', itemFocussed);
- });
-
- // Setup initial state for root element.
- if (items[0]){
- focussedId = items[0].id;
- }
-
- dom.setAttrib(root, 'tabindex', '-1');
-
- // Setup listeners for root element.
- var rootElm = dom.get(root);
- dom.bind(rootElm, 'focus', rootFocussed);
- dom.bind(rootElm, 'keydown', rootKeydown);
- }
- });
-})(tinymce);
-
-(function(tinymce) {
- // Shorten class names
- var DOM = tinymce.DOM, is = tinymce.is;
-
- tinymce.create('tinymce.ui.Control', {
- Control : function(id, s, editor) {
- this.id = id;
- this.settings = s = s || {};
- this.rendered = false;
- this.onRender = new tinymce.util.Dispatcher(this);
- this.classPrefix = '';
- this.scope = s.scope || this;
- this.disabled = 0;
- this.active = 0;
- this.editor = editor;
- },
-
- setAriaProperty : function(property, value) {
- var element = DOM.get(this.id + '_aria') || DOM.get(this.id);
- if (element) {
- DOM.setAttrib(element, 'aria-' + property, !!value);
- }
- },
-
- focus : function() {
- DOM.get(this.id).focus();
- },
-
- setDisabled : function(s) {
- if (s != this.disabled) {
- this.setAriaProperty('disabled', s);
-
- this.setState('Disabled', s);
- this.setState('Enabled', !s);
- this.disabled = s;
- }
- },
-
- isDisabled : function() {
- return this.disabled;
- },
-
- setActive : function(s) {
- if (s != this.active) {
- this.setState('Active', s);
- this.active = s;
- this.setAriaProperty('pressed', s);
- }
- },
-
- isActive : function() {
- return this.active;
- },
-
- setState : function(c, s) {
- var n = DOM.get(this.id);
-
- c = this.classPrefix + c;
-
- if (s)
- DOM.addClass(n, c);
- else
- DOM.removeClass(n, c);
- },
-
- isRendered : function() {
- return this.rendered;
- },
-
- renderHTML : function() {
- },
-
- renderTo : function(n) {
- DOM.setHTML(n, this.renderHTML());
- },
-
- postRender : function() {
- var t = this, b;
-
- // Set pending states
- if (is(t.disabled)) {
- b = t.disabled;
- t.disabled = -1;
- t.setDisabled(b);
- }
-
- if (is(t.active)) {
- b = t.active;
- t.active = -1;
- t.setActive(b);
- }
- },
-
- remove : function() {
- DOM.remove(this.id);
- this.destroy();
- },
-
- destroy : function() {
- tinymce.dom.Event.clear(this.id);
- }
- });
-})(tinymce);
-tinymce.create('tinymce.ui.Container:tinymce.ui.Control', {
- Container : function(id, s, editor) {
- this.parent(id, s, editor);
-
- this.controls = [];
-
- this.lookup = {};
- },
-
- add : function(c) {
- this.lookup[c.id] = c;
- this.controls.push(c);
-
- return c;
- },
-
- get : function(n) {
- return this.lookup[n];
- }
-});
-
-
-tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', {
- Separator : function(id, s) {
- this.parent(id, s);
- this.classPrefix = 'mceSeparator';
- this.setDisabled(true);
- },
-
- renderHTML : function() {
- return tinymce.DOM.createHTML('span', {'class' : this.classPrefix, role : 'separator', 'aria-orientation' : 'vertical', tabindex : '-1'});
- }
-});
-
-(function(tinymce) {
- var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk;
-
- tinymce.create('tinymce.ui.MenuItem:tinymce.ui.Control', {
- MenuItem : function(id, s) {
- this.parent(id, s);
- this.classPrefix = 'mceMenuItem';
- },
-
- setSelected : function(s) {
- this.setState('Selected', s);
- this.setAriaProperty('checked', !!s);
- this.selected = s;
- },
-
- isSelected : function() {
- return this.selected;
- },
-
- postRender : function() {
- var t = this;
-
- t.parent();
-
- // Set pending state
- if (is(t.selected))
- t.setSelected(t.selected);
- }
- });
-})(tinymce);
-
-(function(tinymce) {
- var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk;
-
- tinymce.create('tinymce.ui.Menu:tinymce.ui.MenuItem', {
- Menu : function(id, s) {
- var t = this;
-
- t.parent(id, s);
- t.items = {};
- t.collapsed = false;
- t.menuCount = 0;
- t.onAddItem = new tinymce.util.Dispatcher(this);
- },
-
- expand : function(d) {
- var t = this;
-
- if (d) {
- walk(t, function(o) {
- if (o.expand)
- o.expand();
- }, 'items', t);
- }
-
- t.collapsed = false;
- },
-
- collapse : function(d) {
- var t = this;
-
- if (d) {
- walk(t, function(o) {
- if (o.collapse)
- o.collapse();
- }, 'items', t);
- }
-
- t.collapsed = true;
- },
-
- isCollapsed : function() {
- return this.collapsed;
- },
-
- add : function(o) {
- if (!o.settings)
- o = new tinymce.ui.MenuItem(o.id || DOM.uniqueId(), o);
-
- this.onAddItem.dispatch(this, o);
-
- return this.items[o.id] = o;
- },
-
- addSeparator : function() {
- return this.add({separator : true});
- },
-
- addMenu : function(o) {
- if (!o.collapse)
- o = this.createMenu(o);
-
- this.menuCount++;
-
- return this.add(o);
- },
-
- hasMenus : function() {
- return this.menuCount !== 0;
- },
-
- remove : function(o) {
- delete this.items[o.id];
- },
-
- removeAll : function() {
- var t = this;
-
- walk(t, function(o) {
- if (o.removeAll)
- o.removeAll();
- else
- o.remove();
-
- o.destroy();
- }, 'items', t);
-
- t.items = {};
- },
-
- createMenu : function(o) {
- var m = new tinymce.ui.Menu(o.id || DOM.uniqueId(), o);
-
- m.onAddItem.add(this.onAddItem.dispatch, this.onAddItem);
-
- return m;
- }
- });
-})(tinymce);
-(function(tinymce) {
- var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event, Element = tinymce.dom.Element;
-
- tinymce.create('tinymce.ui.DropMenu:tinymce.ui.Menu', {
- DropMenu : function(id, s) {
- s = s || {};
- s.container = s.container || DOM.doc.body;
- s.offset_x = s.offset_x || 0;
- s.offset_y = s.offset_y || 0;
- s.vp_offset_x = s.vp_offset_x || 0;
- s.vp_offset_y = s.vp_offset_y || 0;
-
- if (is(s.icons) && !s.icons)
- s['class'] += ' mceNoIcons';
-
- this.parent(id, s);
- this.onShowMenu = new tinymce.util.Dispatcher(this);
- this.onHideMenu = new tinymce.util.Dispatcher(this);
- this.classPrefix = 'mceMenu';
- },
-
- createMenu : function(s) {
- var t = this, cs = t.settings, m;
-
- s.container = s.container || cs.container;
- s.parent = t;
- s.constrain = s.constrain || cs.constrain;
- s['class'] = s['class'] || cs['class'];
- s.vp_offset_x = s.vp_offset_x || cs.vp_offset_x;
- s.vp_offset_y = s.vp_offset_y || cs.vp_offset_y;
- s.keyboard_focus = cs.keyboard_focus;
- m = new tinymce.ui.DropMenu(s.id || DOM.uniqueId(), s);
-
- m.onAddItem.add(t.onAddItem.dispatch, t.onAddItem);
-
- return m;
- },
-
- focus : function() {
- var t = this;
- if (t.keyboardNav) {
- t.keyboardNav.focus();
- }
- },
-
- update : function() {
- var t = this, s = t.settings, tb = DOM.get('menu_' + t.id + '_tbl'), co = DOM.get('menu_' + t.id + '_co'), tw, th;
-
- tw = s.max_width ? Math.min(tb.offsetWidth, s.max_width) : tb.offsetWidth;
- th = s.max_height ? Math.min(tb.offsetHeight, s.max_height) : tb.offsetHeight;
-
- if (!DOM.boxModel)
- t.element.setStyles({width : tw + 2, height : th + 2});
- else
- t.element.setStyles({width : tw, height : th});
-
- if (s.max_width)
- DOM.setStyle(co, 'width', tw);
-
- if (s.max_height) {
- DOM.setStyle(co, 'height', th);
-
- if (tb.clientHeight < s.max_height)
- DOM.setStyle(co, 'overflow', 'hidden');
- }
- },
-
- showMenu : function(x, y, px) {
- var t = this, s = t.settings, co, vp = DOM.getViewPort(), w, h, mx, my, ot = 2, dm, tb, cp = t.classPrefix;
-
- t.collapse(1);
-
- if (t.isMenuVisible)
- return;
-
- if (!t.rendered) {
- co = DOM.add(t.settings.container, t.renderNode());
-
- each(t.items, function(o) {
- o.postRender();
- });
-
- t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container});
- } else
- co = DOM.get('menu_' + t.id);
-
- // Move layer out of sight unless it's Opera since it scrolls to top of page due to an bug
- if (!tinymce.isOpera)
- DOM.setStyles(co, {left : -0xFFFF , top : -0xFFFF});
-
- DOM.show(co);
- t.update();
-
- x += s.offset_x || 0;
- y += s.offset_y || 0;
- vp.w -= 4;
- vp.h -= 4;
-
- // Move inside viewport if not submenu
- if (s.constrain) {
- w = co.clientWidth - ot;
- h = co.clientHeight - ot;
- mx = vp.x + vp.w;
- my = vp.y + vp.h;
-
- if ((x + s.vp_offset_x + w) > mx)
- x = px ? px - w : Math.max(0, (mx - s.vp_offset_x) - w);
-
- if ((y + s.vp_offset_y + h) > my)
- y = Math.max(0, (my - s.vp_offset_y) - h);
- }
-
- DOM.setStyles(co, {left : x , top : y});
- t.element.update();
-
- t.isMenuVisible = 1;
- t.mouseClickFunc = Event.add(co, 'click', function(e) {
- var m;
-
- // Added by Zotero
- //
- // Record the modifier keys used with the last menu click -- used by linksmenu plugin
- tinymce.activeEditor.lastClickModifierKeys = {
- altKey: e.altKey,
- ctrlKey: e.ctrlKey,
- metaKey: e.metaKey,
- shiftKey: e.shiftKey
- };
-
- e = e.target;
-
- if (e && (e = DOM.getParent(e, 'tr')) && !DOM.hasClass(e, cp + 'ItemSub')) {
- m = t.items[e.id];
-
- if (m.isDisabled())
- return;
-
- dm = t;
-
- while (dm) {
- if (dm.hideMenu)
- dm.hideMenu();
-
- dm = dm.settings.parent;
- }
-
- if (m.settings.onclick)
- m.settings.onclick(e);
-
- return false; // Cancel to fix onbeforeunload problem
- }
- });
-
- if (t.hasMenus()) {
- t.mouseOverFunc = Event.add(co, 'mouseover', function(e) {
- var m, r, mi;
-
- e = e.target;
- if (e && (e = DOM.getParent(e, 'tr'))) {
- m = t.items[e.id];
-
- if (t.lastMenu)
- t.lastMenu.collapse(1);
-
- if (m.isDisabled())
- return;
-
- if (e && DOM.hasClass(e, cp + 'ItemSub')) {
- //p = DOM.getPos(s.container);
- r = DOM.getRect(e);
- m.showMenu((r.x + r.w - ot), r.y - ot, r.x);
- t.lastMenu = m;
- DOM.addClass(DOM.get(m.id).firstChild, cp + 'ItemActive');
- }
- }
- });
- }
-
- Event.add(co, 'keydown', t._keyHandler, t);
-
- t.onShowMenu.dispatch(t);
-
- if (s.keyboard_focus) {
- t._setupKeyboardNav();
- }
- },
-
- hideMenu : function(c) {
- var t = this, co = DOM.get('menu_' + t.id), e;
-
- if (!t.isMenuVisible)
- return;
-
- if (t.keyboardNav) t.keyboardNav.destroy();
- Event.remove(co, 'mouseover', t.mouseOverFunc);
- Event.remove(co, 'click', t.mouseClickFunc);
- Event.remove(co, 'keydown', t._keyHandler);
- DOM.hide(co);
- t.isMenuVisible = 0;
-
- if (!c)
- t.collapse(1);
-
- if (t.element)
- t.element.hide();
-
- if (e = DOM.get(t.id))
- DOM.removeClass(e.firstChild, t.classPrefix + 'ItemActive');
-
- t.onHideMenu.dispatch(t);
- },
-
- add : function(o) {
- var t = this, co;
-
- o = t.parent(o);
-
- if (t.isRendered && (co = DOM.get('menu_' + t.id)))
- t._add(DOM.select('tbody', co)[0], o);
-
- return o;
- },
-
- collapse : function(d) {
- this.parent(d);
- this.hideMenu(1);
- },
-
- remove : function(o) {
- DOM.remove(o.id);
- this.destroy();
-
- return this.parent(o);
- },
-
- destroy : function() {
- var t = this, co = DOM.get('menu_' + t.id);
-
- if (t.keyboardNav) t.keyboardNav.destroy();
- Event.remove(co, 'mouseover', t.mouseOverFunc);
- Event.remove(DOM.select('a', co), 'focus', t.mouseOverFunc);
- Event.remove(co, 'click', t.mouseClickFunc);
- Event.remove(co, 'keydown', t._keyHandler);
-
- if (t.element)
- t.element.remove();
-
- DOM.remove(co);
- },
-
- renderNode : function() {
- var t = this, s = t.settings, n, tb, co, w;
-
- w = DOM.create('div', {role: 'listbox', id : 'menu_' + t.id, 'class' : s['class'], 'style' : 'position:absolute;left:0;top:0;z-index:200000;outline:0'});
- if (t.settings.parent) {
- DOM.setAttrib(w, 'aria-parent', 'menu_' + t.settings.parent.id);
- }
- co = DOM.add(w, 'div', {role: 'presentation', id : 'menu_' + t.id + '_co', 'class' : t.classPrefix + (s['class'] ? ' ' + s['class'] : '')});
- t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container});
-
- if (s.menu_line)
- DOM.add(co, 'span', {'class' : t.classPrefix + 'Line'});
-
-// n = DOM.add(co, 'div', {id : 'menu_' + t.id + '_co', 'class' : 'mceMenuContainer'});
- n = DOM.add(co, 'table', {role: 'presentation', id : 'menu_' + t.id + '_tbl', border : 0, cellPadding : 0, cellSpacing : 0});
- tb = DOM.add(n, 'tbody');
-
- each(t.items, function(o) {
- t._add(tb, o);
- });
-
- t.rendered = true;
-
- return w;
- },
-
- // Internal functions
- _setupKeyboardNav : function(){
- var contextMenu, menuItems, t=this;
- contextMenu = DOM.get('menu_' + t.id);
- menuItems = DOM.select('a[role=option]', 'menu_' + t.id);
- menuItems.splice(0,0,contextMenu);
- t.keyboardNav = new tinymce.ui.KeyboardNavigation({
- root: 'menu_' + t.id,
- items: menuItems,
- onCancel: function() {
- t.hideMenu();
- },
- enableUpDown: true
- });
- contextMenu.focus();
- },
-
- _keyHandler : function(evt) {
- var t = this, e;
- switch (evt.keyCode) {
- case 37: // Left
- if (t.settings.parent) {
- t.hideMenu();
- t.settings.parent.focus();
- Event.cancel(evt);
- }
- break;
- case 39: // Right
- if (t.mouseOverFunc)
- t.mouseOverFunc(evt);
- break;
- }
- },
-
- _add : function(tb, o) {
- var n, s = o.settings, a, ro, it, cp = this.classPrefix, ic;
-
- if (s.separator) {
- ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'ItemSeparator'});
- DOM.add(ro, 'td', {'class' : cp + 'ItemSeparator'});
-
- if (n = ro.previousSibling)
- DOM.addClass(n, 'mceLast');
-
- return;
- }
-
- n = ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'Item ' + cp + 'ItemEnabled'});
- n = it = DOM.add(n, s.titleItem ? 'th' : 'td');
- n = a = DOM.add(n, 'a', {id: o.id + '_aria', role: s.titleItem ? 'presentation' : 'option', href : 'javascript:;', onclick : "return false;", onmousedown : 'return false;'});
-
- if (s.parent) {
- DOM.setAttrib(a, 'aria-haspopup', 'true');
- DOM.setAttrib(a, 'aria-owns', 'menu_' + o.id);
- }
-
- DOM.addClass(it, s['class']);
-// n = DOM.add(n, 'span', {'class' : 'item'});
-
- ic = DOM.add(n, 'span', {'class' : 'mceIcon' + (s.icon ? ' mce_' + s.icon : '')});
-
- if (s.icon_src)
- DOM.add(ic, 'img', {src : s.icon_src});
-
- n = DOM.add(n, s.element || 'span', {'class' : 'mceText', title : o.settings.title}, o.settings.title);
-
- if (o.settings.style) {
- if (typeof o.settings.style == "function")
- o.settings.style = o.settings.style();
-
- DOM.setAttrib(n, 'style', o.settings.style);
- }
-
- if (tb.childNodes.length == 1)
- DOM.addClass(ro, 'mceFirst');
-
- if ((n = ro.previousSibling) && DOM.hasClass(n, cp + 'ItemSeparator'))
- DOM.addClass(ro, 'mceFirst');
-
- if (o.collapse)
- DOM.addClass(ro, cp + 'ItemSub');
-
- if (n = ro.previousSibling)
- DOM.removeClass(n, 'mceLast');
-
- DOM.addClass(ro, 'mceLast');
- }
- });
-})(tinymce);
-(function(tinymce) {
- var DOM = tinymce.DOM;
-
- tinymce.create('tinymce.ui.Button:tinymce.ui.Control', {
- Button : function(id, s, ed) {
- this.parent(id, s, ed);
- this.classPrefix = 'mceButton';
- },
-
- renderHTML : function() {
- var cp = this.classPrefix, s = this.settings, h, l;
-
- l = DOM.encode(s.label || '');
- h = '<a role="button" id="' + this.id + '" href="javascript:;" class="' + cp + ' ' + cp + 'Enabled ' + s['class'] + (l ? ' ' + cp + 'Labeled' : '') +'" onmousedown="return false;" onclick="return false;" aria-labelledby="' + this.id + '_voice" title="' + DOM.encode(s.title) + '">';
- if (s.image && !(this.editor &&this.editor.forcedHighContrastMode) )
- h += '<span class="mceIcon ' + s['class'] + '"><img class="mceIcon" src="' + s.image + '" alt="' + DOM.encode(s.title) + '" /></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : '');
- else
- h += '<span class="mceIcon ' + s['class'] + '"></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : '');
-
- h += '<span class="mceVoiceLabel mceIconOnly" style="display: none;" id="' + this.id + '_voice">' + s.title + '</span>';
- h += '</a>';
- return h;
- },
-
- postRender : function() {
- var t = this, s = t.settings, imgBookmark;
-
- // In IE a large image that occupies the entire editor area will be deselected when a button is clicked, so
- // need to keep the selection in case the selection is lost
- if (tinymce.isIE && t.editor) {
- tinymce.dom.Event.add(t.id, 'mousedown', function(e) {
- var nodeName = t.editor.selection.getNode().nodeName;
- imgBookmark = nodeName === 'IMG' ? t.editor.selection.getBookmark() : null;
- });
- }
- tinymce.dom.Event.add(t.id, 'click', function(e) {
- if (!t.isDisabled()) {
- // restore the selection in case the selection is lost in IE
- if (tinymce.isIE && t.editor && imgBookmark !== null) {
- t.editor.selection.moveToBookmark(imgBookmark);
- }
- return s.onclick.call(s.scope, e);
- }
- });
- tinymce.dom.Event.add(t.id, 'keyup', function(e) {
- if (!t.isDisabled() && e.keyCode==tinymce.VK.SPACEBAR)
- return s.onclick.call(s.scope, e);
- });
- }
- });
-})(tinymce);
-
-(function(tinymce) {
- var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher, undef;
-
- tinymce.create('tinymce.ui.ListBox:tinymce.ui.Control', {
- ListBox : function(id, s, ed) {
- var t = this;
-
- t.parent(id, s, ed);
-
- t.items = [];
-
- t.onChange = new Dispatcher(t);
-
- t.onPostRender = new Dispatcher(t);
-
- t.onAdd = new Dispatcher(t);
-
- t.onRenderMenu = new tinymce.util.Dispatcher(this);
-
- t.classPrefix = 'mceListBox';
- t.marked = {};
- },
-
- select : function(va) {
- var t = this, fv, f;
-
- t.marked = {};
-
- if (va == undef)
- return t.selectByIndex(-1);
-
- // Is string or number make function selector
- if (va && typeof(va)=="function")
- f = va;
- else {
- f = function(v) {
- return v == va;
- };
- }
-
- // Do we need to do something?
- if (va != t.selectedValue) {
- // Find item
- each(t.items, function(o, i) {
- if (f(o.value)) {
- fv = 1;
- t.selectByIndex(i);
- return false;
- }
- });
-
- if (!fv)
- t.selectByIndex(-1);
- }
- },
-
- selectByIndex : function(idx) {
- var t = this, e, o, label;
-
- t.marked = {};
-
- if (idx != t.selectedIndex) {
- e = DOM.get(t.id + '_text');
- label = DOM.get(t.id + '_voiceDesc');
- o = t.items[idx];
-
- if (o) {
- t.selectedValue = o.value;
- t.selectedIndex = idx;
- DOM.setHTML(e, DOM.encode(o.title));
- DOM.setHTML(label, t.settings.title + " - " + o.title);
- DOM.removeClass(e, 'mceTitle');
- DOM.setAttrib(t.id, 'aria-valuenow', o.title);
- } else {
- DOM.setHTML(e, DOM.encode(t.settings.title));
- DOM.setHTML(label, DOM.encode(t.settings.title));
- DOM.addClass(e, 'mceTitle');
- t.selectedValue = t.selectedIndex = null;
- DOM.setAttrib(t.id, 'aria-valuenow', t.settings.title);
- }
- e = 0;
- }
- },
-
- mark : function(value) {
- this.marked[value] = true;
- },
-
- add : function(n, v, o) {
- var t = this;
-
- o = o || {};
- o = tinymce.extend(o, {
- title : n,
- value : v
- });
-
- t.items.push(o);
- t.onAdd.dispatch(t, o);
- },
-
- getLength : function() {
- return this.items.length;
- },
-
- renderHTML : function() {
- var h = '', t = this, s = t.settings, cp = t.classPrefix;
-
- h = '<span role="listbox" aria-haspopup="true" aria-labelledby="' + t.id +'_voiceDesc" aria-describedby="' + t.id + '_voiceDesc"><table role="presentation" tabindex="0" id="' + t.id + '" cellpadding="0" cellspacing="0" class="' + cp + ' ' + cp + 'Enabled' + (s['class'] ? (' ' + s['class']) : '') + '"><tbody><tr>';
- h += '<td>' + DOM.createHTML('span', {id: t.id + '_voiceDesc', 'class': 'voiceLabel', style:'display:none;'}, t.settings.title);
- h += DOM.createHTML('a', {id : t.id + '_text', tabindex : -1, href : 'javascript:;', 'class' : 'mceText', onclick : "return false;", onmousedown : 'return false;'}, DOM.encode(t.settings.title)) + '</td>';
- h += '<td>' + DOM.createHTML('a', {id : t.id + '_open', tabindex : -1, href : 'javascript:;', 'class' : 'mceOpen', onclick : "return false;", onmousedown : 'return false;'}, '<span><span style="display:none;" class="mceIconOnly" aria-hidden="true">\u25BC</span></span>') + '</td>';
- h += '</tr></tbody></table></span>';
-
- return h;
- },
-
- showMenu : function() {
- var t = this, p2, e = DOM.get(this.id), m;
-
- if (t.isDisabled() || t.items.length === 0)
- return;
-
- if (t.menu && t.menu.isMenuVisible)
- return t.hideMenu();
-
- if (!t.isMenuRendered) {
- t.renderMenu();
- t.isMenuRendered = true;
- }
-
- p2 = DOM.getPos(e);
-
- m = t.menu;
- m.settings.offset_x = p2.x;
- m.settings.offset_y = p2.y;
- m.settings.keyboard_focus = !tinymce.isOpera; // Opera is buggy when it comes to auto focus
-
- // Select in menu
- each(t.items, function(o) {
- if (m.items[o.id]) {
- m.items[o.id].setSelected(0);
- }
- });
-
- each(t.items, function(o) {
- if (m.items[o.id] && t.marked[o.value]) {
- m.items[o.id].setSelected(1);
- }
-
- if (o.value === t.selectedValue) {
- m.items[o.id].setSelected(1);
- }
- });
-
- m.showMenu(0, e.clientHeight);
-
- Event.add(DOM.doc, 'mousedown', t.hideMenu, t);
- DOM.addClass(t.id, t.classPrefix + 'Selected');
-
- //DOM.get(t.id + '_text').focus();
- },
-
- hideMenu : function(e) {
- var t = this;
-
- if (t.menu && t.menu.isMenuVisible) {
- DOM.removeClass(t.id, t.classPrefix + 'Selected');
-
- // Prevent double toogles by canceling the mouse click event to the button
- if (e && e.type == "mousedown" && (e.target.id == t.id + '_text' || e.target.id == t.id + '_open'))
- return;
-
- if (!e || !DOM.getParent(e.target, '.mceMenu')) {
- DOM.removeClass(t.id, t.classPrefix + 'Selected');
- Event.remove(DOM.doc, 'mousedown', t.hideMenu, t);
- t.menu.hideMenu();
- }
- }
- },
-
- renderMenu : function() {
- var t = this, m;
-
- m = t.settings.control_manager.createDropMenu(t.id + '_menu', {
- menu_line : 1,
- 'class' : t.classPrefix + 'Menu mceNoIcons',
- max_width : 250,
- max_height : 150
- });
-
- m.onHideMenu.add(function() {
- t.hideMenu();
- t.focus();
- });
-
- m.add({
- title : t.settings.title,
- 'class' : 'mceMenuItemTitle',
- onclick : function() {
- if (t.settings.onselect('') !== false)
- t.select(''); // Must be runned after
- }
- });
-
- each(t.items, function(o) {
- // No value then treat it as a title
- if (o.value === undef) {
- m.add({
- title : o.title,
- role : "option",
- 'class' : 'mceMenuItemTitle',
- onclick : function() {
- if (t.settings.onselect('') !== false)
- t.select(''); // Must be runned after
- }
- });
- } else {
- o.id = DOM.uniqueId();
- o.role= "option";
- o.onclick = function() {
- if (t.settings.onselect(o.value) !== false)
- t.select(o.value); // Must be runned after
- };
-
- m.add(o);
- }
- });
-
- t.onRenderMenu.dispatch(t, m);
- t.menu = m;
- },
-
- postRender : function() {
- var t = this, cp = t.classPrefix;
-
- Event.add(t.id, 'click', t.showMenu, t);
- Event.add(t.id, 'keydown', function(evt) {
- if (evt.keyCode == 32) { // Space
- t.showMenu(evt);
- Event.cancel(evt);
- }
- });
- Event.add(t.id, 'focus', function() {
- if (!t._focused) {
- t.keyDownHandler = Event.add(t.id, 'keydown', function(e) {
- if (e.keyCode == 40) {
- t.showMenu();
- Event.cancel(e);
- }
- });
- t.keyPressHandler = Event.add(t.id, 'keypress', function(e) {
- var v;
- if (e.keyCode == 13) {
- // Fake select on enter
- v = t.selectedValue;
- t.selectedValue = null; // Needs to be null to fake change
- Event.cancel(e);
- t.settings.onselect(v);
- }
- });
- }
-
- t._focused = 1;
- });
- Event.add(t.id, 'blur', function() {
- Event.remove(t.id, 'keydown', t.keyDownHandler);
- Event.remove(t.id, 'keypress', t.keyPressHandler);
- t._focused = 0;
- });
-
- // Old IE doesn't have hover on all elements
- if (tinymce.isIE6 || !DOM.boxModel) {
- Event.add(t.id, 'mouseover', function() {
- if (!DOM.hasClass(t.id, cp + 'Disabled'))
- DOM.addClass(t.id, cp + 'Hover');
- });
-
- Event.add(t.id, 'mouseout', function() {
- if (!DOM.hasClass(t.id, cp + 'Disabled'))
- DOM.removeClass(t.id, cp + 'Hover');
- });
- }
-
- t.onPostRender.dispatch(t, DOM.get(t.id));
- },
-
- destroy : function() {
- this.parent();
-
- Event.clear(this.id + '_text');
- Event.clear(this.id + '_open');
- }
- });
-})(tinymce);
-
-(function(tinymce) {
- var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher, undef;
-
- tinymce.create('tinymce.ui.NativeListBox:tinymce.ui.ListBox', {
- NativeListBox : function(id, s) {
- this.parent(id, s);
- this.classPrefix = 'mceNativeListBox';
- },
-
- setDisabled : function(s) {
- DOM.get(this.id).disabled = s;
- this.setAriaProperty('disabled', s);
- },
-
- isDisabled : function() {
- return DOM.get(this.id).disabled;
- },
-
- select : function(va) {
- var t = this, fv, f;
-
- if (va == undef)
- return t.selectByIndex(-1);
-
- // Is string or number make function selector
- if (va && typeof(va)=="function")
- f = va;
- else {
- f = function(v) {
- return v == va;
- };
- }
-
- // Do we need to do something?
- if (va != t.selectedValue) {
- // Find item
- each(t.items, function(o, i) {
- if (f(o.value)) {
- fv = 1;
- t.selectByIndex(i);
- return false;
- }
- });
-
- if (!fv)
- t.selectByIndex(-1);
- }
- },
-
- selectByIndex : function(idx) {
- DOM.get(this.id).selectedIndex = idx + 1;
- this.selectedValue = this.items[idx] ? this.items[idx].value : null;
- },
-
- add : function(n, v, a) {
- var o, t = this;
-
- a = a || {};
- a.value = v;
-
- if (t.isRendered())
- DOM.add(DOM.get(this.id), 'option', a, n);
-
- o = {
- title : n,
- value : v,
- attribs : a
- };
-
- t.items.push(o);
- t.onAdd.dispatch(t, o);
- },
-
- getLength : function() {
- return this.items.length;
- },
-
- renderHTML : function() {
- var h, t = this;
-
- h = DOM.createHTML('option', {value : ''}, '-- ' + t.settings.title + ' --');
-
- each(t.items, function(it) {
- h += DOM.createHTML('option', {value : it.value}, it.title);
- });
-
- h = DOM.createHTML('select', {id : t.id, 'class' : 'mceNativeListBox', 'aria-labelledby': t.id + '_aria'}, h);
- h += DOM.createHTML('span', {id : t.id + '_aria', 'style': 'display: none'}, t.settings.title);
- return h;
- },
-
- postRender : function() {
- var t = this, ch, changeListenerAdded = true;
-
- t.rendered = true;
-
- function onChange(e) {
- var v = t.items[e.target.selectedIndex - 1];
-
- if (v && (v = v.value)) {
- t.onChange.dispatch(t, v);
-
- if (t.settings.onselect)
- t.settings.onselect(v);
- }
- };
-
- Event.add(t.id, 'change', onChange);
-
- // Accessibility keyhandler
- Event.add(t.id, 'keydown', function(e) {
- var bf;
-
- Event.remove(t.id, 'change', ch);
- changeListenerAdded = false;
-
- bf = Event.add(t.id, 'blur', function() {
- if (changeListenerAdded) return;
- changeListenerAdded = true;
- Event.add(t.id, 'change', onChange);
- Event.remove(t.id, 'blur', bf);
- });
-
- //prevent default left and right keys on chrome - so that the keyboard navigation is used.
- if (tinymce.isWebKit && (e.keyCode==37 ||e.keyCode==39)) {
- return Event.prevent(e);
- }
-
- if (e.keyCode == 13 || e.keyCode == 32) {
- onChange(e);
- return Event.cancel(e);
- }
- });
-
- t.onPostRender.dispatch(t, DOM.get(t.id));
- }
- });
-})(tinymce);
-
-(function(tinymce) {
- var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each;
-
- tinymce.create('tinymce.ui.MenuButton:tinymce.ui.Button', {
- MenuButton : function(id, s, ed) {
- this.parent(id, s, ed);
-
- this.onRenderMenu = new tinymce.util.Dispatcher(this);
-
- s.menu_container = s.menu_container || DOM.doc.body;
- },
-
- showMenu : function() {
- var t = this, p1, p2, e = DOM.get(t.id), m;
-
- if (t.isDisabled())
- return;
-
- if (!t.isMenuRendered) {
- t.renderMenu();
- t.isMenuRendered = true;
- }
-
- if (t.isMenuVisible)
- return t.hideMenu();
-
- p1 = DOM.getPos(t.settings.menu_container);
- p2 = DOM.getPos(e);
-
- m = t.menu;
- m.settings.offset_x = p2.x;
- m.settings.offset_y = p2.y;
- m.settings.vp_offset_x = p2.x;
- m.settings.vp_offset_y = p2.y;
- m.settings.keyboard_focus = t._focused;
- m.showMenu(0, e.firstChild.clientHeight);
-
- Event.add(DOM.doc, 'mousedown', t.hideMenu, t);
- t.setState('Selected', 1);
-
- t.isMenuVisible = 1;
- },
-
- renderMenu : function() {
- var t = this, m;
-
- m = t.settings.control_manager.createDropMenu(t.id + '_menu', {
- menu_line : 1,
- 'class' : this.classPrefix + 'Menu',
- icons : t.settings.icons
- });
-
- m.onHideMenu.add(function() {
- t.hideMenu();
- t.focus();
- });
-
- t.onRenderMenu.dispatch(t, m);
- t.menu = m;
- },
-
- hideMenu : function(e) {
- var t = this;
-
- // Prevent double toogles by canceling the mouse click event to the button
- if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id || e.id === t.id + '_open';}))
- return;
-
- if (!e || !DOM.getParent(e.target, '.mceMenu')) {
- t.setState('Selected', 0);
- Event.remove(DOM.doc, 'mousedown', t.hideMenu, t);
- if (t.menu)
- t.menu.hideMenu();
- }
-
- t.isMenuVisible = 0;
- },
-
- postRender : function() {
- var t = this, s = t.settings;
-
- Event.add(t.id, 'click', function() {
- if (!t.isDisabled()) {
- if (s.onclick)
- s.onclick(t.value);
-
- t.showMenu();
- }
- });
- }
- });
-})(tinymce);
-
-(function(tinymce) {
- var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each;
-
- tinymce.create('tinymce.ui.SplitButton:tinymce.ui.MenuButton', {
- SplitButton : function(id, s, ed) {
- this.parent(id, s, ed);
- this.classPrefix = 'mceSplitButton';
- },
-
- renderHTML : function() {
- var h, t = this, s = t.settings, h1;
-
- h = '<tbody><tr>';
-
- if (s.image)
- h1 = DOM.createHTML('img ', {src : s.image, role: 'presentation', 'class' : 'mceAction ' + s['class']});
- else
- h1 = DOM.createHTML('span', {'class' : 'mceAction ' + s['class']}, '');
-
- h1 += DOM.createHTML('span', {'class': 'mceVoiceLabel mceIconOnly', id: t.id + '_voice', style: 'display:none;'}, s.title);
- h += '<td >' + DOM.createHTML('a', {role: 'button', id : t.id + '_action', tabindex: '-1', href : 'javascript:;', 'class' : 'mceAction ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + '</td>';
-
- h1 = DOM.createHTML('span', {'class' : 'mceOpen ' + s['class']}, '<span style="display:none;" class="mceIconOnly" aria-hidden="true">\u25BC</span>');
- h += '<td >' + DOM.createHTML('a', {role: 'button', id : t.id + '_open', tabindex: '-1', href : 'javascript:;', 'class' : 'mceOpen ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + '</td>';
-
- h += '</tr></tbody>';
- h = DOM.createHTML('table', { role: 'presentation', 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', title : s.title}, h);
- return DOM.createHTML('div', {id : t.id, role: 'button', tabindex: '0', 'aria-labelledby': t.id + '_voice', 'aria-haspopup': 'true'}, h);
- },
-
- postRender : function() {
- var t = this, s = t.settings, activate;
-
- if (s.onclick) {
- activate = function(evt) {
- if (!t.isDisabled()) {
- s.onclick(t.value);
- Event.cancel(evt);
- }
- };
- Event.add(t.id + '_action', 'click', activate);
- Event.add(t.id, ['click', 'keydown'], function(evt) {
- var DOM_VK_SPACE = 32, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_UP = 38, DOM_VK_DOWN = 40;
- if ((evt.keyCode === 32 || evt.keyCode === 13 || evt.keyCode === 14) && !evt.altKey && !evt.ctrlKey && !evt.metaKey) {
- activate();
- Event.cancel(evt);
- } else if (evt.type === 'click' || evt.keyCode === DOM_VK_DOWN) {
- t.showMenu();
- Event.cancel(evt);
- }
- });
- }
-
- Event.add(t.id + '_open', 'click', function (evt) {
- t.showMenu();
- Event.cancel(evt);
- });
- Event.add([t.id, t.id + '_open'], 'focus', function() {t._focused = 1;});
- Event.add([t.id, t.id + '_open'], 'blur', function() {t._focused = 0;});
-
- // Old IE doesn't have hover on all elements
- if (tinymce.isIE6 || !DOM.boxModel) {
- Event.add(t.id, 'mouseover', function() {
- if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled'))
- DOM.addClass(t.id, 'mceSplitButtonHover');
- });
-
- Event.add(t.id, 'mouseout', function() {
- if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled'))
- DOM.removeClass(t.id, 'mceSplitButtonHover');
- });
- }
- },
-
- destroy : function() {
- this.parent();
-
- Event.clear(this.id + '_action');
- Event.clear(this.id + '_open');
- Event.clear(this.id);
- }
- });
-})(tinymce);
-
-(function(tinymce) {
- var DOM = tinymce.DOM, Event = tinymce.dom.Event, is = tinymce.is, each = tinymce.each;
-
- tinymce.create('tinymce.ui.ColorSplitButton:tinymce.ui.SplitButton', {
- ColorSplitButton : function(id, s, ed) {
- var t = this;
-
- t.parent(id, s, ed);
-
- t.settings = s = tinymce.extend({
- colors : '000000,993300,333300,003300,003366,000080,333399,333333,800000,FF6600,808000,008000,008080,0000FF,666699,808080,FF0000,FF9900,99CC00,339966,33CCCC,3366FF,800080,999999,FF00FF,FFCC00,FFFF00,00FF00,00FFFF,00CCFF,993366,C0C0C0,FF99CC,FFCC99,FFFF99,CCFFCC,CCFFFF,99CCFF,CC99FF,FFFFFF',
- grid_width : 8,
- default_color : '#888888'
- }, t.settings);
-
- t.onShowMenu = new tinymce.util.Dispatcher(t);
-
- t.onHideMenu = new tinymce.util.Dispatcher(t);
-
- t.value = s.default_color;
- },
-
- showMenu : function() {
- var t = this, r, p, e, p2;
-
- if (t.isDisabled())
- return;
-
- if (!t.isMenuRendered) {
- t.renderMenu();
- t.isMenuRendered = true;
- }
-
- if (t.isMenuVisible)
- return t.hideMenu();
-
- e = DOM.get(t.id);
- DOM.show(t.id + '_menu');
- DOM.addClass(e, 'mceSplitButtonSelected');
- p2 = DOM.getPos(e);
- DOM.setStyles(t.id + '_menu', {
- left : p2.x,
- top : p2.y + e.firstChild.clientHeight,
- zIndex : 200000
- });
- e = 0;
-
- Event.add(DOM.doc, 'mousedown', t.hideMenu, t);
- t.onShowMenu.dispatch(t);
-
- if (t._focused) {
- t._keyHandler = Event.add(t.id + '_menu', 'keydown', function(e) {
- if (e.keyCode == 27)
- t.hideMenu();
- });
-
- DOM.select('a', t.id + '_menu')[0].focus(); // Select first link
- }
-
- t.keyboardNav = new tinymce.ui.KeyboardNavigation({
- root: t.id + '_menu',
- items: DOM.select('a', t.id + '_menu'),
- onCancel: function() {
- t.hideMenu();
- t.focus();
- }
- });
-
- t.keyboardNav.focus();
- t.isMenuVisible = 1;
- },
-
- hideMenu : function(e) {
- var t = this;
-
- if (t.isMenuVisible) {
- // Prevent double toogles by canceling the mouse click event to the button
- if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id + '_open';}))
- return;
-
- if (!e || !DOM.getParent(e.target, '.mceSplitButtonMenu')) {
- DOM.removeClass(t.id, 'mceSplitButtonSelected');
- Event.remove(DOM.doc, 'mousedown', t.hideMenu, t);
- Event.remove(t.id + '_menu', 'keydown', t._keyHandler);
- DOM.hide(t.id + '_menu');
- }
-
- t.isMenuVisible = 0;
- t.onHideMenu.dispatch();
- t.keyboardNav.destroy();
- }
- },
-
- renderMenu : function() {
- var t = this, m, i = 0, s = t.settings, n, tb, tr, w, context;
-
- w = DOM.add(s.menu_container, 'div', {role: 'listbox', id : t.id + '_menu', 'class' : s.menu_class + ' ' + s['class'], style : 'position:absolute;left:0;top:-1000px;'});
- m = DOM.add(w, 'div', {'class' : s['class'] + ' mceSplitButtonMenu'});
- DOM.add(m, 'span', {'class' : 'mceMenuLine'});
-
- n = DOM.add(m, 'table', {role: 'presentation', 'class' : 'mceColorSplitMenu'});
- tb = DOM.add(n, 'tbody');
-
- // Generate color grid
- i = 0;
- each(is(s.colors, 'array') ? s.colors : s.colors.split(','), function(c) {
- c = c.replace(/^#/, '');
-
- if (!i--) {
- tr = DOM.add(tb, 'tr');
- i = s.grid_width - 1;
- }
-
- n = DOM.add(tr, 'td');
- var settings = {
- href : 'javascript:;',
- style : {
- backgroundColor : '#' + c
- },
- 'title': t.editor.getLang('colors.' + c, c),
- 'data-mce-color' : '#' + c
- };
-
- // adding a proper ARIA role = button causes JAWS to read things incorrectly on IE.
- if (!tinymce.isIE ) {
- settings.role = 'option';
- }
-
- n = DOM.add(n, 'a', settings);
-
- if (t.editor.forcedHighContrastMode) {
- n = DOM.add(n, 'canvas', { width: 16, height: 16, 'aria-hidden': 'true' });
- if (n.getContext && (context = n.getContext("2d"))) {
- context.fillStyle = '#' + c;
- context.fillRect(0, 0, 16, 16);
- } else {
- // No point leaving a canvas element around if it's not supported for drawing on anyway.
- DOM.remove(n);
- }
- }
- });
-
- if (s.more_colors_func) {
- n = DOM.add(tb, 'tr');
- n = DOM.add(n, 'td', {colspan : s.grid_width, 'class' : 'mceMoreColors'});
- n = DOM.add(n, 'a', {role: 'option', id : t.id + '_more', href : 'javascript:;', onclick : 'return false;', 'class' : 'mceMoreColors'}, s.more_colors_title);
-
- Event.add(n, 'click', function(e) {
- s.more_colors_func.call(s.more_colors_scope || this);
- return Event.cancel(e); // Cancel to fix onbeforeunload problem
- });
- }
-
- DOM.addClass(m, 'mceColorSplitMenu');
-
- // Prevent IE from scrolling and hindering click to occur #4019
- Event.add(t.id + '_menu', 'mousedown', function(e) {return Event.cancel(e);});
-
- Event.add(t.id + '_menu', 'click', function(e) {
- var c;
-
- e = DOM.getParent(e.target, 'a', tb);
-
- if (e && e.nodeName.toLowerCase() == 'a' && (c = e.getAttribute('data-mce-color')))
- t.setColor(c);
-
- return false; // Prevent IE auto save warning
- });
-
- return w;
- },
-
- setColor : function(c) {
- this.displayColor(c);
- this.hideMenu();
- this.settings.onselect(c);
- },
-
- displayColor : function(c) {
- var t = this;
-
- DOM.setStyle(t.id + '_preview', 'backgroundColor', c);
-
- t.value = c;
- },
-
- postRender : function() {
- var t = this, id = t.id;
-
- t.parent();
- DOM.add(id + '_action', 'div', {id : id + '_preview', 'class' : 'mceColorPreview'});
- DOM.setStyle(t.id + '_preview', 'backgroundColor', t.value);
- },
-
- destroy : function() {
- var self = this;
-
- self.parent();
-
- Event.clear(self.id + '_menu');
- Event.clear(self.id + '_more');
- DOM.remove(self.id + '_menu');
-
- if (self.keyboardNav) {
- self.keyboardNav.destroy();
- }
- }
- });
-})(tinymce);
-
-(function(tinymce) {
-// Shorten class names
-var dom = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event;
-tinymce.create('tinymce.ui.ToolbarGroup:tinymce.ui.Container', {
- renderHTML : function() {
- var t = this, h = [], controls = t.controls, each = tinymce.each, settings = t.settings;
-
- h.push('<div id="' + t.id + '" role="group" aria-labelledby="' + t.id + '_voice">');
- //TODO: ACC test this out - adding a role = application for getting the landmarks working well.
- h.push("<span role='application'>");
- h.push('<span id="' + t.id + '_voice" class="mceVoiceLabel" style="display:none;">' + dom.encode(settings.name) + '</span>');
- each(controls, function(toolbar) {
- h.push(toolbar.renderHTML());
- });
- h.push("</span>");
- h.push('</div>');
-
- return h.join('');
- },
-
- focus : function() {
- var t = this;
- dom.get(t.id).focus();
- },
-
- postRender : function() {
- var t = this, items = [];
-
- each(t.controls, function(toolbar) {
- each (toolbar.controls, function(control) {
- if (control.id) {
- items.push(control);
- }
- });
- });
-
- t.keyNav = new tinymce.ui.KeyboardNavigation({
- root: t.id,
- items: items,
- onCancel: function() {
- //Move focus if webkit so that navigation back will read the item.
- if (tinymce.isWebKit) {
- dom.get(t.editor.id+"_ifr").focus();
- }
- t.editor.focus();
- },
- excludeFromTabOrder: !t.settings.tab_focus_toolbar
- });
- },
-
- destroy : function() {
- var self = this;
-
- self.parent();
- self.keyNav.destroy();
- Event.clear(self.id);
- }
-});
-})(tinymce);
-
-(function(tinymce) {
-// Shorten class names
-var dom = tinymce.DOM, each = tinymce.each;
-tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', {
- renderHTML : function() {
- var t = this, h = '', c, co, s = t.settings, i, pr, nx, cl;
-
- cl = t.controls;
- for (i=0; i<cl.length; i++) {
- // Get current control, prev control, next control and if the control is a list box or not
- co = cl[i];
- pr = cl[i - 1];
- nx = cl[i + 1];
-
- // Add toolbar start
- if (i === 0) {
- c = 'mceToolbarStart';
-
- if (co.Button)
- c += ' mceToolbarStartButton';
- else if (co.SplitButton)
- c += ' mceToolbarStartSplitButton';
- else if (co.ListBox)
- c += ' mceToolbarStartListBox';
-
- h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '<!-- IE -->'));
- }
-
- // Add toolbar end before list box and after the previous button
- // This is to fix the o2k7 editor skins
- if (pr && co.ListBox) {
- if (pr.Button || pr.SplitButton)
- h += dom.createHTML('td', {'class' : 'mceToolbarEnd'}, dom.createHTML('span', null, '<!-- IE -->'));
- }
-
- // Render control HTML
-
- // IE 8 quick fix, needed to propertly generate a hit area for anchors
- if (dom.stdMode)
- h += '<td style="position: relative">' + co.renderHTML() + '</td>';
- else
- h += '<td>' + co.renderHTML() + '</td>';
-
- // Add toolbar start after list box and before the next button
- // This is to fix the o2k7 editor skins
- if (nx && co.ListBox) {
- if (nx.Button || nx.SplitButton)
- h += dom.createHTML('td', {'class' : 'mceToolbarStart'}, dom.createHTML('span', null, '<!-- IE -->'));
- }
- }
-
- c = 'mceToolbarEnd';
-
- if (co.Button)
- c += ' mceToolbarEndButton';
- else if (co.SplitButton)
- c += ' mceToolbarEndSplitButton';
- else if (co.ListBox)
- c += ' mceToolbarEndListBox';
-
- h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '<!-- IE -->'));
-
- return dom.createHTML('table', {id : t.id, 'class' : 'mceToolbar' + (s['class'] ? ' ' + s['class'] : ''), cellpadding : '0', cellspacing : '0', align : t.settings.align || '', role: 'presentation', tabindex: '-1'}, '<tbody><tr>' + h + '</tr></tbody>');
- }
-});
-})(tinymce);
-
-(function(tinymce) {
- var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each;
-
- tinymce.create('tinymce.AddOnManager', {
- AddOnManager : function() {
- var self = this;
-
- self.items = [];
- self.urls = {};
- self.lookup = {};
- self.onAdd = new Dispatcher(self);
- },
-
- get : function(n) {
- if (this.lookup[n]) {
- return this.lookup[n].instance;
- } else {
- return undefined;
- }
- },
-
- dependencies : function(n) {
- var result;
- if (this.lookup[n]) {
- result = this.lookup[n].dependencies;
- }
- return result || [];
- },
-
- requireLangPack : function(n) {
- var s = tinymce.settings;
-
- if (s && s.language && s.language_load !== false)
- tinymce.ScriptLoader.add(this.urls[n] + '/langs/' + s.language + '.js');
- },
-
- add : function(id, o, dependencies) {
- this.items.push(o);
- this.lookup[id] = {instance:o, dependencies:dependencies};
- this.onAdd.dispatch(this, id, o);
-
- return o;
- },
- createUrl: function(baseUrl, dep) {
- if (typeof dep === "object") {
- return dep
- } else {
- return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix};
- }
- },
-
- addComponents: function(pluginName, scripts) {
- var pluginUrl = this.urls[pluginName];
- tinymce.each(scripts, function(script){
- tinymce.ScriptLoader.add(pluginUrl+"/"+script);
- });
- },
-
- load : function(n, u, cb, s) {
- var t = this, url = u;
-
- function loadDependencies() {
- var dependencies = t.dependencies(n);
- tinymce.each(dependencies, function(dep) {
- var newUrl = t.createUrl(u, dep);
- t.load(newUrl.resource, newUrl, undefined, undefined);
- });
- if (cb) {
- if (s) {
- cb.call(s);
- } else {
- cb.call(tinymce.ScriptLoader);
- }
- }
- }
-
- if (t.urls[n])
- return;
- if (typeof u === "object")
- url = u.prefix + u.resource + u.suffix;
-
- if (url.indexOf('/') !== 0 && url.indexOf('://') == -1)
- url = tinymce.baseURL + '/' + url;
-
- t.urls[n] = url.substring(0, url.lastIndexOf('/'));
-
- if (t.lookup[n]) {
- loadDependencies();
- } else {
- tinymce.ScriptLoader.add(url, loadDependencies, s);
- }
- }
- });
-
- // Create plugin and theme managers
- tinymce.PluginManager = new tinymce.AddOnManager();
- tinymce.ThemeManager = new tinymce.AddOnManager();
-}(tinymce));
-
-(function(tinymce) {
- // Shorten names
- var each = tinymce.each, extend = tinymce.extend,
- DOM = tinymce.DOM, Event = tinymce.dom.Event,
- ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager,
- explode = tinymce.explode,
- Dispatcher = tinymce.util.Dispatcher, undef, instanceCounter = 0;
-
- // Setup some URLs where the editor API is located and where the document is
- tinymce.documentBaseURL = window.location.href.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
- if (!/[\/\\]$/.test(tinymce.documentBaseURL))
- tinymce.documentBaseURL += '/';
-
- tinymce.baseURL = new tinymce.util.URI(tinymce.documentBaseURL).toAbsolute(tinymce.baseURL);
-
- tinymce.baseURI = new tinymce.util.URI(tinymce.baseURL);
-
- // Add before unload listener
- // This was required since IE was leaking memory if you added and removed beforeunload listeners
- // with attachEvent/detatchEvent so this only adds one listener and instances can the attach to the onBeforeUnload event
- tinymce.onBeforeUnload = new Dispatcher(tinymce);
-
- // Must be on window or IE will leak if the editor is placed in frame or iframe
- Event.add(window, 'beforeunload', function(e) {
- tinymce.onBeforeUnload.dispatch(tinymce, e);
- });
-
- tinymce.onAddEditor = new Dispatcher(tinymce);
-
- tinymce.onRemoveEditor = new Dispatcher(tinymce);
-
- tinymce.EditorManager = extend(tinymce, {
- editors : [],
-
- i18n : {},
-
- activeEditor : null,
-
- init : function(s) {
- var t = this, pl, sl = tinymce.ScriptLoader, e, el = [], ed;
-
- function createId(elm) {
- var id = elm.id;
-
- // Use element id, or unique name or generate a unique id
- if (!id) {
- id = elm.name;
-
- if (id && !DOM.get(id)) {
- id = elm.name;
- } else {
- // Generate unique name
- id = DOM.uniqueId();
- }
-
- elm.setAttribute('id', id);
- }
-
- return id;
- };
-
- function execCallback(se, n, s) {
- var f = se[n];
-
- if (!f)
- return;
-
- if (tinymce.is(f, 'string')) {
- s = f.replace(/\.\w+$/, '');
- s = s ? tinymce.resolve(s) : 0;
- f = tinymce.resolve(f);
- }
-
- return f.apply(s || this, Array.prototype.slice.call(arguments, 2));
- };
-
- function hasClass(n, c) {
- return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c);
- };
-
- t.settings = s;
-
- // Legacy call
- Event.bind(window, 'ready', function() {
- var l, co;
-
- execCallback(s, 'onpageload');
-
- switch (s.mode) {
- case "exact":
- l = s.elements || '';
-
- if(l.length > 0) {
- each(explode(l), function(v) {
- if (DOM.get(v)) {
- ed = new tinymce.Editor(v, s);
- el.push(ed);
- ed.render(1);
- } else {
- each(document.forms, function(f) {
- each(f.elements, function(e) {
- if (e.name === v) {
- v = 'mce_editor_' + instanceCounter++;
- DOM.setAttrib(e, 'id', v);
-
- ed = new tinymce.Editor(v, s);
- el.push(ed);
- ed.render(1);
- }
- });
- });
- }
- });
- }
- break;
-
- case "textareas":
- case "specific_textareas":
- each(DOM.select('textarea'), function(elm) {
- if (s.editor_deselector && hasClass(elm, s.editor_deselector))
- return;
-
- if (!s.editor_selector || hasClass(elm, s.editor_selector)) {
- ed = new tinymce.Editor(createId(elm), s);
- el.push(ed);
- ed.render(1);
- }
- });
- break;
-
- default:
- if (s.types) {
- // Process type specific selector
- each(s.types, function(type) {
- each(DOM.select(type.selector), function(elm) {
- var editor = new tinymce.Editor(createId(elm), tinymce.extend({}, s, type));
- el.push(editor);
- editor.render(1);
- });
- });
- } else if (s.selector) {
- // Process global selector
- each(DOM.select(s.selector), function(elm) {
- var editor = new tinymce.Editor(createId(elm), s);
- el.push(editor);
- editor.render(1);
- });
- }
- }
-
- // Call onInit when all editors are initialized
- if (s.oninit) {
- l = co = 0;
-
- each(el, function(ed) {
- co++;
-
- if (!ed.initialized) {
- // Wait for it
- ed.onInit.add(function() {
- l++;
-
- // All done
- if (l == co)
- execCallback(s, 'oninit');
- });
- } else
- l++;
-
- // All done
- if (l == co)
- execCallback(s, 'oninit');
- });
- }
- });
- },
-
- get : function(id) {
- if (id === undef)
- return this.editors;
-
- if (!this.editors.hasOwnProperty(id))
- return undef;
-
- return this.editors[id];
- },
-
- getInstanceById : function(id) {
- return this.get(id);
- },
-
- add : function(editor) {
- var self = this, editors = self.editors;
-
- // Add named and index editor instance
- editors[editor.id] = editor;
- editors.push(editor);
-
- self._setActive(editor);
- self.onAddEditor.dispatch(self, editor);
-
-
- return editor;
- },
-
- remove : function(editor) {
- var t = this, i, editors = t.editors;
-
- // Not in the collection
- if (!editors[editor.id])
- return null;
-
- delete editors[editor.id];
-
- for (i = 0; i < editors.length; i++) {
- if (editors[i] == editor) {
- editors.splice(i, 1);
- break;
- }
- }
-
- // Select another editor since the active one was removed
- if (t.activeEditor == editor)
- t._setActive(editors[0]);
-
- editor.destroy();
- t.onRemoveEditor.dispatch(t, editor);
-
- return editor;
- },
-
- execCommand : function(c, u, v) {
- var t = this, ed = t.get(v), w;
-
- function clr() {
- ed.destroy();
- w.detachEvent('onunload', clr);
- w = w.tinyMCE = w.tinymce = null; // IE leak
- };
-
- // Manager commands
- switch (c) {
- case "mceFocus":
- ed.focus();
- return true;
-
- case "mceAddEditor":
- case "mceAddControl":
- if (!t.get(v))
- new tinymce.Editor(v, t.settings).render();
-
- return true;
-
- case "mceAddFrameControl":
- w = v.window;
-
- // Add tinyMCE global instance and tinymce namespace to specified window
- w.tinyMCE = tinyMCE;
- w.tinymce = tinymce;
-
- tinymce.DOM.doc = w.document;
- tinymce.DOM.win = w;
-
- ed = new tinymce.Editor(v.element_id, v);
- ed.render();
-
- // Fix IE memory leaks
- if (tinymce.isIE) {
- w.attachEvent('onunload', clr);
- }
-
- v.page_window = null;
-
- return true;
-
- case "mceRemoveEditor":
- case "mceRemoveControl":
- if (ed)
- ed.remove();
-
- return true;
-
- case 'mceToggleEditor':
- if (!ed) {
- t.execCommand('mceAddControl', 0, v);
- return true;
- }
-
- if (ed.isHidden())
- ed.show();
- else
- ed.hide();
-
- return true;
- }
-
- // Run command on active editor
- if (t.activeEditor)
- return t.activeEditor.execCommand(c, u, v);
-
- return false;
- },
-
- execInstanceCommand : function(id, c, u, v) {
- var ed = this.get(id);
-
- if (ed)
- return ed.execCommand(c, u, v);
-
- return false;
- },
-
- triggerSave : function() {
- each(this.editors, function(e) {
- e.save();
- });
- },
-
- addI18n : function(p, o) {
- var lo, i18n = this.i18n;
-
- if (!tinymce.is(p, 'string')) {
- each(p, function(o, lc) {
- each(o, function(o, g) {
- each(o, function(o, k) {
- if (g === 'common')
- i18n[lc + '.' + k] = o;
- else
- i18n[lc + '.' + g + '.' + k] = o;
- });
- });
- });
- } else {
- each(o, function(o, k) {
- i18n[p + '.' + k] = o;
- });
- }
- },
-
- // Private methods
-
- _setActive : function(editor) {
- this.selectedInstance = this.activeEditor = editor;
- }
- });
-})(tinymce);
-
-(function(tinymce) {
- // Shorten these names
- var DOM = tinymce.DOM, Event = tinymce.dom.Event, extend = tinymce.extend,
- each = tinymce.each, isGecko = tinymce.isGecko,
- isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, is = tinymce.is,
- ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager,
- explode = tinymce.explode;
-
- tinymce.create('tinymce.Editor', {
- Editor : function(id, settings) {
- var self = this, TRUE = true;
-
- self.settings = settings = extend({
- id : id,
- language : 'en',
- theme : 'advanced',
- skin : 'default',
- delta_width : 0,
- delta_height : 0,
- popup_css : '',
- plugins : '',
- document_base_url : tinymce.documentBaseURL,
- add_form_submit_trigger : TRUE,
- submit_patch : TRUE,
- add_unload_trigger : TRUE,
- convert_urls : TRUE,
- relative_urls : TRUE,
- remove_script_host : TRUE,
- table_inline_editing : false,
- object_resizing : TRUE,
- accessibility_focus : TRUE,
- doctype : tinymce.isIE6 ? '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">' : '<!DOCTYPE>', // Use old doctype on IE 6 to avoid horizontal scroll
- visual : TRUE,
- font_size_style_values : 'xx-small,x-small,small,medium,large,x-large,xx-large',
- font_size_legacy_values : 'xx-small,small,medium,large,x-large,xx-large,300%', // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size
- apply_source_formatting : TRUE,
- directionality : 'ltr',
- forced_root_block : 'p',
- hidden_input : TRUE,
- padd_empty_editor : TRUE,
- render_ui : TRUE,
- indentation : '30px',
- fix_table_elements : TRUE,
- inline_styles : TRUE,
- convert_fonts_to_spans : TRUE,
- indent : 'simple',
- indent_before : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist',
- indent_after : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist',
- validate : TRUE,
- entity_encoding : 'named',
- url_converter : self.convertURL,
- url_converter_scope : self,
- ie7_compat : TRUE
- }, settings);
-
- self.id = self.editorId = id;
-
- self.isNotDirty = false;
-
- self.plugins = {};
-
- self.documentBaseURI = new tinymce.util.URI(settings.document_base_url || tinymce.documentBaseURL, {
- base_uri : tinyMCE.baseURI
- });
-
- self.baseURI = tinymce.baseURI;
-
- self.contentCSS = [];
-
- self.contentStyles = [];
-
- // Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic
- self.setupEvents();
-
- // Internal command handler objects
- self.execCommands = {};
- self.queryStateCommands = {};
- self.queryValueCommands = {};
-
- // Call setup
- self.execCallback('setup', self);
- },
-
- render : function(nst) {
- var t = this, s = t.settings, id = t.id, sl = tinymce.ScriptLoader;
-
- // Page is not loaded yet, wait for it
- if (!Event.domLoaded) {
- Event.add(window, 'ready', function() {
- t.render();
- });
- return;
- }
-
- tinyMCE.settings = s;
-
- // Element not found, then skip initialization
- if (!t.getElement())
- return;
-
- // Is a iPad/iPhone and not on iOS5, then skip initialization. We need to sniff
- // here since the browser says it has contentEditable support but there is no visible caret.
- if (tinymce.isIDevice && !tinymce.isIOS5)
- return;
-
- // Add hidden input for non input elements inside form elements
- if (!/TEXTAREA|INPUT/i.test(t.getElement().nodeName) && s.hidden_input && DOM.getParent(id, 'form'))
- DOM.insertAfter(DOM.create('input', {type : 'hidden', name : id}), id);
-
- // Hide target element early to prevent content flashing
- if (!s.content_editable) {
- t.orgVisibility = t.getElement().style.visibility;
- t.getElement().style.visibility = 'hidden';
- }
-
- if (tinymce.WindowManager)
- t.windowManager = new tinymce.WindowManager(t);
-
- if (s.encoding == 'xml') {
- t.onGetContent.add(function(ed, o) {
- if (o.save)
- o.content = DOM.encode(o.content);
- });
- }
-
- if (s.add_form_submit_trigger) {
- t.onSubmit.addToTop(function() {
- if (t.initialized) {
- t.save();
- t.isNotDirty = 1;
- }
- });
- }
-
- if (s.add_unload_trigger) {
- t._beforeUnload = tinyMCE.onBeforeUnload.add(function() {
- if (t.initialized && !t.destroyed && !t.isHidden())
- t.save({format : 'raw', no_events : true});
- });
- }
-
- tinymce.addUnload(t.destroy, t);
-
- if (s.submit_patch) {
- t.onBeforeRenderUI.add(function() {
- var n = t.getElement().form;
-
- if (!n)
- return;
-
- // Already patched
- if (n._mceOldSubmit)
- return;
-
- // Check page uses id="submit" or name="submit" for it's submit button
- if (!n.submit.nodeType && !n.submit.length) {
- t.formElement = n;
- n._mceOldSubmit = n.submit;
- n.submit = function() {
- // Save all instances
- tinymce.triggerSave();
- t.isNotDirty = 1;
-
- return t.formElement._mceOldSubmit(t.formElement);
- };
- }
-
- n = null;
- });
- }
-
- // Load scripts
- function loadScripts() {
- if (s.language && s.language_load !== false)
- sl.add(tinymce.baseURL + '/langs/' + s.language + '.js');
-
- if (s.theme && typeof s.theme != "function" && s.theme.charAt(0) != '-' && !ThemeManager.urls[s.theme])
- ThemeManager.load(s.theme, 'themes/' + s.theme + '/editor_template' + tinymce.suffix + '.js');
-
- each(explode(s.plugins), function(p) {
- if (p &&!PluginManager.urls[p]) {
- if (p.charAt(0) == '-') {
- p = p.substr(1, p.length);
- var dependencies = PluginManager.dependencies(p);
- each(dependencies, function(dep) {
- var defaultSettings = {prefix:'plugins/', resource: dep, suffix:'/editor_plugin' + tinymce.suffix + '.js'};
- dep = PluginManager.createUrl(defaultSettings, dep);
- PluginManager.load(dep.resource, dep);
- });
- } else {
- // Skip safari plugin, since it is removed as of 3.3b1
- if (p == 'safari') {
- return;
- }
- PluginManager.load(p, {prefix:'plugins/', resource: p, suffix:'/editor_plugin' + tinymce.suffix + '.js'});
- }
- }
- });
-
- // Init when que is loaded
- sl.loadQueue(function() {
- if (!t.removed)
- t.init();
- });
- };
-
- loadScripts();
- },
-
- init : function() {
- var n, t = this, s = t.settings, w, h, mh, e = t.getElement(), o, ti, u, bi, bc, re, i, initializedPlugins = [];
-
- tinymce.add(t);
-
- s.aria_label = s.aria_label || DOM.getAttrib(e, 'aria-label', t.getLang('aria.rich_text_area'));
-
- if (s.theme) {
- if (typeof s.theme != "function") {
- s.theme = s.theme.replace(/-/, '');
- o = ThemeManager.get(s.theme);
- t.theme = new o();
-
- if (t.theme.init)
- t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, ''));
- } else {
- t.theme = s.theme;
- }
- }
-
- function initPlugin(p) {
- var c = PluginManager.get(p), u = PluginManager.urls[p] || tinymce.documentBaseURL.replace(/\/$/, ''), po;
- if (c && tinymce.inArray(initializedPlugins,p) === -1) {
- each(PluginManager.dependencies(p), function(dep){
- initPlugin(dep);
- });
- po = new c(t, u);
-
- t.plugins[p] = po;
-
- if (po.init) {
- po.init(t, u);
- initializedPlugins.push(p);
- }
- }
- }
-
- // Create all plugins
- each(explode(s.plugins.replace(/\-/g, '')), initPlugin);
-
- // Setup popup CSS path(s)
- if (s.popup_css !== false) {
- if (s.popup_css)
- s.popup_css = t.documentBaseURI.toAbsolute(s.popup_css);
- else
- s.popup_css = t.baseURI.toAbsolute("themes/" + s.theme + "/skins/" + s.skin + "/dialog.css");
- }
-
- if (s.popup_css_add)
- s.popup_css += ',' + t.documentBaseURI.toAbsolute(s.popup_css_add);
-
- t.controlManager = new tinymce.ControlManager(t);
-
- // Enables users to override the control factory
- t.onBeforeRenderUI.dispatch(t, t.controlManager);
-
- // Measure box
- if (s.render_ui && t.theme) {
- t.orgDisplay = e.style.display;
-
- if (typeof s.theme != "function") {
- w = s.width || e.style.width || e.offsetWidth;
- h = s.height || e.style.height || e.offsetHeight;
- mh = s.min_height || 100;
- re = /^[0-9\.]+(|px)$/i;
-
- if (re.test('' + w))
- w = Math.max(parseInt(w, 10) + (o.deltaWidth || 0), 100);
-
- if (re.test('' + h))
- h = Math.max(parseInt(h, 10) + (o.deltaHeight || 0), mh);
-
- // Render UI
- o = t.theme.renderUI({
- targetNode : e,
- width : w,
- height : h,
- deltaWidth : s.delta_width,
- deltaHeight : s.delta_height
- });
-
- // Resize editor
- DOM.setStyles(o.sizeContainer || o.editorContainer, {
- width : w,
- height : h
- });
-
- h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : '');
- if (h < mh)
- h = mh;
- } else {
- o = s.theme(t, e);
-
- // Convert element type to id:s
- if (o.editorContainer.nodeType) {
- o.editorContainer = o.editorContainer.id = o.editorContainer.id || t.id + "_parent";
- }
-
- // Convert element type to id:s
- if (o.iframeContainer.nodeType) {
- o.iframeContainer = o.iframeContainer.id = o.iframeContainer.id || t.id + "_iframecontainer";
- }
-
- // Use specified iframe height or the targets offsetHeight
- h = o.iframeHeight || e.offsetHeight;
-
- // Store away the selection when it's changed to it can be restored later with a editor.focus() call
- if (isIE) {
- t.onInit.add(function(ed) {
- ed.dom.bind(ed.getBody(), 'beforedeactivate keydown', function() {
- ed.lastIERng = ed.selection.getRng();
- });
- });
- }
- }
-
- t.editorContainer = o.editorContainer;
- }
-
- // Load specified content CSS last
- if (s.content_css) {
- each(explode(s.content_css), function(u) {
- t.contentCSS.push(t.documentBaseURI.toAbsolute(u));
- });
- }
-
- // Load specified content CSS last
- if (s.content_style) {
- t.contentStyles.push(s.content_style);
- }
-
- // Content editable mode ends here
- if (s.content_editable) {
- e = n = o = null; // Fix IE leak
- return t.initContentBody();
- }
-
- // User specified a document.domain value
- if (document.domain && location.hostname != document.domain)
- tinymce.relaxedDomain = document.domain;
-
- t.iframeHTML = s.doctype + '<html><head xmlns="http://www.w3.org/1999/xhtml">';
-
- // We only need to override paths if we have to
- // IE has a bug where it remove site absolute urls to relative ones if this is specified
- if (s.document_base_url != tinymce.documentBaseURL)
- t.iframeHTML += '<base href="' + t.documentBaseURI.getURI() + '" />';
-
- // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode.
- if (s.ie7_compat)
- t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />';
- else
- t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=edge" />';
-
- t.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />';
-
- // Load the CSS by injecting them into the HTML this will reduce "flicker"
- for (i = 0; i < t.contentCSS.length; i++) {
- t.iframeHTML += '<link type="text/css" rel="stylesheet" href="' + t.contentCSS[i] + '" />';
- }
-
- t.contentCSS = [];
-
- bi = s.body_id || 'tinymce';
- if (bi.indexOf('=') != -1) {
- bi = t.getParam('body_id', '', 'hash');
- bi = bi[t.id] || bi;
- }
-
- bc = s.body_class || '';
- if (bc.indexOf('=') != -1) {
- bc = t.getParam('body_class', '', 'hash');
- bc = bc[t.id] || '';
- }
-
- t.iframeHTML += '</head><body id="' + bi + '" class="mceContentBody ' + bc + '" onload="window.parent.tinyMCE.get(\'' + t.id + '\').onLoad.dispatch();"><br></body></html>';
-
- // Domain relaxing enabled, then set document domain
- if (tinymce.relaxedDomain && (isIE || (tinymce.isOpera && parseFloat(opera.version()) < 11))) {
- // We need to write the contents here in IE since multiple writes messes up refresh button and back button
- u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";var ed = window.parent.tinyMCE.get("' + t.id + '");document.write(ed.iframeHTML);document.close();ed.initContentBody();})()';
- }
-
- // Create iframe
- // TODO: ACC add the appropriate description on this.
- n = DOM.add(o.iframeContainer, 'iframe', {
- id : t.id + "_ifr",
- src : u || 'javascript:""', // Workaround for HTTPS warning in IE6/7
- frameBorder : '0',
- allowTransparency : "true",
- title : s.aria_label,
- style : {
- width : '100%',
- height : h,
- display : 'block' // Important for Gecko to render the iframe correctly
- }
- });
-
- t.contentAreaContainer = o.iframeContainer;
-
- if (o.editorContainer) {
- DOM.get(o.editorContainer).style.display = t.orgDisplay;
- }
-
- // Restore visibility on target element
- e.style.visibility = t.orgVisibility;
-
- DOM.get(t.id).style.display = 'none';
- DOM.setAttrib(t.id, 'aria-hidden', true);
-
- if (!tinymce.relaxedDomain || !u)
- t.initContentBody();
-
- e = n = o = null; // Cleanup
- },
-
- initContentBody : function() {
- var self = this, settings = self.settings, targetElm = DOM.get(self.id), doc = self.getDoc(), html, body, contentCssText;
-
- // Setup iframe body
- if ((!isIE || !tinymce.relaxedDomain) && !settings.content_editable) {
- doc.open();
- doc.write(self.iframeHTML);
- doc.close();
-
- if (tinymce.relaxedDomain)
- doc.domain = tinymce.relaxedDomain;
- }
-
- if (settings.content_editable) {
- DOM.addClass(targetElm, 'mceContentBody');
- self.contentDocument = doc = settings.content_document || document;
- self.contentWindow = settings.content_window || window;
- self.bodyElement = targetElm;
-
- // Prevent leak in IE
- settings.content_document = settings.content_window = null;
- }
-
- // It will not steal focus while setting contentEditable
- body = self.getBody();
- body.disabled = true;
-
- if (!settings.readonly)
- body.contentEditable = self.getParam('content_editable_state', true);
-
- body.disabled = false;
-
- self.schema = new tinymce.html.Schema(settings);
-
- self.dom = new tinymce.dom.DOMUtils(doc, {
- keep_values : true,
- url_converter : self.convertURL,
- url_converter_scope : self,
- hex_colors : settings.force_hex_style_colors,
- class_filter : settings.class_filter,
- update_styles : true,
- root_element : settings.content_editable ? self.id : null,
- schema : self.schema
- });
-
- self.parser = new tinymce.html.DomParser(settings, self.schema);
-
- // Convert src and href into data-mce-src, data-mce-href and data-mce-style
- self.parser.addAttributeFilter('src,href,style', function(nodes, name) {
- var i = nodes.length, node, dom = self.dom, value, internalName;
-
- while (i--) {
- node = nodes[i];
- value = node.attr(name);
- internalName = 'data-mce-' + name;
-
- // Add internal attribute if we need to we don't on a refresh of the document
- if (!node.attributes.map[internalName]) {
- if (name === "style")
- node.attr(internalName, dom.serializeStyle(dom.parseStyle(value), node.name));
- else
- node.attr(internalName, self.convertURL(value, name, node.name));
- }
- }
- });
-
- // Keep scripts from executing
- self.parser.addNodeFilter('script', function(nodes, name) {
- var i = nodes.length, node;
-
- while (i--) {
- node = nodes[i];
- node.attr('type', 'mce-' + (node.attr('type') || 'text/javascript'));
- }
- });
-
- self.parser.addNodeFilter('#cdata', function(nodes, name) {
- var i = nodes.length, node;
-
- while (i--) {
- node = nodes[i];
- node.type = 8;
- node.name = '#comment';
- node.value = '[CDATA[' + node.value + ']]';
- }
- });
-
- self.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes, name) {
- var i = nodes.length, node, nonEmptyElements = self.schema.getNonEmptyElements();
-
- while (i--) {
- node = nodes[i];
-
- if (node.isEmpty(nonEmptyElements))
- node.empty().append(new tinymce.html.Node('br', 1)).shortEnded = true;
- }
- });
-
- self.serializer = new tinymce.dom.Serializer(settings, self.dom, self.schema);
-
- self.selection = new tinymce.dom.Selection(self.dom, self.getWin(), self.serializer, self);
-
- self.formatter = new tinymce.Formatter(self);
-
- self.undoManager = new tinymce.UndoManager(self);
-
- self.forceBlocks = new tinymce.ForceBlocks(self);
- self.enterKey = new tinymce.EnterKey(self);
- self.editorCommands = new tinymce.EditorCommands(self);
-
- self.onExecCommand.add(function(editor, command) {
- // Don't refresh the select lists until caret move
- if (!/^(FontName|FontSize)$/.test(command))
- self.nodeChanged();
- });
-
- // Pass through
- self.serializer.onPreProcess.add(function(se, o) {
- return self.onPreProcess.dispatch(self, o, se);
- });
-
- self.serializer.onPostProcess.add(function(se, o) {
- return self.onPostProcess.dispatch(self, o, se);
- });
-
- self.onPreInit.dispatch(self);
-
- if (!settings.browser_spellcheck && !settings.gecko_spellcheck)
- doc.body.spellcheck = false;
-
- if (!settings.readonly) {
- self.bindNativeEvents();
- }
-
- self.controlManager.onPostRender.dispatch(self, self.controlManager);
- self.onPostRender.dispatch(self);
-
- self.quirks = tinymce.util.Quirks(self);
-
- if (settings.directionality)
- body.dir = settings.directionality;
-
- if (settings.nowrap)
- body.style.whiteSpace = "nowrap";
-
- if (settings.protect) {
- self.onBeforeSetContent.add(function(ed, o) {
- each(settings.protect, function(pattern) {
- o.content = o.content.replace(pattern, function(str) {
- return '<!--mce:protected ' + escape(str) + '-->';
- });
- });
- });
- }
-
- // Add visual aids when new contents is added
- self.onSetContent.add(function() {
- self.addVisual(self.getBody());
- });
-
- // Remove empty contents
- if (settings.padd_empty_editor) {
- self.onPostProcess.add(function(ed, o) {
- o.content = o.content.replace(/^(<p[^>]*>( | |\s|\u00a0|)<\/p>[\r\n]*|<br \/>[\r\n]*)$/, '');
- });
- }
-
- self.load({initial : true, format : 'html'});
- self.startContent = self.getContent({format : 'raw'});
-
- self.initialized = true;
-
- self.onInit.dispatch(self);
- self.execCallback('setupcontent_callback', self.id, body, doc);
- self.execCallback('init_instance_callback', self);
- self.focus(true);
- self.nodeChanged({initial : true});
-
- // Add editor specific CSS styles
- if (self.contentStyles.length > 0) {
- contentCssText = '';
-
- each(self.contentStyles, function(style) {
- contentCssText += style + "\r\n";
- });
-
- self.dom.addStyle(contentCssText);
- }
-
- // Load specified content CSS last
- each(self.contentCSS, function(url) {
- self.dom.loadCSS(url);
- });
-
- // Handle auto focus
- if (settings.auto_focus) {
- setTimeout(function () {
- var ed = tinymce.get(settings.auto_focus);
-
- ed.selection.select(ed.getBody(), 1);
- ed.selection.collapse(1);
- ed.getBody().focus();
- ed.getWin().focus();
- }, 100);
- }
-
- // Clean up references for IE
- targetElm = doc = body = null;
- },
-
- focus : function(skip_focus) {
- var oed, self = this, selection = self.selection, contentEditable = self.settings.content_editable, ieRng, controlElm, doc = self.getDoc(), body;
-
- if (!skip_focus) {
- if (self.lastIERng) {
- selection.setRng(self.lastIERng);
- }
-
- // Get selected control element
- ieRng = selection.getRng();
- if (ieRng.item) {
- controlElm = ieRng.item(0);
- }
-
- self._refreshContentEditable();
-
- // Focus the window iframe
- if (!contentEditable) {
- self.getWin().focus();
- }
-
- // Focus the body as well since it's contentEditable
- if (tinymce.isGecko || contentEditable) {
- body = self.getBody();
-
- // Check for setActive since it doesn't scroll to the element
- if (body.setActive) {
- body.setActive();
- } else {
- body.focus();
- }
-
- if (contentEditable) {
- selection.normalize();
- }
- }
-
- // Restore selected control element
- // This is needed when for example an image is selected within a
- // layer a call to focus will then remove the control selection
- if (controlElm && controlElm.ownerDocument == doc) {
- ieRng = doc.body.createControlRange();
- ieRng.addElement(controlElm);
- ieRng.select();
- }
- }
-
- if (tinymce.activeEditor != self) {
- if ((oed = tinymce.activeEditor) != null)
- oed.onDeactivate.dispatch(oed, self);
-
- self.onActivate.dispatch(self, oed);
- }
-
- tinymce._setActive(self);
- },
-
- execCallback : function(n) {
- var t = this, f = t.settings[n], s;
-
- if (!f)
- return;
-
- // Look through lookup
- if (t.callbackLookup && (s = t.callbackLookup[n])) {
- f = s.func;
- s = s.scope;
- }
-
- if (is(f, 'string')) {
- s = f.replace(/\.\w+$/, '');
- s = s ? tinymce.resolve(s) : 0;
- f = tinymce.resolve(f);
- t.callbackLookup = t.callbackLookup || {};
- t.callbackLookup[n] = {func : f, scope : s};
- }
-
- return f.apply(s || t, Array.prototype.slice.call(arguments, 1));
- },
-
- translate : function(s) {
- var c = this.settings.language || 'en', i18n = tinymce.i18n;
-
- if (!s)
- return '';
-
- return i18n[c + '.' + s] || s.replace(/\{\#([^\}]+)\}/g, function(a, b) {
- return i18n[c + '.' + b] || '{#' + b + '}';
- });
- },
-
- getLang : function(n, dv) {
- return tinymce.i18n[(this.settings.language || 'en') + '.' + n] || (is(dv) ? dv : '{#' + n + '}');
- },
-
- getParam : function(n, dv, ty) {
- var tr = tinymce.trim, v = is(this.settings[n]) ? this.settings[n] : dv, o;
-
- if (ty === 'hash') {
- o = {};
-
- if (is(v, 'string')) {
- each(v.indexOf('=') > 0 ? v.split(/[;,](?![^=;,]*(?:[;,]|$))/) : v.split(','), function(v) {
- v = v.split('=');
-
- if (v.length > 1)
- o[tr(v[0])] = tr(v[1]);
- else
- o[tr(v[0])] = tr(v);
- });
- } else
- o = v;
-
- return o;
- }
-
- return v;
- },
-
- nodeChanged : function(o) {
- var self = this, selection = self.selection, node;
-
- // Fix for bug #1896577 it seems that this can not be fired while the editor is loading
- if (self.initialized) {
- o = o || {};
-
- // Get start node
- node = selection.getStart() || self.getBody();
- node = isIE && node.ownerDocument != self.getDoc() ? self.getBody() : node; // Fix for IE initial state
-
- // Get parents and add them to object
- o.parents = [];
- self.dom.getParent(node, function(node) {
- if (node.nodeName == 'BODY')
- return true;
-
- o.parents.push(node);
- });
-
- self.onNodeChange.dispatch(
- self,
- o ? o.controlManager || self.controlManager : self.controlManager,
- node,
- selection.isCollapsed(),
- o
- );
- }
- },
-
- addButton : function(name, settings) {
- var self = this;
-
- self.buttons = self.buttons || {};
- self.buttons[name] = settings;
- },
-
- addCommand : function(name, callback, scope) {
- this.execCommands[name] = {func : callback, scope : scope || this};
- },
-
- addQueryStateHandler : function(name, callback, scope) {
- this.queryStateCommands[name] = {func : callback, scope : scope || this};
- },
-
- addQueryValueHandler : function(name, callback, scope) {
- this.queryValueCommands[name] = {func : callback, scope : scope || this};
- },
-
- addShortcut : function(pa, desc, cmd_func, sc) {
- var t = this, c;
-
- if (t.settings.custom_shortcuts === false)
- return false;
-
- t.shortcuts = t.shortcuts || {};
-
- if (is(cmd_func, 'string')) {
- c = cmd_func;
-
- cmd_func = function() {
- t.execCommand(c, false, null);
- };
- }
-
- if (is(cmd_func, 'object')) {
- c = cmd_func;
-
- cmd_func = function() {
- t.execCommand(c[0], c[1], c[2]);
- };
- }
-
- each(explode(pa), function(pa) {
- var o = {
- func : cmd_func,
- scope : sc || this,
- desc : t.translate(desc),
- alt : false,
- ctrl : false,
- shift : false
- };
-
- each(explode(pa, '+'), function(v) {
- switch (v) {
- case 'alt':
- case 'ctrl':
- case 'shift':
- o[v] = true;
- break;
-
- default:
- o.charCode = v.charCodeAt(0);
- o.keyCode = v.toUpperCase().charCodeAt(0);
- }
- });
-
- t.shortcuts[(o.ctrl ? 'ctrl' : '') + ',' + (o.alt ? 'alt' : '') + ',' + (o.shift ? 'shift' : '') + ',' + o.keyCode] = o;
- });
-
- return true;
- },
-
- execCommand : function(cmd, ui, val, a) {
- var t = this, s = 0, o, st;
-
- if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint|SelectAll)$/.test(cmd) && (!a || !a.skip_focus))
- t.focus();
-
- a = extend({}, a);
- t.onBeforeExecCommand.dispatch(t, cmd, ui, val, a);
- if (a.terminate)
- return false;
-
- // Command callback
- if (t.execCallback('execcommand_callback', t.id, t.selection.getNode(), cmd, ui, val)) {
- t.onExecCommand.dispatch(t, cmd, ui, val, a);
- return true;
- }
-
- // Registred commands
- if (o = t.execCommands[cmd]) {
- st = o.func.call(o.scope, ui, val);
-
- // Fall through on true
- if (st !== true) {
- t.onExecCommand.dispatch(t, cmd, ui, val, a);
- return st;
- }
- }
-
- // Plugin commands
- each(t.plugins, function(p) {
- if (p.execCommand && p.execCommand(cmd, ui, val)) {
- t.onExecCommand.dispatch(t, cmd, ui, val, a);
- s = 1;
- return false;
- }
- });
-
- if (s)
- return true;
-
- // Theme commands
- if (t.theme && t.theme.execCommand && t.theme.execCommand(cmd, ui, val)) {
- t.onExecCommand.dispatch(t, cmd, ui, val, a);
- return true;
- }
-
- // Editor commands
- if (t.editorCommands.execCommand(cmd, ui, val)) {
- t.onExecCommand.dispatch(t, cmd, ui, val, a);
- return true;
- }
-
- // Browser commands
- t.getDoc().execCommand(cmd, ui, val);
- t.onExecCommand.dispatch(t, cmd, ui, val, a);
- },
-
- queryCommandState : function(cmd) {
- var t = this, o, s;
-
- // Is hidden then return undefined
- if (t._isHidden())
- return;
-
- // Registred commands
- if (o = t.queryStateCommands[cmd]) {
- s = o.func.call(o.scope);
-
- // Fall though on true
- if (s !== true)
- return s;
- }
-
- // Registred commands
- o = t.editorCommands.queryCommandState(cmd);
- if (o !== -1)
- return o;
-
- // Browser commands
- try {
- return this.getDoc().queryCommandState(cmd);
- } catch (ex) {
- // Fails sometimes see bug: 1896577
- }
- },
-
- queryCommandValue : function(c) {
- var t = this, o, s;
-
- // Is hidden then return undefined
- if (t._isHidden())
- return;
-
- // Registred commands
- if (o = t.queryValueCommands[c]) {
- s = o.func.call(o.scope);
-
- // Fall though on true
- if (s !== true)
- return s;
- }
-
- // Registred commands
- o = t.editorCommands.queryCommandValue(c);
- if (is(o))
- return o;
-
- // Browser commands
- try {
- return this.getDoc().queryCommandValue(c);
- } catch (ex) {
- // Fails sometimes see bug: 1896577
- }
- },
-
- show : function() {
- var self = this;
-
- DOM.show(self.getContainer());
- DOM.hide(self.id);
- self.load();
- },
-
- hide : function() {
- var self = this, doc = self.getDoc();
-
- // Fixed bug where IE has a blinking cursor left from the editor
- if (isIE && doc)
- doc.execCommand('SelectAll');
-
- // We must save before we hide so Safari doesn't crash
- self.save();
-
- // defer the call to hide to prevent an IE9 crash #4921
- setTimeout(function() {
- DOM.hide(self.getContainer());
- }, 1);
- DOM.setStyle(self.id, 'display', self.orgDisplay);
- },
-
- isHidden : function() {
- return !DOM.isHidden(this.id);
- },
-
- setProgressState : function(b, ti, o) {
- this.onSetProgressState.dispatch(this, b, ti, o);
-
- return b;
- },
-
- load : function(o) {
- var t = this, e = t.getElement(), h;
-
- if (e) {
- o = o || {};
- o.load = true;
-
- // Double encode existing entities in the value
- h = t.setContent(is(e.value) ? e.value : e.innerHTML, o);
- o.element = e;
-
- if (!o.no_events)
- t.onLoadContent.dispatch(t, o);
-
- o.element = e = null;
-
- return h;
- }
- },
-
- save : function(o) {
- var t = this, e = t.getElement(), h, f;
-
- if (!e || !t.initialized)
- return;
-
- o = o || {};
- o.save = true;
-
- o.element = e;
- h = o.content = t.getContent(o);
-
- if (!o.no_events)
- t.onSaveContent.dispatch(t, o);
-
- h = o.content;
-
- if (!/TEXTAREA|INPUT/i.test(e.nodeName)) {
- e.innerHTML = h;
-
- // Update hidden form element
- if (f = DOM.getParent(t.id, 'form')) {
- each(f.elements, function(e) {
- if (e.name == t.id) {
- e.value = h;
- return false;
- }
- });
- }
- } else
- e.value = h;
-
- o.element = e = null;
-
- return h;
- },
-
- setContent : function(content, args) {
- var self = this, rootNode, body = self.getBody(), forcedRootBlockName;
-
- // Setup args object
- args = args || {};
- args.format = args.format || 'html';
- args.set = true;
- args.content = content;
-
- // Do preprocessing
- if (!args.no_events)
- self.onBeforeSetContent.dispatch(self, args);
-
- content = args.content;
-
- // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content
- // It will also be impossible to place the caret in the editor unless there is a BR element present
- if (!tinymce.isIE && (content.length === 0 || /^\s+$/.test(content))) {
- forcedRootBlockName = self.settings.forced_root_block;
- if (forcedRootBlockName)
- content = '<' + forcedRootBlockName + '><br data-mce-bogus="1"></' + forcedRootBlockName + '>';
- else
- content = '<br data-mce-bogus="1">';
-
- body.innerHTML = content;
- self.selection.select(body, true);
- self.selection.collapse(true);
- return;
- }
-
- // Parse and serialize the html
- if (args.format !== 'raw') {
- content = new tinymce.html.Serializer({}, self.schema).serialize(
- self.parser.parse(content)
- );
- }
-
- // Set the new cleaned contents to the editor
- args.content = tinymce.trim(content);
- self.dom.setHTML(body, args.content);
-
- // Do post processing
- if (!args.no_events)
- self.onSetContent.dispatch(self, args);
-
- // Don't normalize selection if the focused element isn't the body in content editable mode since it will steal focus otherwise
- if (!self.settings.content_editable || document.activeElement === self.getBody()) {
- self.selection.normalize();
- }
-
- return args.content;
- },
-
- getContent : function(args) {
- var self = this, content, body = self.getBody();
-
- // Setup args object
- args = args || {};
- args.format = args.format || 'html';
- args.get = true;
- args.getInner = true;
-
- // Do preprocessing
- if (!args.no_events)
- self.onBeforeGetContent.dispatch(self, args);
-
- // Get raw contents or by default the cleaned contents
- if (args.format == 'raw')
- content = body.innerHTML;
- else if (args.format == 'text')
- content = body.innerText || body.textContent;
- else
- content = self.serializer.serialize(body, args);
-
- // Trim whitespace in beginning/end of HTML
- if (args.format != 'text') {
- args.content = tinymce.trim(content);
- } else {
- args.content = content;
- }
-
- // Do post processing
- if (!args.no_events)
- self.onGetContent.dispatch(self, args);
-
- return args.content;
- },
-
- isDirty : function() {
- var self = this;
-
- return tinymce.trim(self.startContent) != tinymce.trim(self.getContent({format : 'raw', no_events : 1})) && !self.isNotDirty;
- },
-
- getContainer : function() {
- var self = this;
-
- if (!self.container)
- self.container = DOM.get(self.editorContainer || self.id + '_parent');
-
- return self.container;
- },
-
- getContentAreaContainer : function() {
- return this.contentAreaContainer;
- },
-
- getElement : function() {
- return DOM.get(this.settings.content_element || this.id);
- },
-
- getWin : function() {
- var self = this, elm;
-
- if (!self.contentWindow) {
- elm = DOM.get(self.id + "_ifr");
-
- if (elm)
- self.contentWindow = elm.contentWindow;
- }
-
- return self.contentWindow;
- },
-
- getDoc : function() {
- var self = this, win;
-
- if (!self.contentDocument) {
- win = self.getWin();
-
- if (win)
- self.contentDocument = win.document;
- }
-
- return self.contentDocument;
- },
-
- getBody : function() {
- return this.bodyElement || this.getDoc().body;
- },
-
- convertURL : function(url, name, elm) {
- var self = this, settings = self.settings;
-
- // Use callback instead
- if (settings.urlconverter_callback)
- return self.execCallback('urlconverter_callback', url, elm, true, name);
-
- // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs
- if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0)
- return url;
-
- // Convert to relative
- if (settings.relative_urls)
- return self.documentBaseURI.toRelative(url);
-
- // Convert to absolute
- url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host);
-
- return url;
- },
-
- addVisual : function(elm) {
- var self = this, settings = self.settings, dom = self.dom, cls;
-
- elm = elm || self.getBody();
-
- if (!is(self.hasVisual))
- self.hasVisual = settings.visual;
-
- each(dom.select('table,a', elm), function(elm) {
- var value;
-
- switch (elm.nodeName) {
- case 'TABLE':
- cls = settings.visual_table_class || 'mceItemTable';
- value = dom.getAttrib(elm, 'border');
-
- if (!value || value == '0') {
- if (self.hasVisual)
- dom.addClass(elm, cls);
- else
- dom.removeClass(elm, cls);
- }
-
- return;
-
- case 'A':
- if (!dom.getAttrib(elm, 'href', false)) {
- value = dom.getAttrib(elm, 'name') || elm.id;
- cls = 'mceItemAnchor';
-
- if (value) {
- if (self.hasVisual)
- dom.addClass(elm, cls);
- else
- dom.removeClass(elm, cls);
- }
- }
-
- return;
- }
- });
-
- self.onVisualAid.dispatch(self, elm, self.hasVisual);
- },
-
- remove : function() {
- var self = this, elm = self.getContainer();
-
- if (!self.removed) {
- self.removed = 1; // Cancels post remove event execution
- self.hide();
-
- // Don't clear the window or document if content editable
- // is enabled since other instances might still be present
- if (!self.settings.content_editable) {
- Event.unbind(self.getWin());
- Event.unbind(self.getDoc());
- }
-
- Event.unbind(self.getBody());
- Event.clear(elm);
-
- self.execCallback('remove_instance_callback', self);
- self.onRemove.dispatch(self);
-
- // Clear all execCommand listeners this is required to avoid errors if the editor was removed inside another command
- self.onExecCommand.listeners = [];
-
- tinymce.remove(self);
- DOM.remove(elm);
- }
- },
-
- destroy : function(s) {
- var t = this;
-
- // One time is enough
- if (t.destroyed)
- return;
-
- // We must unbind on Gecko since it would otherwise produce the pesky "attempt to run compile-and-go script on a cleared scope" message
- if (isGecko) {
- Event.unbind(t.getDoc());
- Event.unbind(t.getWin());
- Event.unbind(t.getBody());
- }
-
- if (!s) {
- tinymce.removeUnload(t.destroy);
- tinyMCE.onBeforeUnload.remove(t._beforeUnload);
-
- // Manual destroy
- if (t.theme && t.theme.destroy)
- t.theme.destroy();
-
- // Destroy controls, selection and dom
- t.controlManager.destroy();
- t.selection.destroy();
- t.dom.destroy();
- }
-
- if (t.formElement) {
- t.formElement.submit = t.formElement._mceOldSubmit;
- t.formElement._mceOldSubmit = null;
- }
-
- t.contentAreaContainer = t.formElement = t.container = t.settings.content_element = t.bodyElement = t.contentDocument = t.contentWindow = null;
-
- if (t.selection)
- t.selection = t.selection.win = t.selection.dom = t.selection.dom.doc = null;
-
- t.destroyed = 1;
- },
-
- // Internal functions
-
- _refreshContentEditable : function() {
- var self = this, body, parent;
-
- // Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again
- if (self._isHidden()) {
- body = self.getBody();
- parent = body.parentNode;
-
- parent.removeChild(body);
- parent.appendChild(body);
-
- body.focus();
- }
- },
-
- _isHidden : function() {
- var s;
-
- if (!isGecko)
- return 0;
-
- // Weird, wheres that cursor selection?
- s = this.selection.getSel();
- return (!s || !s.rangeCount || s.rangeCount === 0);
- }
- });
-})(tinymce);
-(function(tinymce) {
- var each = tinymce.each;
-
- tinymce.Editor.prototype.setupEvents = function() {
- var self = this, settings = self.settings;
-
- // Add events to the editor
- each([
- 'onPreInit',
-
- 'onBeforeRenderUI',
-
- 'onPostRender',
-
- 'onLoad',
-
- 'onInit',
-
- 'onRemove',
-
- 'onActivate',
-
- 'onDeactivate',
-
- 'onClick',
-
- 'onEvent',
-
- 'onMouseUp',
-
- 'onMouseDown',
-
- 'onDblClick',
-
- 'onKeyDown',
-
- 'onKeyUp',
-
- 'onKeyPress',
-
- 'onContextMenu',
-
- 'onSubmit',
-
- 'onReset',
-
- 'onPaste',
-
- 'onPreProcess',
-
- 'onPostProcess',
-
- 'onBeforeSetContent',
-
- 'onBeforeGetContent',
-
- 'onSetContent',
-
- 'onGetContent',
-
- 'onLoadContent',
-
- 'onSaveContent',
-
- 'onNodeChange',
-
- 'onChange',
-
- 'onBeforeExecCommand',
-
- 'onExecCommand',
-
- 'onUndo',
-
- 'onRedo',
-
- 'onVisualAid',
-
- 'onSetProgressState',
-
- 'onSetAttrib'
- ], function(name) {
- self[name] = new tinymce.util.Dispatcher(self);
- });
-
- // Handle legacy cleanup_callback option
- if (settings.cleanup_callback) {
- self.onBeforeSetContent.add(function(ed, o) {
- o.content = ed.execCallback('cleanup_callback', 'insert_to_editor', o.content, o);
- });
-
- self.onPreProcess.add(function(ed, o) {
- if (o.set)
- ed.execCallback('cleanup_callback', 'insert_to_editor_dom', o.node, o);
-
- if (o.get)
- ed.execCallback('cleanup_callback', 'get_from_editor_dom', o.node, o);
- });
-
- self.onPostProcess.add(function(ed, o) {
- if (o.set)
- o.content = ed.execCallback('cleanup_callback', 'insert_to_editor', o.content, o);
-
- if (o.get)
- o.content = ed.execCallback('cleanup_callback', 'get_from_editor', o.content, o);
- });
- }
-
- // Handle legacy save_callback option
- if (settings.save_callback) {
- self.onGetContent.add(function(ed, o) {
- if (o.save)
- o.content = ed.execCallback('save_callback', ed.id, o.content, ed.getBody());
- });
- }
-
- // Handle legacy handle_event_callback option
- if (settings.handle_event_callback) {
- self.onEvent.add(function(ed, e, o) {
- if (self.execCallback('handle_event_callback', e, ed, o) === false) {
- e.preventDefault();
- e.stopPropagation();
- }
- });
- }
-
- // Handle legacy handle_node_change_callback option
- if (settings.handle_node_change_callback) {
- self.onNodeChange.add(function(ed, cm, n) {
- ed.execCallback('handle_node_change_callback', ed.id, n, -1, -1, true, ed.selection.isCollapsed());
- });
- }
-
- // Handle legacy save_callback option
- if (settings.save_callback) {
- self.onSaveContent.add(function(ed, o) {
- var h = ed.execCallback('save_callback', ed.id, o.content, ed.getBody());
-
- if (h)
- o.content = h;
- });
- }
-
- // Handle legacy onchange_callback option
- if (settings.onchange_callback) {
- self.onChange.add(function(ed, l) {
- ed.execCallback('onchange_callback', ed, l);
- });
- }
- };
-
- tinymce.Editor.prototype.bindNativeEvents = function() {
- // 'focus', 'blur', 'dblclick', 'beforedeactivate', submit, reset
- var self = this, i, settings = self.settings, dom = self.dom, nativeToDispatcherMap;
-
- nativeToDispatcherMap = {
- mouseup : 'onMouseUp',
- mousedown : 'onMouseDown',
- click : 'onClick',
- keyup : 'onKeyUp',
- keydown : 'onKeyDown',
- keypress : 'onKeyPress',
- submit : 'onSubmit',
- reset : 'onReset',
- contextmenu : 'onContextMenu',
- dblclick : 'onDblClick',
- paste : 'onPaste' // Doesn't work in all browsers yet
- };
-
- // Handler that takes a native event and sends it out to a dispatcher like onKeyDown
- function eventHandler(evt, args) {
- var type = evt.type;
-
- // Don't fire events when it's removed
- if (self.removed)
- return;
-
- // Sends the native event out to a global dispatcher then to the specific event dispatcher
- if (self.onEvent.dispatch(self, evt, args) !== false) {
- self[nativeToDispatcherMap[evt.fakeType || evt.type]].dispatch(self, evt, args);
- }
- };
-
- // Opera doesn't support focus event for contentEditable elements so we need to fake it
- function doOperaFocus(e) {
- self.focus(true);
- };
-
- function nodeChanged(ed, e) {
- // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> except for Ctrl+A since it selects everything
- if (e.keyCode != 65 || !tinymce.VK.metaKeyPressed(e)) {
- self.selection.normalize();
- }
-
- self.nodeChanged();
- }
-
- // Add DOM events
- each(nativeToDispatcherMap, function(dispatcherName, nativeName) {
- var root = settings.content_editable ? self.getBody() : self.getDoc();
-
- switch (nativeName) {
- case 'contextmenu':
- dom.bind(root, nativeName, eventHandler);
- break;
-
- case 'paste':
- dom.bind(self.getBody(), nativeName, eventHandler);
- break;
-
- case 'submit':
- case 'reset':
- dom.bind(self.getElement().form || tinymce.DOM.getParent(self.id, 'form'), nativeName, eventHandler);
- break;
-
- default:
- dom.bind(root, nativeName, eventHandler);
- }
- });
-
- // Set the editor as active when focused
- dom.bind(settings.content_editable ? self.getBody() : (tinymce.isGecko ? self.getDoc() : self.getWin()), 'focus', function(e) {
- self.focus(true);
- });
-
- if (settings.content_editable && tinymce.isOpera) {
- dom.bind(self.getBody(), 'click', doOperaFocus);
- dom.bind(self.getBody(), 'keydown', doOperaFocus);
- }
-
- // Add node change handler
- self.onMouseUp.add(nodeChanged);
-
- self.onKeyUp.add(function(ed, e) {
- var keyCode = e.keyCode;
-
- if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45 || keyCode == 46 || keyCode == 8 || (tinymce.isMac && (keyCode == 91 || keyCode == 93)) || e.ctrlKey)
- nodeChanged(ed, e);
- });
-
- // Add reset handler
- self.onReset.add(function() {
- self.setContent(self.startContent, {format : 'raw'});
- });
-
- // Add shortcuts
- function handleShortcut(e, execute) {
- if (e.altKey || e.ctrlKey || e.metaKey) {
- each(self.shortcuts, function(shortcut) {
- var ctrlState = tinymce.isMac ? e.metaKey : e.ctrlKey;
-
- if (shortcut.ctrl != ctrlState || shortcut.alt != e.altKey || shortcut.shift != e.shiftKey)
- return;
-
- if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) {
- e.preventDefault();
-
- if (execute) {
- shortcut.func.call(shortcut.scope);
- }
-
- return true;
- }
- });
- }
- };
-
- self.onKeyUp.add(function(ed, e) {
- handleShortcut(e);
- });
-
- self.onKeyPress.add(function(ed, e) {
- handleShortcut(e);
- });
-
- self.onKeyDown.add(function(ed, e) {
- handleShortcut(e, true);
- });
-
- if (tinymce.isOpera) {
- self.onClick.add(function(ed, e) {
- e.preventDefault();
- });
- }
- };
-})(tinymce);
-(function(tinymce) {
- // Added for compression purposes
- var each = tinymce.each, undef, TRUE = true, FALSE = false;
-
- tinymce.EditorCommands = function(editor) {
- var dom = editor.dom,
- selection = editor.selection,
- commands = {state: {}, exec : {}, value : {}},
- settings = editor.settings,
- formatter = editor.formatter,
- bookmark;
-
- function execCommand(command, ui, value) {
- var func;
-
- command = command.toLowerCase();
- if (func = commands.exec[command]) {
- func(command, ui, value);
- return TRUE;
- }
-
- return FALSE;
- };
-
- function queryCommandState(command) {
- var func;
-
- command = command.toLowerCase();
- if (func = commands.state[command])
- return func(command);
-
- return -1;
- };
-
- function queryCommandValue(command) {
- var func;
-
- command = command.toLowerCase();
- if (func = commands.value[command])
- return func(command);
-
- return FALSE;
- };
-
- function addCommands(command_list, type) {
- type = type || 'exec';
-
- each(command_list, function(callback, command) {
- each(command.toLowerCase().split(','), function(command) {
- commands[type][command] = callback;
- });
- });
- };
-
- // Expose public methods
- tinymce.extend(this, {
- execCommand : execCommand,
- queryCommandState : queryCommandState,
- queryCommandValue : queryCommandValue,
- addCommands : addCommands
- });
-
- // Private methods
-
- function execNativeCommand(command, ui, value) {
- if (ui === undef)
- ui = FALSE;
-
- if (value === undef)
- value = null;
-
- return editor.getDoc().execCommand(command, ui, value);
- };
-
- function isFormatMatch(name) {
- return formatter.match(name);
- };
-
- function toggleFormat(name, value) {
- formatter.toggle(name, value ? {value : value} : undef);
- };
-
- function storeSelection(type) {
- bookmark = selection.getBookmark(type);
- };
-
- function restoreSelection() {
- selection.moveToBookmark(bookmark);
- };
-
- // Add execCommand overrides
- addCommands({
- // Ignore these, added for compatibility
- 'mceResetDesignMode,mceBeginUndoLevel' : function() {},
-
- // Add undo manager logic
- 'mceEndUndoLevel,mceAddUndoLevel' : function() {
- editor.undoManager.add();
- },
-
- 'Cut,Copy,Paste' : function(command) {
- var doc = editor.getDoc(), failed;
-
- // Try executing the native command
- try {
- execNativeCommand(command);
- } catch (ex) {
- // Command failed
- failed = TRUE;
- }
-
- // Present alert message about clipboard access not being available
- if (failed || !doc.queryCommandSupported(command)) {
- if (tinymce.isGecko) {
- editor.windowManager.confirm(editor.getLang('clipboard_msg'), function(state) {
- if (state)
- open('http://www.mozilla.org/editor/midasdemo/securityprefs.html', '_blank');
- });
- } else
- editor.windowManager.alert(editor.getLang('clipboard_no_support'));
- }
- },
-
- // Override unlink command
- unlink : function(command) {
- if (selection.isCollapsed())
- selection.select(selection.getNode());
-
- execNativeCommand(command);
- selection.collapse(FALSE);
- },
-
- // Override justify commands to use the text formatter engine
- 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
- var align = command.substring(7);
-
- // Remove all other alignments first
- each('left,center,right,full'.split(','), function(name) {
- if (align != name)
- formatter.remove('align' + name);
- });
-
- toggleFormat('align' + align);
- execCommand('mceRepaint');
- },
-
- // Override list commands to fix WebKit bug
- 'InsertUnorderedList,InsertOrderedList' : function(command) {
- var listElm, listParent;
-
- execNativeCommand(command);
-
- // WebKit produces lists within block elements so we need to split them
- // we will replace the native list creation logic to custom logic later on
- // TODO: Remove this when the list creation logic is removed
- listElm = dom.getParent(selection.getNode(), 'ol,ul');
- if (listElm) {
- listParent = listElm.parentNode;
-
- // If list is within a text block then split that block
- if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) {
- storeSelection();
- dom.split(listParent, listElm);
- restoreSelection();
- }
- }
- },
-
- // Override commands to use the text formatter engine
- 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {
- toggleFormat(command);
- },
-
- // Override commands to use the text formatter engine
- 'ForeColor,HiliteColor,FontName' : function(command, ui, value) {
- toggleFormat(command, value);
- },
-
- FontSize : function(command, ui, value) {
- var fontClasses, fontSizes;
-
- // Convert font size 1-7 to styles
- if (value >= 1 && value <= 7) {
- fontSizes = tinymce.explode(settings.font_size_style_values);
- fontClasses = tinymce.explode(settings.font_size_classes);
-
- if (fontClasses)
- value = fontClasses[value - 1] || value;
- else
- value = fontSizes[value - 1] || value;
- }
-
- toggleFormat(command, value);
- },
-
- RemoveFormat : function(command) {
- formatter.remove(command);
- },
-
- mceBlockQuote : function(command) {
- toggleFormat('blockquote');
- },
-
- FormatBlock : function(command, ui, value) {
- return toggleFormat(value || 'p');
- },
-
- mceCleanup : function() {
- var bookmark = selection.getBookmark();
-
- editor.setContent(editor.getContent({cleanup : TRUE}), {cleanup : TRUE});
-
- selection.moveToBookmark(bookmark);
- },
-
- mceRemoveNode : function(command, ui, value) {
- var node = value || selection.getNode();
-
- // Make sure that the body node isn't removed
- if (node != editor.getBody()) {
- storeSelection();
- editor.dom.remove(node, TRUE);
- restoreSelection();
- }
- },
-
- mceSelectNodeDepth : function(command, ui, value) {
- var counter = 0;
-
- dom.getParent(selection.getNode(), function(node) {
- if (node.nodeType == 1 && counter++ == value) {
- selection.select(node);
- return FALSE;
- }
- }, editor.getBody());
- },
-
- mceSelectNode : function(command, ui, value) {
- selection.select(value);
- },
-
- mceInsertContent : function(command, ui, value) {
- var parser, serializer, parentNode, rootNode, fragment, args,
- marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement;
-
- //selection.normalize();
-
- // Setup parser and serializer
- parser = editor.parser;
- serializer = new tinymce.html.Serializer({}, editor.schema);
- bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">\uFEFF</span>';
-
- // Run beforeSetContent handlers on the HTML to be inserted
- args = {content: value, format: 'html'};
- selection.onBeforeSetContent.dispatch(selection, args);
- value = args.content;
-
- // Add caret at end of contents if it's missing
- if (value.indexOf('{$caret}') == -1)
- value += '{$caret}';
-
- // Replace the caret marker with a span bookmark element
- value = value.replace(/\{\$caret\}/, bookmarkHtml);
-
- // Insert node maker where we will insert the new HTML and get it's parent
- if (!selection.isCollapsed())
- editor.getDoc().execCommand('Delete', false, null);
-
- parentNode = selection.getNode();
-
- // Parse the fragment within the context of the parent node
- args = {context : parentNode.nodeName.toLowerCase()};
- fragment = parser.parse(value, args);
-
- // Move the caret to a more suitable location
- node = fragment.lastChild;
- if (node.attr('id') == 'mce_marker') {
- marker = node;
-
- for (node = node.prev; node; node = node.walk(true)) {
- if (node.type == 3 || !dom.isBlock(node.name)) {
- node.parent.insert(marker, node, node.name === 'br');
- break;
- }
- }
- }
-
- // If parser says valid we can insert the contents into that parent
- if (!args.invalid) {
- value = serializer.serialize(fragment);
-
- // Check if parent is empty or only has one BR element then set the innerHTML of that parent
- node = parentNode.firstChild;
- node2 = parentNode.lastChild;
- if (!node || (node === node2 && node.nodeName === 'BR'))
- dom.setHTML(parentNode, value);
- else
- selection.setContent(value);
- } else {
- // If the fragment was invalid within that context then we need
- // to parse and process the parent it's inserted into
-
- // Insert bookmark node and get the parent
- selection.setContent(bookmarkHtml);
- parentNode = selection.getNode();
- rootNode = editor.getBody();
-
- // Opera will return the document node when selection is in root
- if (parentNode.nodeType == 9)
- parentNode = node = rootNode;
- else
- node = parentNode;
-
- // Find the ancestor just before the root element
- while (node !== rootNode) {
- parentNode = node;
- node = node.parentNode;
- }
-
- // Get the outer/inner HTML depending on if we are in the root and parser and serialize that
- value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode);
- value = serializer.serialize(
- parser.parse(
- // Need to replace by using a function since $ in the contents would otherwise be a problem
- value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function() {
- return serializer.serialize(fragment);
- })
- )
- );
-
- // Set the inner/outer HTML depending on if we are in the root or not
- if (parentNode == rootNode)
- dom.setHTML(rootNode, value);
- else
- dom.setOuterHTML(parentNode, value);
- }
-
- marker = dom.get('mce_marker');
-
- // Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well
- nodeRect = dom.getRect(marker);
- viewPortRect = dom.getViewPort(editor.getWin());
-
- // Check if node is out side the viewport if it is then scroll to it
- if ((nodeRect.y + nodeRect.h > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) ||
- (nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) {
- viewportBodyElement = tinymce.isIE ? editor.getDoc().documentElement : editor.getBody();
- viewportBodyElement.scrollLeft = nodeRect.x;
- viewportBodyElement.scrollTop = nodeRect.y - viewPortRect.h + 25;
- }
-
- // Move selection before marker and remove it
- rng = dom.createRng();
-
- // If previous sibling is a text node set the selection to the end of that node
- node = marker.previousSibling;
- if (node && node.nodeType == 3) {
- rng.setStart(node, node.nodeValue.length);
- } else {
- // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node
- rng.setStartBefore(marker);
- rng.setEndBefore(marker);
- }
-
- // Remove the marker node and set the new range
- dom.remove(marker);
- selection.setRng(rng);
-
- // Dispatch after event and add any visual elements needed
- selection.onSetContent.dispatch(selection, args);
- editor.addVisual();
- },
-
- mceInsertRawHTML : function(command, ui, value) {
- selection.setContent('tiny_mce_marker');
- editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value }));
- },
-
- mceToggleFormat : function(command, ui, value) {
- toggleFormat(value);
- },
-
- mceSetContent : function(command, ui, value) {
- editor.setContent(value);
- },
-
- 'Indent,Outdent' : function(command) {
- var intentValue, indentUnit, value;
-
- // Setup indent level
- intentValue = settings.indentation;
- indentUnit = /[a-z%]+$/i.exec(intentValue);
- intentValue = parseInt(intentValue);
-
- if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) {
- // If forced_root_blocks is set to false we don't have a block to indent so lets create a div
- if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) {
- formatter.apply('div');
- }
-
- each(selection.getSelectedBlocks(), function(element) {
- if (command == 'outdent') {
- value = Math.max(0, parseInt(element.style.paddingLeft || 0) - intentValue);
- dom.setStyle(element, 'paddingLeft', value ? value + indentUnit : '');
- } else
- dom.setStyle(element, 'paddingLeft', (parseInt(element.style.paddingLeft || 0) + intentValue) + indentUnit);
- });
- } else
- execNativeCommand(command);
- },
-
- mceRepaint : function() {
- var bookmark;
-
- if (tinymce.isGecko) {
- try {
- storeSelection(TRUE);
-
- if (selection.getSel())
- selection.getSel().selectAllChildren(editor.getBody());
-
- selection.collapse(TRUE);
- restoreSelection();
- } catch (ex) {
- // Ignore
- }
- }
- },
-
- mceToggleFormat : function(command, ui, value) {
- formatter.toggle(value);
- },
-
- InsertHorizontalRule : function() {
- editor.execCommand('mceInsertContent', false, '<hr />');
- },
-
- mceToggleVisualAid : function() {
- editor.hasVisual = !editor.hasVisual;
- editor.addVisual();
- },
-
- mceReplaceContent : function(command, ui, value) {
- editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'})));
- },
-
- mceInsertLink : function(command, ui, value) {
- var anchor;
-
- if (typeof(value) == 'string')
- value = {href : value};
-
- anchor = dom.getParent(selection.getNode(), 'a');
-
- // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here.
- value.href = value.href.replace(' ', '%20');
-
- // Remove existing links if there could be child links or that the href isn't specified
- if (!anchor || !value.href) {
- formatter.remove('link');
- }
-
- // Apply new link to selection
- if (value.href) {
- formatter.apply('link', value, anchor);
- }
- },
-
- selectAll : function() {
- var root = dom.getRoot(), rng = dom.createRng();
-
- // Old IE does a better job with selectall than new versions
- if (selection.getRng().setStart) {
- rng.setStart(root, 0);
- rng.setEnd(root, root.childNodes.length);
-
- selection.setRng(rng);
- } else {
- execNativeCommand('SelectAll');
- }
- }
- });
-
- // Add queryCommandState overrides
- addCommands({
- // Override justify commands
- 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
- var name = 'align' + command.substring(7);
- var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks();
- var matches = tinymce.map(nodes, function(node) {
- return !!formatter.matchNode(node, name);
- });
- return tinymce.inArray(matches, TRUE) !== -1;
- },
-
- 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {
- return isFormatMatch(command);
- },
-
- mceBlockQuote : function() {
- return isFormatMatch('blockquote');
- },
-
- Outdent : function() {
- var node;
-
- if (settings.inline_styles) {
- if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
- return TRUE;
-
- if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
- return TRUE;
- }
-
- return queryCommandState('InsertUnorderedList') || queryCommandState('InsertOrderedList') || (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE'));
- },
-
- 'InsertUnorderedList,InsertOrderedList' : function(command) {
- var list = dom.getParent(selection.getNode(), 'ul,ol');
- return list &&
- (command === 'insertunorderedlist' && list.tagName === 'UL'
- || command === 'insertorderedlist' && list.tagName === 'OL');
- }
- }, 'state');
-
- // Add queryCommandValue overrides
- addCommands({
- 'FontSize,FontName' : function(command) {
- var value = 0, parent;
-
- if (parent = dom.getParent(selection.getNode(), 'span')) {
- if (command == 'fontsize')
- value = parent.style.fontSize;
- else
- value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase();
- }
-
- return value;
- }
- }, 'value');
-
- // Add undo manager logic
- addCommands({
- Undo : function() {
- editor.undoManager.undo();
- },
-
- Redo : function() {
- editor.undoManager.redo();
- }
- });
- };
-})(tinymce);
-
-(function(tinymce) {
- var Dispatcher = tinymce.util.Dispatcher;
-
- tinymce.UndoManager = function(editor) {
- var self, index = 0, data = [], beforeBookmark, onAdd, onUndo, onRedo;
-
- function getContent() {
- // Remove whitespace before/after and remove pure bogus nodes
- return tinymce.trim(editor.getContent({format : 'raw', no_events : 1}).replace(/<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\/span>/g, ''));
- };
-
- function addNonTypingUndoLevel() {
- self.typing = false;
- self.add();
- };
-
- // Create event instances
- onBeforeAdd = new Dispatcher(self);
- onAdd = new Dispatcher(self);
- onUndo = new Dispatcher(self);
- onRedo = new Dispatcher(self);
-
- // Pass though onAdd event from UndoManager to Editor as onChange
- onAdd.add(function(undoman, level) {
- if (undoman.hasUndo())
- return editor.onChange.dispatch(editor, level, undoman);
- });
-
- // Pass though onUndo event from UndoManager to Editor
- onUndo.add(function(undoman, level) {
- return editor.onUndo.dispatch(editor, level, undoman);
- });
-
- // Pass though onRedo event from UndoManager to Editor
- onRedo.add(function(undoman, level) {
- return editor.onRedo.dispatch(editor, level, undoman);
- });
-
- // Add initial undo level when the editor is initialized
- editor.onInit.add(function() {
- self.add();
- });
-
- // Get position before an execCommand is processed
- editor.onBeforeExecCommand.add(function(ed, cmd, ui, val, args) {
- if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!args || !args.skip_undo)) {
- self.beforeChange();
- }
- });
-
- // Add undo level after an execCommand call was made
- editor.onExecCommand.add(function(ed, cmd, ui, val, args) {
- if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!args || !args.skip_undo)) {
- self.add();
- }
- });
-
- // Add undo level on save contents, drag end and blur/focusout
- editor.onSaveContent.add(addNonTypingUndoLevel);
- editor.dom.bind(editor.dom.getRoot(), 'dragend', addNonTypingUndoLevel);
- editor.dom.bind(editor.getDoc(), tinymce.isGecko ? 'blur' : 'focusout', function(e) {
- if (!editor.removed && self.typing) {
- addNonTypingUndoLevel();
- }
- });
-
- editor.onKeyUp.add(function(editor, e) {
- var keyCode = e.keyCode;
-
- if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) {
- addNonTypingUndoLevel();
- }
- });
-
- editor.onKeyDown.add(function(editor, e) {
- var keyCode = e.keyCode;
-
- // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter
- if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45) {
- if (self.typing) {
- addNonTypingUndoLevel();
- }
-
- return;
- }
-
- // If key isn't shift,ctrl,alt,capslock,metakey
- if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !self.typing) {
- self.beforeChange();
- self.typing = true;
- self.add();
- }
- });
-
- editor.onMouseDown.add(function(editor, e) {
- if (self.typing) {
- addNonTypingUndoLevel();
- }
- });
-
- // Add keyboard shortcuts for undo/redo keys
- editor.addShortcut('ctrl+z', 'undo_desc', 'Undo');
- editor.addShortcut('ctrl+y', 'redo_desc', 'Redo');
-
- self = {
- // Explose for debugging reasons
- data : data,
-
- typing : false,
-
- onBeforeAdd: onBeforeAdd,
-
- onAdd : onAdd,
-
- onUndo : onUndo,
-
- onRedo : onRedo,
-
- beforeChange : function() {
- beforeBookmark = editor.selection.getBookmark(2, true);
- },
-
- add : function(level) {
- var i, settings = editor.settings, lastLevel;
-
- level = level || {};
- level.content = getContent();
-
- self.onBeforeAdd.dispatch(self, level);
-
- // Add undo level if needed
- lastLevel = data[index];
- if (lastLevel && lastLevel.content == level.content)
- return null;
-
- // Set before bookmark on previous level
- if (data[index])
- data[index].beforeBookmark = beforeBookmark;
-
- // Time to compress
- if (settings.custom_undo_redo_levels) {
- if (data.length > settings.custom_undo_redo_levels) {
- for (i = 0; i < data.length - 1; i++)
- data[i] = data[i + 1];
-
- data.length--;
- index = data.length;
- }
- }
-
- // Get a non intrusive normalized bookmark
- level.bookmark = editor.selection.getBookmark(2, true);
-
- // Crop array if needed
- if (index < data.length - 1)
- data.length = index + 1;
-
- data.push(level);
- index = data.length - 1;
-
- self.onAdd.dispatch(self, level);
- editor.isNotDirty = 0;
-
- return level;
- },
-
- undo : function() {
- var level, i;
-
- if (self.typing) {
- self.add();
- self.typing = false;
- }
-
- if (index > 0) {
- level = data[--index];
-
- editor.setContent(level.content, {format : 'raw'});
- editor.selection.moveToBookmark(level.beforeBookmark);
-
- self.onUndo.dispatch(self, level);
- }
-
- return level;
- },
-
- redo : function() {
- var level;
-
- if (index < data.length - 1) {
- level = data[++index];
-
- editor.setContent(level.content, {format : 'raw'});
- editor.selection.moveToBookmark(level.bookmark);
-
- self.onRedo.dispatch(self, level);
- }
-
- return level;
- },
-
- clear : function() {
- data = [];
- index = 0;
- self.typing = false;
- },
-
- hasUndo : function() {
- return index > 0 || this.typing;
- },
-
- hasRedo : function() {
- return index < data.length - 1 && !this.typing;
- }
- };
-
- return self;
- };
-})(tinymce);
-
-tinymce.ForceBlocks = function(editor) {
- var settings = editor.settings, dom = editor.dom, selection = editor.selection, blockElements = editor.schema.getBlockElements();
-
- function addRootBlocks() {
- var node = selection.getStart(), rootNode = editor.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF, wrapped, isInEditorDocument;
-
- if (!node || node.nodeType !== 1 || !settings.forced_root_block)
- return;
-
- // Check if node is wrapped in block
- while (node && node != rootNode) {
- if (blockElements[node.nodeName])
- return;
-
- node = node.parentNode;
- }
-
- // Get current selection
- rng = selection.getRng();
- if (rng.setStart) {
- startContainer = rng.startContainer;
- startOffset = rng.startOffset;
- endContainer = rng.endContainer;
- endOffset = rng.endOffset;
- } else {
- // Force control range into text range
- if (rng.item) {
- node = rng.item(0);
- rng = editor.getDoc().body.createTextRange();
- rng.moveToElementText(node);
- }
-
- isInEditorDocument = rng.parentElement().ownerDocument === editor.getDoc();
- tmpRng = rng.duplicate();
- tmpRng.collapse(true);
- startOffset = tmpRng.move('character', offset) * -1;
-
- if (!tmpRng.collapsed) {
- tmpRng = rng.duplicate();
- tmpRng.collapse(false);
- endOffset = (tmpRng.move('character', offset) * -1) - startOffset;
- }
- }
-
- // Wrap non block elements and text nodes
- node = rootNode.firstChild;
- while (node) {
- if (node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName])) {
- // Remove empty text nodes
- if (node.nodeType === 3 && node.nodeValue.length == 0) {
- tempNode = node;
- node = node.nextSibling;
- dom.remove(tempNode);
- continue;
- }
-
- if (!rootBlockNode) {
- rootBlockNode = dom.create(settings.forced_root_block);
- node.parentNode.insertBefore(rootBlockNode, node);
- wrapped = true;
- }
-
- tempNode = node;
- node = node.nextSibling;
- rootBlockNode.appendChild(tempNode);
- } else {
- rootBlockNode = null;
- node = node.nextSibling;
- }
- }
-
- if (wrapped) {
- if (rng.setStart) {
- rng.setStart(startContainer, startOffset);
- rng.setEnd(endContainer, endOffset);
- selection.setRng(rng);
- } else {
- // Only select if the previous selection was inside the document to prevent auto focus in quirks mode
- if (isInEditorDocument) {
- try {
- rng = editor.getDoc().body.createTextRange();
- rng.moveToElementText(rootNode);
- rng.collapse(true);
- rng.moveStart('character', startOffset);
-
- if (endOffset > 0)
- rng.moveEnd('character', endOffset);
-
- rng.select();
- } catch (ex) {
- // Ignore
- }
- }
- }
-
- editor.nodeChanged();
- }
- };
-
- // Force root blocks
- if (settings.forced_root_block) {
- editor.onKeyUp.add(addRootBlocks);
- editor.onNodeChange.add(addRootBlocks);
- }
-};
-
-(function(tinymce) {
- // Shorten names
- var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, extend = tinymce.extend;
-
- tinymce.create('tinymce.ControlManager', {
- ControlManager : function(ed, s) {
- var t = this, i;
-
- s = s || {};
- t.editor = ed;
- t.controls = {};
- t.onAdd = new tinymce.util.Dispatcher(t);
- t.onPostRender = new tinymce.util.Dispatcher(t);
- t.prefix = s.prefix || ed.id + '_';
- t._cls = {};
-
- t.onPostRender.add(function() {
- each(t.controls, function(c) {
- c.postRender();
- });
- });
- },
-
- get : function(id) {
- return this.controls[this.prefix + id] || this.controls[id];
- },
-
- setActive : function(id, s) {
- var c = null;
-
- if (c = this.get(id))
- c.setActive(s);
-
- return c;
- },
-
- setDisabled : function(id, s) {
- var c = null;
-
- if (c = this.get(id))
- c.setDisabled(s);
-
- return c;
- },
-
- add : function(c) {
- var t = this;
-
- if (c) {
- t.controls[c.id] = c;
- t.onAdd.dispatch(c, t);
- }
-
- return c;
- },
-
- createControl : function(name) {
- var ctrl, i, l, self = this, editor = self.editor, factories, ctrlName;
-
- // Build control factory cache
- if (!self.controlFactories) {
- self.controlFactories = [];
- each(editor.plugins, function(plugin) {
- if (plugin.createControl) {
- self.controlFactories.push(plugin);
- }
- });
- }
-
- // Create controls by asking cached factories
- factories = self.controlFactories;
- for (i = 0, l = factories.length; i < l; i++) {
- ctrl = factories[i].createControl(name, self);
-
- if (ctrl) {
- return self.add(ctrl);
- }
- }
-
- // Create sepearator
- if (name === "|" || name === "separator") {
- return self.createSeparator();
- }
-
- // Create control from button collection
- if (editor.buttons && (ctrl = editor.buttons[name])) {
- return self.createButton(name, ctrl);
- }
-
- return self.add(ctrl);
- },
-
- createDropMenu : function(id, s, cc) {
- var t = this, ed = t.editor, c, bm, v, cls;
-
- s = extend({
- 'class' : 'mceDropDown',
- constrain : ed.settings.constrain_menus
- }, s);
-
- s['class'] = s['class'] + ' ' + ed.getParam('skin') + 'Skin';
- if (v = ed.getParam('skin_variant'))
- s['class'] += ' ' + ed.getParam('skin') + 'Skin' + v.substring(0, 1).toUpperCase() + v.substring(1);
-
- s['class'] += ed.settings.directionality == "rtl" ? ' mceRtl' : '';
-
- id = t.prefix + id;
- cls = cc || t._cls.dropmenu || tinymce.ui.DropMenu;
- c = t.controls[id] = new cls(id, s);
- c.onAddItem.add(function(c, o) {
- var s = o.settings;
-
- s.title = ed.getLang(s.title, s.title);
-
- if (!s.onclick) {
- s.onclick = function(v) {
- if (s.cmd)
- ed.execCommand(s.cmd, s.ui || false, s.value);
- };
- }
- });
-
- ed.onRemove.add(function() {
- c.destroy();
- });
-
- // Fix for bug #1897785, #1898007
- if (tinymce.isIE) {
- c.onShowMenu.add(function() {
- // IE 8 needs focus in order to store away a range with the current collapsed caret location
- ed.focus();
-
- bm = ed.selection.getBookmark(1);
- });
-
- c.onHideMenu.add(function() {
- if (bm) {
- ed.selection.moveToBookmark(bm);
- bm = 0;
- }
- });
- }
-
- return t.add(c);
- },
-
- createListBox : function(id, s, cc) {
- var t = this, ed = t.editor, cmd, c, cls;
-
- if (t.get(id))
- return null;
-
- s.title = ed.translate(s.title);
- s.scope = s.scope || ed;
-
- if (!s.onselect) {
- s.onselect = function(v) {
- ed.execCommand(s.cmd, s.ui || false, v || s.value);
- };
- }
-
- s = extend({
- title : s.title,
- 'class' : 'mce_' + id,
- scope : s.scope,
- control_manager : t
- }, s);
-
- id = t.prefix + id;
-
-
- function useNativeListForAccessibility(ed) {
- return ed.settings.use_accessible_selects && !tinymce.isGecko
- }
-
- if (ed.settings.use_native_selects || useNativeListForAccessibility(ed))
- c = new tinymce.ui.NativeListBox(id, s);
- else {
- cls = cc || t._cls.listbox || tinymce.ui.ListBox;
- c = new cls(id, s, ed);
- }
-
- t.controls[id] = c;
-
- // Fix focus problem in Safari
- if (tinymce.isWebKit) {
- c.onPostRender.add(function(c, n) {
- // Store bookmark on mousedown
- Event.add(n, 'mousedown', function() {
- ed.bookmark = ed.selection.getBookmark(1);
- });
-
- // Restore on focus, since it might be lost
- Event.add(n, 'focus', function() {
- ed.selection.moveToBookmark(ed.bookmark);
- ed.bookmark = null;
- });
- });
- }
-
- if (c.hideMenu)
- ed.onMouseDown.add(c.hideMenu, c);
-
- return t.add(c);
- },
-
- createButton : function(id, s, cc) {
- var t = this, ed = t.editor, o, c, cls;
-
- if (t.get(id))
- return null;
-
- s.title = ed.translate(s.title);
- s.label = ed.translate(s.label);
- s.scope = s.scope || ed;
-
- if (!s.onclick && !s.menu_button) {
- s.onclick = function() {
- ed.execCommand(s.cmd, s.ui || false, s.value);
- };
- }
-
- s = extend({
- title : s.title,
- 'class' : 'mce_' + id,
- unavailable_prefix : ed.getLang('unavailable', ''),
- scope : s.scope,
- control_manager : t
- }, s);
-
- id = t.prefix + id;
-
- if (s.menu_button) {
- cls = cc || t._cls.menubutton || tinymce.ui.MenuButton;
- c = new cls(id, s, ed);
- ed.onMouseDown.add(c.hideMenu, c);
- } else {
- cls = t._cls.button || tinymce.ui.Button;
- c = new cls(id, s, ed);
- }
-
- return t.add(c);
- },
-
- createMenuButton : function(id, s, cc) {
- s = s || {};
- s.menu_button = 1;
-
- return this.createButton(id, s, cc);
- },
-
- createSplitButton : function(id, s, cc) {
- var t = this, ed = t.editor, cmd, c, cls;
-
- if (t.get(id))
- return null;
-
- s.title = ed.translate(s.title);
- s.scope = s.scope || ed;
-
- if (!s.onclick) {
- s.onclick = function(v) {
- ed.execCommand(s.cmd, s.ui || false, v || s.value);
- };
- }
-
- if (!s.onselect) {
- s.onselect = function(v) {
- ed.execCommand(s.cmd, s.ui || false, v || s.value);
- };
- }
-
- s = extend({
- title : s.title,
- 'class' : 'mce_' + id,
- scope : s.scope,
- control_manager : t
- }, s);
-
- id = t.prefix + id;
- cls = cc || t._cls.splitbutton || tinymce.ui.SplitButton;
- c = t.add(new cls(id, s, ed));
- ed.onMouseDown.add(c.hideMenu, c);
-
- return c;
- },
-
- createColorSplitButton : function(id, s, cc) {
- var t = this, ed = t.editor, cmd, c, cls, bm;
-
- if (t.get(id))
- return null;
-
- s.title = ed.translate(s.title);
- s.scope = s.scope || ed;
-
- if (!s.onclick) {
- s.onclick = function(v) {
- if (tinymce.isIE)
- bm = ed.selection.getBookmark(1);
-
- ed.execCommand(s.cmd, s.ui || false, v || s.value);
- };
- }
-
- if (!s.onselect) {
- s.onselect = function(v) {
- ed.execCommand(s.cmd, s.ui || false, v || s.value);
- };
- }
-
- s = extend({
- title : s.title,
- 'class' : 'mce_' + id,
- 'menu_class' : ed.getParam('skin') + 'Skin',
- scope : s.scope,
- more_colors_title : ed.getLang('more_colors')
- }, s);
-
- id = t.prefix + id;
- cls = cc || t._cls.colorsplitbutton || tinymce.ui.ColorSplitButton;
- c = new cls(id, s, ed);
- ed.onMouseDown.add(c.hideMenu, c);
-
- // Remove the menu element when the editor is removed
- ed.onRemove.add(function() {
- c.destroy();
- });
-
- // Fix for bug #1897785, #1898007
- if (tinymce.isIE) {
- c.onShowMenu.add(function() {
- // IE 8 needs focus in order to store away a range with the current collapsed caret location
- ed.focus();
- bm = ed.selection.getBookmark(1);
- });
-
- c.onHideMenu.add(function() {
- if (bm) {
- ed.selection.moveToBookmark(bm);
- bm = 0;
- }
- });
- }
-
- return t.add(c);
- },
-
- createToolbar : function(id, s, cc) {
- var c, t = this, cls;
-
- id = t.prefix + id;
- cls = cc || t._cls.toolbar || tinymce.ui.Toolbar;
- c = new cls(id, s, t.editor);
-
- if (t.get(id))
- return null;
-
- return t.add(c);
- },
-
- createToolbarGroup : function(id, s, cc) {
- var c, t = this, cls;
- id = t.prefix + id;
- cls = cc || this._cls.toolbarGroup || tinymce.ui.ToolbarGroup;
- c = new cls(id, s, t.editor);
-
- if (t.get(id))
- return null;
-
- return t.add(c);
- },
-
- createSeparator : function(cc) {
- var cls = cc || this._cls.separator || tinymce.ui.Separator;
-
- return new cls();
- },
-
- setControlType : function(n, c) {
- return this._cls[n.toLowerCase()] = c;
- },
-
- destroy : function() {
- each(this.controls, function(c) {
- c.destroy();
- });
-
- this.controls = null;
- }
- });
-})(tinymce);
-
-(function(tinymce) {
- var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isIE = tinymce.isIE, isOpera = tinymce.isOpera;
-
- tinymce.create('tinymce.WindowManager', {
- WindowManager : function(ed) {
- var t = this;
-
- t.editor = ed;
- t.onOpen = new Dispatcher(t);
- t.onClose = new Dispatcher(t);
- t.params = {};
- t.features = {};
- },
-
- open : function(s, p) {
- var t = this, f = '', x, y, mo = t.editor.settings.dialog_type == 'modal', w, sw, sh, vp = tinymce.DOM.getViewPort(), u;
-
- // Default some options
- s = s || {};
- p = p || {};
- sw = isOpera ? vp.w : screen.width; // Opera uses windows inside the Opera window
- sh = isOpera ? vp.h : screen.height;
- s.name = s.name || 'mc_' + new Date().getTime();
- s.width = parseInt(s.width || 320);
- s.height = parseInt(s.height || 240);
- s.resizable = true;
- s.left = s.left || parseInt(sw / 2.0) - (s.width / 2.0);
- s.top = s.top || parseInt(sh / 2.0) - (s.height / 2.0);
- p.inline = false;
- p.mce_width = s.width;
- p.mce_height = s.height;
- p.mce_auto_focus = s.auto_focus;
-
- if (mo) {
- if (isIE) {
- s.center = true;
- s.help = false;
- s.dialogWidth = s.width + 'px';
- s.dialogHeight = s.height + 'px';
- s.scroll = s.scrollbars || false;
- }
- }
-
- // Build features string
- each(s, function(v, k) {
- if (tinymce.is(v, 'boolean'))
- v = v ? 'yes' : 'no';
-
- if (!/^(name|url)$/.test(k)) {
- if (isIE && mo)
- f += (f ? ';' : '') + k + ':' + v;
- else
- f += (f ? ',' : '') + k + '=' + v;
- }
- });
-
- t.features = s;
- t.params = p;
- t.onOpen.dispatch(t, s, p);
-
- u = s.url || s.file;
- u = tinymce._addVer(u);
-
- try {
- if (isIE && mo) {
- w = 1;
- window.showModalDialog(u, window, f);
- } else
- w = window.open(u, s.name, f);
- } catch (ex) {
- // Ignore
- }
-
- // Added by Dan S./Zotero
- zoteroFixWindow(w);
-
- if (!w)
- alert(t.editor.getLang('popup_blocked'));
- },
-
- close : function(w) {
- w.close();
- this.onClose.dispatch(this);
- },
-
- createInstance : function(cl, a, b, c, d, e) {
- var f = tinymce.resolve(cl);
-
- return new f(a, b, c, d, e);
- },
-
- confirm : function(t, cb, s, w) {
- w = w || window;
-
- cb.call(s || this, w.confirm(this._decode(this.editor.getLang(t, t))));
- },
-
- alert : function(tx, cb, s, w) {
- var t = this;
-
- w = w || window;
- w.alert(t._decode(t.editor.getLang(tx, tx)));
-
- if (cb)
- cb.call(s || t);
- },
-
- resizeBy : function(dw, dh, win) {
- win.resizeBy(dw, dh);
- },
-
- // Internal functions
-
- _decode : function(s) {
- return tinymce.DOM.decode(s).replace(/\\n/g, '\n');
- }
- });
-}(tinymce));
-(function(tinymce) {
- tinymce.Formatter = function(ed) {
- var formats = {},
- each = tinymce.each,
- dom = ed.dom,
- selection = ed.selection,
- TreeWalker = tinymce.dom.TreeWalker,
- rangeUtils = new tinymce.dom.RangeUtils(dom),
- isValid = ed.schema.isValidChild,
- isArray = tinymce.isArray,
- isBlock = dom.isBlock,
- forcedRootBlock = ed.settings.forced_root_block,
- nodeIndex = dom.nodeIndex,
- INVISIBLE_CHAR = '\uFEFF',
- MCE_ATTR_RE = /^(src|href|style)$/,
- FALSE = false,
- TRUE = true,
- formatChangeData,
- undef,
- getContentEditable = dom.getContentEditable;
-
- function isTextBlock(name) {
- return !!ed.schema.getTextBlocks()[name.toLowerCase()];
- }
-
- function getParents(node, selector) {
- return dom.getParents(node, selector, dom.getRoot());
- };
-
- function isCaretNode(node) {
- return node.nodeType === 1 && node.id === '_mce_caret';
- };
-
- function defaultFormats() {
- register({
- alignleft : [
- {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'left'}, defaultBlock: 'div'},
- {selector : 'img,table', collapsed : false, styles : {'float' : 'left'}}
- ],
-
- aligncenter : [
- {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'center'}, defaultBlock: 'div'},
- {selector : 'img', collapsed : false, styles : {display : 'block', marginLeft : 'auto', marginRight : 'auto'}},
- {selector : 'table', collapsed : false, styles : {marginLeft : 'auto', marginRight : 'auto'}}
- ],
-
- alignright : [
- {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'right'}, defaultBlock: 'div'},
- {selector : 'img,table', collapsed : false, styles : {'float' : 'right'}}
- ],
-
- alignfull : [
- {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'justify'}, defaultBlock: 'div'}
- ],
-
- bold : [
- {inline : 'strong', remove : 'all'},
- {inline : 'span', styles : {fontWeight : 'bold'}},
- {inline : 'b', remove : 'all'}
- ],
-
- italic : [
- {inline : 'em', remove : 'all'},
- {inline : 'span', styles : {fontStyle : 'italic'}},
- {inline : 'i', remove : 'all'}
- ],
-
- underline : [
- {inline : 'span', styles : {textDecoration : 'underline'}, exact : true},
- {inline : 'u', remove : 'all'}
- ],
-
- strikethrough : [
- {inline : 'span', styles : {textDecoration : 'line-through'}, exact : true},
- {inline : 'strike', remove : 'all'}
- ],
-
- forecolor : {inline : 'span', styles : {color : '%value'}, wrap_links : false},
- hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}, wrap_links : false},
- fontname : {inline : 'span', styles : {fontFamily : '%value'}},
- fontsize : {inline : 'span', styles : {fontSize : '%value'}},
- fontsize_class : {inline : 'span', attributes : {'class' : '%value'}},
- blockquote : {block : 'blockquote', wrapper : 1, remove : 'all'},
- subscript : {inline : 'sub'},
- superscript : {inline : 'sup'},
-
- link : {inline : 'a', selector : 'a', remove : 'all', split : true, deep : true,
- onmatch : function(node) {
- return true;
- },
-
- onformat : function(elm, fmt, vars) {
- each(vars, function(value, key) {
- dom.setAttrib(elm, key, value);
- });
- }
- },
-
- removeformat : [
- {selector : 'b,strong,em,i,font,u,strike', remove : 'all', split : true, expand : false, block_expand : true, deep : true},
- {selector : 'span', attributes : ['style', 'class'], remove : 'empty', split : true, expand : false, deep : true},
- {selector : '*', attributes : ['style', 'class'], split : false, expand : false, deep : true}
- ]
- });
-
- // Register default block formats
- each('p h1 h2 h3 h4 h5 h6 div address pre div code dt dd samp'.split(/\s/), function(name) {
- register(name, {block : name, remove : 'all'});
- });
-
- // Register user defined formats
- register(ed.settings.formats);
- };
-
- function addKeyboardShortcuts() {
- // Add some inline shortcuts
- ed.addShortcut('ctrl+b', 'bold_desc', 'Bold');
- ed.addShortcut('ctrl+i', 'italic_desc', 'Italic');
- ed.addShortcut('ctrl+u', 'underline_desc', 'Underline');
-
- // BlockFormat shortcuts keys
- for (var i = 1; i <= 6; i++) {
- ed.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]);
- }
-
- ed.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']);
- ed.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']);
- ed.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']);
- };
-
- // Public functions
-
- function get(name) {
- return name ? formats[name] : formats;
- };
-
- function register(name, format) {
- if (name) {
- if (typeof(name) !== 'string') {
- each(name, function(format, name) {
- register(name, format);
- });
- } else {
- // Force format into array and add it to internal collection
- format = format.length ? format : [format];
-
- each(format, function(format) {
- // Set deep to false by default on selector formats this to avoid removing
- // alignment on images inside paragraphs when alignment is changed on paragraphs
- if (format.deep === undef)
- format.deep = !format.selector;
-
- // Default to true
- if (format.split === undef)
- format.split = !format.selector || format.inline;
-
- // Default to true
- if (format.remove === undef && format.selector && !format.inline)
- format.remove = 'none';
-
- // Mark format as a mixed format inline + block level
- if (format.selector && format.inline) {
- format.mixed = true;
- format.block_expand = true;
- }
-
- // Split classes if needed
- if (typeof(format.classes) === 'string')
- format.classes = format.classes.split(/\s+/);
- });
-
- formats[name] = format;
- }
- }
- };
-
- var getTextDecoration = function(node) {
- var decoration;
-
- ed.dom.getParent(node, function(n) {
- decoration = ed.dom.getStyle(n, 'text-decoration');
- return decoration && decoration !== 'none';
- });
-
- return decoration;
- };
-
- var processUnderlineAndColor = function(node) {
- var textDecoration;
- if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) {
- textDecoration = getTextDecoration(node.parentNode);
- if (ed.dom.getStyle(node, 'color') && textDecoration) {
- ed.dom.setStyle(node, 'text-decoration', textDecoration);
- } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) {
- ed.dom.setStyle(node, 'text-decoration', null);
- }
- }
- };
-
- function apply(name, vars, node) {
- var formatList = get(name), format = formatList[0], bookmark, rng, i, isCollapsed = selection.isCollapsed();
-
- function setElementFormat(elm, fmt) {
- fmt = fmt || format;
-
- if (elm) {
- if (fmt.onformat) {
- fmt.onformat(elm, fmt, vars, node);
- }
-
- each(fmt.styles, function(value, name) {
- dom.setStyle(elm, name, replaceVars(value, vars));
- });
-
- each(fmt.attributes, function(value, name) {
- dom.setAttrib(elm, name, replaceVars(value, vars));
- });
-
- each(fmt.classes, function(value) {
- value = replaceVars(value, vars);
-
- if (!dom.hasClass(elm, value))
- dom.addClass(elm, value);
- });
- }
- };
- function adjustSelectionToVisibleSelection() {
- function findSelectionEnd(start, end) {
- var walker = new TreeWalker(end);
- for (node = walker.current(); node; node = walker.prev()) {
- if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') {
- return node;
- }
- }
- };
-
- // Adjust selection so that a end container with a end offset of zero is not included in the selection
- // as this isn't visible to the user.
- var rng = ed.selection.getRng();
- var start = rng.startContainer;
- var end = rng.endContainer;
-
- if (start != end && rng.endOffset === 0) {
- var newEnd = findSelectionEnd(start, end);
- var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length;
-
- rng.setEnd(newEnd, endOffset);
- }
-
- return rng;
- }
-
- function applyStyleToList(node, bookmark, wrapElm, newWrappers, process){
- var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm;
-
- // find the index of the first child list.
- each(node.childNodes, function(n, index) {
- if (n.nodeName === "UL" || n.nodeName === "OL") {
- listIndex = index;
- list = n;
- return false;
- }
- });
-
- // get the index of the bookmarks
- each(node.childNodes, function(n, index) {
- if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") {
- if (n.id == bookmark.id + "_start") {
- startIndex = index;
- } else if (n.id == bookmark.id + "_end") {
- endIndex = index;
- }
- }
- });
-
- // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally
- if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) {
- each(tinymce.grep(node.childNodes), process);
- return 0;
- } else {
- currentWrapElm = dom.clone(wrapElm, FALSE);
-
- // create a list of the nodes on the same side of the list as the selection
- each(tinymce.grep(node.childNodes), function(n, index) {
- if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) {
- nodes.push(n);
- n.parentNode.removeChild(n);
- }
- });
-
- // insert the wrapping element either before or after the list.
- if (startIndex < listIndex) {
- node.insertBefore(currentWrapElm, list);
- } else if (startIndex > listIndex) {
- node.insertBefore(currentWrapElm, list.nextSibling);
- }
-
- // add the new nodes to the list.
- newWrappers.push(currentWrapElm);
-
- each(nodes, function(node) {
- currentWrapElm.appendChild(node);
- });
-
- return currentWrapElm;
- }
- };
-
- function applyRngStyle(rng, bookmark, node_specific) {
- var newWrappers = [], wrapName, wrapElm, contentEditable = true;
-
- // Setup wrapper element
- wrapName = format.inline || format.block;
- wrapElm = dom.create(wrapName);
- setElementFormat(wrapElm);
-
- rangeUtils.walk(rng, function(nodes) {
- var currentWrapElm;
-
- function process(node) {
- var nodeName, parentName, found, hasContentEditableState, lastContentEditable;
-
- lastContentEditable = contentEditable;
- nodeName = node.nodeName.toLowerCase();
- parentName = node.parentNode.nodeName.toLowerCase();
-
- // Node has a contentEditable value
- if (node.nodeType === 1 && getContentEditable(node)) {
- lastContentEditable = contentEditable;
- contentEditable = getContentEditable(node) === "true";
- hasContentEditableState = true; // We don't want to wrap the container only it's children
- }
-
- // Stop wrapping on br elements
- if (isEq(nodeName, 'br')) {
- currentWrapElm = 0;
-
- // Remove any br elements when we wrap things
- if (format.block)
- dom.remove(node);
-
- return;
- }
-
- // If node is wrapper type
- if (format.wrapper && matchNode(node, name, vars)) {
- currentWrapElm = 0;
- return;
- }
-
- // Can we rename the block
- if (contentEditable && !hasContentEditableState && format.block && !format.wrapper && isTextBlock(nodeName)) {
- node = dom.rename(node, wrapName);
- setElementFormat(node);
- newWrappers.push(node);
- currentWrapElm = 0;
- return;
- }
-
- // Handle selector patterns
- if (format.selector) {
- // Look for matching formats
- each(formatList, function(format) {
- // Check collapsed state if it exists
- if ('collapsed' in format && format.collapsed !== isCollapsed) {
- return;
- }
-
- if (dom.is(node, format.selector) && !isCaretNode(node)) {
- setElementFormat(node, format);
- found = true;
- }
- });
-
- // Continue processing if a selector match wasn't found and a inline element is defined
- if (!format.inline || found) {
- currentWrapElm = 0;
- return;
- }
- }
-
- // Is it valid to wrap this item
- if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) &&
- !(!node_specific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && !isCaretNode(node)) {
- // Start wrapping
- if (!currentWrapElm) {
- // Wrap the node
- currentWrapElm = dom.clone(wrapElm, FALSE);
- node.parentNode.insertBefore(currentWrapElm, node);
- newWrappers.push(currentWrapElm);
- }
-
- currentWrapElm.appendChild(node);
- } else if (nodeName == 'li' && bookmark) {
- // Start wrapping - if we are in a list node and have a bookmark, then we will always begin by wrapping in a new element.
- currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process);
- } else {
- // Start a new wrapper for possible children
- currentWrapElm = 0;
-
- each(tinymce.grep(node.childNodes), process);
-
- if (hasContentEditableState) {
- contentEditable = lastContentEditable; // Restore last contentEditable state from stack
- }
-
- // End the last wrapper
- currentWrapElm = 0;
- }
- };
-
- // Process siblings from range
- each(nodes, process);
- });
-
- // Wrap links inside as well, for example color inside a link when the wrapper is around the link
- if (format.wrap_links === false) {
- each(newWrappers, function(node) {
- function process(node) {
- var i, currentWrapElm, children;
-
- if (node.nodeName === 'A') {
- currentWrapElm = dom.clone(wrapElm, FALSE);
- newWrappers.push(currentWrapElm);
-
- children = tinymce.grep(node.childNodes);
- for (i = 0; i < children.length; i++)
- currentWrapElm.appendChild(children[i]);
-
- node.appendChild(currentWrapElm);
- }
-
- each(tinymce.grep(node.childNodes), process);
- };
-
- process(node);
- });
- }
-
- // Cleanup
-
- each(newWrappers, function(node) {
- var childCount;
-
- function getChildCount(node) {
- var count = 0;
-
- each(node.childNodes, function(node) {
- if (!isWhiteSpaceNode(node) && !isBookmarkNode(node))
- count++;
- });
-
- return count;
- };
-
- function mergeStyles(node) {
- var child, clone;
-
- each(node.childNodes, function(node) {
- if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) {
- child = node;
- return FALSE; // break loop
- }
- });
-
- // If child was found and of the same type as the current node
- if (child && matchName(child, format)) {
- clone = dom.clone(child, FALSE);
- setElementFormat(clone);
-
- dom.replace(clone, node, TRUE);
- dom.remove(child, 1);
- }
-
- return clone || node;
- };
-
- childCount = getChildCount(node);
-
- // Remove empty nodes but only if there is multiple wrappers and they are not block
- // elements so never remove single <h1></h1> since that would remove the currrent empty block element where the caret is at
- if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) {
- dom.remove(node, 1);
- return;
- }
-
- if (format.inline || format.wrapper) {
- // Merges the current node with it's children of similar type to reduce the number of elements
- if (!format.exact && childCount === 1)
- node = mergeStyles(node);
-
- // Remove/merge children
- each(formatList, function(format) {
- // Merge all children of similar type will move styles from child to parent
- // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span>
- // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span>
- each(dom.select(format.inline, node), function(child) {
- var parent;
-
- // When wrap_links is set to false we don't want
- // to remove the format on children within links
- if (format.wrap_links === false) {
- parent = child.parentNode;
-
- do {
- if (parent.nodeName === 'A')
- return;
- } while (parent = parent.parentNode);
- }
-
- removeFormat(format, vars, child, format.exact ? child : null);
- });
- });
-
- // Remove child if direct parent is of same type
- if (matchNode(node.parentNode, name, vars)) {
- dom.remove(node, 1);
- node = 0;
- return TRUE;
- }
-
- // Look for parent with similar style format
- if (format.merge_with_parents) {
- dom.getParent(node.parentNode, function(parent) {
- if (matchNode(parent, name, vars)) {
- dom.remove(node, 1);
- node = 0;
- return TRUE;
- }
- });
- }
-
- // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b>
- if (node && format.merge_siblings !== false) {
- node = mergeSiblings(getNonWhiteSpaceSibling(node), node);
- node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE));
- }
- }
- });
- };
-
- if (format) {
- if (node) {
- if (node.nodeType) {
- rng = dom.createRng();
- rng.setStartBefore(node);
- rng.setEndAfter(node);
- applyRngStyle(expandRng(rng, formatList), null, true);
- } else {
- applyRngStyle(node, null, true);
- }
- } else {
- if (!isCollapsed || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) {
- // Obtain selection node before selection is unselected by applyRngStyle()
- var curSelNode = ed.selection.getNode();
-
- // If the formats have a default block and we can't find a parent block then start wrapping it with a DIV this is for forced_root_blocks: false
- // It's kind of a hack but people should be using the default block type P since all desktop editors work that way
- if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) {
- apply(formatList[0].defaultBlock);
- }
-
- // Apply formatting to selection
- ed.selection.setRng(adjustSelectionToVisibleSelection());
- bookmark = selection.getBookmark();
- applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark);
-
- // Colored nodes should be underlined so that the color of the underline matches the text color.
- if (format.styles && (format.styles.color || format.styles.textDecoration)) {
- tinymce.walk(curSelNode, processUnderlineAndColor, 'childNodes');
- processUnderlineAndColor(curSelNode);
- }
-
- selection.moveToBookmark(bookmark);
- moveStart(selection.getRng(TRUE));
- ed.nodeChanged();
- } else
- performCaretAction('apply', name, vars);
- }
- }
- };
-
- function remove(name, vars, node) {
- var formatList = get(name), format = formatList[0], bookmark, i, rng, contentEditable = true;
-
- // Merges the styles for each node
- function process(node) {
- var children, i, l, localContentEditable, lastContentEditable, hasContentEditableState;
-
- // Node has a contentEditable value
- if (node.nodeType === 1 && getContentEditable(node)) {
- lastContentEditable = contentEditable;
- contentEditable = getContentEditable(node) === "true";
- hasContentEditableState = true; // We don't want to wrap the container only it's children
- }
-
- // Grab the children first since the nodelist might be changed
- children = tinymce.grep(node.childNodes);
-
- // Process current node
- if (contentEditable && !hasContentEditableState) {
- for (i = 0, l = formatList.length; i < l; i++) {
- if (removeFormat(formatList[i], vars, node, node))
- break;
- }
- }
-
- // Process the children
- if (format.deep) {
- if (children.length) {
- for (i = 0, l = children.length; i < l; i++)
- process(children[i]);
-
- if (hasContentEditableState) {
- contentEditable = lastContentEditable; // Restore last contentEditable state from stack
- }
- }
- }
- };
-
- function findFormatRoot(container) {
- var formatRoot;
-
- // Find format root
- each(getParents(container.parentNode).reverse(), function(parent) {
- var format;
-
- // Find format root element
- if (!formatRoot && parent.id != '_start' && parent.id != '_end') {
- // Is the node matching the format we are looking for
- format = matchNode(parent, name, vars);
- if (format && format.split !== false)
- formatRoot = parent;
- }
- });
-
- return formatRoot;
- };
-
- function wrapAndSplit(format_root, container, target, split) {
- var parent, clone, lastClone, firstClone, i, formatRootParent;
-
- // Format root found then clone formats and split it
- if (format_root) {
- formatRootParent = format_root.parentNode;
-
- for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) {
- clone = dom.clone(parent, FALSE);
-
- for (i = 0; i < formatList.length; i++) {
- if (removeFormat(formatList[i], vars, clone, clone)) {
- clone = 0;
- break;
- }
- }
-
- // Build wrapper node
- if (clone) {
- if (lastClone)
- clone.appendChild(lastClone);
-
- if (!firstClone)
- firstClone = clone;
-
- lastClone = clone;
- }
- }
-
- // Never split block elements if the format is mixed
- if (split && (!format.mixed || !isBlock(format_root)))
- container = dom.split(format_root, container);
-
- // Wrap container in cloned formats
- if (lastClone) {
- target.parentNode.insertBefore(lastClone, target);
- firstClone.appendChild(target);
- }
- }
-
- return container;
- };
-
- function splitToFormatRoot(container) {
- return wrapAndSplit(findFormatRoot(container), container, container, true);
- };
-
- function unwrap(start) {
- var node = dom.get(start ? '_start' : '_end'),
- out = node[start ? 'firstChild' : 'lastChild'];
-
- // If the end is placed within the start the result will be removed
- // So this checks if the out node is a bookmark node if it is it
- // checks for another more suitable node
- if (isBookmarkNode(out))
- out = out[start ? 'firstChild' : 'lastChild'];
-
- dom.remove(node, true);
-
- return out;
- };
-
- function removeRngStyle(rng) {
- var startContainer, endContainer, node;
-
- rng = expandRng(rng, formatList, TRUE);
-
- if (format.split) {
- startContainer = getContainer(rng, TRUE);
- endContainer = getContainer(rng);
-
- if (startContainer != endContainer) {
- // WebKit will render the table incorrectly if we wrap a TD in a SPAN so lets see if the can use the first child instead
- // This will happen if you tripple click a table cell and use remove formatting
- if (/^(TR|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) {
- startContainer = (startContainer.nodeName == "TD" ? startContainer.firstChild : startContainer.firstChild.firstChild) || startContainer;
- }
-
- // Wrap start/end nodes in span element since these might be cloned/moved
- startContainer = wrap(startContainer, 'span', {id : '_start', 'data-mce-type' : 'bookmark'});
- endContainer = wrap(endContainer, 'span', {id : '_end', 'data-mce-type' : 'bookmark'});
-
- // Split start/end
- splitToFormatRoot(startContainer);
- splitToFormatRoot(endContainer);
-
- // Unwrap start/end to get real elements again
- startContainer = unwrap(TRUE);
- endContainer = unwrap();
- } else
- startContainer = endContainer = splitToFormatRoot(startContainer);
-
- // Update range positions since they might have changed after the split operations
- rng.startContainer = startContainer.parentNode;
- rng.startOffset = nodeIndex(startContainer);
- rng.endContainer = endContainer.parentNode;
- rng.endOffset = nodeIndex(endContainer) + 1;
- }
-
- // Remove items between start/end
- rangeUtils.walk(rng, function(nodes) {
- each(nodes, function(node) {
- process(node);
-
- // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined.
- if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && node.parentNode && getTextDecoration(node.parentNode) === 'underline') {
- removeFormat({'deep': false, 'exact': true, 'inline': 'span', 'styles': {'textDecoration' : 'underline'}}, null, node);
- }
- });
- });
- };
-
- // Handle node
- if (node) {
- if (node.nodeType) {
- rng = dom.createRng();
- rng.setStartBefore(node);
- rng.setEndAfter(node);
- removeRngStyle(rng);
- } else {
- removeRngStyle(node);
- }
-
- return;
- }
-
- if (!selection.isCollapsed() || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) {
- bookmark = selection.getBookmark();
- removeRngStyle(selection.getRng(TRUE));
- selection.moveToBookmark(bookmark);
-
- // Check if start element still has formatting then we are at: "<b>text|</b>text" and need to move the start into the next text node
- if (format.inline && match(name, vars, selection.getStart())) {
- moveStart(selection.getRng(true));
- }
-
- ed.nodeChanged();
- } else
- performCaretAction('remove', name, vars);
- };
-
- function toggle(name, vars, node) {
- var fmt = get(name);
-
- if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle))
- remove(name, vars, node);
- else
- apply(name, vars, node);
- };
-
- function matchNode(node, name, vars, similar) {
- var formatList = get(name), format, i, classes;
-
- function matchItems(node, format, item_name) {
- var key, value, items = format[item_name], i;
-
- // Custom match
- if (format.onmatch) {
- return format.onmatch(node, format, item_name);
- }
-
- // Check all items
- if (items) {
- // Non indexed object
- if (items.length === undef) {
- for (key in items) {
- if (items.hasOwnProperty(key)) {
- if (item_name === 'attributes')
- value = dom.getAttrib(node, key);
- else
- value = getStyle(node, key);
-
- if (similar && !value && !format.exact)
- return;
-
- if ((!similar || format.exact) && !isEq(value, replaceVars(items[key], vars)))
- return;
- }
- }
- } else {
- // Only one match needed for indexed arrays
- for (i = 0; i < items.length; i++) {
- if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i]))
- return format;
- }
- }
- }
-
- return format;
- };
-
- if (formatList && node) {
- // Check each format in list
- for (i = 0; i < formatList.length; i++) {
- format = formatList[i];
-
- // Name name, attributes, styles and classes
- if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) {
- // Match classes
- if (classes = format.classes) {
- for (i = 0; i < classes.length; i++) {
- if (!dom.hasClass(node, classes[i]))
- return;
- }
- }
-
- return format;
- }
- }
- }
- };
-
- function match(name, vars, node) {
- var startNode;
-
- function matchParents(node) {
- // Find first node with similar format settings
- node = dom.getParent(node, function(node) {
- return !!matchNode(node, name, vars, true);
- });
-
- // Do an exact check on the similar format element
- return matchNode(node, name, vars);
- };
-
- // Check specified node
- if (node)
- return matchParents(node);
-
- // Check selected node
- node = selection.getNode();
- if (matchParents(node))
- return TRUE;
-
- // Check start node if it's different
- startNode = selection.getStart();
- if (startNode != node) {
- if (matchParents(startNode))
- return TRUE;
- }
-
- return FALSE;
- };
-
- function matchAll(names, vars) {
- var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name;
-
- // Check start of selection for formats
- startElement = selection.getStart();
- dom.getParent(startElement, function(node) {
- var i, name;
-
- for (i = 0; i < names.length; i++) {
- name = names[i];
-
- if (!checkedMap[name] && matchNode(node, name, vars)) {
- checkedMap[name] = true;
- matchedFormatNames.push(name);
- }
- }
- }, dom.getRoot());
-
- return matchedFormatNames;
- };
-
- function canApply(name) {
- var formatList = get(name), startNode, parents, i, x, selector;
-
- if (formatList) {
- startNode = selection.getStart();
- parents = getParents(startNode);
-
- for (x = formatList.length - 1; x >= 0; x--) {
- selector = formatList[x].selector;
-
- // Format is not selector based, then always return TRUE
- if (!selector)
- return TRUE;
-
- for (i = parents.length - 1; i >= 0; i--) {
- if (dom.is(parents[i], selector))
- return TRUE;
- }
- }
- }
-
- return FALSE;
- };
-
- function formatChanged(formats, callback, similar) {
- var currentFormats;
-
- // Setup format node change logic
- if (!formatChangeData) {
- formatChangeData = {};
- currentFormats = {};
-
- ed.onNodeChange.addToTop(function(ed, cm, node) {
- var parents = getParents(node), matchedFormats = {};
-
- // Check for new formats
- each(formatChangeData, function(callbacks, format) {
- each(parents, function(node) {
- if (matchNode(node, format, {}, callbacks.similar)) {
- if (!currentFormats[format]) {
- // Execute callbacks
- each(callbacks, function(callback) {
- callback(true, {node: node, format: format, parents: parents});
- });
-
- currentFormats[format] = callbacks;
- }
-
- matchedFormats[format] = callbacks;
- return false;
- }
- });
- });
-
- // Check if current formats still match
- each(currentFormats, function(callbacks, format) {
- if (!matchedFormats[format]) {
- delete currentFormats[format];
-
- each(callbacks, function(callback) {
- callback(false, {node: node, format: format, parents: parents});
- });
- }
- });
- });
- }
-
- // Add format listeners
- each(formats.split(','), function(format) {
- if (!formatChangeData[format]) {
- formatChangeData[format] = [];
- formatChangeData[format].similar = similar;
- }
-
- formatChangeData[format].push(callback);
- });
-
- return this;
- };
-
- // Expose to public
- tinymce.extend(this, {
- get : get,
- register : register,
- apply : apply,
- remove : remove,
- toggle : toggle,
- match : match,
- matchAll : matchAll,
- matchNode : matchNode,
- canApply : canApply,
- formatChanged: formatChanged
- });
-
- // Initialize
- defaultFormats();
- addKeyboardShortcuts();
-
- // Private functions
-
- function matchName(node, format) {
- // Check for inline match
- if (isEq(node, format.inline))
- return TRUE;
-
- // Check for block match
- if (isEq(node, format.block))
- return TRUE;
-
- // Check for selector match
- if (format.selector)
- return dom.is(node, format.selector);
- };
-
- function isEq(str1, str2) {
- str1 = str1 || '';
- str2 = str2 || '';
-
- str1 = '' + (str1.nodeName || str1);
- str2 = '' + (str2.nodeName || str2);
-
- return str1.toLowerCase() == str2.toLowerCase();
- };
-
- function getStyle(node, name) {
- var styleVal = dom.getStyle(node, name);
-
- // Force the format to hex
- if (name == 'color' || name == 'backgroundColor')
- styleVal = dom.toHex(styleVal);
-
- // Opera will return bold as 700
- if (name == 'fontWeight' && styleVal == 700)
- styleVal = 'bold';
-
- return '' + styleVal;
- };
-
- function replaceVars(value, vars) {
- if (typeof(value) != "string")
- value = value(vars);
- else if (vars) {
- value = value.replace(/%(\w+)/g, function(str, name) {
- return vars[name] || str;
- });
- }
-
- return value;
- };
-
- function isWhiteSpaceNode(node) {
- return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue);
- };
-
- function wrap(node, name, attrs) {
- var wrapper = dom.create(name, attrs);
-
- node.parentNode.insertBefore(wrapper, node);
- wrapper.appendChild(node);
-
- return wrapper;
- };
-
- function expandRng(rng, format, remove) {
- var sibling, lastIdx, leaf, endPoint,
- startContainer = rng.startContainer,
- startOffset = rng.startOffset,
- endContainer = rng.endContainer,
- endOffset = rng.endOffset;
-
- // This function walks up the tree if there is no siblings before/after the node
- function findParentContainer(start) {
- var container, parent, child, sibling, siblingName, root;
-
- container = parent = start ? startContainer : endContainer;
- siblingName = start ? 'previousSibling' : 'nextSibling';
- root = dom.getRoot();
-
- function isBogusBr(node) {
- return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling;
- };
-
- // If it's a text node and the offset is inside the text
- if (container.nodeType == 3 && !isWhiteSpaceNode(container)) {
- if (start ? startOffset > 0 : endOffset < container.nodeValue.length) {
- return container;
- }
- }
-
- for (;;) {
- // Stop expanding on block elements
- if (!format[0].block_expand && isBlock(parent))
- return parent;
-
- // Walk left/right
- for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) {
- if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) {
- return parent;
- }
- }
-
- // Check if we can move up are we at root level or body level
- if (parent.parentNode == root) {
- container = parent;
- break;
- }
-
- parent = parent.parentNode;
- }
-
- return container;
- };
-
- // This function walks down the tree to find the leaf at the selection.
- // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node.
- function findLeaf(node, offset) {
- if (offset === undef)
- offset = node.nodeType === 3 ? node.length : node.childNodes.length;
- while (node && node.hasChildNodes()) {
- node = node.childNodes[offset];
- if (node)
- offset = node.nodeType === 3 ? node.length : node.childNodes.length;
- }
- return { node: node, offset: offset };
- }
-
- // If index based start position then resolve it
- if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
- lastIdx = startContainer.childNodes.length - 1;
- startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset];
-
- if (startContainer.nodeType == 3)
- startOffset = 0;
- }
-
- // If index based end position then resolve it
- if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
- lastIdx = endContainer.childNodes.length - 1;
- endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1];
-
- if (endContainer.nodeType == 3)
- endOffset = endContainer.nodeValue.length;
- }
-
- // Expands the node to the closes contentEditable false element if it exists
- function findParentContentEditable(node) {
- var parent = node;
-
- while (parent) {
- if (parent.nodeType === 1 && getContentEditable(parent)) {
- return getContentEditable(parent) === "false" ? parent : node;
- }
-
- parent = parent.parentNode;
- }
-
- return node;
- };
-
- function findWordEndPoint(container, offset, start) {
- var walker, node, pos, lastTextNode;
-
- function findSpace(node, offset) {
- var pos, pos2, str = node.nodeValue;
-
- if (typeof(offset) == "undefined") {
- offset = start ? str.length : 0;
- }
-
- if (start) {
- pos = str.lastIndexOf(' ', offset);
- pos2 = str.lastIndexOf('\u00a0', offset);
- pos = pos > pos2 ? pos : pos2;
-
- // Include the space on remove to avoid tag soup
- if (pos !== -1 && !remove) {
- pos++;
- }
- } else {
- pos = str.indexOf(' ', offset);
- pos2 = str.indexOf('\u00a0', offset);
- pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2;
- }
-
- return pos;
- };
-
- if (container.nodeType === 3) {
- pos = findSpace(container, offset);
-
- if (pos !== -1) {
- return {container : container, offset : pos};
- }
-
- lastTextNode = container;
- }
-
- // Walk the nodes inside the block
- walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody());
- while (node = walker[start ? 'prev' : 'next']()) {
- if (node.nodeType === 3) {
- lastTextNode = node;
- pos = findSpace(node);
-
- if (pos !== -1) {
- return {container : node, offset : pos};
- }
- } else if (isBlock(node)) {
- break;
- }
- }
-
- if (lastTextNode) {
- if (start) {
- offset = 0;
- } else {
- offset = lastTextNode.length;
- }
-
- return {container: lastTextNode, offset: offset};
- }
- };
-
- function findSelectorEndPoint(container, sibling_name) {
- var parents, i, y, curFormat;
-
- if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name])
- container = container[sibling_name];
-
- parents = getParents(container);
- for (i = 0; i < parents.length; i++) {
- for (y = 0; y < format.length; y++) {
- curFormat = format[y];
-
- // If collapsed state is set then skip formats that doesn't match that
- if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed)
- continue;
-
- if (dom.is(parents[i], curFormat.selector))
- return parents[i];
- }
- }
-
- return container;
- };
-
- function findBlockEndPoint(container, sibling_name, sibling_name2) {
- var node;
-
- // Expand to block of similar type
- if (!format[0].wrapper)
- node = dom.getParent(container, format[0].block);
-
- // Expand to first wrappable block element or any block element
- if (!node)
- node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isTextBlock);
-
- // Exclude inner lists from wrapping
- if (node && format[0].wrapper)
- node = getParents(node, 'ul,ol').reverse()[0] || node;
-
- // Didn't find a block element look for first/last wrappable element
- if (!node) {
- node = container;
-
- while (node[sibling_name] && !isBlock(node[sibling_name])) {
- node = node[sibling_name];
-
- // Break on BR but include it will be removed later on
- // we can't remove it now since we need to check if it can be wrapped
- if (isEq(node, 'br'))
- break;
- }
- }
-
- return node || container;
- };
-
- // Expand to closest contentEditable element
- startContainer = findParentContentEditable(startContainer);
- endContainer = findParentContentEditable(endContainer);
-
- // Exclude bookmark nodes if possible
- if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) {
- startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode;
- startContainer = startContainer.nextSibling || startContainer;
-
- if (startContainer.nodeType == 3)
- startOffset = 0;
- }
-
- if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) {
- endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode;
- endContainer = endContainer.previousSibling || endContainer;
-
- if (endContainer.nodeType == 3)
- endOffset = endContainer.length;
- }
-
- if (format[0].inline) {
- if (rng.collapsed) {
- // Expand left to closest word boundery
- endPoint = findWordEndPoint(startContainer, startOffset, true);
- if (endPoint) {
- startContainer = endPoint.container;
- startOffset = endPoint.offset;
- }
-
- // Expand right to closest word boundery
- endPoint = findWordEndPoint(endContainer, endOffset);
- if (endPoint) {
- endContainer = endPoint.container;
- endOffset = endPoint.offset;
- }
- }
-
- // Avoid applying formatting to a trailing space.
- leaf = findLeaf(endContainer, endOffset);
- if (leaf.node) {
- while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling)
- leaf = findLeaf(leaf.node.previousSibling);
-
- if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 &&
- leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') {
-
- if (leaf.offset > 1) {
- endContainer = leaf.node;
- endContainer.splitText(leaf.offset - 1);
- }
- }
- }
- }
-
- // Move start/end point up the tree if the leaves are sharp and if we are in different containers
- // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>!
- // This will reduce the number of wrapper elements that needs to be created
- // Move start point up the tree
- if (format[0].inline || format[0].block_expand) {
- if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) {
- startContainer = findParentContainer(true);
- }
-
- if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) {
- endContainer = findParentContainer();
- }
- }
-
- // Expand start/end container to matching selector
- if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) {
- // Find new startContainer/endContainer if there is better one
- startContainer = findSelectorEndPoint(startContainer, 'previousSibling');
- endContainer = findSelectorEndPoint(endContainer, 'nextSibling');
- }
-
- // Expand start/end container to matching block element or text node
- if (format[0].block || format[0].selector) {
- // Find new startContainer/endContainer if there is better one
- startContainer = findBlockEndPoint(startContainer, 'previousSibling');
- endContainer = findBlockEndPoint(endContainer, 'nextSibling');
-
- // Non block element then try to expand up the leaf
- if (format[0].block) {
- if (!isBlock(startContainer))
- startContainer = findParentContainer(true);
-
- if (!isBlock(endContainer))
- endContainer = findParentContainer();
- }
- }
-
- // Setup index for startContainer
- if (startContainer.nodeType == 1) {
- startOffset = nodeIndex(startContainer);
- startContainer = startContainer.parentNode;
- }
-
- // Setup index for endContainer
- if (endContainer.nodeType == 1) {
- endOffset = nodeIndex(endContainer) + 1;
- endContainer = endContainer.parentNode;
- }
-
- // Return new range like object
- return {
- startContainer : startContainer,
- startOffset : startOffset,
- endContainer : endContainer,
- endOffset : endOffset
- };
- }
-
- function removeFormat(format, vars, node, compare_node) {
- var i, attrs, stylesModified;
-
- // Check if node matches format
- if (!matchName(node, format))
- return FALSE;
-
- // Should we compare with format attribs and styles
- if (format.remove != 'all') {
- // Remove styles
- each(format.styles, function(value, name) {
- value = replaceVars(value, vars);
-
- // Indexed array
- if (typeof(name) === 'number') {
- name = value;
- compare_node = 0;
- }
-
- if (!compare_node || isEq(getStyle(compare_node, name), value))
- dom.setStyle(node, name, '');
-
- stylesModified = 1;
- });
-
- // Remove style attribute if it's empty
- if (stylesModified && dom.getAttrib(node, 'style') == '') {
- node.removeAttribute('style');
- node.removeAttribute('data-mce-style');
- }
-
- // Remove attributes
- each(format.attributes, function(value, name) {
- var valueOut;
-
- value = replaceVars(value, vars);
-
- // Indexed array
- if (typeof(name) === 'number') {
- name = value;
- compare_node = 0;
- }
-
- if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) {
- // Keep internal classes
- if (name == 'class') {
- value = dom.getAttrib(node, name);
- if (value) {
- // Build new class value where everything is removed except the internal prefixed classes
- valueOut = '';
- each(value.split(/\s+/), function(cls) {
- if (/mce\w+/.test(cls))
- valueOut += (valueOut ? ' ' : '') + cls;
- });
-
- // We got some internal classes left
- if (valueOut) {
- dom.setAttrib(node, name, valueOut);
- return;
- }
- }
- }
-
- // IE6 has a bug where the attribute doesn't get removed correctly
- if (name == "class")
- node.removeAttribute('className');
-
- // Remove mce prefixed attributes
- if (MCE_ATTR_RE.test(name))
- node.removeAttribute('data-mce-' + name);
-
- node.removeAttribute(name);
- }
- });
-
- // Remove classes
- each(format.classes, function(value) {
- value = replaceVars(value, vars);
-
- if (!compare_node || dom.hasClass(compare_node, value))
- dom.removeClass(node, value);
- });
-
- // Check for non internal attributes
- attrs = dom.getAttribs(node);
- for (i = 0; i < attrs.length; i++) {
- if (attrs[i].nodeName.indexOf('_') !== 0)
- return FALSE;
- }
- }
-
- // Remove the inline child if it's empty for example <b> or <span>
- if (format.remove != 'none') {
- removeNode(node, format);
- return TRUE;
- }
- };
-
- function removeNode(node, format) {
- var parentNode = node.parentNode, rootBlockElm;
-
- function find(node, next, inc) {
- node = getNonWhiteSpaceSibling(node, next, inc);
-
- return !node || (node.nodeName == 'BR' || isBlock(node));
- };
-
- if (format.block) {
- if (!forcedRootBlock) {
- // Append BR elements if needed before we remove the block
- if (isBlock(node) && !isBlock(parentNode)) {
- if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1))
- node.insertBefore(dom.create('br'), node.firstChild);
-
- if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1))
- node.appendChild(dom.create('br'));
- }
- } else {
- // Wrap the block in a forcedRootBlock if we are at the root of document
- if (parentNode == dom.getRoot()) {
- if (!format.list_block || !isEq(node, format.list_block)) {
- each(tinymce.grep(node.childNodes), function(node) {
- if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) {
- if (!rootBlockElm)
- rootBlockElm = wrap(node, forcedRootBlock);
- else
- rootBlockElm.appendChild(node);
- } else
- rootBlockElm = 0;
- });
- }
- }
- }
- }
-
- // Never remove nodes that isn't the specified inline element if a selector is specified too
- if (format.selector && format.inline && !isEq(format.inline, node))
- return;
-
- dom.remove(node, 1);
- };
-
- function getNonWhiteSpaceSibling(node, next, inc) {
- if (node) {
- next = next ? 'nextSibling' : 'previousSibling';
-
- for (node = inc ? node : node[next]; node; node = node[next]) {
- if (node.nodeType == 1 || !isWhiteSpaceNode(node))
- return node;
- }
- }
- };
-
- function isBookmarkNode(node) {
- return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark';
- };
-
- function mergeSiblings(prev, next) {
- var marker, sibling, tmpSibling;
-
- function compareElements(node1, node2) {
- // Not the same name
- if (node1.nodeName != node2.nodeName)
- return FALSE;
-
- function getAttribs(node) {
- var attribs = {};
-
- each(dom.getAttribs(node), function(attr) {
- var name = attr.nodeName.toLowerCase();
-
- // Don't compare internal attributes or style
- if (name.indexOf('_') !== 0 && name !== 'style')
- attribs[name] = dom.getAttrib(node, name);
- });
-
- return attribs;
- };
-
- function compareObjects(obj1, obj2) {
- var value, name;
-
- for (name in obj1) {
- // Obj1 has item obj2 doesn't have
- if (obj1.hasOwnProperty(name)) {
- value = obj2[name];
-
- // Obj2 doesn't have obj1 item
- if (value === undef)
- return FALSE;
-
- // Obj2 item has a different value
- if (obj1[name] != value)
- return FALSE;
-
- // Delete similar value
- delete obj2[name];
- }
- }
-
- // Check if obj 2 has something obj 1 doesn't have
- for (name in obj2) {
- // Obj2 has item obj1 doesn't have
- if (obj2.hasOwnProperty(name))
- return FALSE;
- }
-
- return TRUE;
- };
-
- // Attribs are not the same
- if (!compareObjects(getAttribs(node1), getAttribs(node2)))
- return FALSE;
-
- // Styles are not the same
- if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style'))))
- return FALSE;
-
- return TRUE;
- };
-
- function findElementSibling(node, sibling_name) {
- for (sibling = node; sibling; sibling = sibling[sibling_name]) {
- if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0)
- return node;
-
- if (sibling.nodeType == 1 && !isBookmarkNode(sibling))
- return sibling;
- }
-
- return node;
- };
-
- // Check if next/prev exists and that they are elements
- if (prev && next) {
- // If previous sibling is empty then jump over it
- prev = findElementSibling(prev, 'previousSibling');
- next = findElementSibling(next, 'nextSibling');
-
- // Compare next and previous nodes
- if (compareElements(prev, next)) {
- // Append nodes between
- for (sibling = prev.nextSibling; sibling && sibling != next;) {
- tmpSibling = sibling;
- sibling = sibling.nextSibling;
- prev.appendChild(tmpSibling);
- }
-
- // Remove next node
- dom.remove(next);
-
- // Move children into prev node
- each(tinymce.grep(next.childNodes), function(node) {
- prev.appendChild(node);
- });
-
- return prev;
- }
- }
-
- return next;
- };
-
- function isTextBlock(name) {
- return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name);
- };
-
- function getContainer(rng, start) {
- var container, offset, lastIdx, walker;
-
- container = rng[start ? 'startContainer' : 'endContainer'];
- offset = rng[start ? 'startOffset' : 'endOffset'];
-
- if (container.nodeType == 1) {
- lastIdx = container.childNodes.length - 1;
-
- if (!start && offset)
- offset--;
-
- container = container.childNodes[offset > lastIdx ? lastIdx : offset];
- }
-
- // If start text node is excluded then walk to the next node
- if (container.nodeType === 3 && start && offset >= container.nodeValue.length) {
- container = new TreeWalker(container, ed.getBody()).next() || container;
- }
-
- // If end text node is excluded then walk to the previous node
- if (container.nodeType === 3 && !start && offset === 0) {
- container = new TreeWalker(container, ed.getBody()).prev() || container;
- }
-
- return container;
- };
-
- function performCaretAction(type, name, vars) {
- var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug;
-
- // Creates a caret container bogus element
- function createCaretContainer(fill) {
- var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''});
-
- if (fill) {
- caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR));
- }
-
- return caretContainer;
- };
-
- function isCaretContainerEmpty(node, nodes) {
- while (node) {
- if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) {
- return false;
- }
-
- // Collect nodes
- if (nodes && node.nodeType === 1) {
- nodes.push(node);
- }
-
- node = node.firstChild;
- }
-
- return true;
- };
-
- // Returns any parent caret container element
- function getParentCaretContainer(node) {
- while (node) {
- if (node.id === caretContainerId) {
- return node;
- }
-
- node = node.parentNode;
- }
- };
-
- // Finds the first text node in the specified node
- function findFirstTextNode(node) {
- var walker;
-
- if (node) {
- walker = new TreeWalker(node, node);
-
- for (node = walker.current(); node; node = walker.next()) {
- if (node.nodeType === 3) {
- return node;
- }
- }
- }
- };
-
- // Removes the caret container for the specified node or all on the current document
- function removeCaretContainer(node, move_caret) {
- var child, rng;
-
- if (!node) {
- node = getParentCaretContainer(selection.getStart());
-
- if (!node) {
- while (node = dom.get(caretContainerId)) {
- removeCaretContainer(node, false);
- }
- }
- } else {
- rng = selection.getRng(true);
-
- if (isCaretContainerEmpty(node)) {
- if (move_caret !== false) {
- rng.setStartBefore(node);
- rng.setEndBefore(node);
- }
-
- dom.remove(node);
- } else {
- child = findFirstTextNode(node);
-
- if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) {
- child = child.deleteData(0, 1);
- }
-
- dom.remove(node, 1);
- }
-
- selection.setRng(rng);
- }
- };
-
- // Applies formatting to the caret postion
- function applyCaretFormat() {
- var rng, caretContainer, textNode, offset, bookmark, container, text;
-
- rng = selection.getRng(true);
- offset = rng.startOffset;
- container = rng.startContainer;
- text = container.nodeValue;
-
- caretContainer = getParentCaretContainer(selection.getStart());
- if (caretContainer) {
- textNode = findFirstTextNode(caretContainer);
- }
-
- // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character
- if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) {
- // Get bookmark of caret position
- bookmark = selection.getBookmark();
-
- // Collapse bookmark range (WebKit)
- rng.collapse(true);
-
- // Expand the range to the closest word and split it at those points
- rng = expandRng(rng, get(name));
- rng = rangeUtils.split(rng);
-
- // Apply the format to the range
- apply(name, vars, rng);
-
- // Move selection back to caret position
- selection.moveToBookmark(bookmark);
- } else {
- if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) {
- caretContainer = createCaretContainer(true);
- textNode = caretContainer.firstChild;
-
- rng.insertNode(caretContainer);
- offset = 1;
-
- apply(name, vars, caretContainer);
- } else {
- apply(name, vars, caretContainer);
- }
-
- // Move selection to text node
- selection.setCursorLocation(textNode, offset);
- }
- };
-
- function removeCaretFormat() {
- var rng = selection.getRng(true), container, offset, bookmark,
- hasContentAfter, node, formatNode, parents = [], i, caretContainer;
-
- container = rng.startContainer;
- offset = rng.startOffset;
- node = container;
-
- if (container.nodeType == 3) {
- if (offset != container.nodeValue.length || container.nodeValue === INVISIBLE_CHAR) {
- hasContentAfter = true;
- }
-
- node = node.parentNode;
- }
-
- while (node) {
- if (matchNode(node, name, vars)) {
- formatNode = node;
- break;
- }
-
- if (node.nextSibling) {
- hasContentAfter = true;
- }
-
- parents.push(node);
- node = node.parentNode;
- }
-
- // Node doesn't have the specified format
- if (!formatNode) {
- return;
- }
-
- // Is there contents after the caret then remove the format on the element
- if (hasContentAfter) {
- // Get bookmark of caret position
- bookmark = selection.getBookmark();
-
- // Collapse bookmark range (WebKit)
- rng.collapse(true);
-
- // Expand the range to the closest word and split it at those points
- rng = expandRng(rng, get(name), true);
- rng = rangeUtils.split(rng);
-
- // Remove the format from the range
- remove(name, vars, rng);
-
- // Move selection back to caret position
- selection.moveToBookmark(bookmark);
- } else {
- caretContainer = createCaretContainer();
-
- node = caretContainer;
- for (i = parents.length - 1; i >= 0; i--) {
- node.appendChild(dom.clone(parents[i], false));
- node = node.firstChild;
- }
-
- // Insert invisible character into inner most format element
- node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR));
- node = node.firstChild;
-
- // Insert caret container after the formated node
- dom.insertAfter(caretContainer, formatNode);
-
- // Move selection to text node
- selection.setCursorLocation(node, 1);
- }
- };
-
- // Checks if the parent caret container node isn't empty if that is the case it
- // will remove the bogus state on all children that isn't empty
- function unmarkBogusCaretParents() {
- var i, caretContainer, node;
-
- caretContainer = getParentCaretContainer(selection.getStart());
- if (caretContainer && !dom.isEmpty(caretContainer)) {
- tinymce.walk(caretContainer, function(node) {
- if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) {
- dom.setAttrib(node, 'data-mce-bogus', null);
- }
- }, 'childNodes');
- }
- };
-
- // Only bind the caret events once
- if (!self._hasCaretEvents) {
- // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements
- ed.onBeforeGetContent.addToTop(function() {
- var nodes = [], i;
-
- if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) {
- // Mark children
- i = nodes.length;
- while (i--) {
- dom.setAttrib(nodes[i], 'data-mce-bogus', '1');
- }
- }
- });
-
- // Remove caret container on mouse up and on key up
- tinymce.each('onMouseUp onKeyUp'.split(' '), function(name) {
- ed[name].addToTop(function() {
- removeCaretContainer();
- unmarkBogusCaretParents();
- });
- });
-
- // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys
- ed.onKeyDown.addToTop(function(ed, e) {
- var keyCode = e.keyCode;
-
- if (keyCode == 8 || keyCode == 37 || keyCode == 39) {
- removeCaretContainer(getParentCaretContainer(selection.getStart()));
- }
-
- unmarkBogusCaretParents();
- });
-
- // Remove bogus state if they got filled by contents using editor.selection.setContent
- selection.onSetContent.add(unmarkBogusCaretParents);
-
- self._hasCaretEvents = true;
- }
-
- // Do apply or remove caret format
- if (type == "apply") {
- applyCaretFormat();
- } else {
- removeCaretFormat();
- }
- };
-
- function moveStart(rng) {
- var container = rng.startContainer,
- offset = rng.startOffset, isAtEndOfText,
- walker, node, nodes, tmpNode;
-
- // Convert text node into index if possible
- if (container.nodeType == 3 && offset >= container.nodeValue.length) {
- // Get the parent container location and walk from there
- offset = nodeIndex(container);
- container = container.parentNode;
- isAtEndOfText = true;
- }
-
- // Move startContainer/startOffset in to a suitable node
- if (container.nodeType == 1) {
- nodes = container.childNodes;
- container = nodes[Math.min(offset, nodes.length - 1)];
- walker = new TreeWalker(container, dom.getParent(container, dom.isBlock));
-
- // If offset is at end of the parent node walk to the next one
- if (offset > nodes.length - 1 || isAtEndOfText)
- walker.next();
-
- for (node = walker.current(); node; node = walker.next()) {
- if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
- // IE has a "neat" feature where it moves the start node into the closest element
- // we can avoid this by inserting an element before it and then remove it after we set the selection
- tmpNode = dom.create('a', null, INVISIBLE_CHAR);
- node.parentNode.insertBefore(tmpNode, node);
-
- // Set selection and remove tmpNode
- rng.setStart(node, 0);
- selection.setRng(rng);
- dom.remove(tmpNode);
-
- return;
- }
- }
- }
- };
- };
-})(tinymce);
-
-tinymce.onAddEditor.add(function(tinymce, ed) {
- var filters, fontSizes, dom, settings = ed.settings;
-
- function replaceWithSpan(node, styles) {
- tinymce.each(styles, function(value, name) {
- if (value)
- dom.setStyle(node, name, value);
- });
-
- dom.rename(node, 'span');
- };
-
- function convert(editor, params) {
- dom = editor.dom;
-
- if (settings.convert_fonts_to_spans) {
- tinymce.each(dom.select('font,u,strike', params.node), function(node) {
- filters[node.nodeName.toLowerCase()](ed.dom, node);
- });
- }
- };
-
- if (settings.inline_styles) {
- fontSizes = tinymce.explode(settings.font_size_legacy_values);
-
- filters = {
- font : function(dom, node) {
- replaceWithSpan(node, {
- backgroundColor : node.style.backgroundColor,
- color : node.color,
- fontFamily : node.face,
- fontSize : fontSizes[parseInt(node.size, 10) - 1]
- });
- },
-
- u : function(dom, node) {
- replaceWithSpan(node, {
- textDecoration : 'underline'
- });
- },
-
- strike : function(dom, node) {
- replaceWithSpan(node, {
- textDecoration : 'line-through'
- });
- }
- };
-
- ed.onPreProcess.add(convert);
- ed.onSetContent.add(convert);
-
- ed.onInit.add(function() {
- ed.selection.onSetContent.add(convert);
- });
- }
-});
-
-(function(tinymce) {
- var TreeWalker = tinymce.dom.TreeWalker;
-
- tinymce.EnterKey = function(editor) {
- var dom = editor.dom, selection = editor.selection, settings = editor.settings, undoManager = editor.undoManager, nonEmptyElementsMap = editor.schema.getNonEmptyElements();
-
- function handleEnterKey(evt) {
- var rng = selection.getRng(true), tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey,
- newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer;
-
- // Returns true if the block can be split into two blocks or not
- function canSplitBlock(node) {
- return node &&
- dom.isBlock(node) &&
- !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) &&
- !/^(fixed|absolute)/i.test(node.style.position) &&
- dom.getContentEditable(node) !== "true";
- };
-
- // Renders empty block on IE
- function renderBlockOnIE(block) {
- var oldRng;
-
- if (tinymce.isIE && dom.isBlock(block)) {
- oldRng = selection.getRng();
- block.appendChild(dom.create('span', null, '\u00a0'));
- selection.select(block);
- block.lastChild.outerHTML = '';
- selection.setRng(oldRng);
- }
- };
-
- // Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p>
- function trimInlineElementsOnLeftSideOfBlock(block) {
- var node = block, firstChilds = [], i;
-
- // Find inner most first child ex: <p><i><b>*</b></i></p>
- while (node = node.firstChild) {
- if (dom.isBlock(node)) {
- return;
- }
-
- if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
- firstChilds.push(node);
- }
- }
-
- i = firstChilds.length;
- while (i--) {
- node = firstChilds[i];
- if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) {
- dom.remove(node);
- } else {
- // Remove <a> </a> see #5381
- if (node.nodeName == "A" && (node.innerText || node.textContent) === ' ') {
- dom.remove(node);
- }
- }
- }
- };
-
- // Moves the caret to a suitable position within the root for example in the first non pure whitespace text node or before an image
- function moveToCaretPosition(root) {
- var walker, node, rng, y, viewPort, lastNode = root, tempElm;
-
- rng = dom.createRng();
-
- if (root.hasChildNodes()) {
- walker = new TreeWalker(root, root);
-
- while (node = walker.current()) {
- if (node.nodeType == 3) {
- rng.setStart(node, 0);
- rng.setEnd(node, 0);
- break;
- }
-
- if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
- rng.setStartBefore(node);
- rng.setEndBefore(node);
- break;
- }
-
- lastNode = node;
- node = walker.next();
- }
-
- if (!node) {
- rng.setStart(lastNode, 0);
- rng.setEnd(lastNode, 0);
- }
- } else {
- if (root.nodeName == 'BR') {
- if (root.nextSibling && dom.isBlock(root.nextSibling)) {
- // Trick on older IE versions to render the caret before the BR between two lists
- if (!documentMode || documentMode < 9) {
- tempElm = dom.create('br');
- root.parentNode.insertBefore(tempElm, root);
- }
-
- rng.setStartBefore(root);
- rng.setEndBefore(root);
- } else {
- rng.setStartAfter(root);
- rng.setEndAfter(root);
- }
- } else {
- rng.setStart(root, 0);
- rng.setEnd(root, 0);
- }
- }
-
- selection.setRng(rng);
-
- // Remove tempElm created for old IE:s
- dom.remove(tempElm);
-
- viewPort = dom.getViewPort(editor.getWin());
-
- // scrollIntoView seems to scroll the parent window in most browsers now including FF 3.0b4 so it's time to stop using it and do it our selfs
- y = dom.getPos(root).y;
- if (y < viewPort.y || y + 25 > viewPort.y + viewPort.h) {
- editor.getWin().scrollTo(0, y < viewPort.y ? y : y - viewPort.h + 25); // Needs to be hardcoded to roughly one line of text if a huge text block is broken into two blocks
- }
- };
-
- // Creates a new block element by cloning the current one or creating a new one if the name is specified
- // This function will also copy any text formatting from the parent block and add it to the new one
- function createNewBlock(name) {
- var node = container, block, clonedNode, caretNode;
-
- block = name || parentBlockName == "TABLE" ? dom.create(name || newBlockName) : parentBlock.cloneNode(false);
- caretNode = block;
-
- // Clone any parent styles
- if (settings.keep_styles !== false) {
- do {
- if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(node.nodeName)) {
- // Never clone a caret containers
- if (node.id == '_mce_caret') {
- continue;
- }
-
- clonedNode = node.cloneNode(false);
- dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique
-
- if (block.hasChildNodes()) {
- clonedNode.appendChild(block.firstChild);
- block.appendChild(clonedNode);
- } else {
- caretNode = clonedNode;
- block.appendChild(clonedNode);
- }
- }
- } while (node = node.parentNode);
- }
-
- // BR is needed in empty blocks on non IE browsers
- if (!tinymce.isIE) {
- caretNode.innerHTML = '<br data-mce-bogus="1">';
- }
-
- return block;
- };
-
- // Returns true/false if the caret is at the start/end of the parent block element
- function isCaretAtStartOrEndOfBlock(start) {
- var walker, node, name;
-
- // Caret is in the middle of a text node like "a|b"
- if (container.nodeType == 3 && (start ? offset > 0 : offset < container.nodeValue.length)) {
- return false;
- }
-
- // If after the last element in block node edge case for #5091
- if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) {
- return true;
- }
-
- // If the caret if before the first element in parentBlock
- if (start && container.nodeType == 1 && container == parentBlock.firstChild) {
- return true;
- }
-
- // Caret can be before/after a table
- if (container.nodeName === "TABLE" || (container.previousSibling && container.previousSibling.nodeName == "TABLE")) {
- return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start);
- }
-
- // Walk the DOM and look for text nodes or non empty elements
- walker = new TreeWalker(container, parentBlock);
-
- // If caret is in beginning or end of a text block then jump to the next/previous node
- if (container.nodeType == 3) {
- if (start && offset == 0) {
- walker.prev();
- } else if (!start && offset == container.nodeValue.length) {
- walker.next();
- }
- }
-
- while (node = walker.current()) {
- if (node.nodeType === 1) {
- // Ignore bogus elements
- if (!node.getAttribute('data-mce-bogus')) {
- // Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p>
- name = node.nodeName.toLowerCase();
- if (nonEmptyElementsMap[name] && name !== 'br') {
- return false;
- }
- }
- } else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) {
- return false;
- }
-
- if (start) {
- walker.prev();
- } else {
- walker.next();
- }
- }
-
- return true;
- };
-
- // Wraps any text nodes or inline elements in the specified forced root block name
- function wrapSelfAndSiblingsInDefaultBlock(container, offset) {
- var newBlock, parentBlock, startNode, node, next, blockName = newBlockName || 'P';
-
- // Not in a block element or in a table cell or caption
- parentBlock = dom.getParent(container, dom.isBlock);
- if (!parentBlock || !canSplitBlock(parentBlock)) {
- parentBlock = parentBlock || editableRoot;
-
- if (!parentBlock.hasChildNodes()) {
- newBlock = dom.create(blockName);
- parentBlock.appendChild(newBlock);
- rng.setStart(newBlock, 0);
- rng.setEnd(newBlock, 0);
- return newBlock;
- }
-
- // Find parent that is the first child of parentBlock
- node = container;
- while (node.parentNode != parentBlock) {
- node = node.parentNode;
- }
-
- // Loop left to find start node start wrapping at
- while (node && !dom.isBlock(node)) {
- startNode = node;
- node = node.previousSibling;
- }
-
- if (startNode) {
- newBlock = dom.create(blockName);
- startNode.parentNode.insertBefore(newBlock, startNode);
-
- // Start wrapping until we hit a block
- node = startNode;
- while (node && !dom.isBlock(node)) {
- next = node.nextSibling;
- newBlock.appendChild(node);
- node = next;
- }
-
- // Restore range to it's past location
- rng.setStart(container, offset);
- rng.setEnd(container, offset);
- }
- }
-
- return container;
- };
-
- // Inserts a block or br before/after or in the middle of a split list of the LI is empty
- function handleEmptyListItem() {
- function isFirstOrLastLi(first) {
- var node = containerBlock[first ? 'firstChild' : 'lastChild'];
-
- // Find first/last element since there might be whitespace there
- while (node) {
- if (node.nodeType == 1) {
- break;
- }
-
- node = node[first ? 'nextSibling' : 'previousSibling'];
- }
-
- return node === parentBlock;
- };
-
- newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR');
-
- if (isFirstOrLastLi(true) && isFirstOrLastLi()) {
- // Is first and last list item then replace the OL/UL with a text block
- dom.replace(newBlock, containerBlock);
- } else if (isFirstOrLastLi(true)) {
- // First LI in list then remove LI and add text block before list
- containerBlock.parentNode.insertBefore(newBlock, containerBlock);
- } else if (isFirstOrLastLi()) {
- // Last LI in list then temove LI and add text block after list
- dom.insertAfter(newBlock, containerBlock);
- renderBlockOnIE(newBlock);
- } else {
- // Middle LI in list the split the list and insert a text block in the middle
- // Extract after fragment and insert it after the current block
- tmpRng = rng.cloneRange();
- tmpRng.setStartAfter(parentBlock);
- tmpRng.setEndAfter(containerBlock);
- fragment = tmpRng.extractContents();
- dom.insertAfter(fragment, containerBlock);
- dom.insertAfter(newBlock, containerBlock);
- }
-
- dom.remove(parentBlock);
- moveToCaretPosition(newBlock);
- undoManager.add();
- };
-
- // Walks the parent block to the right and look for BR elements
- function hasRightSideBr() {
- var walker = new TreeWalker(container, parentBlock), node;
-
- while (node = walker.current()) {
- if (node.nodeName == 'BR') {
- return true;
- }
-
- node = walker.next();
- }
- }
-
- // Inserts a BR element if the forced_root_block option is set to false or empty string
- function insertBr() {
- var brElm, extraBr;
-
- if (container && container.nodeType == 3 && offset >= container.nodeValue.length) {
- // Insert extra BR element at the end block elements
- if (!tinymce.isIE && !hasRightSideBr()) {
- brElm = dom.create('br');
- rng.insertNode(brElm);
- rng.setStartAfter(brElm);
- rng.setEndAfter(brElm);
- extraBr = true;
- }
- }
-
- brElm = dom.create('br');
- rng.insertNode(brElm);
-
- // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it
- if (tinymce.isIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) {
- brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm);
- }
-
- if (!extraBr) {
- rng.setStartAfter(brElm);
- rng.setEndAfter(brElm);
- } else {
- rng.setStartBefore(brElm);
- rng.setEndBefore(brElm);
- }
-
- selection.setRng(rng);
- undoManager.add();
- };
-
- // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element
- function trimLeadingLineBreaks(node) {
- do {
- if (node.nodeType === 3) {
- node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, '');
- }
-
- node = node.firstChild;
- } while (node);
- };
-
- function getEditableRoot(node) {
- var root = dom.getRoot(), parent, editableRoot;
-
- // Get all parents until we hit a non editable parent or the root
- parent = node;
- while (parent !== root && dom.getContentEditable(parent) !== "false") {
- if (dom.getContentEditable(parent) === "true") {
- editableRoot = parent;
- }
-
- parent = parent.parentNode;
- }
-
- return parent !== root ? editableRoot : root;
- };
-
- // Adds a BR at the end of blocks that only contains an IMG or INPUT since these might be floated and then they won't expand the block
- function addBrToBlockIfNeeded(block) {
- var lastChild;
-
- // IE will render the blocks correctly other browsers needs a BR
- if (!tinymce.isIE) {
- block.normalize(); // Remove empty text nodes that got left behind by the extract
-
- // Check if the block is empty or contains a floated last child
- lastChild = block.lastChild;
- if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) {
- dom.add(block, 'br');
- }
- }
- };
-
- // Delete any selected contents
- if (!rng.collapsed) {
- editor.execCommand('Delete');
- return;
- }
-
- // Event is blocked by some other handler for example the lists plugin
- if (evt.isDefaultPrevented()) {
- return;
- }
-
- // Setup range items and newBlockName
- container = rng.startContainer;
- offset = rng.startOffset;
- newBlockName = (settings.force_p_newlines ? 'p' : '') || settings.forced_root_block;
- newBlockName = newBlockName ? newBlockName.toUpperCase() : '';
- documentMode = dom.doc.documentMode;
- shiftKey = evt.shiftKey;
-
- // Resolve node index
- if (container.nodeType == 1 && container.hasChildNodes()) {
- isAfterLastNodeInContainer = offset > container.childNodes.length - 1;
- container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
- if (isAfterLastNodeInContainer && container.nodeType == 3) {
- offset = container.nodeValue.length;
- } else {
- offset = 0;
- }
- }
-
- // Get editable root node normaly the body element but sometimes a div or span
- editableRoot = getEditableRoot(container);
-
- // If there is no editable root then enter is done inside a contentEditable false element
- if (!editableRoot) {
- return;
- }
-
- undoManager.beforeChange();
-
- // If editable root isn't block nor the root of the editor
- if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) {
- if (!newBlockName || shiftKey) {
- insertBr();
- }
-
- return;
- }
-
- // Wrap the current node and it's sibling in a default block if it's needed.
- // for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td>
- // This won't happen if root blocks are disabled or the shiftKey is pressed
- if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) {
- container = wrapSelfAndSiblingsInDefaultBlock(container, offset);
- }
-
- // Find parent block and setup empty block paddings
- parentBlock = dom.getParent(container, dom.isBlock);
- containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null;
-
- // Setup block names
- parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
- containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
-
- // Enter inside block contained within a LI then split or insert before/after LI
- if (containerBlockName == 'LI' && !evt.ctrlKey) {
- parentBlock = containerBlock;
- parentBlockName = containerBlockName;
- }
-
- // Handle enter in LI
- if (parentBlockName == 'LI') {
- if (!newBlockName && shiftKey) {
- insertBr();
- return;
- }
-
- // Handle enter inside an empty list item
- if (dom.isEmpty(parentBlock)) {
- // Let the list plugin or browser handle nested lists for now
- if (/^(UL|OL|LI)$/.test(containerBlock.parentNode.nodeName)) {
- return false;
- }
-
- handleEmptyListItem();
- return;
- }
- }
-
- // Don't split PRE tags but insert a BR instead easier when writing code samples etc
- if (parentBlockName == 'PRE' && settings.br_in_pre !== false) {
- if (!shiftKey) {
- insertBr();
- return;
- }
- } else {
- // If no root block is configured then insert a BR by default or if the shiftKey is pressed
- if ((!newBlockName && !shiftKey && parentBlockName != 'LI') || (newBlockName && shiftKey)) {
- insertBr();
- return;
- }
- }
-
- // Default block name if it's not configured
- newBlockName = newBlockName || 'P';
-
- // Insert new block before/after the parent block depending on caret location
- if (isCaretAtStartOrEndOfBlock()) {
- // If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup
- if (/^(H[1-6]|PRE)$/.test(parentBlockName) && containerBlockName != 'HGROUP') {
- newBlock = createNewBlock(newBlockName);
- } else {
- newBlock = createNewBlock();
- }
-
- // Split the current container block element if enter is pressed inside an empty inner block element
- if (settings.end_container_on_empty_block && canSplitBlock(containerBlock) && dom.isEmpty(parentBlock)) {
- // Split container block for example a BLOCKQUOTE at the current blockParent location for example a P
- newBlock = dom.split(containerBlock, parentBlock);
- } else {
- dom.insertAfter(newBlock, parentBlock);
- }
-
- moveToCaretPosition(newBlock);
- } else if (isCaretAtStartOrEndOfBlock(true)) {
- // Insert new block before
- newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock);
- renderBlockOnIE(newBlock);
- } else {
- // Extract after fragment and insert it after the current block
- tmpRng = rng.cloneRange();
- tmpRng.setEndAfter(parentBlock);
- fragment = tmpRng.extractContents();
- trimLeadingLineBreaks(fragment);
- newBlock = fragment.firstChild;
- dom.insertAfter(fragment, parentBlock);
- trimInlineElementsOnLeftSideOfBlock(newBlock);
- addBrToBlockIfNeeded(parentBlock);
- moveToCaretPosition(newBlock);
- }
-
- dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique
- undoManager.add();
- }
-
- editor.onKeyDown.add(function(ed, evt) {
- if (evt.keyCode == 13) {
- if (handleEnterKey(evt) !== false) {
- evt.preventDefault();
- }
- }
- });
- };
-})(tinymce);
-
diff --git a/resource/tinymce/tiny_mce_popup.js b/resource/tinymce/tiny_mce_popup.js
@@ -1,5 +0,0 @@
-
-// Uncomment and change this document.domain value if you are loading the script cross subdomains
-// document.domain = 'moxiecode.com';
-
-var tinymce=null,tinyMCEPopup,tinyMCE;tinyMCEPopup={init:function(){var b=this,a,c;a=b.getWin();tinymce=a.tinymce;tinyMCE=a.tinyMCE;b.editor=tinymce.EditorManager.activeEditor;b.params=b.editor.windowManager.params;b.features=b.editor.windowManager.features;b.dom=b.editor.windowManager.createInstance("tinymce.dom.DOMUtils",document,{ownEvents:true,proxy:tinyMCEPopup._eventProxy});b.dom.bind(window,"ready",b._onDOMLoaded,b);if(b.features.popup_css!==false){b.dom.loadCSS(b.features.popup_css||b.editor.settings.popup_css)}b.listeners=[];b.onInit={add:function(e,d){b.listeners.push({func:e,scope:d})}};b.isWindow=!b.getWindowArg("mce_inline");b.id=b.getWindowArg("mce_window_id");b.editor.windowManager.onOpen.dispatch(b.editor.windowManager,window)},getWin:function(){return(!window.frameElement&&window.dialogArguments)||opener||parent||top},getWindowArg:function(c,b){var a=this.params[c];return tinymce.is(a)?a:b},getParam:function(b,a){return this.editor.getParam(b,a)},getLang:function(b,a){return this.editor.getLang(b,a)},execCommand:function(d,c,e,b){b=b||{};b.skip_focus=1;this.restoreSelection();return this.editor.execCommand(d,c,e,b)},resizeToInnerSize:function(){var a=this;setTimeout(function(){var b=a.dom.getViewPort(window);a.editor.windowManager.resizeBy(a.getWindowArg("mce_width")-b.w,a.getWindowArg("mce_height")-b.h,a.id||window)},10)},executeOnLoad:function(s){this.onInit.add(function(){eval(s)})},storeSelection:function(){this.editor.windowManager.bookmark=tinyMCEPopup.editor.selection.getBookmark(1)},restoreSelection:function(){var a=tinyMCEPopup;if(!a.isWindow&&tinymce.isIE){a.editor.selection.moveToBookmark(a.editor.windowManager.bookmark)}},requireLangPack:function(){var b=this,a=b.getWindowArg("plugin_url")||b.getWindowArg("theme_url");if(a&&b.editor.settings.language&&b.features.translate_i18n!==false&&b.editor.settings.language_load!==false){a+="/langs/"+b.editor.settings.language+"_dlg.js";if(!tinymce.ScriptLoader.isDone(a)){document.write('<script type="text/javascript" src="'+tinymce._addVer(a)+'"><\/script>');tinymce.ScriptLoader.markDone(a)}}},pickColor:function(b,a){this.execCommand("mceColorPicker",true,{color:document.getElementById(a).value,func:function(e){document.getElementById(a).value=e;try{document.getElementById(a).onchange()}catch(d){}}})},openBrowser:function(a,c,b){tinyMCEPopup.restoreSelection();this.editor.execCallback("file_browser_callback",a,document.getElementById(a).value,c,window)},confirm:function(b,a,c){this.editor.windowManager.confirm(b,a,c,window)},alert:function(b,a,c){this.editor.windowManager.alert(b,a,c,window)},close:function(){var a=this;function b(){a.editor.windowManager.close(window);tinymce=tinyMCE=a.editor=a.params=a.dom=a.dom.doc=null}if(tinymce.isOpera){a.getWin().setTimeout(b,0)}else{b()}},_restoreSelection:function(){var a=window.event.srcElement;if(a.nodeName=="INPUT"&&(a.type=="submit"||a.type=="button")){tinyMCEPopup.restoreSelection()}},_onDOMLoaded:function(){var b=tinyMCEPopup,d=document.title,e,c,a;if(b.features.translate_i18n!==false){c=document.body.innerHTML;if(tinymce.isIE){c=c.replace(/ (value|title|alt)=([^"][^\s>]+)/gi,' $1="$2"')}document.dir=b.editor.getParam("directionality","");if((a=b.editor.translate(c))&&a!=c){document.body.innerHTML=a}if((a=b.editor.translate(d))&&a!=d){document.title=d=a}}if(!b.editor.getParam("browser_preferred_colors",false)||!b.isWindow){b.dom.addClass(document.body,"forceColors")}document.body.style.display="";if(tinymce.isIE){document.attachEvent("onmouseup",tinyMCEPopup._restoreSelection);b.dom.add(b.dom.select("head")[0],"base",{target:"_self"})}b.restoreSelection();b.resizeToInnerSize();if(!b.isWindow){b.editor.windowManager.setTitle(window,d)}else{window.focus()}if(!tinymce.isIE&&!b.isWindow){b.dom.bind(document,"focus",function(){b.editor.windowManager.focus(b.id)})}tinymce.each(b.dom.select("select"),function(f){f.onkeydown=tinyMCEPopup._accessHandler});tinymce.each(b.listeners,function(f){f.func.call(f.scope,b.editor)});if(b.getWindowArg("mce_auto_focus",true)){window.focus();tinymce.each(document.forms,function(g){tinymce.each(g.elements,function(f){if(b.dom.hasClass(f,"mceFocus")&&!f.disabled){f.focus();return false}})})}document.onkeyup=tinyMCEPopup._closeWinKeyHandler},_accessHandler:function(a){a=a||window.event;if(a.keyCode==13||a.keyCode==32){var b=a.target||a.srcElement;if(b.onchange){b.onchange()}return tinymce.dom.Event.cancel(a)}},_closeWinKeyHandler:function(a){a=a||window.event;if(a.keyCode==27){tinyMCEPopup.close()}},_eventProxy:function(a){return function(b){tinyMCEPopup.dom.events.callNativeHandler(a,b)}}};tinyMCEPopup.init();
-\ No newline at end of file
diff --git a/resource/tinymce/tinymce.js b/resource/tinymce/tinymce.js
@@ -0,0 +1,48792 @@
+// 4.5.1 (2016-12-07)
+
+/**
+ * Compiled inline version. (Library mode)
+ */
+
+/*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */
+/*globals $code */
+
+(function(exports, undefined) {
+ "use strict";
+
+ var modules = {};
+
+ function require(ids, callback) {
+ var module, defs = [];
+
+ for (var i = 0; i < ids.length; ++i) {
+ module = modules[ids[i]] || resolve(ids[i]);
+ if (!module) {
+ throw 'module definition dependecy not found: ' + ids[i];
+ }
+
+ defs.push(module);
+ }
+
+ callback.apply(null, defs);
+ }
+
+ function define(id, dependencies, definition) {
+ if (typeof id !== 'string') {
+ throw 'invalid module definition, module id must be defined and be a string';
+ }
+
+ if (dependencies === undefined) {
+ throw 'invalid module definition, dependencies must be specified';
+ }
+
+ if (definition === undefined) {
+ throw 'invalid module definition, definition function must be specified';
+ }
+
+ require(dependencies, function() {
+ modules[id] = definition.apply(null, arguments);
+ });
+ }
+
+ function defined(id) {
+ return !!modules[id];
+ }
+
+ function resolve(id) {
+ var target = exports;
+ var fragments = id.split(/[.\/]/);
+
+ for (var fi = 0; fi < fragments.length; ++fi) {
+ if (!target[fragments[fi]]) {
+ return;
+ }
+
+ target = target[fragments[fi]];
+ }
+
+ return target;
+ }
+
+ function expose(ids) {
+ var i, target, id, fragments, privateModules;
+
+ for (i = 0; i < ids.length; i++) {
+ target = exports;
+ id = ids[i];
+ fragments = id.split(/[.\/]/);
+
+ for (var fi = 0; fi < fragments.length - 1; ++fi) {
+ if (target[fragments[fi]] === undefined) {
+ target[fragments[fi]] = {};
+ }
+
+ target = target[fragments[fi]];
+ }
+
+ target[fragments[fragments.length - 1]] = modules[id];
+ }
+
+ // Expose private modules for unit tests
+ if (exports.AMDLC_TESTS) {
+ privateModules = exports.privateModules || {};
+
+ for (id in modules) {
+ privateModules[id] = modules[id];
+ }
+
+ for (i = 0; i < ids.length; i++) {
+ delete privateModules[ids[i]];
+ }
+
+ exports.privateModules = privateModules;
+ }
+ }
+
+// Included from: js/tinymce/classes/geom/Rect.js
+
+/**
+ * Rect.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Contains various tools for rect/position calculation.
+ *
+ * @class tinymce.geom.Rect
+ */
+define("tinymce/geom/Rect", [
+], function() {
+ "use strict";
+
+ var min = Math.min, max = Math.max, round = Math.round;
+
+ /**
+ * Returns the rect positioned based on the relative position name
+ * to the target rect.
+ *
+ * @method relativePosition
+ * @param {Rect} rect Source rect to modify into a new rect.
+ * @param {Rect} targetRect Rect to move relative to based on the rel option.
+ * @param {String} rel Relative position. For example: tr-bl.
+ */
+ function relativePosition(rect, targetRect, rel) {
+ var x, y, w, h, targetW, targetH;
+
+ x = targetRect.x;
+ y = targetRect.y;
+ w = rect.w;
+ h = rect.h;
+ targetW = targetRect.w;
+ targetH = targetRect.h;
+
+ rel = (rel || '').split('');
+
+ if (rel[0] === 'b') {
+ y += targetH;
+ }
+
+ if (rel[1] === 'r') {
+ x += targetW;
+ }
+
+ if (rel[0] === 'c') {
+ y += round(targetH / 2);
+ }
+
+ if (rel[1] === 'c') {
+ x += round(targetW / 2);
+ }
+
+ if (rel[3] === 'b') {
+ y -= h;
+ }
+
+ if (rel[4] === 'r') {
+ x -= w;
+ }
+
+ if (rel[3] === 'c') {
+ y -= round(h / 2);
+ }
+
+ if (rel[4] === 'c') {
+ x -= round(w / 2);
+ }
+
+ return create(x, y, w, h);
+ }
+
+ /**
+ * Tests various positions to get the most suitable one.
+ *
+ * @method findBestRelativePosition
+ * @param {Rect} rect Rect to use as source.
+ * @param {Rect} targetRect Rect to move relative to.
+ * @param {Rect} constrainRect Rect to constrain within.
+ * @param {Array} rels Array of relative positions to test against.
+ */
+ function findBestRelativePosition(rect, targetRect, constrainRect, rels) {
+ var pos, i;
+
+ for (i = 0; i < rels.length; i++) {
+ pos = relativePosition(rect, targetRect, rels[i]);
+
+ if (pos.x >= constrainRect.x && pos.x + pos.w <= constrainRect.w + constrainRect.x &&
+ pos.y >= constrainRect.y && pos.y + pos.h <= constrainRect.h + constrainRect.y) {
+ return rels[i];
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Inflates the rect in all directions.
+ *
+ * @method inflate
+ * @param {Rect} rect Rect to expand.
+ * @param {Number} w Relative width to expand by.
+ * @param {Number} h Relative height to expand by.
+ * @return {Rect} New expanded rect.
+ */
+ function inflate(rect, w, h) {
+ return create(rect.x - w, rect.y - h, rect.w + w * 2, rect.h + h * 2);
+ }
+
+ /**
+ * Returns the intersection of the specified rectangles.
+ *
+ * @method intersect
+ * @param {Rect} rect The first rectangle to compare.
+ * @param {Rect} cropRect The second rectangle to compare.
+ * @return {Rect} The intersection of the two rectangles or null if they don't intersect.
+ */
+ function intersect(rect, cropRect) {
+ var x1, y1, x2, y2;
+
+ x1 = max(rect.x, cropRect.x);
+ y1 = max(rect.y, cropRect.y);
+ x2 = min(rect.x + rect.w, cropRect.x + cropRect.w);
+ y2 = min(rect.y + rect.h, cropRect.y + cropRect.h);
+
+ if (x2 - x1 < 0 || y2 - y1 < 0) {
+ return null;
+ }
+
+ return create(x1, y1, x2 - x1, y2 - y1);
+ }
+
+ /**
+ * Returns a rect clamped within the specified clamp rect. This forces the
+ * rect to be inside the clamp rect.
+ *
+ * @method clamp
+ * @param {Rect} rect Rectangle to force within clamp rect.
+ * @param {Rect} clampRect Rectable to force within.
+ * @param {Boolean} fixedSize True/false if size should be fixed.
+ * @return {Rect} Clamped rect.
+ */
+ function clamp(rect, clampRect, fixedSize) {
+ var underflowX1, underflowY1, overflowX2, overflowY2,
+ x1, y1, x2, y2, cx2, cy2;
+
+ x1 = rect.x;
+ y1 = rect.y;
+ x2 = rect.x + rect.w;
+ y2 = rect.y + rect.h;
+ cx2 = clampRect.x + clampRect.w;
+ cy2 = clampRect.y + clampRect.h;
+
+ underflowX1 = max(0, clampRect.x - x1);
+ underflowY1 = max(0, clampRect.y - y1);
+ overflowX2 = max(0, x2 - cx2);
+ overflowY2 = max(0, y2 - cy2);
+
+ x1 += underflowX1;
+ y1 += underflowY1;
+
+ if (fixedSize) {
+ x2 += underflowX1;
+ y2 += underflowY1;
+ x1 -= overflowX2;
+ y1 -= overflowY2;
+ }
+
+ x2 -= overflowX2;
+ y2 -= overflowY2;
+
+ return create(x1, y1, x2 - x1, y2 - y1);
+ }
+
+ /**
+ * Creates a new rectangle object.
+ *
+ * @method create
+ * @param {Number} x Rectangle x location.
+ * @param {Number} y Rectangle y location.
+ * @param {Number} w Rectangle width.
+ * @param {Number} h Rectangle height.
+ * @return {Rect} New rectangle object.
+ */
+ function create(x, y, w, h) {
+ return {x: x, y: y, w: w, h: h};
+ }
+
+ /**
+ * Creates a new rectangle object form a clientRects object.
+ *
+ * @method fromClientRect
+ * @param {ClientRect} clientRect DOM ClientRect object.
+ * @return {Rect} New rectangle object.
+ */
+ function fromClientRect(clientRect) {
+ return create(clientRect.left, clientRect.top, clientRect.width, clientRect.height);
+ }
+
+ return {
+ inflate: inflate,
+ relativePosition: relativePosition,
+ findBestRelativePosition: findBestRelativePosition,
+ intersect: intersect,
+ clamp: clamp,
+ create: create,
+ fromClientRect: fromClientRect
+ };
+});
+
+// Included from: js/tinymce/classes/util/Promise.js
+
+/**
+ * Promise.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * Promise polyfill under MIT license: https://github.com/taylorhakes/promise-polyfill
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/* eslint-disable */
+/* jshint ignore:start */
+
+/**
+ * Modifed to be a feature fill and wrapped as tinymce module.
+ */
+define("tinymce/util/Promise", [], function() {
+ if (window.Promise) {
+ return window.Promise;
+ }
+
+ // Use polyfill for setImmediate for performance gains
+ var asap = Promise.immediateFn || (typeof setImmediate === 'function' && setImmediate) ||
+ function(fn) { setTimeout(fn, 1); };
+
+ // Polyfill for Function.prototype.bind
+ function bind(fn, thisArg) {
+ return function() {
+ fn.apply(thisArg, arguments);
+ };
+ }
+
+ var isArray = Array.isArray || function(value) { return Object.prototype.toString.call(value) === "[object Array]"; };
+
+ function Promise(fn) {
+ if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new');
+ if (typeof fn !== 'function') throw new TypeError('not a function');
+ this._state = null;
+ this._value = null;
+ this._deferreds = [];
+
+ doResolve(fn, bind(resolve, this), bind(reject, this));
+ }
+
+ function handle(deferred) {
+ var me = this;
+ if (this._state === null) {
+ this._deferreds.push(deferred);
+ return;
+ }
+ asap(function() {
+ var cb = me._state ? deferred.onFulfilled : deferred.onRejected;
+ if (cb === null) {
+ (me._state ? deferred.resolve : deferred.reject)(me._value);
+ return;
+ }
+ var ret;
+ try {
+ ret = cb(me._value);
+ }
+ catch (e) {
+ deferred.reject(e);
+ return;
+ }
+ deferred.resolve(ret);
+ });
+ }
+
+ function resolve(newValue) {
+ try { //Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure
+ if (newValue === this) throw new TypeError('A promise cannot be resolved with itself.');
+ if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
+ var then = newValue.then;
+ if (typeof then === 'function') {
+ doResolve(bind(then, newValue), bind(resolve, this), bind(reject, this));
+ return;
+ }
+ }
+ this._state = true;
+ this._value = newValue;
+ finale.call(this);
+ } catch (e) { reject.call(this, e); }
+ }
+
+ function reject(newValue) {
+ this._state = false;
+ this._value = newValue;
+ finale.call(this);
+ }
+
+ function finale() {
+ for (var i = 0, len = this._deferreds.length; i < len; i++) {
+ handle.call(this, this._deferreds[i]);
+ }
+ this._deferreds = null;
+ }
+
+ function Handler(onFulfilled, onRejected, resolve, reject){
+ this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
+ this.onRejected = typeof onRejected === 'function' ? onRejected : null;
+ this.resolve = resolve;
+ this.reject = reject;
+ }
+
+ /**
+ * Take a potentially misbehaving resolver function and make sure
+ * onFulfilled and onRejected are only called once.
+ *
+ * Makes no guarantees about asynchrony.
+ */
+ function doResolve(fn, onFulfilled, onRejected) {
+ var done = false;
+ try {
+ fn(function (value) {
+ if (done) return;
+ done = true;
+ onFulfilled(value);
+ }, function (reason) {
+ if (done) return;
+ done = true;
+ onRejected(reason);
+ });
+ } catch (ex) {
+ if (done) return;
+ done = true;
+ onRejected(ex);
+ }
+ }
+
+ Promise.prototype['catch'] = function (onRejected) {
+ return this.then(null, onRejected);
+ };
+
+ Promise.prototype.then = function(onFulfilled, onRejected) {
+ var me = this;
+ return new Promise(function(resolve, reject) {
+ handle.call(me, new Handler(onFulfilled, onRejected, resolve, reject));
+ });
+ };
+
+ Promise.all = function () {
+ var args = Array.prototype.slice.call(arguments.length === 1 && isArray(arguments[0]) ? arguments[0] : arguments);
+
+ return new Promise(function (resolve, reject) {
+ if (args.length === 0) return resolve([]);
+ var remaining = args.length;
+ function res(i, val) {
+ try {
+ if (val && (typeof val === 'object' || typeof val === 'function')) {
+ var then = val.then;
+ if (typeof then === 'function') {
+ then.call(val, function (val) { res(i, val); }, reject);
+ return;
+ }
+ }
+ args[i] = val;
+ if (--remaining === 0) {
+ resolve(args);
+ }
+ } catch (ex) {
+ reject(ex);
+ }
+ }
+ for (var i = 0; i < args.length; i++) {
+ res(i, args[i]);
+ }
+ });
+ };
+
+ Promise.resolve = function (value) {
+ if (value && typeof value === 'object' && value.constructor === Promise) {
+ return value;
+ }
+
+ return new Promise(function (resolve) {
+ resolve(value);
+ });
+ };
+
+ Promise.reject = function (value) {
+ return new Promise(function (resolve, reject) {
+ reject(value);
+ });
+ };
+
+ Promise.race = function (values) {
+ return new Promise(function (resolve, reject) {
+ for(var i = 0, len = values.length; i < len; i++) {
+ values[i].then(resolve, reject);
+ }
+ });
+ };
+
+ return Promise;
+});
+
+/* jshint ignore:end */
+/* eslint-enable */
+
+// Included from: js/tinymce/classes/util/Delay.js
+
+/**
+ * Delay.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Utility class for working with delayed actions like setTimeout.
+ *
+ * @class tinymce.util.Delay
+ */
+define("tinymce/util/Delay", [
+ "tinymce/util/Promise"
+], function(Promise) {
+ var requestAnimationFramePromise;
+
+ function requestAnimationFrame(callback, element) {
+ var i, requestAnimationFrameFunc = window.requestAnimationFrame, vendors = ['ms', 'moz', 'webkit'];
+
+ function featurefill(callback) {
+ window.setTimeout(callback, 0);
+ }
+
+ for (i = 0; i < vendors.length && !requestAnimationFrameFunc; i++) {
+ requestAnimationFrameFunc = window[vendors[i] + 'RequestAnimationFrame'];
+ }
+
+ if (!requestAnimationFrameFunc) {
+ requestAnimationFrameFunc = featurefill;
+ }
+
+ requestAnimationFrameFunc(callback, element);
+ }
+
+ function wrappedSetTimeout(callback, time) {
+ if (typeof time != 'number') {
+ time = 0;
+ }
+
+ return setTimeout(callback, time);
+ }
+
+ function wrappedSetInterval(callback, time) {
+ if (typeof time != 'number') {
+ time = 1; // IE 8 needs it to be > 0
+ }
+
+ return setInterval(callback, time);
+ }
+
+ function wrappedClearTimeout(id) {
+ return clearTimeout(id);
+ }
+
+ function wrappedClearInterval(id) {
+ return clearInterval(id);
+ }
+
+ function debounce(callback, time) {
+ var timer, func;
+
+ func = function() {
+ var args = arguments;
+
+ clearTimeout(timer);
+
+ timer = wrappedSetTimeout(function() {
+ callback.apply(this, args);
+ }, time);
+ };
+
+ func.stop = function() {
+ clearTimeout(timer);
+ };
+
+ return func;
+ }
+
+ return {
+ /**
+ * Requests an animation frame and fallbacks to a timeout on older browsers.
+ *
+ * @method requestAnimationFrame
+ * @param {function} callback Callback to execute when a new frame is available.
+ * @param {DOMElement} element Optional element to scope it to.
+ */
+ requestAnimationFrame: function(callback, element) {
+ if (requestAnimationFramePromise) {
+ requestAnimationFramePromise.then(callback);
+ return;
+ }
+
+ requestAnimationFramePromise = new Promise(function(resolve) {
+ if (!element) {
+ element = document.body;
+ }
+
+ requestAnimationFrame(resolve, element);
+ }).then(callback);
+ },
+
+ /**
+ * Sets a timer in ms and executes the specified callback when the timer runs out.
+ *
+ * @method setTimeout
+ * @param {function} callback Callback to execute when timer runs out.
+ * @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
+ * @return {Number} Timeout id number.
+ */
+ setTimeout: wrappedSetTimeout,
+
+ /**
+ * Sets an interval timer in ms and executes the specified callback at every interval of that time.
+ *
+ * @method setInterval
+ * @param {function} callback Callback to execute when interval time runs out.
+ * @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
+ * @return {Number} Timeout id number.
+ */
+ setInterval: wrappedSetInterval,
+
+ /**
+ * Sets an editor timeout it's similar to setTimeout except that it checks if the editor instance is
+ * still alive when the callback gets executed.
+ *
+ * @method setEditorTimeout
+ * @param {tinymce.Editor} editor Editor instance to check the removed state on.
+ * @param {function} callback Callback to execute when timer runs out.
+ * @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
+ * @return {Number} Timeout id number.
+ */
+ setEditorTimeout: function(editor, callback, time) {
+ return wrappedSetTimeout(function() {
+ if (!editor.removed) {
+ callback();
+ }
+ }, time);
+ },
+
+ /**
+ * Sets an interval timer it's similar to setInterval except that it checks if the editor instance is
+ * still alive when the callback gets executed.
+ *
+ * @method setEditorInterval
+ * @param {function} callback Callback to execute when interval time runs out.
+ * @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
+ * @return {Number} Timeout id number.
+ */
+ setEditorInterval: function(editor, callback, time) {
+ var timer;
+
+ timer = wrappedSetInterval(function() {
+ if (!editor.removed) {
+ callback();
+ } else {
+ clearInterval(timer);
+ }
+ }, time);
+
+ return timer;
+ },
+
+ /**
+ * Creates debounced callback function that only gets executed once within the specified time.
+ *
+ * @method debounce
+ * @param {function} callback Callback to execute when timer finishes.
+ * @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
+ * @return {Function} debounced function callback.
+ */
+ debounce: debounce,
+
+ // Throttle needs to be debounce due to backwards compatibility.
+ throttle: debounce,
+
+ /**
+ * Clears an interval timer so it won't execute.
+ *
+ * @method clearInterval
+ * @param {Number} Interval timer id number.
+ */
+ clearInterval: wrappedClearInterval,
+
+ /**
+ * Clears an timeout timer so it won't execute.
+ *
+ * @method clearTimeout
+ * @param {Number} Timeout timer id number.
+ */
+ clearTimeout: wrappedClearTimeout
+ };
+});
+
+// Included from: js/tinymce/classes/Env.js
+
+/**
+ * Env.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class contains various environment constants like browser versions etc.
+ * Normally you don't want to sniff specific browser versions but sometimes you have
+ * to when it's impossible to feature detect. So use this with care.
+ *
+ * @class tinymce.Env
+ * @static
+ */
+define("tinymce/Env", [], function() {
+ var nav = navigator, userAgent = nav.userAgent;
+ var opera, webkit, ie, ie11, ie12, gecko, mac, iDevice, android, fileApi, phone, tablet, windowsPhone;
+
+ function matchMediaQuery(query) {
+ return "matchMedia" in window ? matchMedia(query).matches : false;
+ }
+
+ opera = window.opera && window.opera.buildNumber;
+ android = /Android/.test(userAgent);
+ webkit = /WebKit/.test(userAgent);
+ ie = !webkit && !opera && (/MSIE/gi).test(userAgent) && (/Explorer/gi).test(nav.appName);
+ ie = ie && /MSIE (\w+)\./.exec(userAgent)[1];
+ ie11 = userAgent.indexOf('Trident/') != -1 && (userAgent.indexOf('rv:') != -1 || nav.appName.indexOf('Netscape') != -1) ? 11 : false;
+ ie12 = (userAgent.indexOf('Edge/') != -1 && !ie && !ie11) ? 12 : false;
+ ie = ie || ie11 || ie12;
+ gecko = !webkit && !ie11 && /Gecko/.test(userAgent);
+ mac = userAgent.indexOf('Mac') != -1;
+ iDevice = /(iPad|iPhone)/.test(userAgent);
+ fileApi = "FormData" in window && "FileReader" in window && "URL" in window && !!URL.createObjectURL;
+ phone = matchMediaQuery("only screen and (max-device-width: 480px)") && (android || iDevice);
+ tablet = matchMediaQuery("only screen and (min-width: 800px)") && (android || iDevice);
+ windowsPhone = userAgent.indexOf('Windows Phone') != -1;
+
+ if (ie12) {
+ webkit = false;
+ }
+
+ // Is a iPad/iPhone and not on iOS5 sniff the WebKit version since older iOS WebKit versions
+ // says it has contentEditable support but there is no visible caret.
+ var contentEditable = !iDevice || fileApi || userAgent.match(/AppleWebKit\/(\d*)/)[1] >= 534;
+
+ return {
+ /**
+ * Constant that is true if the browser is Opera.
+ *
+ * @property opera
+ * @type Boolean
+ * @final
+ */
+ opera: opera,
+
+ /**
+ * Constant that is true if the browser is WebKit (Safari/Chrome).
+ *
+ * @property webKit
+ * @type Boolean
+ * @final
+ */
+ webkit: webkit,
+
+ /**
+ * Constant that is more than zero if the browser is IE.
+ *
+ * @property ie
+ * @type Boolean
+ * @final
+ */
+ ie: ie,
+
+ /**
+ * Constant that is true if the browser is Gecko.
+ *
+ * @property gecko
+ * @type Boolean
+ * @final
+ */
+ gecko: gecko,
+
+ /**
+ * Constant that is true if the os is Mac OS.
+ *
+ * @property mac
+ * @type Boolean
+ * @final
+ */
+ mac: mac,
+
+ /**
+ * Constant that is true if the os is iOS.
+ *
+ * @property iOS
+ * @type Boolean
+ * @final
+ */
+ iOS: iDevice,
+
+ /**
+ * Constant that is true if the os is android.
+ *
+ * @property android
+ * @type Boolean
+ * @final
+ */
+ android: android,
+
+ /**
+ * Constant that is true if the browser supports editing.
+ *
+ * @property contentEditable
+ * @type Boolean
+ * @final
+ */
+ contentEditable: contentEditable,
+
+ /**
+ * Transparent image data url.
+ *
+ * @property transparentSrc
+ * @type Boolean
+ * @final
+ */
+ transparentSrc: "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
+
+ /**
+ * Returns true/false if the browser can or can't place the caret after a inline block like an image.
+ *
+ * @property noCaretAfter
+ * @type Boolean
+ * @final
+ */
+ caretAfter: ie != 8,
+
+ /**
+ * Constant that is true if the browser supports native DOM Ranges. IE 9+.
+ *
+ * @property range
+ * @type Boolean
+ */
+ range: window.getSelection && "Range" in window,
+
+ /**
+ * Returns the IE document mode for non IE browsers this will fake IE 10.
+ *
+ * @property documentMode
+ * @type Number
+ */
+ documentMode: ie && !ie12 ? (document.documentMode || 7) : 10,
+
+ /**
+ * Constant that is true if the browser has a modern file api.
+ *
+ * @property fileApi
+ * @type Boolean
+ */
+ fileApi: fileApi,
+
+ /**
+ * Constant that is true if the browser supports contentEditable=false regions.
+ *
+ * @property ceFalse
+ * @type Boolean
+ */
+ ceFalse: (ie === false || ie > 8),
+
+ /**
+ * Constant if CSP mode is possible or not. Meaning we can't use script urls for the iframe.
+ */
+ canHaveCSP: (ie === false || ie > 11),
+
+ desktop: !phone && !tablet,
+ windowsPhone: windowsPhone
+ };
+});
+
+// Included from: js/tinymce/classes/dom/EventUtils.js
+
+/**
+ * EventUtils.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*jshint loopfunc:true*/
+/*eslint no-loop-func:0 */
+
+/**
+ * This class wraps the browsers native event logic with more convenient methods.
+ *
+ * @class tinymce.dom.EventUtils
+ */
+define("tinymce/dom/EventUtils", [
+ "tinymce/util/Delay",
+ "tinymce/Env"
+], function(Delay, Env) {
+ "use strict";
+
+ var eventExpandoPrefix = "mce-data-";
+ var mouseEventRe = /^(?:mouse|contextmenu)|click/;
+ var deprecated = {
+ keyLocation: 1, layerX: 1, layerY: 1, returnValue: 1,
+ webkitMovementX: 1, webkitMovementY: 1, keyIdentifier: 1
+ };
+
+ /**
+ * Binds a native event to a callback on the speified target.
+ */
+ function addEvent(target, name, callback, capture) {
+ if (target.addEventListener) {
+ target.addEventListener(name, callback, capture || false);
+ } else if (target.attachEvent) {
+ target.attachEvent('on' + name, callback);
+ }
+ }
+
+ /**
+ * Unbinds a native event callback on the specified target.
+ */
+ function removeEvent(target, name, callback, capture) {
+ if (target.removeEventListener) {
+ target.removeEventListener(name, callback, capture || false);
+ } else if (target.detachEvent) {
+ target.detachEvent('on' + name, callback);
+ }
+ }
+
+ /**
+ * Gets the event target based on shadow dom properties like path and deepPath.
+ */
+ function getTargetFromShadowDom(event, defaultTarget) {
+ var path, target = defaultTarget;
+
+ // When target element is inside Shadow DOM we need to take first element from path
+ // otherwise we'll get Shadow Root parent, not actual target element
+
+ // Normalize target for WebComponents v0 implementation (in Chrome)
+ path = event.path;
+ if (path && path.length > 0) {
+ target = path[0];
+ }
+
+ // Normalize target for WebComponents v1 implementation (standard)
+ if (event.deepPath) {
+ path = event.deepPath();
+ if (path && path.length > 0) {
+ target = path[0];
+ }
+ }
+
+ return target;
+ }
+
+ /**
+ * Normalizes a native event object or just adds the event specific methods on a custom event.
+ */
+ function fix(originalEvent, data) {
+ var name, event = data || {}, undef;
+
+ // Dummy function that gets replaced on the delegation state functions
+ function returnFalse() {
+ return false;
+ }
+
+ // Dummy function that gets replaced on the delegation state functions
+ function returnTrue() {
+ return true;
+ }
+
+ // Copy all properties from the original event
+ for (name in originalEvent) {
+ // layerX/layerY is deprecated in Chrome and produces a warning
+ if (!deprecated[name]) {
+ event[name] = originalEvent[name];
+ }
+ }
+
+ // Normalize target IE uses srcElement
+ if (!event.target) {
+ event.target = event.srcElement || document;
+ }
+
+ // Experimental shadow dom support
+ if (Env.experimentalShadowDom) {
+ event.target = getTargetFromShadowDom(originalEvent, event.target);
+ }
+
+ // Calculate pageX/Y if missing and clientX/Y available
+ if (originalEvent && mouseEventRe.test(originalEvent.type) && originalEvent.pageX === undef && originalEvent.clientX !== undef) {
+ var eventDoc = event.target.ownerDocument || document;
+ var doc = eventDoc.documentElement;
+ var body = eventDoc.body;
+
+ event.pageX = originalEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
+ (doc && doc.clientLeft || body && body.clientLeft || 0);
+
+ event.pageY = originalEvent.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) -
+ (doc && doc.clientTop || body && body.clientTop || 0);
+ }
+
+ // Add preventDefault method
+ event.preventDefault = function() {
+ event.isDefaultPrevented = returnTrue;
+
+ // Execute preventDefault on the original event object
+ if (originalEvent) {
+ if (originalEvent.preventDefault) {
+ originalEvent.preventDefault();
+ } else {
+ originalEvent.returnValue = false; // IE
+ }
+ }
+ };
+
+ // Add stopPropagation
+ event.stopPropagation = function() {
+ event.isPropagationStopped = returnTrue;
+
+ // Execute stopPropagation on the original event object
+ if (originalEvent) {
+ if (originalEvent.stopPropagation) {
+ originalEvent.stopPropagation();
+ } else {
+ originalEvent.cancelBubble = true; // IE
+ }
+ }
+ };
+
+ // Add stopImmediatePropagation
+ event.stopImmediatePropagation = function() {
+ event.isImmediatePropagationStopped = returnTrue;
+ event.stopPropagation();
+ };
+
+ // Add event delegation states
+ if (!event.isDefaultPrevented) {
+ event.isDefaultPrevented = returnFalse;
+ event.isPropagationStopped = returnFalse;
+ event.isImmediatePropagationStopped = returnFalse;
+ }
+
+ // Add missing metaKey for IE 8
+ if (typeof event.metaKey == 'undefined') {
+ event.metaKey = false;
+ }
+
+ return event;
+ }
+
+ /**
+ * Bind a DOMContentLoaded event across browsers and executes the callback once the page DOM is initialized.
+ * It will also set/check the domLoaded state of the event_utils instance so ready isn't called multiple times.
+ */
+ function bindOnReady(win, callback, eventUtils) {
+ var doc = win.document, event = {type: 'ready'};
+
+ if (eventUtils.domLoaded) {
+ callback(event);
+ return;
+ }
+
+ // Gets called when the DOM is ready
+ function readyHandler() {
+ if (!eventUtils.domLoaded) {
+ eventUtils.domLoaded = true;
+ callback(event);
+ }
+ }
+
+ function waitForDomLoaded() {
+ // Check complete or interactive state if there is a body
+ // element on some iframes IE 8 will produce a null body
+ if (doc.readyState === "complete" || (doc.readyState === "interactive" && doc.body)) {
+ removeEvent(doc, "readystatechange", waitForDomLoaded);
+ readyHandler();
+ }
+ }
+
+ function tryScroll() {
+ try {
+ // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author.
+ // http://javascript.nwbox.com/IEContentLoaded/
+ doc.documentElement.doScroll("left");
+ } catch (ex) {
+ Delay.setTimeout(tryScroll);
+ return;
+ }
+
+ readyHandler();
+ }
+
+ // Use W3C method
+ if (doc.addEventListener) {
+ if (doc.readyState === "complete") {
+ readyHandler();
+ } else {
+ addEvent(win, 'DOMContentLoaded', readyHandler);
+ }
+ } else {
+ // Use IE method
+ addEvent(doc, "readystatechange", waitForDomLoaded);
+
+ // Wait until we can scroll, when we can the DOM is initialized
+ if (doc.documentElement.doScroll && win.self === win.top) {
+ tryScroll();
+ }
+ }
+
+ // Fallback if any of the above methods should fail for some odd reason
+ addEvent(win, 'load', readyHandler);
+ }
+
+ /**
+ * This class enables you to bind/unbind native events to elements and normalize it's behavior across browsers.
+ */
+ function EventUtils() {
+ var self = this, events = {}, count, expando, hasFocusIn, hasMouseEnterLeave, mouseEnterLeave;
+
+ expando = eventExpandoPrefix + (+new Date()).toString(32);
+ hasMouseEnterLeave = "onmouseenter" in document.documentElement;
+ hasFocusIn = "onfocusin" in document.documentElement;
+ mouseEnterLeave = {mouseenter: 'mouseover', mouseleave: 'mouseout'};
+ count = 1;
+
+ // State if the DOMContentLoaded was executed or not
+ self.domLoaded = false;
+ self.events = events;
+
+ /**
+ * Executes all event handler callbacks for a specific event.
+ *
+ * @private
+ * @param {Event} evt Event object.
+ * @param {String} id Expando id value to look for.
+ */
+ function executeHandlers(evt, id) {
+ var callbackList, i, l, callback, container = events[id];
+
+ callbackList = container && container[evt.type];
+ if (callbackList) {
+ for (i = 0, l = callbackList.length; i < l; i++) {
+ callback = callbackList[i];
+
+ // Check if callback exists might be removed if a unbind is called inside the callback
+ if (callback && callback.func.call(callback.scope, evt) === false) {
+ evt.preventDefault();
+ }
+
+ // Should we stop propagation to immediate listeners
+ if (evt.isImmediatePropagationStopped()) {
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Binds a callback to an event on the specified target.
+ *
+ * @method bind
+ * @param {Object} target Target node/window or custom object.
+ * @param {String} names Name of the event to bind.
+ * @param {function} callback Callback function to execute when the event occurs.
+ * @param {Object} scope Scope to call the callback function on, defaults to target.
+ * @return {function} Callback function that got bound.
+ */
+ self.bind = function(target, names, callback, scope) {
+ var id, callbackList, i, name, fakeName, nativeHandler, capture, win = window;
+
+ // Native event handler function patches the event and executes the callbacks for the expando
+ function defaultNativeHandler(evt) {
+ executeHandlers(fix(evt || win.event), id);
+ }
+
+ // Don't bind to text nodes or comments
+ if (!target || target.nodeType === 3 || target.nodeType === 8) {
+ return;
+ }
+
+ // Create or get events id for the target
+ if (!target[expando]) {
+ id = count++;
+ target[expando] = id;
+ events[id] = {};
+ } else {
+ id = target[expando];
+ }
+
+ // Setup the specified scope or use the target as a default
+ scope = scope || target;
+
+ // Split names and bind each event, enables you to bind multiple events with one call
+ names = names.split(' ');
+ i = names.length;
+ while (i--) {
+ name = names[i];
+ nativeHandler = defaultNativeHandler;
+ fakeName = capture = false;
+
+ // Use ready instead of DOMContentLoaded
+ if (name === "DOMContentLoaded") {
+ name = "ready";
+ }
+
+ // DOM is already ready
+ if (self.domLoaded && name === "ready" && target.readyState == 'complete') {
+ callback.call(scope, fix({type: name}));
+ continue;
+ }
+
+ // Handle mouseenter/mouseleaver
+ if (!hasMouseEnterLeave) {
+ fakeName = mouseEnterLeave[name];
+
+ if (fakeName) {
+ nativeHandler = function(evt) {
+ var current, related;
+
+ current = evt.currentTarget;
+ related = evt.relatedTarget;
+
+ // Check if related is inside the current target if it's not then the event should
+ // be ignored since it's a mouseover/mouseout inside the element
+ if (related && current.contains) {
+ // Use contains for performance
+ related = current.contains(related);
+ } else {
+ while (related && related !== current) {
+ related = related.parentNode;
+ }
+ }
+
+ // Fire fake event
+ if (!related) {
+ evt = fix(evt || win.event);
+ evt.type = evt.type === 'mouseout' ? 'mouseleave' : 'mouseenter';
+ evt.target = current;
+ executeHandlers(evt, id);
+ }
+ };
+ }
+ }
+
+ // Fake bubbling of focusin/focusout
+ if (!hasFocusIn && (name === "focusin" || name === "focusout")) {
+ capture = true;
+ fakeName = name === "focusin" ? "focus" : "blur";
+ nativeHandler = function(evt) {
+ evt = fix(evt || win.event);
+ evt.type = evt.type === 'focus' ? 'focusin' : 'focusout';
+ executeHandlers(evt, id);
+ };
+ }
+
+ // Setup callback list and bind native event
+ callbackList = events[id][name];
+ if (!callbackList) {
+ events[id][name] = callbackList = [{func: callback, scope: scope}];
+ callbackList.fakeName = fakeName;
+ callbackList.capture = capture;
+ //callbackList.callback = callback;
+
+ // Add the nativeHandler to the callback list so that we can later unbind it
+ callbackList.nativeHandler = nativeHandler;
+
+ // Check if the target has native events support
+
+ if (name === "ready") {
+ bindOnReady(target, nativeHandler, self);
+ } else {
+ addEvent(target, fakeName || name, nativeHandler, capture);
+ }
+ } else {
+ if (name === "ready" && self.domLoaded) {
+ callback({type: name});
+ } else {
+ // If it already has an native handler then just push the callback
+ callbackList.push({func: callback, scope: scope});
+ }
+ }
+ }
+
+ target = callbackList = 0; // Clean memory for IE
+
+ return callback;
+ };
+
+ /**
+ * Unbinds the specified event by name, name and callback or all events on the target.
+ *
+ * @method unbind
+ * @param {Object} target Target node/window or custom object.
+ * @param {String} names Optional event name to unbind.
+ * @param {function} callback Optional callback function to unbind.
+ * @return {EventUtils} Event utils instance.
+ */
+ self.unbind = function(target, names, callback) {
+ var id, callbackList, i, ci, name, eventMap;
+
+ // Don't bind to text nodes or comments
+ if (!target || target.nodeType === 3 || target.nodeType === 8) {
+ return self;
+ }
+
+ // Unbind event or events if the target has the expando
+ id = target[expando];
+ if (id) {
+ eventMap = events[id];
+
+ // Specific callback
+ if (names) {
+ names = names.split(' ');
+ i = names.length;
+ while (i--) {
+ name = names[i];
+ callbackList = eventMap[name];
+
+ // Unbind the event if it exists in the map
+ if (callbackList) {
+ // Remove specified callback
+ if (callback) {
+ ci = callbackList.length;
+ while (ci--) {
+ if (callbackList[ci].func === callback) {
+ var nativeHandler = callbackList.nativeHandler;
+ var fakeName = callbackList.fakeName, capture = callbackList.capture;
+
+ // Clone callbackList since unbind inside a callback would otherwise break the handlers loop
+ callbackList = callbackList.slice(0, ci).concat(callbackList.slice(ci + 1));
+ callbackList.nativeHandler = nativeHandler;
+ callbackList.fakeName = fakeName;
+ callbackList.capture = capture;
+
+ eventMap[name] = callbackList;
+ }
+ }
+ }
+
+ // Remove all callbacks if there isn't a specified callback or there is no callbacks left
+ if (!callback || callbackList.length === 0) {
+ delete eventMap[name];
+ removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture);
+ }
+ }
+ }
+ } else {
+ // All events for a specific element
+ for (name in eventMap) {
+ callbackList = eventMap[name];
+ removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture);
+ }
+
+ eventMap = {};
+ }
+
+ // Check if object is empty, if it isn't then we won't remove the expando map
+ for (name in eventMap) {
+ return self;
+ }
+
+ // Delete event object
+ delete events[id];
+
+ // Remove expando from target
+ try {
+ // IE will fail here since it can't delete properties from window
+ delete target[expando];
+ } catch (ex) {
+ // IE will set it to null
+ target[expando] = null;
+ }
+ }
+
+ return self;
+ };
+
+ /**
+ * Fires the specified event on the specified target.
+ *
+ * @method fire
+ * @param {Object} target Target node/window or custom object.
+ * @param {String} name Event name to fire.
+ * @param {Object} args Optional arguments to send to the observers.
+ * @return {EventUtils} Event utils instance.
+ */
+ self.fire = function(target, name, args) {
+ var id;
+
+ // Don't bind to text nodes or comments
+ if (!target || target.nodeType === 3 || target.nodeType === 8) {
+ return self;
+ }
+
+ // Build event object by patching the args
+ args = fix(null, args);
+ args.type = name;
+ args.target = target;
+
+ do {
+ // Found an expando that means there is listeners to execute
+ id = target[expando];
+ if (id) {
+ executeHandlers(args, id);
+ }
+
+ // Walk up the DOM
+ target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow;
+ } while (target && !args.isPropagationStopped());
+
+ return self;
+ };
+
+ /**
+ * Removes all bound event listeners for the specified target. This will also remove any bound
+ * listeners to child nodes within that target.
+ *
+ * @method clean
+ * @param {Object} target Target node/window object.
+ * @return {EventUtils} Event utils instance.
+ */
+ self.clean = function(target) {
+ var i, children, unbind = self.unbind;
+
+ // Don't bind to text nodes or comments
+ if (!target || target.nodeType === 3 || target.nodeType === 8) {
+ return self;
+ }
+
+ // Unbind any element on the specified target
+ if (target[expando]) {
+ unbind(target);
+ }
+
+ // Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children
+ if (!target.getElementsByTagName) {
+ target = target.document;
+ }
+
+ // Remove events from each child element
+ if (target && target.getElementsByTagName) {
+ unbind(target);
+
+ children = target.getElementsByTagName('*');
+ i = children.length;
+ while (i--) {
+ target = children[i];
+
+ if (target[expando]) {
+ unbind(target);
+ }
+ }
+ }
+
+ return self;
+ };
+
+ /**
+ * Destroys the event object. Call this on IE to remove memory leaks.
+ */
+ self.destroy = function() {
+ events = {};
+ };
+
+ // Legacy function for canceling events
+ self.cancel = function(e) {
+ if (e) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ }
+
+ return false;
+ };
+ }
+
+ EventUtils.Event = new EventUtils();
+ EventUtils.Event.bind(window, 'ready', function() {});
+
+ return EventUtils;
+});
+
+// Included from: js/tinymce/classes/dom/Sizzle.js
+
+/**
+ * Sizzle.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ *
+ * @ignore-file
+ */
+
+/*jshint bitwise:false, expr:true, noempty:false, sub:true, eqnull:true, latedef:false, maxlen:255 */
+/*eslint-disable */
+
+/**
+ * Sizzle CSS Selector Engine v@VERSION
+ * http://sizzlejs.com/
+ *
+ * Copyright 2008, 2014 jQuery Foundation, Inc. and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: @DATE
+ */
+define("tinymce/dom/Sizzle", [], function() {
+var i,
+ support,
+ Expr,
+ getText,
+ isXML,
+ tokenize,
+ compile,
+ select,
+ outermostContext,
+ sortInput,
+ hasDuplicate,
+
+ // Local document vars
+ setDocument,
+ document,
+ docElem,
+ documentIsHTML,
+ rbuggyQSA,
+ rbuggyMatches,
+ matches,
+ contains,
+
+ // Instance-specific data
+ expando = "sizzle" + -(new Date()),
+ preferredDoc = window.document,
+ dirruns = 0,
+ done = 0,
+ classCache = createCache(),
+ tokenCache = createCache(),
+ compilerCache = createCache(),
+ sortOrder = function( a, b ) {
+ if ( a === b ) {
+ hasDuplicate = true;
+ }
+ return 0;
+ },
+
+ // General-purpose constants
+ strundefined = typeof undefined,
+ MAX_NEGATIVE = 1 << 31,
+
+ // Instance methods
+ hasOwn = ({}).hasOwnProperty,
+ arr = [],
+ pop = arr.pop,
+ push_native = arr.push,
+ push = arr.push,
+ slice = arr.slice,
+ // Use a stripped-down indexOf if we can't use a native one
+ indexOf = arr.indexOf || function( elem ) {
+ var i = 0,
+ len = this.length;
+ for ( ; i < len; i++ ) {
+ if ( this[i] === elem ) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
+
+ // Regular expressions
+
+ // http://www.w3.org/TR/css3-selectors/#whitespace
+ whitespace = "[\\x20\\t\\r\\n\\f]",
+
+ // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
+ identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",
+
+ // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
+ attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
+ // Operator (capture 2)
+ "*([*^$|!~]?=)" + whitespace +
+ // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
+ "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace +
+ "*\\]",
+
+ pseudos = ":(" + identifier + ")(?:\\((" +
+ // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
+ // 1. quoted (capture 3; capture 4 or capture 5)
+ "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
+ // 2. simple (capture 6)
+ "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
+ // 3. anything else (capture 2)
+ ".*" +
+ ")\\)|)",
+
+ // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+ rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
+
+ rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
+ rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
+
+ rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ),
+
+ rpseudo = new RegExp( pseudos ),
+ ridentifier = new RegExp( "^" + identifier + "$" ),
+
+ matchExpr = {
+ "ID": new RegExp( "^#(" + identifier + ")" ),
+ "CLASS": new RegExp( "^\\.(" + identifier + ")" ),
+ "TAG": new RegExp( "^(" + identifier + "|[*])" ),
+ "ATTR": new RegExp( "^" + attributes ),
+ "PSEUDO": new RegExp( "^" + pseudos ),
+ "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
+ "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
+ "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+ "bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
+ // For use in libraries implementing .is()
+ // We use this for POS matching in `select`
+ "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
+ whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
+ },
+
+ rinputs = /^(?:input|select|textarea|button)$/i,
+ rheader = /^h\d$/i,
+
+ rnative = /^[^{]+\{\s*\[native \w/,
+
+ // Easily-parseable/retrievable ID or TAG or CLASS selectors
+ rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
+
+ rsibling = /[+~]/,
+ rescape = /'|\\/g,
+
+ // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
+ runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
+ funescape = function( _, escaped, escapedWhitespace ) {
+ var high = "0x" + escaped - 0x10000;
+ // NaN means non-codepoint
+ // Support: Firefox<24
+ // Workaround erroneous numeric interpretation of +"0x"
+ return high !== high || escapedWhitespace ?
+ escaped :
+ high < 0 ?
+ // BMP codepoint
+ String.fromCharCode( high + 0x10000 ) :
+ // Supplemental Plane codepoint (surrogate pair)
+ String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
+ };
+
+// Optimize for push.apply( _, NodeList )
+try {
+ push.apply(
+ (arr = slice.call( preferredDoc.childNodes )),
+ preferredDoc.childNodes
+ );
+ // Support: Android<4.0
+ // Detect silently failing push.apply
+ arr[ preferredDoc.childNodes.length ].nodeType;
+} catch ( e ) {
+ push = { apply: arr.length ?
+
+ // Leverage slice if possible
+ function( target, els ) {
+ push_native.apply( target, slice.call(els) );
+ } :
+
+ // Support: IE<9
+ // Otherwise append directly
+ function( target, els ) {
+ var j = target.length,
+ i = 0;
+ // Can't trust NodeList.length
+ while ( (target[j++] = els[i++]) ) {}
+ target.length = j - 1;
+ }
+ };
+}
+
+function Sizzle( selector, context, results, seed ) {
+ var match, elem, m, nodeType,
+ // QSA vars
+ i, groups, old, nid, newContext, newSelector;
+
+ if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
+ setDocument( context );
+ }
+
+ context = context || document;
+ results = results || [];
+
+ if ( !selector || typeof selector !== "string" ) {
+ return results;
+ }
+
+ if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) {
+ return [];
+ }
+
+ if ( documentIsHTML && !seed ) {
+
+ // Shortcuts
+ if ( (match = rquickExpr.exec( selector )) ) {
+ // Speed-up: Sizzle("#ID")
+ if ( (m = match[1]) ) {
+ if ( nodeType === 9 ) {
+ elem = context.getElementById( m );
+ // Check parentNode to catch when Blackberry 4.6 returns
+ // nodes that are no longer in the document (jQuery #6963)
+ if ( elem && elem.parentNode ) {
+ // Handle the case where IE, Opera, and Webkit return items
+ // by name instead of ID
+ if ( elem.id === m ) {
+ results.push( elem );
+ return results;
+ }
+ } else {
+ return results;
+ }
+ } else {
+ // Context is not a document
+ if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&
+ contains( context, elem ) && elem.id === m ) {
+ results.push( elem );
+ return results;
+ }
+ }
+
+ // Speed-up: Sizzle("TAG")
+ } else if ( match[2] ) {
+ push.apply( results, context.getElementsByTagName( selector ) );
+ return results;
+
+ // Speed-up: Sizzle(".CLASS")
+ } else if ( (m = match[3]) && support.getElementsByClassName ) {
+ push.apply( results, context.getElementsByClassName( m ) );
+ return results;
+ }
+ }
+
+ // QSA path
+ if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
+ nid = old = expando;
+ newContext = context;
+ newSelector = nodeType === 9 && selector;
+
+ // qSA works strangely on Element-rooted queries
+ // We can work around this by specifying an extra ID on the root
+ // and working up from there (Thanks to Andrew Dupont for the technique)
+ // IE 8 doesn't work on object elements
+ if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
+ groups = tokenize( selector );
+
+ if ( (old = context.getAttribute("id")) ) {
+ nid = old.replace( rescape, "\\$&" );
+ } else {
+ context.setAttribute( "id", nid );
+ }
+ nid = "[id='" + nid + "'] ";
+
+ i = groups.length;
+ while ( i-- ) {
+ groups[i] = nid + toSelector( groups[i] );
+ }
+ newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context;
+ newSelector = groups.join(",");
+ }
+
+ if ( newSelector ) {
+ try {
+ push.apply( results,
+ newContext.querySelectorAll( newSelector )
+ );
+ return results;
+ } catch(qsaError) {
+ } finally {
+ if ( !old ) {
+ context.removeAttribute("id");
+ }
+ }
+ }
+ }
+ }
+
+ // All others
+ return select( selector.replace( rtrim, "$1" ), context, results, seed );
+}
+
+/**
+ * Create key-value caches of limited size
+ * @returns {Function(string, Object)} Returns the Object data after storing it on itself with
+ * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
+ * deleting the oldest entry
+ */
+function createCache() {
+ var keys = [];
+
+ function cache( key, value ) {
+ // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
+ if ( keys.push( key + " " ) > Expr.cacheLength ) {
+ // Only keep the most recent entries
+ delete cache[ keys.shift() ];
+ }
+ return (cache[ key + " " ] = value);
+ }
+ return cache;
+}
+
+/**
+ * Mark a function for special use by Sizzle
+ * @param {Function} fn The function to mark
+ */
+function markFunction( fn ) {
+ fn[ expando ] = true;
+ return fn;
+}
+
+/**
+ * Support testing using an element
+ * @param {Function} fn Passed the created div and expects a boolean result
+ */
+function assert( fn ) {
+ var div = document.createElement("div");
+
+ try {
+ return !!fn( div );
+ } catch (e) {
+ return false;
+ } finally {
+ // Remove from its parent by default
+ if ( div.parentNode ) {
+ div.parentNode.removeChild( div );
+ }
+ // release memory in IE
+ div = null;
+ }
+}
+
+/**
+ * Adds the same handler for all of the specified attrs
+ * @param {String} attrs Pipe-separated list of attributes
+ * @param {Function} handler The method that will be applied
+ */
+function addHandle( attrs, handler ) {
+ var arr = attrs.split("|"),
+ i = attrs.length;
+
+ while ( i-- ) {
+ Expr.attrHandle[ arr[i] ] = handler;
+ }
+}
+
+/**
+ * Checks document order of two siblings
+ * @param {Element} a
+ * @param {Element} b
+ * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
+ */
+function siblingCheck( a, b ) {
+ var cur = b && a,
+ diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
+ ( ~b.sourceIndex || MAX_NEGATIVE ) -
+ ( ~a.sourceIndex || MAX_NEGATIVE );
+
+ // Use IE sourceIndex if available on both nodes
+ if ( diff ) {
+ return diff;
+ }
+
+ // Check if b follows a
+ if ( cur ) {
+ while ( (cur = cur.nextSibling) ) {
+ if ( cur === b ) {
+ return -1;
+ }
+ }
+ }
+
+ return a ? 1 : -1;
+}
+
+/**
+ * Returns a function to use in pseudos for input types
+ * @param {String} type
+ */
+function createInputPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === type;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for buttons
+ * @param {String} type
+ */
+function createButtonPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return (name === "input" || name === "button") && elem.type === type;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for positionals
+ * @param {Function} fn
+ */
+function createPositionalPseudo( fn ) {
+ return markFunction(function( argument ) {
+ argument = +argument;
+ return markFunction(function( seed, matches ) {
+ var j,
+ matchIndexes = fn( [], seed.length, argument ),
+ i = matchIndexes.length;
+
+ // Match elements found at the specified indexes
+ while ( i-- ) {
+ if ( seed[ (j = matchIndexes[i]) ] ) {
+ seed[j] = !(matches[j] = seed[j]);
+ }
+ }
+ });
+ });
+}
+
+/**
+ * Checks a node for validity as a Sizzle context
+ * @param {Element|Object=} context
+ * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
+ */
+function testContext( context ) {
+ return context && typeof context.getElementsByTagName !== strundefined && context;
+}
+
+// Expose support vars for convenience
+support = Sizzle.support = {};
+
+/**
+ * Detects XML nodes
+ * @param {Element|Object} elem An element or a document
+ * @returns {Boolean} True iff elem is a non-HTML XML node
+ */
+isXML = Sizzle.isXML = function( elem ) {
+ // documentElement is verified for cases where it doesn't yet exist
+ // (such as loading iframes in IE - #4833)
+ var documentElement = elem && (elem.ownerDocument || elem).documentElement;
+ return documentElement ? documentElement.nodeName !== "HTML" : false;
+};
+
+/**
+ * Sets document-related variables once based on the current document
+ * @param {Element|Object} [doc] An element or document object to use to set the document
+ * @returns {Object} Returns the current document
+ */
+setDocument = Sizzle.setDocument = function( node ) {
+ var hasCompare,
+ doc = node ? node.ownerDocument || node : preferredDoc,
+ parent = doc.defaultView;
+
+ function getTop(win) {
+ // Edge throws a lovely Object expected if you try to get top on a detached reference see #2642
+ try {
+ return win.top;
+ } catch (ex) {
+ // Ignore
+ }
+
+ return null;
+ }
+
+ // If no document and documentElement is available, return
+ if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
+ return document;
+ }
+
+ // Set our document
+ document = doc;
+ docElem = doc.documentElement;
+
+ // Support tests
+ documentIsHTML = !isXML( doc );
+
+ // Support: IE>8
+ // If iframe document is assigned to "document" variable and if iframe has been reloaded,
+ // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936
+ // IE6-8 do not support the defaultView property so parent will be undefined
+ if ( parent && parent !== getTop(parent) ) {
+ // IE11 does not have attachEvent, so all must suffer
+ if ( parent.addEventListener ) {
+ parent.addEventListener( "unload", function() {
+ setDocument();
+ }, false );
+ } else if ( parent.attachEvent ) {
+ parent.attachEvent( "onunload", function() {
+ setDocument();
+ });
+ }
+ }
+
+ /* Attributes
+ ---------------------------------------------------------------------- */
+
+ // Support: IE<8
+ // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans)
+ support.attributes = assert(function( div ) {
+ div.className = "i";
+ return !div.getAttribute("className");
+ });
+
+ /* getElement(s)By*
+ ---------------------------------------------------------------------- */
+
+ // Check if getElementsByTagName("*") returns only elements
+ support.getElementsByTagName = assert(function( div ) {
+ div.appendChild( doc.createComment("") );
+ return !div.getElementsByTagName("*").length;
+ });
+
+ // Support: IE<9
+ support.getElementsByClassName = rnative.test( doc.getElementsByClassName );
+
+ // Support: IE<10
+ // Check if getElementById returns elements by name
+ // The broken getElementById methods don't pick up programatically-set names,
+ // so use a roundabout getElementsByName test
+ support.getById = assert(function( div ) {
+ docElem.appendChild( div ).id = expando;
+ return !doc.getElementsByName || !doc.getElementsByName( expando ).length;
+ });
+
+ // ID find and filter
+ if ( support.getById ) {
+ Expr.find["ID"] = function( id, context ) {
+ if ( typeof context.getElementById !== strundefined && documentIsHTML ) {
+ var m = context.getElementById( id );
+ // Check parentNode to catch when Blackberry 4.6 returns
+ // nodes that are no longer in the document #6963
+ return m && m.parentNode ? [ m ] : [];
+ }
+ };
+ Expr.filter["ID"] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ return elem.getAttribute("id") === attrId;
+ };
+ };
+ } else {
+ // Support: IE6/7
+ // getElementById is not reliable as a find shortcut
+ delete Expr.find["ID"];
+
+ Expr.filter["ID"] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
+ return node && node.value === attrId;
+ };
+ };
+ }
+
+ // Tag
+ Expr.find["TAG"] = support.getElementsByTagName ?
+ function( tag, context ) {
+ if ( typeof context.getElementsByTagName !== strundefined ) {
+ return context.getElementsByTagName( tag );
+ }
+ } :
+ function( tag, context ) {
+ var elem,
+ tmp = [],
+ i = 0,
+ results = context.getElementsByTagName( tag );
+
+ // Filter out possible comments
+ if ( tag === "*" ) {
+ while ( (elem = results[i++]) ) {
+ if ( elem.nodeType === 1 ) {
+ tmp.push( elem );
+ }
+ }
+
+ return tmp;
+ }
+ return results;
+ };
+
+ // Class
+ Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
+ if ( documentIsHTML ) {
+ return context.getElementsByClassName( className );
+ }
+ };
+
+ /* QSA/matchesSelector
+ ---------------------------------------------------------------------- */
+
+ // QSA and matchesSelector support
+
+ // matchesSelector(:active) reports false when true (IE9/Opera 11.5)
+ rbuggyMatches = [];
+
+ // qSa(:focus) reports false when true (Chrome 21)
+ // We allow this because of a bug in IE8/9 that throws an error
+ // whenever `document.activeElement` is accessed on an iframe
+ // So, we allow :focus to pass through QSA all the time to avoid the IE error
+ // See http://bugs.jquery.com/ticket/13378
+ rbuggyQSA = [];
+
+ if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) {
+ // Build QSA regex
+ // Regex strategy adopted from Diego Perini
+ assert(function( div ) {
+ // Select is set to empty string on purpose
+ // This is to test IE's treatment of not explicitly
+ // setting a boolean content attribute,
+ // since its presence should be enough
+ // http://bugs.jquery.com/ticket/12359
+ div.innerHTML = "<select msallowcapture=''><option selected=''></option></select>";
+
+ // Support: IE8, Opera 11-12.16
+ // Nothing should be selected when empty strings follow ^= or $= or *=
+ // The test attribute must be unknown in Opera but "safe" for WinRT
+ // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
+ if ( div.querySelectorAll("[msallowcapture^='']").length ) {
+ rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
+ }
+
+ // Support: IE8
+ // Boolean attributes and "value" are not treated correctly
+ if ( !div.querySelectorAll("[selected]").length ) {
+ rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
+ }
+
+ // Webkit/Opera - :checked should return selected option elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ // IE8 throws error here and will not see later tests
+ if ( !div.querySelectorAll(":checked").length ) {
+ rbuggyQSA.push(":checked");
+ }
+ });
+
+ assert(function( div ) {
+ // Support: Windows 8 Native Apps
+ // The type and name attributes are restricted during .innerHTML assignment
+ var input = doc.createElement("input");
+ input.setAttribute( "type", "hidden" );
+ div.appendChild( input ).setAttribute( "name", "D" );
+
+ // Support: IE8
+ // Enforce case-sensitivity of name attribute
+ if ( div.querySelectorAll("[name=d]").length ) {
+ rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
+ }
+
+ // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
+ // IE8 throws error here and will not see later tests
+ if ( !div.querySelectorAll(":enabled").length ) {
+ rbuggyQSA.push( ":enabled", ":disabled" );
+ }
+
+ // Opera 10-11 does not throw on post-comma invalid pseudos
+ div.querySelectorAll("*,:x");
+ rbuggyQSA.push(",.*:");
+ });
+ }
+
+ if ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||
+ docElem.webkitMatchesSelector ||
+ docElem.mozMatchesSelector ||
+ docElem.oMatchesSelector ||
+ docElem.msMatchesSelector) )) ) {
+
+ assert(function( div ) {
+ // Check to see if it's possible to do matchesSelector
+ // on a disconnected node (IE 9)
+ support.disconnectedMatch = matches.call( div, "div" );
+
+ // This should fail with an exception
+ // Gecko does not error, returns false instead
+ matches.call( div, "[s!='']:x" );
+ rbuggyMatches.push( "!=", pseudos );
+ });
+ }
+
+ rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
+ rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
+
+ /* Contains
+ ---------------------------------------------------------------------- */
+ hasCompare = rnative.test( docElem.compareDocumentPosition );
+
+ // Element contains another
+ // Purposefully does not implement inclusive descendent
+ // As in, an element does not contain itself
+ contains = hasCompare || rnative.test( docElem.contains ) ?
+ function( a, b ) {
+ var adown = a.nodeType === 9 ? a.documentElement : a,
+ bup = b && b.parentNode;
+ return a === bup || !!( bup && bup.nodeType === 1 && (
+ adown.contains ?
+ adown.contains( bup ) :
+ a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
+ ));
+ } :
+ function( a, b ) {
+ if ( b ) {
+ while ( (b = b.parentNode) ) {
+ if ( b === a ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ /* Sorting
+ ---------------------------------------------------------------------- */
+
+ // Document order sorting
+ sortOrder = hasCompare ?
+ function( a, b ) {
+
+ // Flag for duplicate removal
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+ }
+
+ // Sort on method existence if only one input has compareDocumentPosition
+ var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
+ if ( compare ) {
+ return compare;
+ }
+
+ // Calculate position if both inputs belong to the same document
+ compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?
+ a.compareDocumentPosition( b ) :
+
+ // Otherwise we know they are disconnected
+ 1;
+
+ // Disconnected nodes
+ if ( compare & 1 ||
+ (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {
+
+ // Choose the first element that is related to our preferred document
+ if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {
+ return -1;
+ }
+ if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {
+ return 1;
+ }
+
+ // Maintain original order
+ return sortInput ?
+ ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
+ 0;
+ }
+
+ return compare & 4 ? -1 : 1;
+ } :
+ function( a, b ) {
+ // Exit early if the nodes are identical
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+ }
+
+ var cur,
+ i = 0,
+ aup = a.parentNode,
+ bup = b.parentNode,
+ ap = [ a ],
+ bp = [ b ];
+
+ // Parentless nodes are either documents or disconnected
+ if ( !aup || !bup ) {
+ return a === doc ? -1 :
+ b === doc ? 1 :
+ aup ? -1 :
+ bup ? 1 :
+ sortInput ?
+ ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
+ 0;
+
+ // If the nodes are siblings, we can do a quick check
+ } else if ( aup === bup ) {
+ return siblingCheck( a, b );
+ }
+
+ // Otherwise we need full lists of their ancestors for comparison
+ cur = a;
+ while ( (cur = cur.parentNode) ) {
+ ap.unshift( cur );
+ }
+ cur = b;
+ while ( (cur = cur.parentNode) ) {
+ bp.unshift( cur );
+ }
+
+ // Walk down the tree looking for a discrepancy
+ while ( ap[i] === bp[i] ) {
+ i++;
+ }
+
+ return i ?
+ // Do a sibling check if the nodes have a common ancestor
+ siblingCheck( ap[i], bp[i] ) :
+
+ // Otherwise nodes in our document sort first
+ ap[i] === preferredDoc ? -1 :
+ bp[i] === preferredDoc ? 1 :
+ 0;
+ };
+
+ return doc;
+};
+
+Sizzle.matches = function( expr, elements ) {
+ return Sizzle( expr, null, null, elements );
+};
+
+Sizzle.matchesSelector = function( elem, expr ) {
+ // Set document vars if needed
+ if ( ( elem.ownerDocument || elem ) !== document ) {
+ setDocument( elem );
+ }
+
+ // Make sure that attribute selectors are quoted
+ expr = expr.replace( rattributeQuotes, "='$1']" );
+
+ if ( support.matchesSelector && documentIsHTML &&
+ ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
+ ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) {
+
+ try {
+ var ret = matches.call( elem, expr );
+
+ // IE 9's matchesSelector returns false on disconnected nodes
+ if ( ret || support.disconnectedMatch ||
+ // As well, disconnected nodes are said to be in a document
+ // fragment in IE 9
+ elem.document && elem.document.nodeType !== 11 ) {
+ return ret;
+ }
+ } catch(e) {}
+ }
+
+ return Sizzle( expr, document, null, [ elem ] ).length > 0;
+};
+
+Sizzle.contains = function( context, elem ) {
+ // Set document vars if needed
+ if ( ( context.ownerDocument || context ) !== document ) {
+ setDocument( context );
+ }
+ return contains( context, elem );
+};
+
+Sizzle.attr = function( elem, name ) {
+ // Set document vars if needed
+ if ( ( elem.ownerDocument || elem ) !== document ) {
+ setDocument( elem );
+ }
+
+ var fn = Expr.attrHandle[ name.toLowerCase() ],
+ // Don't get fooled by Object.prototype properties (jQuery #13807)
+ val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
+ fn( elem, name, !documentIsHTML ) :
+ undefined;
+
+ return val !== undefined ?
+ val :
+ support.attributes || !documentIsHTML ?
+ elem.getAttribute( name ) :
+ (val = elem.getAttributeNode(name)) && val.specified ?
+ val.value :
+ null;
+};
+
+Sizzle.error = function( msg ) {
+ throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+/**
+ * Document sorting and removing duplicates
+ * @param {ArrayLike} results
+ */
+Sizzle.uniqueSort = function( results ) {
+ var elem,
+ duplicates = [],
+ j = 0,
+ i = 0;
+
+ // Unless we *know* we can detect duplicates, assume their presence
+ hasDuplicate = !support.detectDuplicates;
+ sortInput = !support.sortStable && results.slice( 0 );
+ results.sort( sortOrder );
+
+ if ( hasDuplicate ) {
+ while ( (elem = results[i++]) ) {
+ if ( elem === results[ i ] ) {
+ j = duplicates.push( i );
+ }
+ }
+ while ( j-- ) {
+ results.splice( duplicates[ j ], 1 );
+ }
+ }
+
+ // Clear input after sorting to release objects
+ // See https://github.com/jquery/sizzle/pull/225
+ sortInput = null;
+
+ return results;
+};
+
+/**
+ * Utility function for retrieving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+getText = Sizzle.getText = function( elem ) {
+ var node,
+ ret = "",
+ i = 0,
+ nodeType = elem.nodeType;
+
+ if ( !nodeType ) {
+ // If no nodeType, this is expected to be an array
+ while ( (node = elem[i++]) ) {
+ // Do not traverse comment nodes
+ ret += getText( node );
+ }
+ } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+ // Use textContent for elements
+ // innerText usage removed for consistency of new lines (jQuery #11153)
+ if ( typeof elem.textContent === "string" ) {
+ return elem.textContent;
+ } else {
+ // Traverse its children
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ ret += getText( elem );
+ }
+ }
+ } else if ( nodeType === 3 || nodeType === 4 ) {
+ return elem.nodeValue;
+ }
+ // Do not include comment or processing instruction nodes
+
+ return ret;
+};
+
+Expr = Sizzle.selectors = {
+
+ // Can be adjusted by the user
+ cacheLength: 50,
+
+ createPseudo: markFunction,
+
+ match: matchExpr,
+
+ attrHandle: {},
+
+ find: {},
+
+ relative: {
+ ">": { dir: "parentNode", first: true },
+ " ": { dir: "parentNode" },
+ "+": { dir: "previousSibling", first: true },
+ "~": { dir: "previousSibling" }
+ },
+
+ preFilter: {
+ "ATTR": function( match ) {
+ match[1] = match[1].replace( runescape, funescape );
+
+ // Move the given value to match[3] whether quoted or unquoted
+ match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape );
+
+ if ( match[2] === "~=" ) {
+ match[3] = " " + match[3] + " ";
+ }
+
+ return match.slice( 0, 4 );
+ },
+
+ "CHILD": function( match ) {
+ /* matches from matchExpr["CHILD"]
+ 1 type (only|nth|...)
+ 2 what (child|of-type)
+ 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
+ 4 xn-component of xn+y argument ([+-]?\d*n|)
+ 5 sign of xn-component
+ 6 x of xn-component
+ 7 sign of y-component
+ 8 y of y-component
+ */
+ match[1] = match[1].toLowerCase();
+
+ if ( match[1].slice( 0, 3 ) === "nth" ) {
+ // nth-* requires argument
+ if ( !match[3] ) {
+ Sizzle.error( match[0] );
+ }
+
+ // numeric x and y parameters for Expr.filter.CHILD
+ // remember that false/true cast respectively to 0/1
+ match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
+ match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
+
+ // other types prohibit arguments
+ } else if ( match[3] ) {
+ Sizzle.error( match[0] );
+ }
+
+ return match;
+ },
+
+ "PSEUDO": function( match ) {
+ var excess,
+ unquoted = !match[6] && match[2];
+
+ if ( matchExpr["CHILD"].test( match[0] ) ) {
+ return null;
+ }
+
+ // Accept quoted arguments as-is
+ if ( match[3] ) {
+ match[2] = match[4] || match[5] || "";
+
+ // Strip excess characters from unquoted arguments
+ } else if ( unquoted && rpseudo.test( unquoted ) &&
+ // Get excess from tokenize (recursively)
+ (excess = tokenize( unquoted, true )) &&
+ // advance to the next closing parenthesis
+ (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
+
+ // excess is a negative index
+ match[0] = match[0].slice( 0, excess );
+ match[2] = unquoted.slice( 0, excess );
+ }
+
+ // Return only captures needed by the pseudo filter method (type and argument)
+ return match.slice( 0, 3 );
+ }
+ },
+
+ filter: {
+
+ "TAG": function( nodeNameSelector ) {
+ var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
+ return nodeNameSelector === "*" ?
+ function() { return true; } :
+ function( elem ) {
+ return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
+ };
+ },
+
+ "CLASS": function( className ) {
+ var pattern = classCache[ className + " " ];
+
+ return pattern ||
+ (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
+ classCache( className, function( elem ) {
+ return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" );
+ });
+ },
+
+ "ATTR": function( name, operator, check ) {
+ return function( elem ) {
+ var result = Sizzle.attr( elem, name );
+
+ if ( result == null ) {
+ return operator === "!=";
+ }
+ if ( !operator ) {
+ return true;
+ }
+
+ result += "";
+
+ return operator === "=" ? result === check :
+ operator === "!=" ? result !== check :
+ operator === "^=" ? check && result.indexOf( check ) === 0 :
+ operator === "*=" ? check && result.indexOf( check ) > -1 :
+ operator === "$=" ? check && result.slice( -check.length ) === check :
+ operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 :
+ operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
+ false;
+ };
+ },
+
+ "CHILD": function( type, what, argument, first, last ) {
+ var simple = type.slice( 0, 3 ) !== "nth",
+ forward = type.slice( -4 ) !== "last",
+ ofType = what === "of-type";
+
+ return first === 1 && last === 0 ?
+
+ // Shortcut for :nth-*(n)
+ function( elem ) {
+ return !!elem.parentNode;
+ } :
+
+ function( elem, context, xml ) {
+ var cache, outerCache, node, diff, nodeIndex, start,
+ dir = simple !== forward ? "nextSibling" : "previousSibling",
+ parent = elem.parentNode,
+ name = ofType && elem.nodeName.toLowerCase(),
+ useCache = !xml && !ofType;
+
+ if ( parent ) {
+
+ // :(first|last|only)-(child|of-type)
+ if ( simple ) {
+ while ( dir ) {
+ node = elem;
+ while ( (node = node[ dir ]) ) {
+ if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) {
+ return false;
+ }
+ }
+ // Reverse direction for :only-* (if we haven't yet done so)
+ start = dir = type === "only" && !start && "nextSibling";
+ }
+ return true;
+ }
+
+ start = [ forward ? parent.firstChild : parent.lastChild ];
+
+ // non-xml :nth-child(...) stores cache data on `parent`
+ if ( forward && useCache ) {
+ // Seek `elem` from a previously-cached index
+ outerCache = parent[ expando ] || (parent[ expando ] = {});
+ cache = outerCache[ type ] || [];
+ nodeIndex = cache[0] === dirruns && cache[1];
+ diff = cache[0] === dirruns && cache[2];
+ node = nodeIndex && parent.childNodes[ nodeIndex ];
+
+ while ( (node = ++nodeIndex && node && node[ dir ] ||
+
+ // Fallback to seeking `elem` from the start
+ (diff = nodeIndex = 0) || start.pop()) ) {
+
+ // When found, cache indexes on `parent` and break
+ if ( node.nodeType === 1 && ++diff && node === elem ) {
+ outerCache[ type ] = [ dirruns, nodeIndex, diff ];
+ break;
+ }
+ }
+
+ // Use previously-cached element index if available
+ } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) {
+ diff = cache[1];
+
+ // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...)
+ } else {
+ // Use the same loop as above to seek `elem` from the start
+ while ( (node = ++nodeIndex && node && node[ dir ] ||
+ (diff = nodeIndex = 0) || start.pop()) ) {
+
+ if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) {
+ // Cache the index of each encountered element
+ if ( useCache ) {
+ (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ];
+ }
+
+ if ( node === elem ) {
+ break;
+ }
+ }
+ }
+ }
+
+ // Incorporate the offset, then check against cycle size
+ diff -= last;
+ return diff === first || ( diff % first === 0 && diff / first >= 0 );
+ }
+ };
+ },
+
+ "PSEUDO": function( pseudo, argument ) {
+ // pseudo-class names are case-insensitive
+ // http://www.w3.org/TR/selectors/#pseudo-classes
+ // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
+ // Remember that setFilters inherits from pseudos
+ var args,
+ fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
+ Sizzle.error( "unsupported pseudo: " + pseudo );
+
+ // The user may use createPseudo to indicate that
+ // arguments are needed to create the filter function
+ // just as Sizzle does
+ if ( fn[ expando ] ) {
+ return fn( argument );
+ }
+
+ // But maintain support for old signatures
+ if ( fn.length > 1 ) {
+ args = [ pseudo, pseudo, "", argument ];
+ return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
+ markFunction(function( seed, matches ) {
+ var idx,
+ matched = fn( seed, argument ),
+ i = matched.length;
+ while ( i-- ) {
+ idx = indexOf.call( seed, matched[i] );
+ seed[ idx ] = !( matches[ idx ] = matched[i] );
+ }
+ }) :
+ function( elem ) {
+ return fn( elem, 0, args );
+ };
+ }
+
+ return fn;
+ }
+ },
+
+ pseudos: {
+ // Potentially complex pseudos
+ "not": markFunction(function( selector ) {
+ // Trim the selector passed to compile
+ // to avoid treating leading and trailing
+ // spaces as combinators
+ var input = [],
+ results = [],
+ matcher = compile( selector.replace( rtrim, "$1" ) );
+
+ return matcher[ expando ] ?
+ markFunction(function( seed, matches, context, xml ) {
+ var elem,
+ unmatched = matcher( seed, null, xml, [] ),
+ i = seed.length;
+
+ // Match elements unmatched by `matcher`
+ while ( i-- ) {
+ if ( (elem = unmatched[i]) ) {
+ seed[i] = !(matches[i] = elem);
+ }
+ }
+ }) :
+ function( elem, context, xml ) {
+ input[0] = elem;
+ matcher( input, null, xml, results );
+ return !results.pop();
+ };
+ }),
+
+ "has": markFunction(function( selector ) {
+ return function( elem ) {
+ return Sizzle( selector, elem ).length > 0;
+ };
+ }),
+
+ "contains": markFunction(function( text ) {
+ text = text.replace( runescape, funescape );
+ return function( elem ) {
+ return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
+ };
+ }),
+
+ // "Whether an element is represented by a :lang() selector
+ // is based solely on the element's language value
+ // being equal to the identifier C,
+ // or beginning with the identifier C immediately followed by "-".
+ // The matching of C against the element's language value is performed case-insensitively.
+ // The identifier C does not have to be a valid language name."
+ // http://www.w3.org/TR/selectors/#lang-pseudo
+ "lang": markFunction( function( lang ) {
+ // lang value must be a valid identifier
+ if ( !ridentifier.test(lang || "") ) {
+ Sizzle.error( "unsupported lang: " + lang );
+ }
+ lang = lang.replace( runescape, funescape ).toLowerCase();
+ return function( elem ) {
+ var elemLang;
+ do {
+ if ( (elemLang = documentIsHTML ?
+ elem.lang :
+ elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
+
+ elemLang = elemLang.toLowerCase();
+ return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
+ }
+ } while ( (elem = elem.parentNode) && elem.nodeType === 1 );
+ return false;
+ };
+ }),
+
+ // Miscellaneous
+ "target": function( elem ) {
+ var hash = window.location && window.location.hash;
+ return hash && hash.slice( 1 ) === elem.id;
+ },
+
+ "root": function( elem ) {
+ return elem === docElem;
+ },
+
+ "focus": function( elem ) {
+ return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
+ },
+
+ // Boolean properties
+ "enabled": function( elem ) {
+ return elem.disabled === false;
+ },
+
+ "disabled": function( elem ) {
+ return elem.disabled === true;
+ },
+
+ "checked": function( elem ) {
+ // In CSS3, :checked should return both checked and selected elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ var nodeName = elem.nodeName.toLowerCase();
+ return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
+ },
+
+ "selected": function( elem ) {
+ // Accessing this property makes selected-by-default
+ // options in Safari work properly
+ if ( elem.parentNode ) {
+ elem.parentNode.selectedIndex;
+ }
+
+ return elem.selected === true;
+ },
+
+ // Contents
+ "empty": function( elem ) {
+ // http://www.w3.org/TR/selectors/#empty-pseudo
+ // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
+ // but not by others (comment: 8; processing instruction: 7; etc.)
+ // nodeType < 6 works because attributes (2) do not appear as children
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ if ( elem.nodeType < 6 ) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ "parent": function( elem ) {
+ return !Expr.pseudos["empty"]( elem );
+ },
+
+ // Element/input types
+ "header": function( elem ) {
+ return rheader.test( elem.nodeName );
+ },
+
+ "input": function( elem ) {
+ return rinputs.test( elem.nodeName );
+ },
+
+ "button": function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === "button" || name === "button";
+ },
+
+ "text": function( elem ) {
+ var attr;
+ return elem.nodeName.toLowerCase() === "input" &&
+ elem.type === "text" &&
+
+ // Support: IE<8
+ // New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
+ ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" );
+ },
+
+ // Position-in-collection
+ "first": createPositionalPseudo(function() {
+ return [ 0 ];
+ }),
+
+ "last": createPositionalPseudo(function( matchIndexes, length ) {
+ return [ length - 1 ];
+ }),
+
+ "eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ return [ argument < 0 ? argument + length : argument ];
+ }),
+
+ "even": createPositionalPseudo(function( matchIndexes, length ) {
+ var i = 0;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "odd": createPositionalPseudo(function( matchIndexes, length ) {
+ var i = 1;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ var i = argument < 0 ? argument + length : argument;
+ for ( ; --i >= 0; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ var i = argument < 0 ? argument + length : argument;
+ for ( ; ++i < length; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ })
+ }
+};
+
+Expr.pseudos["nth"] = Expr.pseudos["eq"];
+
+// Add button/input type pseudos
+for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
+ Expr.pseudos[ i ] = createInputPseudo( i );
+}
+for ( i in { submit: true, reset: true } ) {
+ Expr.pseudos[ i ] = createButtonPseudo( i );
+}
+
+// Easy API for creating new setFilters
+function setFilters() {}
+setFilters.prototype = Expr.filters = Expr.pseudos;
+Expr.setFilters = new setFilters();
+
+tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
+ var matched, match, tokens, type,
+ soFar, groups, preFilters,
+ cached = tokenCache[ selector + " " ];
+
+ if ( cached ) {
+ return parseOnly ? 0 : cached.slice( 0 );
+ }
+
+ soFar = selector;
+ groups = [];
+ preFilters = Expr.preFilter;
+
+ while ( soFar ) {
+
+ // Comma and first run
+ if ( !matched || (match = rcomma.exec( soFar )) ) {
+ if ( match ) {
+ // Don't consume trailing commas as valid
+ soFar = soFar.slice( match[0].length ) || soFar;
+ }
+ groups.push( (tokens = []) );
+ }
+
+ matched = false;
+
+ // Combinators
+ if ( (match = rcombinators.exec( soFar )) ) {
+ matched = match.shift();
+ tokens.push({
+ value: matched,
+ // Cast descendant combinators to space
+ type: match[0].replace( rtrim, " " )
+ });
+ soFar = soFar.slice( matched.length );
+ }
+
+ // Filters
+ for ( type in Expr.filter ) {
+ if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
+ (match = preFilters[ type ]( match ))) ) {
+ matched = match.shift();
+ tokens.push({
+ value: matched,
+ type: type,
+ matches: match
+ });
+ soFar = soFar.slice( matched.length );
+ }
+ }
+
+ if ( !matched ) {
+ break;
+ }
+ }
+
+ // Return the length of the invalid excess
+ // if we're just parsing
+ // Otherwise, throw an error or return tokens
+ return parseOnly ?
+ soFar.length :
+ soFar ?
+ Sizzle.error( selector ) :
+ // Cache the tokens
+ tokenCache( selector, groups ).slice( 0 );
+};
+
+function toSelector( tokens ) {
+ var i = 0,
+ len = tokens.length,
+ selector = "";
+ for ( ; i < len; i++ ) {
+ selector += tokens[i].value;
+ }
+ return selector;
+}
+
+function addCombinator( matcher, combinator, base ) {
+ var dir = combinator.dir,
+ checkNonElements = base && dir === "parentNode",
+ doneName = done++;
+
+ return combinator.first ?
+ // Check against closest ancestor/preceding element
+ function( elem, context, xml ) {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ return matcher( elem, context, xml );
+ }
+ }
+ } :
+
+ // Check against all ancestor/preceding elements
+ function( elem, context, xml ) {
+ var oldCache, outerCache,
+ newCache = [ dirruns, doneName ];
+
+ // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
+ if ( xml ) {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ if ( matcher( elem, context, xml ) ) {
+ return true;
+ }
+ }
+ }
+ } else {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ outerCache = elem[ expando ] || (elem[ expando ] = {});
+ if ( (oldCache = outerCache[ dir ]) &&
+ oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {
+
+ // Assign to newCache so results back-propagate to previous elements
+ return (newCache[ 2 ] = oldCache[ 2 ]);
+ } else {
+ // Reuse newcache so results back-propagate to previous elements
+ outerCache[ dir ] = newCache;
+
+ // A match means we're done; a fail means we have to keep checking
+ if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ };
+}
+
+function elementMatcher( matchers ) {
+ return matchers.length > 1 ?
+ function( elem, context, xml ) {
+ var i = matchers.length;
+ while ( i-- ) {
+ if ( !matchers[i]( elem, context, xml ) ) {
+ return false;
+ }
+ }
+ return true;
+ } :
+ matchers[0];
+}
+
+function multipleContexts( selector, contexts, results ) {
+ var i = 0,
+ len = contexts.length;
+ for ( ; i < len; i++ ) {
+ Sizzle( selector, contexts[i], results );
+ }
+ return results;
+}
+
+function condense( unmatched, map, filter, context, xml ) {
+ var elem,
+ newUnmatched = [],
+ i = 0,
+ len = unmatched.length,
+ mapped = map != null;
+
+ for ( ; i < len; i++ ) {
+ if ( (elem = unmatched[i]) ) {
+ if ( !filter || filter( elem, context, xml ) ) {
+ newUnmatched.push( elem );
+ if ( mapped ) {
+ map.push( i );
+ }
+ }
+ }
+ }
+
+ return newUnmatched;
+}
+
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
+ if ( postFilter && !postFilter[ expando ] ) {
+ postFilter = setMatcher( postFilter );
+ }
+ if ( postFinder && !postFinder[ expando ] ) {
+ postFinder = setMatcher( postFinder, postSelector );
+ }
+ return markFunction(function( seed, results, context, xml ) {
+ var temp, i, elem,
+ preMap = [],
+ postMap = [],
+ preexisting = results.length,
+
+ // Get initial elements from seed or context
+ elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
+
+ // Prefilter to get matcher input, preserving a map for seed-results synchronization
+ matcherIn = preFilter && ( seed || !selector ) ?
+ condense( elems, preMap, preFilter, context, xml ) :
+ elems,
+
+ matcherOut = matcher ?
+ // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
+ postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
+
+ // ...intermediate processing is necessary
+ [] :
+
+ // ...otherwise use results directly
+ results :
+ matcherIn;
+
+ // Find primary matches
+ if ( matcher ) {
+ matcher( matcherIn, matcherOut, context, xml );
+ }
+
+ // Apply postFilter
+ if ( postFilter ) {
+ temp = condense( matcherOut, postMap );
+ postFilter( temp, [], context, xml );
+
+ // Un-match failing elements by moving them back to matcherIn
+ i = temp.length;
+ while ( i-- ) {
+ if ( (elem = temp[i]) ) {
+ matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
+ }
+ }
+ }
+
+ if ( seed ) {
+ if ( postFinder || preFilter ) {
+ if ( postFinder ) {
+ // Get the final matcherOut by condensing this intermediate into postFinder contexts
+ temp = [];
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( (elem = matcherOut[i]) ) {
+ // Restore matcherIn since elem is not yet a final match
+ temp.push( (matcherIn[i] = elem) );
+ }
+ }
+ postFinder( null, (matcherOut = []), temp, xml );
+ }
+
+ // Move matched elements from seed to results to keep them synchronized
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( (elem = matcherOut[i]) &&
+ (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) {
+
+ seed[temp] = !(results[temp] = elem);
+ }
+ }
+ }
+
+ // Add elements to results, through postFinder if defined
+ } else {
+ matcherOut = condense(
+ matcherOut === results ?
+ matcherOut.splice( preexisting, matcherOut.length ) :
+ matcherOut
+ );
+ if ( postFinder ) {
+ postFinder( null, results, matcherOut, xml );
+ } else {
+ push.apply( results, matcherOut );
+ }
+ }
+ });
+}
+
+function matcherFromTokens( tokens ) {
+ var checkContext, matcher, j,
+ len = tokens.length,
+ leadingRelative = Expr.relative[ tokens[0].type ],
+ implicitRelative = leadingRelative || Expr.relative[" "],
+ i = leadingRelative ? 1 : 0,
+
+ // The foundational matcher ensures that elements are reachable from top-level context(s)
+ matchContext = addCombinator( function( elem ) {
+ return elem === checkContext;
+ }, implicitRelative, true ),
+ matchAnyContext = addCombinator( function( elem ) {
+ return indexOf.call( checkContext, elem ) > -1;
+ }, implicitRelative, true ),
+ matchers = [ function( elem, context, xml ) {
+ return ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+ (checkContext = context).nodeType ?
+ matchContext( elem, context, xml ) :
+ matchAnyContext( elem, context, xml ) );
+ } ];
+
+ for ( ; i < len; i++ ) {
+ if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
+ matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
+ } else {
+ matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
+
+ // Return special upon seeing a positional matcher
+ if ( matcher[ expando ] ) {
+ // Find the next relative operator (if any) for proper handling
+ j = ++i;
+ for ( ; j < len; j++ ) {
+ if ( Expr.relative[ tokens[j].type ] ) {
+ break;
+ }
+ }
+ return setMatcher(
+ i > 1 && elementMatcher( matchers ),
+ i > 1 && toSelector(
+ // If the preceding token was a descendant combinator, insert an implicit any-element `*`
+ tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
+ ).replace( rtrim, "$1" ),
+ matcher,
+ i < j && matcherFromTokens( tokens.slice( i, j ) ),
+ j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
+ j < len && toSelector( tokens )
+ );
+ }
+ matchers.push( matcher );
+ }
+ }
+
+ return elementMatcher( matchers );
+}
+
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
+ var bySet = setMatchers.length > 0,
+ byElement = elementMatchers.length > 0,
+ superMatcher = function( seed, context, xml, results, outermost ) {
+ var elem, j, matcher,
+ matchedCount = 0,
+ i = "0",
+ unmatched = seed && [],
+ setMatched = [],
+ contextBackup = outermostContext,
+ // We must always have either seed elements or outermost context
+ elems = seed || byElement && Expr.find["TAG"]( "*", outermost ),
+ // Use integer dirruns iff this is the outermost matcher
+ dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
+ len = elems.length;
+
+ if ( outermost ) {
+ outermostContext = context !== document && context;
+ }
+
+ // Add elements passing elementMatchers directly to results
+ // Keep `i` a string if there are no elements so `matchedCount` will be "00" below
+ // Support: IE<9, Safari
+ // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
+ for ( ; i !== len && (elem = elems[i]) != null; i++ ) {
+ if ( byElement && elem ) {
+ j = 0;
+ while ( (matcher = elementMatchers[j++]) ) {
+ if ( matcher( elem, context, xml ) ) {
+ results.push( elem );
+ break;
+ }
+ }
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ }
+ }
+
+ // Track unmatched elements for set filters
+ if ( bySet ) {
+ // They will have gone through all possible matchers
+ if ( (elem = !matcher && elem) ) {
+ matchedCount--;
+ }
+
+ // Lengthen the array for every element, matched or not
+ if ( seed ) {
+ unmatched.push( elem );
+ }
+ }
+ }
+
+ // Apply set filters to unmatched elements
+ matchedCount += i;
+ if ( bySet && i !== matchedCount ) {
+ j = 0;
+ while ( (matcher = setMatchers[j++]) ) {
+ matcher( unmatched, setMatched, context, xml );
+ }
+
+ if ( seed ) {
+ // Reintegrate element matches to eliminate the need for sorting
+ if ( matchedCount > 0 ) {
+ while ( i-- ) {
+ if ( !(unmatched[i] || setMatched[i]) ) {
+ setMatched[i] = pop.call( results );
+ }
+ }
+ }
+
+ // Discard index placeholder values to get only actual matches
+ setMatched = condense( setMatched );
+ }
+
+ // Add matches to results
+ push.apply( results, setMatched );
+
+ // Seedless set matches succeeding multiple successful matchers stipulate sorting
+ if ( outermost && !seed && setMatched.length > 0 &&
+ ( matchedCount + setMatchers.length ) > 1 ) {
+
+ Sizzle.uniqueSort( results );
+ }
+ }
+
+ // Override manipulation of globals by nested matchers
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ outermostContext = contextBackup;
+ }
+
+ return unmatched;
+ };
+
+ return bySet ?
+ markFunction( superMatcher ) :
+ superMatcher;
+}
+
+compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
+ var i,
+ setMatchers = [],
+ elementMatchers = [],
+ cached = compilerCache[ selector + " " ];
+
+ if ( !cached ) {
+ // Generate a function of recursive functions that can be used to check each element
+ if ( !match ) {
+ match = tokenize( selector );
+ }
+ i = match.length;
+ while ( i-- ) {
+ cached = matcherFromTokens( match[i] );
+ if ( cached[ expando ] ) {
+ setMatchers.push( cached );
+ } else {
+ elementMatchers.push( cached );
+ }
+ }
+
+ // Cache the compiled function
+ cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
+
+ // Save selector and tokenization
+ cached.selector = selector;
+ }
+ return cached;
+};
+
+/**
+ * A low-level selection function that works with Sizzle's compiled
+ * selector functions
+ * @param {String|Function} selector A selector or a pre-compiled
+ * selector function built with Sizzle.compile
+ * @param {Element} context
+ * @param {Array} [results]
+ * @param {Array} [seed] A set of elements to match against
+ */
+select = Sizzle.select = function( selector, context, results, seed ) {
+ var i, tokens, token, type, find,
+ compiled = typeof selector === "function" && selector,
+ match = !seed && tokenize( (selector = compiled.selector || selector) );
+
+ results = results || [];
+
+ // Try to minimize operations if there is no seed and only one group
+ if ( match.length === 1 ) {
+
+ // Take a shortcut and set the context if the root selector is an ID
+ tokens = match[0] = match[0].slice( 0 );
+ if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
+ support.getById && context.nodeType === 9 && documentIsHTML &&
+ Expr.relative[ tokens[1].type ] ) {
+
+ context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
+ if ( !context ) {
+ return results;
+
+ // Precompiled matchers will still verify ancestry, so step up a level
+ } else if ( compiled ) {
+ context = context.parentNode;
+ }
+
+ selector = selector.slice( tokens.shift().value.length );
+ }
+
+ // Fetch a seed set for right-to-left matching
+ i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
+ while ( i-- ) {
+ token = tokens[i];
+
+ // Abort if we hit a combinator
+ if ( Expr.relative[ (type = token.type) ] ) {
+ break;
+ }
+ if ( (find = Expr.find[ type ]) ) {
+ // Search, expanding context for leading sibling combinators
+ if ( (seed = find(
+ token.matches[0].replace( runescape, funescape ),
+ rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context
+ )) ) {
+
+ // If seed is empty or no tokens remain, we can return early
+ tokens.splice( i, 1 );
+ selector = seed.length && toSelector( tokens );
+ if ( !selector ) {
+ push.apply( results, seed );
+ return results;
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ // Compile and execute a filtering function if one is not provided
+ // Provide `match` to avoid retokenization if we modified the selector above
+ ( compiled || compile( selector, match ) )(
+ seed,
+ context,
+ !documentIsHTML,
+ results,
+ rsibling.test( selector ) && testContext( context.parentNode ) || context
+ );
+ return results;
+};
+
+// One-time assignments
+
+// Sort stability
+support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
+
+// Support: Chrome 14-35+
+// Always assume duplicates if they aren't passed to the comparison function
+support.detectDuplicates = !!hasDuplicate;
+
+// Initialize against the default document
+setDocument();
+
+// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
+// Detached nodes confoundingly follow *each other*
+support.sortDetached = assert(function( div1 ) {
+ // Should return 1, but returns 4 (following)
+ return div1.compareDocumentPosition( document.createElement("div") ) & 1;
+});
+
+// Support: IE<8
+// Prevent attribute/property "interpolation"
+// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+if ( !assert(function( div ) {
+ div.innerHTML = "<a href='#'></a>";
+ return div.firstChild.getAttribute("href") === "#" ;
+}) ) {
+ addHandle( "type|href|height|width", function( elem, name, isXML ) {
+ if ( !isXML ) {
+ return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
+ }
+ });
+}
+
+// Support: IE<9
+// Use defaultValue in place of getAttribute("value")
+if ( !support.attributes || !assert(function( div ) {
+ div.innerHTML = "<input/>";
+ div.firstChild.setAttribute( "value", "" );
+ return div.firstChild.getAttribute( "value" ) === "";
+}) ) {
+ addHandle( "value", function( elem, name, isXML ) {
+ if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
+ return elem.defaultValue;
+ }
+ });
+}
+
+// Support: IE<9
+// Use getAttributeNode to fetch booleans when getAttribute lies
+if ( !assert(function( div ) {
+ return div.getAttribute("disabled") == null;
+}) ) {
+ addHandle( booleans, function( elem, name, isXML ) {
+ var val;
+ if ( !isXML ) {
+ return elem[ name ] === true ? name.toLowerCase() :
+ (val = elem.getAttributeNode( name )) && val.specified ?
+ val.value :
+ null;
+ }
+ });
+}
+
+// EXPOSE
+return Sizzle;
+});
+
+/*eslint-enable */
+
+// Included from: js/tinymce/classes/util/Arr.js
+
+/**
+ * Arr.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Array utility class.
+ *
+ * @private
+ * @class tinymce.util.Arr
+ */
+define("tinymce/util/Arr", [], function() {
+ var isArray = Array.isArray || function(obj) {
+ return Object.prototype.toString.call(obj) === "[object Array]";
+ };
+
+ function toArray(obj) {
+ var array = obj, i, l;
+
+ if (!isArray(obj)) {
+ array = [];
+ for (i = 0, l = obj.length; i < l; i++) {
+ array[i] = obj[i];
+ }
+ }
+
+ return array;
+ }
+
+ function each(o, cb, s) {
+ var n, l;
+
+ if (!o) {
+ return 0;
+ }
+
+ s = s || o;
+
+ if (o.length !== undefined) {
+ // Indexed arrays, needed for Safari
+ for (n = 0, l = o.length; n < l; n++) {
+ if (cb.call(s, o[n], n, o) === false) {
+ return 0;
+ }
+ }
+ } else {
+ // Hashtables
+ for (n in o) {
+ if (o.hasOwnProperty(n)) {
+ if (cb.call(s, o[n], n, o) === false) {
+ return 0;
+ }
+ }
+ }
+ }
+
+ return 1;
+ }
+
+ function map(array, callback) {
+ var out = [];
+
+ each(array, function(item, index) {
+ out.push(callback(item, index, array));
+ });
+
+ return out;
+ }
+
+ function filter(a, f) {
+ var o = [];
+
+ each(a, function(v, index) {
+ if (!f || f(v, index, a)) {
+ o.push(v);
+ }
+ });
+
+ return o;
+ }
+
+ function indexOf(a, v) {
+ var i, l;
+
+ if (a) {
+ for (i = 0, l = a.length; i < l; i++) {
+ if (a[i] === v) {
+ return i;
+ }
+ }
+ }
+
+ return -1;
+ }
+
+ function reduce(collection, iteratee, accumulator, thisArg) {
+ var i = 0;
+
+ if (arguments.length < 3) {
+ accumulator = collection[0];
+ }
+
+ for (; i < collection.length; i++) {
+ accumulator = iteratee.call(thisArg, accumulator, collection[i], i);
+ }
+
+ return accumulator;
+ }
+
+ function findIndex(array, predicate, thisArg) {
+ var i, l;
+
+ for (i = 0, l = array.length; i < l; i++) {
+ if (predicate.call(thisArg, array[i], i, array)) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ function find(array, predicate, thisArg) {
+ var idx = findIndex(array, predicate, thisArg);
+
+ if (idx !== -1) {
+ return array[idx];
+ }
+
+ return undefined;
+ }
+
+ function last(collection) {
+ return collection[collection.length - 1];
+ }
+
+ return {
+ isArray: isArray,
+ toArray: toArray,
+ each: each,
+ map: map,
+ filter: filter,
+ indexOf: indexOf,
+ reduce: reduce,
+ findIndex: findIndex,
+ find: find,
+ last: last
+ };
+});
+
+// Included from: js/tinymce/classes/util/Tools.js
+
+/**
+ * Tools.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class contains various utlity functions. These are also exposed
+ * directly on the tinymce namespace.
+ *
+ * @class tinymce.util.Tools
+ */
+define("tinymce/util/Tools", [
+ "tinymce/Env",
+ "tinymce/util/Arr"
+], function(Env, Arr) {
+ /**
+ * Removes whitespace from the beginning and end of a string.
+ *
+ * @method trim
+ * @param {String} s String to remove whitespace from.
+ * @return {String} New string with removed whitespace.
+ */
+ var whiteSpaceRegExp = /^\s*|\s*$/g;
+
+ function trim(str) {
+ return (str === null || str === undefined) ? '' : ("" + str).replace(whiteSpaceRegExp, '');
+ }
+
+ /**
+ * Checks if a object is of a specific type for example an array.
+ *
+ * @method is
+ * @param {Object} obj Object to check type of.
+ * @param {string} type Optional type to check for.
+ * @return {Boolean} true/false if the object is of the specified type.
+ */
+ function is(obj, type) {
+ if (!type) {
+ return obj !== undefined;
+ }
+
+ if (type == 'array' && Arr.isArray(obj)) {
+ return true;
+ }
+
+ return typeof obj == type;
+ }
+
+ /**
+ * Makes a name/object map out of an array with names.
+ *
+ * @method makeMap
+ * @param {Array/String} items Items to make map out of.
+ * @param {String} delim Optional delimiter to split string by.
+ * @param {Object} map Optional map to add items to.
+ * @return {Object} Name/value map of items.
+ */
+ function makeMap(items, delim, map) {
+ var i;
+
+ items = items || [];
+ delim = delim || ',';
+
+ if (typeof items == "string") {
+ items = items.split(delim);
+ }
+
+ map = map || {};
+
+ i = items.length;
+ while (i--) {
+ map[items[i]] = {};
+ }
+
+ return map;
+ }
+
+ /**
+ * JavaScript does not protect hasOwnProperty method, so it is possible to overwrite it. This is
+ * object independent version.
+ *
+ * @param {Object} obj
+ * @param {String} prop
+ * @returns {Boolean}
+ */
+ function hasOwnProperty(obj, prop) {
+ return Object.prototype.hasOwnProperty.call(obj, prop);
+ }
+
+ /**
+ * Creates a class, subclass or static singleton.
+ * More details on this method can be found in the Wiki.
+ *
+ * @method create
+ * @param {String} s Class name, inheritance and prefix.
+ * @param {Object} p Collection of methods to add to the class.
+ * @param {Object} root Optional root object defaults to the global window object.
+ * @example
+ * // Creates a basic class
+ * tinymce.create('tinymce.somepackage.SomeClass', {
+ * SomeClass: function() {
+ * // Class constructor
+ * },
+ *
+ * method: function() {
+ * // Some method
+ * }
+ * });
+ *
+ * // Creates a basic subclass class
+ * tinymce.create('tinymce.somepackage.SomeSubClass:tinymce.somepackage.SomeClass', {
+ * SomeSubClass: function() {
+ * // Class constructor
+ * this.parent(); // Call parent constructor
+ * },
+ *
+ * method: function() {
+ * // Some method
+ * this.parent(); // Call parent method
+ * },
+ *
+ * 'static': {
+ * staticMethod: function() {
+ * // Static method
+ * }
+ * }
+ * });
+ *
+ * // Creates a singleton/static class
+ * tinymce.create('static tinymce.somepackage.SomeSingletonClass', {
+ * method: function() {
+ * // Some method
+ * }
+ * });
+ */
+ function create(s, p, root) {
+ var self = this, sp, ns, cn, scn, c, de = 0;
+
+ // Parse : <prefix> <class>:<super class>
+ s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s);
+ cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name
+
+ // Create namespace for new class
+ ns = self.createNS(s[3].replace(/\.\w+$/, ''), root);
+
+ // Class already exists
+ if (ns[cn]) {
+ return;
+ }
+
+ // Make pure static class
+ if (s[2] == 'static') {
+ ns[cn] = p;
+
+ if (this.onCreate) {
+ this.onCreate(s[2], s[3], ns[cn]);
+ }
+
+ return;
+ }
+
+ // Create default constructor
+ if (!p[cn]) {
+ p[cn] = function() {};
+ de = 1;
+ }
+
+ // Add constructor and methods
+ ns[cn] = p[cn];
+ self.extend(ns[cn].prototype, p);
+
+ // Extend
+ if (s[5]) {
+ sp = self.resolve(s[5]).prototype;
+ scn = s[5].match(/\.(\w+)$/i)[1]; // Class name
+
+ // Extend constructor
+ c = ns[cn];
+ if (de) {
+ // Add passthrough constructor
+ ns[cn] = function() {
+ return sp[scn].apply(this, arguments);
+ };
+ } else {
+ // Add inherit constructor
+ ns[cn] = function() {
+ this.parent = sp[scn];
+ return c.apply(this, arguments);
+ };
+ }
+ ns[cn].prototype[cn] = ns[cn];
+
+ // Add super methods
+ self.each(sp, function(f, n) {
+ ns[cn].prototype[n] = sp[n];
+ });
+
+ // Add overridden methods
+ self.each(p, function(f, n) {
+ // Extend methods if needed
+ if (sp[n]) {
+ ns[cn].prototype[n] = function() {
+ this.parent = sp[n];
+ return f.apply(this, arguments);
+ };
+ } else {
+ if (n != cn) {
+ ns[cn].prototype[n] = f;
+ }
+ }
+ });
+ }
+
+ // Add static methods
+ /*jshint sub:true*/
+ /*eslint dot-notation:0*/
+ self.each(p['static'], function(f, n) {
+ ns[cn][n] = f;
+ });
+ }
+
+ function extend(obj, ext) {
+ var i, l, name, args = arguments, value;
+
+ for (i = 1, l = args.length; i < l; i++) {
+ ext = args[i];
+ for (name in ext) {
+ if (ext.hasOwnProperty(name)) {
+ value = ext[name];
+
+ if (value !== undefined) {
+ obj[name] = value;
+ }
+ }
+ }
+ }
+
+ return obj;
+ }
+
+ /**
+ * Executed the specified function for each item in a object tree.
+ *
+ * @method walk
+ * @param {Object} o Object tree to walk though.
+ * @param {function} f Function to call for each item.
+ * @param {String} n Optional name of collection inside the objects to walk for example childNodes.
+ * @param {String} s Optional scope to execute the function in.
+ */
+ function walk(o, f, n, s) {
+ s = s || this;
+
+ if (o) {
+ if (n) {
+ o = o[n];
+ }
+
+ Arr.each(o, function(o, i) {
+ if (f.call(s, o, i, n) === false) {
+ return false;
+ }
+
+ walk(o, f, n, s);
+ });
+ }
+ }
+
+ /**
+ * Creates a namespace on a specific object.
+ *
+ * @method createNS
+ * @param {String} n Namespace to create for example a.b.c.d.
+ * @param {Object} o Optional object to add namespace to, defaults to window.
+ * @return {Object} New namespace object the last item in path.
+ * @example
+ * // Create some namespace
+ * tinymce.createNS('tinymce.somepackage.subpackage');
+ *
+ * // Add a singleton
+ * var tinymce.somepackage.subpackage.SomeSingleton = {
+ * method: function() {
+ * // Some method
+ * }
+ * };
+ */
+ function createNS(n, o) {
+ var i, v;
+
+ o = o || window;
+
+ n = n.split('.');
+ for (i = 0; i < n.length; i++) {
+ v = n[i];
+
+ if (!o[v]) {
+ o[v] = {};
+ }
+
+ o = o[v];
+ }
+
+ return o;
+ }
+
+ /**
+ * Resolves a string and returns the object from a specific structure.
+ *
+ * @method resolve
+ * @param {String} n Path to resolve for example a.b.c.d.
+ * @param {Object} o Optional object to search though, defaults to window.
+ * @return {Object} Last object in path or null if it couldn't be resolved.
+ * @example
+ * // Resolve a path into an object reference
+ * var obj = tinymce.resolve('a.b.c.d');
+ */
+ function resolve(n, o) {
+ var i, l;
+
+ o = o || window;
+
+ n = n.split('.');
+ for (i = 0, l = n.length; i < l; i++) {
+ o = o[n[i]];
+
+ if (!o) {
+ break;
+ }
+ }
+
+ return o;
+ }
+
+ /**
+ * Splits a string but removes the whitespace before and after each value.
+ *
+ * @method explode
+ * @param {string} s String to split.
+ * @param {string} d Delimiter to split by.
+ * @example
+ * // Split a string into an array with a,b,c
+ * var arr = tinymce.explode('a, b, c');
+ */
+ function explode(s, d) {
+ if (!s || is(s, 'array')) {
+ return s;
+ }
+
+ return Arr.map(s.split(d || ','), trim);
+ }
+
+ function _addCacheSuffix(url) {
+ var cacheSuffix = Env.cacheSuffix;
+
+ if (cacheSuffix) {
+ url += (url.indexOf('?') === -1 ? '?' : '&') + cacheSuffix;
+ }
+
+ return url;
+ }
+
+ return {
+ trim: trim,
+
+ /**
+ * Returns true/false if the object is an array or not.
+ *
+ * @method isArray
+ * @param {Object} obj Object to check.
+ * @return {boolean} true/false state if the object is an array or not.
+ */
+ isArray: Arr.isArray,
+
+ is: is,
+
+ /**
+ * Converts the specified object into a real JavaScript array.
+ *
+ * @method toArray
+ * @param {Object} obj Object to convert into array.
+ * @return {Array} Array object based in input.
+ */
+ toArray: Arr.toArray,
+ makeMap: makeMap,
+
+ /**
+ * Performs an iteration of all items in a collection such as an object or array. This method will execure the
+ * callback function for each item in the collection, if the callback returns false the iteration will terminate.
+ * The callback has the following format: cb(value, key_or_index).
+ *
+ * @method each
+ * @param {Object} o Collection to iterate.
+ * @param {function} cb Callback function to execute for each item.
+ * @param {Object} s Optional scope to execute the callback in.
+ * @example
+ * // Iterate an array
+ * tinymce.each([1,2,3], function(v, i) {
+ * console.debug("Value: " + v + ", Index: " + i);
+ * });
+ *
+ * // Iterate an object
+ * tinymce.each({a: 1, b: 2, c: 3], function(v, k) {
+ * console.debug("Value: " + v + ", Key: " + k);
+ * });
+ */
+ each: Arr.each,
+
+ /**
+ * Creates a new array by the return value of each iteration function call. This enables you to convert
+ * one array list into another.
+ *
+ * @method map
+ * @param {Array} array Array of items to iterate.
+ * @param {function} callback Function to call for each item. It's return value will be the new value.
+ * @return {Array} Array with new values based on function return values.
+ */
+ map: Arr.map,
+
+ /**
+ * Filters out items from the input array by calling the specified function for each item.
+ * If the function returns false the item will be excluded if it returns true it will be included.
+ *
+ * @method grep
+ * @param {Array} a Array of items to loop though.
+ * @param {function} f Function to call for each item. Include/exclude depends on it's return value.
+ * @return {Array} New array with values imported and filtered based in input.
+ * @example
+ * // Filter out some items, this will return an array with 4 and 5
+ * var items = tinymce.grep([1,2,3,4,5], function(v) {return v > 3;});
+ */
+ grep: Arr.filter,
+
+ /**
+ * Returns an index of the item or -1 if item is not present in the array.
+ *
+ * @method inArray
+ * @param {any} item Item to search for.
+ * @param {Array} arr Array to search in.
+ * @return {Number} index of the item or -1 if item was not found.
+ */
+ inArray: Arr.indexOf,
+
+ hasOwn: hasOwnProperty,
+
+ extend: extend,
+ create: create,
+ walk: walk,
+ createNS: createNS,
+ resolve: resolve,
+ explode: explode,
+ _addCacheSuffix: _addCacheSuffix
+ };
+});
+
+// Included from: js/tinymce/classes/dom/DomQuery.js
+
+/**
+ * DomQuery.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class mimics most of the jQuery API:
+ *
+ * This is whats currently implemented:
+ * - Utility functions
+ * - DOM traversial
+ * - DOM manipulation
+ * - Event binding
+ *
+ * This is not currently implemented:
+ * - Dimension
+ * - Ajax
+ * - Animation
+ * - Advanced chaining
+ *
+ * @example
+ * var $ = tinymce.dom.DomQuery;
+ * $('p').attr('attr', 'value').addClass('class');
+ *
+ * @class tinymce.dom.DomQuery
+ */
+define("tinymce/dom/DomQuery", [
+ "tinymce/dom/EventUtils",
+ "tinymce/dom/Sizzle",
+ "tinymce/util/Tools",
+ "tinymce/Env"
+], function(EventUtils, Sizzle, Tools, Env) {
+ var doc = document, push = Array.prototype.push, slice = Array.prototype.slice;
+ var rquickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/;
+ var Event = EventUtils.Event, undef;
+ var skipUniques = Tools.makeMap('children,contents,next,prev');
+
+ function isDefined(obj) {
+ return typeof obj !== 'undefined';
+ }
+
+ function isString(obj) {
+ return typeof obj === 'string';
+ }
+
+ function isWindow(obj) {
+ return obj && obj == obj.window;
+ }
+
+ function createFragment(html, fragDoc) {
+ var frag, node, container;
+
+ fragDoc = fragDoc || doc;
+ container = fragDoc.createElement('div');
+ frag = fragDoc.createDocumentFragment();
+ container.innerHTML = html;
+
+ while ((node = container.firstChild)) {
+ frag.appendChild(node);
+ }
+
+ return frag;
+ }
+
+ function domManipulate(targetNodes, sourceItem, callback, reverse) {
+ var i;
+
+ if (isString(sourceItem)) {
+ sourceItem = createFragment(sourceItem, getElementDocument(targetNodes[0]));
+ } else if (sourceItem.length && !sourceItem.nodeType) {
+ sourceItem = DomQuery.makeArray(sourceItem);
+
+ if (reverse) {
+ for (i = sourceItem.length - 1; i >= 0; i--) {
+ domManipulate(targetNodes, sourceItem[i], callback, reverse);
+ }
+ } else {
+ for (i = 0; i < sourceItem.length; i++) {
+ domManipulate(targetNodes, sourceItem[i], callback, reverse);
+ }
+ }
+
+ return targetNodes;
+ }
+
+ if (sourceItem.nodeType) {
+ i = targetNodes.length;
+ while (i--) {
+ callback.call(targetNodes[i], sourceItem);
+ }
+ }
+
+ return targetNodes;
+ }
+
+ function hasClass(node, className) {
+ return node && className && (' ' + node.className + ' ').indexOf(' ' + className + ' ') !== -1;
+ }
+
+ function wrap(elements, wrapper, all) {
+ var lastParent, newWrapper;
+
+ wrapper = DomQuery(wrapper)[0];
+
+ elements.each(function() {
+ var self = this;
+
+ if (!all || lastParent != self.parentNode) {
+ lastParent = self.parentNode;
+ newWrapper = wrapper.cloneNode(false);
+ self.parentNode.insertBefore(newWrapper, self);
+ newWrapper.appendChild(self);
+ } else {
+ newWrapper.appendChild(self);
+ }
+ });
+
+ return elements;
+ }
+
+ var numericCssMap = Tools.makeMap('fillOpacity fontWeight lineHeight opacity orphans widows zIndex zoom', ' ');
+ var booleanMap = Tools.makeMap('checked compact declare defer disabled ismap multiple nohref noshade nowrap readonly selected', ' ');
+ var propFix = {
+ 'for': 'htmlFor',
+ 'class': 'className',
+ 'readonly': 'readOnly'
+ };
+ var cssFix = {
+ 'float': 'cssFloat'
+ };
+
+ var attrHooks = {}, cssHooks = {};
+
+ function DomQuery(selector, context) {
+ /*eslint new-cap:0 */
+ return new DomQuery.fn.init(selector, context);
+ }
+
+ function inArray(item, array) {
+ var i;
+
+ if (array.indexOf) {
+ return array.indexOf(item);
+ }
+
+ i = array.length;
+ while (i--) {
+ if (array[i] === item) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ var whiteSpaceRegExp = /^\s*|\s*$/g;
+
+ function trim(str) {
+ return (str === null || str === undef) ? '' : ("" + str).replace(whiteSpaceRegExp, '');
+ }
+
+ function each(obj, callback) {
+ var length, key, i, undef, value;
+
+ if (obj) {
+ length = obj.length;
+
+ if (length === undef) {
+ // Loop object items
+ for (key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ value = obj[key];
+ if (callback.call(value, key, value) === false) {
+ break;
+ }
+ }
+ }
+ } else {
+ // Loop array items
+ for (i = 0; i < length; i++) {
+ value = obj[i];
+ if (callback.call(value, i, value) === false) {
+ break;
+ }
+ }
+ }
+ }
+
+ return obj;
+ }
+
+ function grep(array, callback) {
+ var out = [];
+
+ each(array, function(i, item) {
+ if (callback(item, i)) {
+ out.push(item);
+ }
+ });
+
+ return out;
+ }
+
+ function getElementDocument(element) {
+ if (!element) {
+ return doc;
+ }
+
+ if (element.nodeType == 9) {
+ return element;
+ }
+
+ return element.ownerDocument;
+ }
+
+ DomQuery.fn = DomQuery.prototype = {
+ constructor: DomQuery,
+
+ /**
+ * Selector for the current set.
+ *
+ * @property selector
+ * @type String
+ */
+ selector: "",
+
+ /**
+ * Context used to create the set.
+ *
+ * @property context
+ * @type Element
+ */
+ context: null,
+
+ /**
+ * Number of items in the current set.
+ *
+ * @property length
+ * @type Number
+ */
+ length: 0,
+
+ /**
+ * Constructs a new DomQuery instance with the specified selector or context.
+ *
+ * @constructor
+ * @method init
+ * @param {String/Array/DomQuery} selector Optional CSS selector/Array or array like object or HTML string.
+ * @param {Document/Element} context Optional context to search in.
+ */
+ init: function(selector, context) {
+ var self = this, match, node;
+
+ if (!selector) {
+ return self;
+ }
+
+ if (selector.nodeType) {
+ self.context = self[0] = selector;
+ self.length = 1;
+
+ return self;
+ }
+
+ if (context && context.nodeType) {
+ self.context = context;
+ } else {
+ if (context) {
+ return DomQuery(selector).attr(context);
+ }
+
+ self.context = context = document;
+ }
+
+ if (isString(selector)) {
+ self.selector = selector;
+
+ if (selector.charAt(0) === "<" && selector.charAt(selector.length - 1) === ">" && selector.length >= 3) {
+ match = [null, selector, null];
+ } else {
+ match = rquickExpr.exec(selector);
+ }
+
+ if (match) {
+ if (match[1]) {
+ node = createFragment(selector, getElementDocument(context)).firstChild;
+
+ while (node) {
+ push.call(self, node);
+ node = node.nextSibling;
+ }
+ } else {
+ node = getElementDocument(context).getElementById(match[2]);
+
+ if (!node) {
+ return self;
+ }
+
+ if (node.id !== match[2]) {
+ return self.find(selector);
+ }
+
+ self.length = 1;
+ self[0] = node;
+ }
+ } else {
+ return DomQuery(context).find(selector);
+ }
+ } else {
+ this.add(selector, false);
+ }
+
+ return self;
+ },
+
+ /**
+ * Converts the current set to an array.
+ *
+ * @method toArray
+ * @return {Array} Array of all nodes in set.
+ */
+ toArray: function() {
+ return Tools.toArray(this);
+ },
+
+ /**
+ * Adds new nodes to the set.
+ *
+ * @method add
+ * @param {Array/tinymce.dom.DomQuery} items Array of all nodes to add to set.
+ * @param {Boolean} sort Optional sort flag that enables sorting of elements.
+ * @return {tinymce.dom.DomQuery} New instance with nodes added.
+ */
+ add: function(items, sort) {
+ var self = this, nodes, i;
+
+ if (isString(items)) {
+ return self.add(DomQuery(items));
+ }
+
+ if (sort !== false) {
+ nodes = DomQuery.unique(self.toArray().concat(DomQuery.makeArray(items)));
+ self.length = nodes.length;
+ for (i = 0; i < nodes.length; i++) {
+ self[i] = nodes[i];
+ }
+ } else {
+ push.apply(self, DomQuery.makeArray(items));
+ }
+
+ return self;
+ },
+
+ /**
+ * Sets/gets attributes on the elements in the current set.
+ *
+ * @method attr
+ * @param {String/Object} name Name of attribute to get or an object with attributes to set.
+ * @param {String} value Optional value to set.
+ * @return {tinymce.dom.DomQuery/String} Current set or the specified attribute when only the name is specified.
+ */
+ attr: function(name, value) {
+ var self = this, hook;
+
+ if (typeof name === "object") {
+ each(name, function(name, value) {
+ self.attr(name, value);
+ });
+ } else if (isDefined(value)) {
+ this.each(function() {
+ var hook;
+
+ if (this.nodeType === 1) {
+ hook = attrHooks[name];
+ if (hook && hook.set) {
+ hook.set(this, value);
+ return;
+ }
+
+ if (value === null) {
+ this.removeAttribute(name, 2);
+ } else {
+ this.setAttribute(name, value, 2);
+ }
+ }
+ });
+ } else {
+ if (self[0] && self[0].nodeType === 1) {
+ hook = attrHooks[name];
+ if (hook && hook.get) {
+ return hook.get(self[0], name);
+ }
+
+ if (booleanMap[name]) {
+ return self.prop(name) ? name : undef;
+ }
+
+ value = self[0].getAttribute(name, 2);
+
+ if (value === null) {
+ value = undef;
+ }
+ }
+
+ return value;
+ }
+
+ return self;
+ },
+
+ /**
+ * Removes attributse on the elements in the current set.
+ *
+ * @method removeAttr
+ * @param {String/Object} name Name of attribute to remove.
+ * @return {tinymce.dom.DomQuery/String} Current set.
+ */
+ removeAttr: function(name) {
+ return this.attr(name, null);
+ },
+
+ /**
+ * Sets/gets properties on the elements in the current set.
+ *
+ * @method attr
+ * @param {String/Object} name Name of property to get or an object with properties to set.
+ * @param {String} value Optional value to set.
+ * @return {tinymce.dom.DomQuery/String} Current set or the specified property when only the name is specified.
+ */
+ prop: function(name, value) {
+ var self = this;
+
+ name = propFix[name] || name;
+
+ if (typeof name === "object") {
+ each(name, function(name, value) {
+ self.prop(name, value);
+ });
+ } else if (isDefined(value)) {
+ this.each(function() {
+ if (this.nodeType == 1) {
+ this[name] = value;
+ }
+ });
+ } else {
+ if (self[0] && self[0].nodeType && name in self[0]) {
+ return self[0][name];
+ }
+
+ return value;
+ }
+
+ return self;
+ },
+
+ /**
+ * Sets/gets styles on the elements in the current set.
+ *
+ * @method css
+ * @param {String/Object} name Name of style to get or an object with styles to set.
+ * @param {String} value Optional value to set.
+ * @return {tinymce.dom.DomQuery/String} Current set or the specified style when only the name is specified.
+ */
+ css: function(name, value) {
+ var self = this, elm, hook;
+
+ function camel(name) {
+ return name.replace(/-(\D)/g, function(a, b) {
+ return b.toUpperCase();
+ });
+ }
+
+ function dashed(name) {
+ return name.replace(/[A-Z]/g, function(a) {
+ return '-' + a;
+ });
+ }
+
+ if (typeof name === "object") {
+ each(name, function(name, value) {
+ self.css(name, value);
+ });
+ } else {
+ if (isDefined(value)) {
+ name = camel(name);
+
+ // Default px suffix on these
+ if (typeof value === 'number' && !numericCssMap[name]) {
+ value += 'px';
+ }
+
+ self.each(function() {
+ var style = this.style;
+
+ hook = cssHooks[name];
+ if (hook && hook.set) {
+ hook.set(this, value);
+ return;
+ }
+
+ try {
+ this.style[cssFix[name] || name] = value;
+ } catch (ex) {
+ // Ignore
+ }
+
+ if (value === null || value === '') {
+ if (style.removeProperty) {
+ style.removeProperty(dashed(name));
+ } else {
+ style.removeAttribute(name);
+ }
+ }
+ });
+ } else {
+ elm = self[0];
+
+ hook = cssHooks[name];
+ if (hook && hook.get) {
+ return hook.get(elm);
+ }
+
+ if (elm.ownerDocument.defaultView) {
+ try {
+ return elm.ownerDocument.defaultView.getComputedStyle(elm, null).getPropertyValue(dashed(name));
+ } catch (ex) {
+ return undef;
+ }
+ } else if (elm.currentStyle) {
+ return elm.currentStyle[camel(name)];
+ }
+ }
+ }
+
+ return self;
+ },
+
+ /**
+ * Removes all nodes in set from the document.
+ *
+ * @method remove
+ * @return {tinymce.dom.DomQuery} Current set with the removed nodes.
+ */
+ remove: function() {
+ var self = this, node, i = this.length;
+
+ while (i--) {
+ node = self[i];
+ Event.clean(node);
+
+ if (node.parentNode) {
+ node.parentNode.removeChild(node);
+ }
+ }
+
+ return this;
+ },
+
+ /**
+ * Empties all elements in set.
+ *
+ * @method empty
+ * @return {tinymce.dom.DomQuery} Current set with the empty nodes.
+ */
+ empty: function() {
+ var self = this, node, i = this.length;
+
+ while (i--) {
+ node = self[i];
+ while (node.firstChild) {
+ node.removeChild(node.firstChild);
+ }
+ }
+
+ return this;
+ },
+
+ /**
+ * Sets or gets the HTML of the current set or first set node.
+ *
+ * @method html
+ * @param {String} value Optional innerHTML value to set on each element.
+ * @return {tinymce.dom.DomQuery/String} Current set or the innerHTML of the first element.
+ */
+ html: function(value) {
+ var self = this, i;
+
+ if (isDefined(value)) {
+ i = self.length;
+
+ try {
+ while (i--) {
+ self[i].innerHTML = value;
+ }
+ } catch (ex) {
+ // Workaround for "Unknown runtime error" when DIV is added to P on IE
+ DomQuery(self[i]).empty().append(value);
+ }
+
+ return self;
+ }
+
+ return self[0] ? self[0].innerHTML : '';
+ },
+
+ /**
+ * Sets or gets the text of the current set or first set node.
+ *
+ * @method text
+ * @param {String} value Optional innerText value to set on each element.
+ * @return {tinymce.dom.DomQuery/String} Current set or the innerText of the first element.
+ */
+ text: function(value) {
+ var self = this, i;
+
+ if (isDefined(value)) {
+ i = self.length;
+ while (i--) {
+ if ("innerText" in self[i]) {
+ self[i].innerText = value;
+ } else {
+ self[0].textContent = value;
+ }
+ }
+
+ return self;
+ }
+
+ return self[0] ? (self[0].innerText || self[0].textContent) : '';
+ },
+
+ /**
+ * Appends the specified node/html or node set to the current set nodes.
+ *
+ * @method append
+ * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to append to each element in set.
+ * @return {tinymce.dom.DomQuery} Current set.
+ */
+ append: function() {
+ return domManipulate(this, arguments, function(node) {
+ // Either element or Shadow Root
+ if (this.nodeType === 1 || (this.host && this.host.nodeType === 1)) {
+ this.appendChild(node);
+ }
+ });
+ },
+
+ /**
+ * Prepends the specified node/html or node set to the current set nodes.
+ *
+ * @method prepend
+ * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to prepend to each element in set.
+ * @return {tinymce.dom.DomQuery} Current set.
+ */
+ prepend: function() {
+ return domManipulate(this, arguments, function(node) {
+ // Either element or Shadow Root
+ if (this.nodeType === 1 || (this.host && this.host.nodeType === 1)) {
+ this.insertBefore(node, this.firstChild);
+ }
+ }, true);
+ },
+
+ /**
+ * Adds the specified elements before current set nodes.
+ *
+ * @method before
+ * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to add before to each element in set.
+ * @return {tinymce.dom.DomQuery} Current set.
+ */
+ before: function() {
+ var self = this;
+
+ if (self[0] && self[0].parentNode) {
+ return domManipulate(self, arguments, function(node) {
+ this.parentNode.insertBefore(node, this);
+ });
+ }
+
+ return self;
+ },
+
+ /**
+ * Adds the specified elements after current set nodes.
+ *
+ * @method after
+ * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to add after to each element in set.
+ * @return {tinymce.dom.DomQuery} Current set.
+ */
+ after: function() {
+ var self = this;
+
+ if (self[0] && self[0].parentNode) {
+ return domManipulate(self, arguments, function(node) {
+ this.parentNode.insertBefore(node, this.nextSibling);
+ }, true);
+ }
+
+ return self;
+ },
+
+ /**
+ * Appends the specified set nodes to the specified selector/instance.
+ *
+ * @method appendTo
+ * @param {String/Element/Array/tinymce.dom.DomQuery} val Item to append the current set to.
+ * @return {tinymce.dom.DomQuery} Current set with the appended nodes.
+ */
+ appendTo: function(val) {
+ DomQuery(val).append(this);
+
+ return this;
+ },
+
+ /**
+ * Prepends the specified set nodes to the specified selector/instance.
+ *
+ * @method prependTo
+ * @param {String/Element/Array/tinymce.dom.DomQuery} val Item to prepend the current set to.
+ * @return {tinymce.dom.DomQuery} Current set with the prepended nodes.
+ */
+ prependTo: function(val) {
+ DomQuery(val).prepend(this);
+
+ return this;
+ },
+
+ /**
+ * Replaces the nodes in set with the specified content.
+ *
+ * @method replaceWith
+ * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to replace nodes with.
+ * @return {tinymce.dom.DomQuery} Set with replaced nodes.
+ */
+ replaceWith: function(content) {
+ return this.before(content).remove();
+ },
+
+ /**
+ * Wraps all elements in set with the specified wrapper.
+ *
+ * @method wrap
+ * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to wrap nodes with.
+ * @return {tinymce.dom.DomQuery} Set with wrapped nodes.
+ */
+ wrap: function(content) {
+ return wrap(this, content);
+ },
+
+ /**
+ * Wraps all nodes in set with the specified wrapper. If the nodes are siblings all of them
+ * will be wrapped in the same wrapper.
+ *
+ * @method wrapAll
+ * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to wrap nodes with.
+ * @return {tinymce.dom.DomQuery} Set with wrapped nodes.
+ */
+ wrapAll: function(content) {
+ return wrap(this, content, true);
+ },
+
+ /**
+ * Wraps all elements inner contents in set with the specified wrapper.
+ *
+ * @method wrapInner
+ * @param {String/Element/Array/tinymce.dom.DomQuery} content Content to wrap nodes with.
+ * @return {tinymce.dom.DomQuery} Set with wrapped nodes.
+ */
+ wrapInner: function(content) {
+ this.each(function() {
+ DomQuery(this).contents().wrapAll(content);
+ });
+
+ return this;
+ },
+
+ /**
+ * Unwraps all elements by removing the parent element of each item in set.
+ *
+ * @method unwrap
+ * @return {tinymce.dom.DomQuery} Set with unwrapped nodes.
+ */
+ unwrap: function() {
+ return this.parent().each(function() {
+ DomQuery(this).replaceWith(this.childNodes);
+ });
+ },
+
+ /**
+ * Clones all nodes in set.
+ *
+ * @method clone
+ * @return {tinymce.dom.DomQuery} Set with cloned nodes.
+ */
+ clone: function() {
+ var result = [];
+
+ this.each(function() {
+ result.push(this.cloneNode(true));
+ });
+
+ return DomQuery(result);
+ },
+
+ /**
+ * Adds the specified class name to the current set elements.
+ *
+ * @method addClass
+ * @param {String} className Class name to add.
+ * @return {tinymce.dom.DomQuery} Current set.
+ */
+ addClass: function(className) {
+ return this.toggleClass(className, true);
+ },
+
+ /**
+ * Removes the specified class name to the current set elements.
+ *
+ * @method removeClass
+ * @param {String} className Class name to remove.
+ * @return {tinymce.dom.DomQuery} Current set.
+ */
+ removeClass: function(className) {
+ return this.toggleClass(className, false);
+ },
+
+ /**
+ * Toggles the specified class name on the current set elements.
+ *
+ * @method toggleClass
+ * @param {String} className Class name to add/remove.
+ * @param {Boolean} state Optional state to toggle on/off.
+ * @return {tinymce.dom.DomQuery} Current set.
+ */
+ toggleClass: function(className, state) {
+ var self = this;
+
+ // Functions are not supported
+ if (typeof className != 'string') {
+ return self;
+ }
+
+ if (className.indexOf(' ') !== -1) {
+ each(className.split(' '), function() {
+ self.toggleClass(this, state);
+ });
+ } else {
+ self.each(function(index, node) {
+ var existingClassName, classState;
+
+ classState = hasClass(node, className);
+ if (classState !== state) {
+ existingClassName = node.className;
+
+ if (classState) {
+ node.className = trim((" " + existingClassName + " ").replace(' ' + className + ' ', ' '));
+ } else {
+ node.className += existingClassName ? ' ' + className : className;
+ }
+ }
+ });
+ }
+
+ return self;
+ },
+
+ /**
+ * Returns true/false if the first item in set has the specified class.
+ *
+ * @method hasClass
+ * @param {String} className Class name to check for.
+ * @return {Boolean} True/false if the set has the specified class.
+ */
+ hasClass: function(className) {
+ return hasClass(this[0], className);
+ },
+
+ /**
+ * Executes the callback function for each item DomQuery collection. If you return false in the
+ * callback it will break the loop.
+ *
+ * @method each
+ * @param {function} callback Callback function to execute for each item.
+ * @return {tinymce.dom.DomQuery} Current set.
+ */
+ each: function(callback) {
+ return each(this, callback);
+ },
+
+ /**
+ * Binds an event with callback function to the elements in set.
+ *
+ * @method on
+ * @param {String} name Name of the event to bind.
+ * @param {function} callback Callback function to execute when the event occurs.
+ * @return {tinymce.dom.DomQuery} Current set.
+ */
+ on: function(name, callback) {
+ return this.each(function() {
+ Event.bind(this, name, callback);
+ });
+ },
+
+ /**
+ * Unbinds an event with callback function to the elements in set.
+ *
+ * @method off
+ * @param {String} name Optional name of the event to bind.
+ * @param {function} callback Optional callback function to execute when the event occurs.
+ * @return {tinymce.dom.DomQuery} Current set.
+ */
+ off: function(name, callback) {
+ return this.each(function() {
+ Event.unbind(this, name, callback);
+ });
+ },
+
+ /**
+ * Triggers the specified event by name or event object.
+ *
+ * @method trigger
+ * @param {String/Object} name Name of the event to trigger or event object.
+ * @return {tinymce.dom.DomQuery} Current set.
+ */
+ trigger: function(name) {
+ return this.each(function() {
+ if (typeof name == 'object') {
+ Event.fire(this, name.type, name);
+ } else {
+ Event.fire(this, name);
+ }
+ });
+ },
+
+ /**
+ * Shows all elements in set.
+ *
+ * @method show
+ * @return {tinymce.dom.DomQuery} Current set.
+ */
+ show: function() {
+ return this.css('display', '');
+ },
+
+ /**
+ * Hides all elements in set.
+ *
+ * @method hide
+ * @return {tinymce.dom.DomQuery} Current set.
+ */
+ hide: function() {
+ return this.css('display', 'none');
+ },
+
+ /**
+ * Slices the current set.
+ *
+ * @method slice
+ * @param {Number} start Start index to slice at.
+ * @param {Number} end Optional end index to end slice at.
+ * @return {tinymce.dom.DomQuery} Sliced set.
+ */
+ slice: function() {
+ return new DomQuery(slice.apply(this, arguments));
+ },
+
+ /**
+ * Makes the set equal to the specified index.
+ *
+ * @method eq
+ * @param {Number} index Index to set it equal to.
+ * @return {tinymce.dom.DomQuery} Single item set.
+ */
+ eq: function(index) {
+ return index === -1 ? this.slice(index) : this.slice(index, +index + 1);
+ },
+
+ /**
+ * Makes the set equal to first element in set.
+ *
+ * @method first
+ * @return {tinymce.dom.DomQuery} Single item set.
+ */
+ first: function() {
+ return this.eq(0);
+ },
+
+ /**
+ * Makes the set equal to last element in set.
+ *
+ * @method last
+ * @return {tinymce.dom.DomQuery} Single item set.
+ */
+ last: function() {
+ return this.eq(-1);
+ },
+
+ /**
+ * Finds elements by the specified selector for each element in set.
+ *
+ * @method find
+ * @param {String} selector Selector to find elements by.
+ * @return {tinymce.dom.DomQuery} Set with matches elements.
+ */
+ find: function(selector) {
+ var i, l, ret = [];
+
+ for (i = 0, l = this.length; i < l; i++) {
+ DomQuery.find(selector, this[i], ret);
+ }
+
+ return DomQuery(ret);
+ },
+
+ /**
+ * Filters the current set with the specified selector.
+ *
+ * @method filter
+ * @param {String/function} selector Selector to filter elements by.
+ * @return {tinymce.dom.DomQuery} Set with filtered elements.
+ */
+ filter: function(selector) {
+ if (typeof selector == 'function') {
+ return DomQuery(grep(this.toArray(), function(item, i) {
+ return selector(i, item);
+ }));
+ }
+
+ return DomQuery(DomQuery.filter(selector, this.toArray()));
+ },
+
+ /**
+ * Gets the current node or any parent matching the specified selector.
+ *
+ * @method closest
+ * @param {String/Element/tinymce.dom.DomQuery} selector Selector or element to find.
+ * @return {tinymce.dom.DomQuery} Set with closest elements.
+ */
+ closest: function(selector) {
+ var result = [];
+
+ if (selector instanceof DomQuery) {
+ selector = selector[0];
+ }
+
+ this.each(function(i, node) {
+ while (node) {
+ if (typeof selector == 'string' && DomQuery(node).is(selector)) {
+ result.push(node);
+ break;
+ } else if (node == selector) {
+ result.push(node);
+ break;
+ }
+
+ node = node.parentNode;
+ }
+ });
+
+ return DomQuery(result);
+ },
+
+ /**
+ * Returns the offset of the first element in set or sets the top/left css properties of all elements in set.
+ *
+ * @method offset
+ * @param {Object} offset Optional offset object to set on each item.
+ * @return {Object/tinymce.dom.DomQuery} Returns the first element offset or the current set if you specified an offset.
+ */
+ offset: function(offset) {
+ var elm, doc, docElm;
+ var x = 0, y = 0, pos;
+
+ if (!offset) {
+ elm = this[0];
+
+ if (elm) {
+ doc = elm.ownerDocument;
+ docElm = doc.documentElement;
+
+ if (elm.getBoundingClientRect) {
+ pos = elm.getBoundingClientRect();
+ x = pos.left + (docElm.scrollLeft || doc.body.scrollLeft) - docElm.clientLeft;
+ y = pos.top + (docElm.scrollTop || doc.body.scrollTop) - docElm.clientTop;
+ }
+ }
+
+ return {
+ left: x,
+ top: y
+ };
+ }
+
+ return this.css(offset);
+ },
+
+ push: push,
+ sort: [].sort,
+ splice: [].splice
+ };
+
+ // Static members
+ Tools.extend(DomQuery, {
+ /**
+ * Extends the specified object with one or more objects.
+ *
+ * @static
+ * @method extend
+ * @param {Object} target Target object to extend with new items.
+ * @param {Object..} object Object to extend the target with.
+ * @return {Object} Extended input object.
+ */
+ extend: Tools.extend,
+
+ /**
+ * Creates an array out of an array like object.
+ *
+ * @static
+ * @method makeArray
+ * @param {Object} object Object to convert to array.
+ * @return {Array} Array produced from object.
+ */
+ makeArray: function(object) {
+ if (isWindow(object) || object.nodeType) {
+ return [object];
+ }
+
+ return Tools.toArray(object);
+ },
+
+ /**
+ * Returns the index of the specified item inside the array.
+ *
+ * @static
+ * @method inArray
+ * @param {Object} item Item to look for.
+ * @param {Array} array Array to look for item in.
+ * @return {Number} Index of the item or -1.
+ */
+ inArray: inArray,
+
+ /**
+ * Returns true/false if the specified object is an array or not.
+ *
+ * @static
+ * @method isArray
+ * @param {Object} array Object to check if it's an array or not.
+ * @return {Boolean} True/false if the object is an array.
+ */
+ isArray: Tools.isArray,
+
+ /**
+ * Executes the callback function for each item in array/object. If you return false in the
+ * callback it will break the loop.
+ *
+ * @static
+ * @method each
+ * @param {Object} obj Object to iterate.
+ * @param {function} callback Callback function to execute for each item.
+ */
+ each: each,
+
+ /**
+ * Removes whitespace from the beginning and end of a string.
+ *
+ * @static
+ * @method trim
+ * @param {String} str String to remove whitespace from.
+ * @return {String} New string with removed whitespace.
+ */
+ trim: trim,
+
+ /**
+ * Filters out items from the input array by calling the specified function for each item.
+ * If the function returns false the item will be excluded if it returns true it will be included.
+ *
+ * @static
+ * @method grep
+ * @param {Array} array Array of items to loop though.
+ * @param {function} callback Function to call for each item. Include/exclude depends on it's return value.
+ * @return {Array} New array with values imported and filtered based in input.
+ * @example
+ * // Filter out some items, this will return an array with 4 and 5
+ * var items = DomQuery.grep([1, 2, 3, 4, 5], function(v) {return v > 3;});
+ */
+ grep: grep,
+
+ // Sizzle
+ find: Sizzle,
+ expr: Sizzle.selectors,
+ unique: Sizzle.uniqueSort,
+ text: Sizzle.getText,
+ contains: Sizzle.contains,
+ filter: function(expr, elems, not) {
+ var i = elems.length;
+
+ if (not) {
+ expr = ":not(" + expr + ")";
+ }
+
+ while (i--) {
+ if (elems[i].nodeType != 1) {
+ elems.splice(i, 1);
+ }
+ }
+
+ if (elems.length === 1) {
+ elems = DomQuery.find.matchesSelector(elems[0], expr) ? [elems[0]] : [];
+ } else {
+ elems = DomQuery.find.matches(expr, elems);
+ }
+
+ return elems;
+ }
+ });
+
+ function dir(el, prop, until) {
+ var matched = [], cur = el[prop];
+
+ if (typeof until != 'string' && until instanceof DomQuery) {
+ until = until[0];
+ }
+
+ while (cur && cur.nodeType !== 9) {
+ if (until !== undefined) {
+ if (cur === until) {
+ break;
+ }
+
+ if (typeof until == 'string' && DomQuery(cur).is(until)) {
+ break;
+ }
+ }
+
+ if (cur.nodeType === 1) {
+ matched.push(cur);
+ }
+
+ cur = cur[prop];
+ }
+
+ return matched;
+ }
+
+ function sibling(node, siblingName, nodeType, until) {
+ var result = [];
+
+ if (until instanceof DomQuery) {
+ until = until[0];
+ }
+
+ for (; node; node = node[siblingName]) {
+ if (nodeType && node.nodeType !== nodeType) {
+ continue;
+ }
+
+ if (until !== undefined) {
+ if (node === until) {
+ break;
+ }
+
+ if (typeof until == 'string' && DomQuery(node).is(until)) {
+ break;
+ }
+ }
+
+ result.push(node);
+ }
+
+ return result;
+ }
+
+ function firstSibling(node, siblingName, nodeType) {
+ for (node = node[siblingName]; node; node = node[siblingName]) {
+ if (node.nodeType == nodeType) {
+ return node;
+ }
+ }
+
+ return null;
+ }
+
+ each({
+ /**
+ * Returns a new collection with the parent of each item in current collection matching the optional selector.
+ *
+ * @method parent
+ * @param {Element/tinymce.dom.DomQuery} node Node to match parents against.
+ * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents.
+ */
+ parent: function(node) {
+ var parent = node.parentNode;
+
+ return parent && parent.nodeType !== 11 ? parent : null;
+ },
+
+ /**
+ * Returns a new collection with the all the parents of each item in current collection matching the optional selector.
+ *
+ * @method parents
+ * @param {Element/tinymce.dom.DomQuery} node Node to match parents against.
+ * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents.
+ */
+ parents: function(node) {
+ return dir(node, "parentNode");
+ },
+
+ /**
+ * Returns a new collection with next sibling of each item in current collection matching the optional selector.
+ *
+ * @method next
+ * @param {Element/tinymce.dom.DomQuery} node Node to match the next element against.
+ * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
+ */
+ next: function(node) {
+ return firstSibling(node, 'nextSibling', 1);
+ },
+
+ /**
+ * Returns a new collection with previous sibling of each item in current collection matching the optional selector.
+ *
+ * @method prev
+ * @param {Element/tinymce.dom.DomQuery} node Node to match the previous element against.
+ * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
+ */
+ prev: function(node) {
+ return firstSibling(node, 'previousSibling', 1);
+ },
+
+ /**
+ * Returns all child elements matching the optional selector.
+ *
+ * @method children
+ * @param {Element/tinymce.dom.DomQuery} node Node to match the elements against.
+ * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
+ */
+ children: function(node) {
+ return sibling(node.firstChild, 'nextSibling', 1);
+ },
+
+ /**
+ * Returns all child nodes matching the optional selector.
+ *
+ * @method contents
+ * @param {Element/tinymce.dom.DomQuery} node Node to get the contents of.
+ * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
+ */
+ contents: function(node) {
+ return Tools.toArray((node.nodeName === "iframe" ? node.contentDocument || node.contentWindow.document : node).childNodes);
+ }
+ }, function(name, fn) {
+ DomQuery.fn[name] = function(selector) {
+ var self = this, result = [];
+
+ self.each(function() {
+ var nodes = fn.call(result, this, selector, result);
+
+ if (nodes) {
+ if (DomQuery.isArray(nodes)) {
+ result.push.apply(result, nodes);
+ } else {
+ result.push(nodes);
+ }
+ }
+ });
+
+ // If traversing on multiple elements we might get the same elements twice
+ if (this.length > 1) {
+ if (!skipUniques[name]) {
+ result = DomQuery.unique(result);
+ }
+
+ if (name.indexOf('parents') === 0) {
+ result = result.reverse();
+ }
+ }
+
+ result = DomQuery(result);
+
+ if (selector) {
+ return result.filter(selector);
+ }
+
+ return result;
+ };
+ });
+
+ each({
+ /**
+ * Returns a new collection with the all the parents until the matching selector/element
+ * of each item in current collection matching the optional selector.
+ *
+ * @method parentsUntil
+ * @param {Element/tinymce.dom.DomQuery} node Node to find parent of.
+ * @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element.
+ * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents.
+ */
+ parentsUntil: function(node, until) {
+ return dir(node, "parentNode", until);
+ },
+
+ /**
+ * Returns a new collection with all next siblings of each item in current collection matching the optional selector.
+ *
+ * @method nextUntil
+ * @param {Element/tinymce.dom.DomQuery} node Node to find next siblings on.
+ * @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element.
+ * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
+ */
+ nextUntil: function(node, until) {
+ return sibling(node, 'nextSibling', 1, until).slice(1);
+ },
+
+ /**
+ * Returns a new collection with all previous siblings of each item in current collection matching the optional selector.
+ *
+ * @method prevUntil
+ * @param {Element/tinymce.dom.DomQuery} node Node to find previous siblings on.
+ * @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element.
+ * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
+ */
+ prevUntil: function(node, until) {
+ return sibling(node, 'previousSibling', 1, until).slice(1);
+ }
+ }, function(name, fn) {
+ DomQuery.fn[name] = function(selector, filter) {
+ var self = this, result = [];
+
+ self.each(function() {
+ var nodes = fn.call(result, this, selector, result);
+
+ if (nodes) {
+ if (DomQuery.isArray(nodes)) {
+ result.push.apply(result, nodes);
+ } else {
+ result.push(nodes);
+ }
+ }
+ });
+
+ // If traversing on multiple elements we might get the same elements twice
+ if (this.length > 1) {
+ result = DomQuery.unique(result);
+
+ if (name.indexOf('parents') === 0 || name === 'prevUntil') {
+ result = result.reverse();
+ }
+ }
+
+ result = DomQuery(result);
+
+ if (filter) {
+ return result.filter(filter);
+ }
+
+ return result;
+ };
+ });
+
+ /**
+ * Returns true/false if the current set items matches the selector.
+ *
+ * @method is
+ * @param {String} selector Selector to match the elements against.
+ * @return {Boolean} True/false if the current set matches the selector.
+ */
+ DomQuery.fn.is = function(selector) {
+ return !!selector && this.filter(selector).length > 0;
+ };
+
+ DomQuery.fn.init.prototype = DomQuery.fn;
+
+ DomQuery.overrideDefaults = function(callback) {
+ var defaults;
+
+ function sub(selector, context) {
+ defaults = defaults || callback();
+
+ if (arguments.length === 0) {
+ selector = defaults.element;
+ }
+
+ if (!context) {
+ context = defaults.context;
+ }
+
+ return new sub.fn.init(selector, context);
+ }
+
+ DomQuery.extend(sub, this);
+
+ return sub;
+ };
+
+ function appendHooks(targetHooks, prop, hooks) {
+ each(hooks, function(name, func) {
+ targetHooks[name] = targetHooks[name] || {};
+ targetHooks[name][prop] = func;
+ });
+ }
+
+ if (Env.ie && Env.ie < 8) {
+ appendHooks(attrHooks, 'get', {
+ maxlength: function(elm) {
+ var value = elm.maxLength;
+
+ if (value === 0x7fffffff) {
+ return undef;
+ }
+
+ return value;
+ },
+
+ size: function(elm) {
+ var value = elm.size;
+
+ if (value === 20) {
+ return undef;
+ }
+
+ return value;
+ },
+
+ 'class': function(elm) {
+ return elm.className;
+ },
+
+ style: function(elm) {
+ var value = elm.style.cssText;
+
+ if (value.length === 0) {
+ return undef;
+ }
+
+ return value;
+ }
+ });
+
+ appendHooks(attrHooks, 'set', {
+ 'class': function(elm, value) {
+ elm.className = value;
+ },
+
+ style: function(elm, value) {
+ elm.style.cssText = value;
+ }
+ });
+ }
+
+ if (Env.ie && Env.ie < 9) {
+ /*jshint sub:true */
+ /*eslint dot-notation: 0*/
+ cssFix['float'] = 'styleFloat';
+
+ appendHooks(cssHooks, 'set', {
+ opacity: function(elm, value) {
+ var style = elm.style;
+
+ if (value === null || value === '') {
+ style.removeAttribute('filter');
+ } else {
+ style.zoom = 1;
+ style.filter = 'alpha(opacity=' + (value * 100) + ')';
+ }
+ }
+ });
+ }
+
+ DomQuery.attrHooks = attrHooks;
+ DomQuery.cssHooks = cssHooks;
+
+ return DomQuery;
+});
+
+// Included from: js/tinymce/classes/html/Styles.js
+
+/**
+ * Styles.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class is used to parse CSS styles it also compresses styles to reduce the output size.
+ *
+ * @example
+ * var Styles = new tinymce.html.Styles({
+ * url_converter: function(url) {
+ * return url;
+ * }
+ * });
+ *
+ * styles = Styles.parse('border: 1px solid red');
+ * styles.color = 'red';
+ *
+ * console.log(new tinymce.html.StyleSerializer().serialize(styles));
+ *
+ * @class tinymce.html.Styles
+ * @version 3.4
+ */
+define("tinymce/html/Styles", [], function() {
+ return function(settings, schema) {
+ /*jshint maxlen:255 */
+ /*eslint max-len:0 */
+ var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi,
+ urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi,
+ styleRegExp = /\s*([^:]+):\s*([^;]+);?/g,
+ trimRightRegExp = /\s+$/,
+ i, encodingLookup = {}, encodingItems, validStyles, invalidStyles, invisibleChar = '\uFEFF';
+
+ settings = settings || {};
+
+ if (schema) {
+ validStyles = schema.getValidStyles();
+ invalidStyles = schema.getInvalidStyles();
+ }
+
+ encodingItems = ('\\" \\\' \\; \\: ; : ' + invisibleChar).split(' ');
+ for (i = 0; i < encodingItems.length; i++) {
+ encodingLookup[encodingItems[i]] = invisibleChar + i;
+ encodingLookup[invisibleChar + i] = encodingItems[i];
+ }
+
+ function toHex(match, r, g, b) {
+ function hex(val) {
+ val = parseInt(val, 10).toString(16);
+
+ return val.length > 1 ? val : '0' + val; // 0 -> 00
+ }
+
+ return '#' + hex(r) + hex(g) + hex(b);
+ }
+
+ return {
+ /**
+ * Parses the specified RGB color value and returns a hex version of that color.
+ *
+ * @method toHex
+ * @param {String} color RGB string value like rgb(1,2,3)
+ * @return {String} Hex version of that RGB value like #FF00FF.
+ */
+ toHex: function(color) {
+ return color.replace(rgbRegExp, toHex);
+ },
+
+ /**
+ * Parses the specified style value into an object collection. This parser will also
+ * merge and remove any redundant items that browsers might have added. It will also convert non hex
+ * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings.
+ *
+ * @method parse
+ * @param {String} css Style value to parse for example: border:1px solid red;.
+ * @return {Object} Object representation of that style like {border: '1px solid red'}
+ */
+ parse: function(css) {
+ var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter;
+ var urlConverterScope = settings.url_converter_scope || this;
+
+ function compress(prefix, suffix, noJoin) {
+ var top, right, bottom, left;
+
+ top = styles[prefix + '-top' + suffix];
+ if (!top) {
+ return;
+ }
+
+ right = styles[prefix + '-right' + suffix];
+ if (!right) {
+ return;
+ }
+
+ bottom = styles[prefix + '-bottom' + suffix];
+ if (!bottom) {
+ return;
+ }
+
+ left = styles[prefix + '-left' + suffix];
+ if (!left) {
+ return;
+ }
+
+ var box = [top, right, bottom, left];
+ i = box.length - 1;
+ while (i--) {
+ if (box[i] !== box[i + 1]) {
+ break;
+ }
+ }
+
+ if (i > -1 && noJoin) {
+ return;
+ }
+
+ styles[prefix + suffix] = i == -1 ? box[0] : box.join(' ');
+ delete styles[prefix + '-top' + suffix];
+ delete styles[prefix + '-right' + suffix];
+ delete styles[prefix + '-bottom' + suffix];
+ delete styles[prefix + '-left' + suffix];
+ }
+
+ /**
+ * Checks if the specific style can be compressed in other words if all border-width are equal.
+ */
+ function canCompress(key) {
+ var value = styles[key], i;
+
+ if (!value) {
+ return;
+ }
+
+ value = value.split(' ');
+ i = value.length;
+ while (i--) {
+ if (value[i] !== value[0]) {
+ return false;
+ }
+ }
+
+ styles[key] = value[0];
+
+ return true;
+ }
+
+ /**
+ * Compresses multiple styles into one style.
+ */
+ function compress2(target, a, b, c) {
+ if (!canCompress(a)) {
+ return;
+ }
+
+ if (!canCompress(b)) {
+ return;
+ }
+
+ if (!canCompress(c)) {
+ return;
+ }
+
+ // Compress
+ styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c];
+ delete styles[a];
+ delete styles[b];
+ delete styles[c];
+ }
+
+ // Encodes the specified string by replacing all \" \' ; : with _<num>
+ function encode(str) {
+ isEncoded = true;
+
+ return encodingLookup[str];
+ }
+
+ // Decodes the specified string by replacing all _<num> with it's original value \" \' etc
+ // It will also decode the \" \' if keep_slashes is set to fale or omitted
+ function decode(str, keep_slashes) {
+ if (isEncoded) {
+ str = str.replace(/\uFEFF[0-9]/g, function(str) {
+ return encodingLookup[str];
+ });
+ }
+
+ if (!keep_slashes) {
+ str = str.replace(/\\([\'\";:])/g, "$1");
+ }
+
+ return str;
+ }
+
+ function decodeSingleHexSequence(escSeq) {
+ return String.fromCharCode(parseInt(escSeq.slice(1), 16));
+ }
+
+ function decodeHexSequences(value) {
+ return value.replace(/\\[0-9a-f]+/gi, decodeSingleHexSequence);
+ }
+
+ function processUrl(match, url, url2, url3, str, str2) {
+ str = str || str2;
+
+ if (str) {
+ str = decode(str);
+
+ // Force strings into single quote format
+ return "'" + str.replace(/\'/g, "\\'") + "'";
+ }
+
+ url = decode(url || url2 || url3);
+
+ if (!settings.allow_script_urls) {
+ var scriptUrl = url.replace(/[\s\r\n]+/g, '');
+
+ if (/(java|vb)script:/i.test(scriptUrl)) {
+ return "";
+ }
+
+ if (!settings.allow_svg_data_urls && /^data:image\/svg/i.test(scriptUrl)) {
+ return "";
+ }
+ }
+
+ // Convert the URL to relative/absolute depending on config
+ if (urlConverter) {
+ url = urlConverter.call(urlConverterScope, url, 'style');
+ }
+
+ // Output new URL format
+ return "url('" + url.replace(/\'/g, "\\'") + "')";
+ }
+
+ if (css) {
+ css = css.replace(/[\u0000-\u001F]/g, '');
+
+ // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing
+ css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) {
+ return str.replace(/[;:]/g, encode);
+ });
+
+ // Parse styles
+ while ((matches = styleRegExp.exec(css))) {
+ styleRegExp.lastIndex = matches.index + matches[0].length;
+ name = matches[1].replace(trimRightRegExp, '').toLowerCase();
+ value = matches[2].replace(trimRightRegExp, '');
+
+ if (name && value) {
+ // Decode escaped sequences like \65 -> e
+ name = decodeHexSequences(name);
+ value = decodeHexSequences(value);
+
+ // Skip properties with double quotes and sequences like \" \' in their names
+ // See 'mXSS Attacks: Attacking well-secured Web-Applications by using innerHTML Mutations'
+ // https://cure53.de/fp170.pdf
+ if (name.indexOf(invisibleChar) !== -1 || name.indexOf('"') !== -1) {
+ continue;
+ }
+
+ // Don't allow behavior name or expression/comments within the values
+ if (!settings.allow_script_urls && (name == "behavior" || /expression\s*\(|\/\*|\*\//.test(value))) {
+ continue;
+ }
+
+ // Opera will produce 700 instead of bold in their style values
+ if (name === 'font-weight' && value === '700') {
+ value = 'bold';
+ } else if (name === 'color' || name === 'background-color') { // Lowercase colors like RED
+ value = value.toLowerCase();
+ }
+
+ // Convert RGB colors to HEX
+ value = value.replace(rgbRegExp, toHex);
+
+ // Convert URLs and force them into url('value') format
+ value = value.replace(urlOrStrRegExp, processUrl);
+ styles[name] = isEncoded ? decode(value, true) : value;
+ }
+ }
+ // Compress the styles to reduce it's size for example IE will expand styles
+ compress("border", "", true);
+ compress("border", "-width");
+ compress("border", "-color");
+ compress("border", "-style");
+ compress("padding", "");
+ compress("margin", "");
+ compress2('border', 'border-width', 'border-style', 'border-color');
+
+ // Remove pointless border, IE produces these
+ if (styles.border === 'medium none') {
+ delete styles.border;
+ }
+
+ // IE 11 will produce a border-image: none when getting the style attribute from <p style="border: 1px solid red"></p>
+ // So let us assume it shouldn't be there
+ if (styles['border-image'] === 'none') {
+ delete styles['border-image'];
+ }
+ }
+
+ return styles;
+ },
+
+ /**
+ * Serializes the specified style object into a string.
+ *
+ * @method serialize
+ * @param {Object} styles Object to serialize as string for example: {border: '1px solid red'}
+ * @param {String} elementName Optional element name, if specified only the styles that matches the schema will be serialized.
+ * @return {String} String representation of the style object for example: border: 1px solid red.
+ */
+ serialize: function(styles, elementName) {
+ var css = '', name, value;
+
+ function serializeStyles(name) {
+ var styleList, i, l, value;
+
+ styleList = validStyles[name];
+ if (styleList) {
+ for (i = 0, l = styleList.length; i < l; i++) {
+ name = styleList[i];
+ value = styles[name];
+
+ if (value) {
+ css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
+ }
+ }
+ }
+ }
+
+ function isValid(name, elementName) {
+ var styleMap;
+
+ styleMap = invalidStyles['*'];
+ if (styleMap && styleMap[name]) {
+ return false;
+ }
+
+ styleMap = invalidStyles[elementName];
+ if (styleMap && styleMap[name]) {
+ return false;
+ }
+
+ return true;
+ }
+
+ // Serialize styles according to schema
+ if (elementName && validStyles) {
+ // Serialize global styles and element specific styles
+ serializeStyles('*');
+ serializeStyles(elementName);
+ } else {
+ // Output the styles in the order they are inside the object
+ for (name in styles) {
+ value = styles[name];
+
+ if (value && (!invalidStyles || isValid(name, elementName))) {
+ css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
+ }
+ }
+ }
+
+ return css;
+ }
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/dom/TreeWalker.js
+
+/**
+ * TreeWalker.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * TreeWalker class enables you to walk the DOM in a linear manner.
+ *
+ * @class tinymce.dom.TreeWalker
+ * @example
+ * var walker = new tinymce.dom.TreeWalker(startNode);
+ *
+ * do {
+ * console.log(walker.current());
+ * } while (walker.next());
+ */
+define("tinymce/dom/TreeWalker", [], function() {
+ /**
+ * Constructs a new TreeWalker instance.
+ *
+ * @constructor
+ * @method TreeWalker
+ * @param {Node} startNode Node to start walking from.
+ * @param {node} rootNode Optional root node to never walk out of.
+ */
+ return function(startNode, rootNode) {
+ var node = startNode;
+
+ function findSibling(node, startName, siblingName, shallow) {
+ var sibling, parent;
+
+ if (node) {
+ // Walk into nodes if it has a start
+ if (!shallow && node[startName]) {
+ return node[startName];
+ }
+
+ // Return the sibling if it has one
+ if (node != rootNode) {
+ sibling = node[siblingName];
+ if (sibling) {
+ return sibling;
+ }
+
+ // Walk up the parents to look for siblings
+ for (parent = node.parentNode; parent && parent != rootNode; parent = parent.parentNode) {
+ sibling = parent[siblingName];
+ if (sibling) {
+ return sibling;
+ }
+ }
+ }
+ }
+ }
+
+ function findPreviousNode(node, startName, siblingName, shallow) {
+ var sibling, parent, child;
+
+ if (node) {
+ sibling = node[siblingName];
+ if (rootNode && sibling === rootNode) {
+ return;
+ }
+
+ if (sibling) {
+ if (!shallow) {
+ // Walk up the parents to look for siblings
+ for (child = sibling[startName]; child; child = child[startName]) {
+ if (!child[startName]) {
+ return child;
+ }
+ }
+ }
+
+ return sibling;
+ }
+
+ parent = node.parentNode;
+ if (parent && parent !== rootNode) {
+ return parent;
+ }
+ }
+ }
+
+ /**
+ * Returns the current node.
+ *
+ * @method current
+ * @return {Node} Current node where the walker is.
+ */
+ this.current = function() {
+ return node;
+ };
+
+ /**
+ * Walks to the next node in tree.
+ *
+ * @method next
+ * @return {Node} Current node where the walker is after moving to the next node.
+ */
+ this.next = function(shallow) {
+ node = findSibling(node, 'firstChild', 'nextSibling', shallow);
+ return node;
+ };
+
+ /**
+ * Walks to the previous node in tree.
+ *
+ * @method prev
+ * @return {Node} Current node where the walker is after moving to the previous node.
+ */
+ this.prev = function(shallow) {
+ node = findSibling(node, 'lastChild', 'previousSibling', shallow);
+ return node;
+ };
+
+ this.prev2 = function(shallow) {
+ node = findPreviousNode(node, 'lastChild', 'previousSibling', shallow);
+ return node;
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/dom/Range.js
+
+/**
+ * Range.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Old IE Range.
+ *
+ * @private
+ * @class tinymce.dom.Range
+ */
+define("tinymce/dom/Range", [
+ "tinymce/util/Tools"
+], function(Tools) {
+ // Range constructor
+ function Range(dom) {
+ var self = this,
+ doc = dom.doc,
+ EXTRACT = 0,
+ CLONE = 1,
+ DELETE = 2,
+ TRUE = true,
+ FALSE = false,
+ START_OFFSET = 'startOffset',
+ START_CONTAINER = 'startContainer',
+ END_CONTAINER = 'endContainer',
+ END_OFFSET = 'endOffset',
+ extend = Tools.extend,
+ nodeIndex = dom.nodeIndex;
+
+ function createDocumentFragment() {
+ return doc.createDocumentFragment();
+ }
+
+ function setStart(n, o) {
+ _setEndPoint(TRUE, n, o);
+ }
+
+ function setEnd(n, o) {
+ _setEndPoint(FALSE, n, o);
+ }
+
+ function setStartBefore(n) {
+ setStart(n.parentNode, nodeIndex(n));
+ }
+
+ function setStartAfter(n) {
+ setStart(n.parentNode, nodeIndex(n) + 1);
+ }
+
+ function setEndBefore(n) {
+ setEnd(n.parentNode, nodeIndex(n));
+ }
+
+ function setEndAfter(n) {
+ setEnd(n.parentNode, nodeIndex(n) + 1);
+ }
+
+ function collapse(ts) {
+ if (ts) {
+ self[END_CONTAINER] = self[START_CONTAINER];
+ self[END_OFFSET] = self[START_OFFSET];
+ } else {
+ self[START_CONTAINER] = self[END_CONTAINER];
+ self[START_OFFSET] = self[END_OFFSET];
+ }
+
+ self.collapsed = TRUE;
+ }
+
+ function selectNode(n) {
+ setStartBefore(n);
+ setEndAfter(n);
+ }
+
+ function selectNodeContents(n) {
+ setStart(n, 0);
+ setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length);
+ }
+
+ function compareBoundaryPoints(h, r) {
+ var sc = self[START_CONTAINER], so = self[START_OFFSET], ec = self[END_CONTAINER], eo = self[END_OFFSET],
+ rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset;
+
+ // Check START_TO_START
+ if (h === 0) {
+ return _compareBoundaryPoints(sc, so, rsc, rso);
+ }
+
+ // Check START_TO_END
+ if (h === 1) {
+ return _compareBoundaryPoints(ec, eo, rsc, rso);
+ }
+
+ // Check END_TO_END
+ if (h === 2) {
+ return _compareBoundaryPoints(ec, eo, rec, reo);
+ }
+
+ // Check END_TO_START
+ if (h === 3) {
+ return _compareBoundaryPoints(sc, so, rec, reo);
+ }
+ }
+
+ function deleteContents() {
+ _traverse(DELETE);
+ }
+
+ function extractContents() {
+ return _traverse(EXTRACT);
+ }
+
+ function cloneContents() {
+ return _traverse(CLONE);
+ }
+
+ function insertNode(n) {
+ var startContainer = this[START_CONTAINER],
+ startOffset = this[START_OFFSET], nn, o;
+
+ // Node is TEXT_NODE or CDATA
+ if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) {
+ if (!startOffset) {
+ // At the start of text
+ startContainer.parentNode.insertBefore(n, startContainer);
+ } else if (startOffset >= startContainer.nodeValue.length) {
+ // At the end of text
+ dom.insertAfter(n, startContainer);
+ } else {
+ // Middle, need to split
+ nn = startContainer.splitText(startOffset);
+ startContainer.parentNode.insertBefore(n, nn);
+ }
+ } else {
+ // Insert element node
+ if (startContainer.childNodes.length > 0) {
+ o = startContainer.childNodes[startOffset];
+ }
+
+ if (o) {
+ startContainer.insertBefore(n, o);
+ } else {
+ if (startContainer.nodeType == 3) {
+ dom.insertAfter(n, startContainer);
+ } else {
+ startContainer.appendChild(n);
+ }
+ }
+ }
+ }
+
+ function surroundContents(n) {
+ var f = self.extractContents();
+
+ self.insertNode(n);
+ n.appendChild(f);
+ self.selectNode(n);
+ }
+
+ function cloneRange() {
+ return extend(new Range(dom), {
+ startContainer: self[START_CONTAINER],
+ startOffset: self[START_OFFSET],
+ endContainer: self[END_CONTAINER],
+ endOffset: self[END_OFFSET],
+ collapsed: self.collapsed,
+ commonAncestorContainer: self.commonAncestorContainer
+ });
+ }
+
+ // Private methods
+
+ function _getSelectedNode(container, offset) {
+ var child;
+
+ // TEXT_NODE
+ if (container.nodeType == 3) {
+ return container;
+ }
+
+ if (offset < 0) {
+ return container;
+ }
+
+ child = container.firstChild;
+ while (child && offset > 0) {
+ --offset;
+ child = child.nextSibling;
+ }
+
+ if (child) {
+ return child;
+ }
+
+ return container;
+ }
+
+ function _isCollapsed() {
+ return (self[START_CONTAINER] == self[END_CONTAINER] && self[START_OFFSET] == self[END_OFFSET]);
+ }
+
+ function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) {
+ var c, offsetC, n, cmnRoot, childA, childB;
+
+ // In the first case the boundary-points have the same container. A is before B
+ // if its offset is less than the offset of B, A is equal to B if its offset is
+ // equal to the offset of B, and A is after B if its offset is greater than the
+ // offset of B.
+ if (containerA == containerB) {
+ if (offsetA == offsetB) {
+ return 0; // equal
+ }
+
+ if (offsetA < offsetB) {
+ return -1; // before
+ }
+
+ return 1; // after
+ }
+
+ // In the second case a child node C of the container of A is an ancestor
+ // container of B. In this case, A is before B if the offset of A is less than or
+ // equal to the index of the child node C and A is after B otherwise.
+ c = containerB;
+ while (c && c.parentNode != containerA) {
+ c = c.parentNode;
+ }
+
+ if (c) {
+ offsetC = 0;
+ n = containerA.firstChild;
+
+ while (n != c && offsetC < offsetA) {
+ offsetC++;
+ n = n.nextSibling;
+ }
+
+ if (offsetA <= offsetC) {
+ return -1; // before
+ }
+
+ return 1; // after
+ }
+
+ // In the third case a child node C of the container of B is an ancestor container
+ // of A. In this case, A is before B if the index of the child node C is less than
+ // the offset of B and A is after B otherwise.
+ c = containerA;
+ while (c && c.parentNode != containerB) {
+ c = c.parentNode;
+ }
+
+ if (c) {
+ offsetC = 0;
+ n = containerB.firstChild;
+
+ while (n != c && offsetC < offsetB) {
+ offsetC++;
+ n = n.nextSibling;
+ }
+
+ if (offsetC < offsetB) {
+ return -1; // before
+ }
+
+ return 1; // after
+ }
+
+ // In the fourth case, none of three other cases hold: the containers of A and B
+ // are siblings or descendants of sibling nodes. In this case, A is before B if
+ // the container of A is before the container of B in a pre-order traversal of the
+ // Ranges' context tree and A is after B otherwise.
+ cmnRoot = dom.findCommonAncestor(containerA, containerB);
+ childA = containerA;
+
+ while (childA && childA.parentNode != cmnRoot) {
+ childA = childA.parentNode;
+ }
+
+ if (!childA) {
+ childA = cmnRoot;
+ }
+
+ childB = containerB;
+ while (childB && childB.parentNode != cmnRoot) {
+ childB = childB.parentNode;
+ }
+
+ if (!childB) {
+ childB = cmnRoot;
+ }
+
+ if (childA == childB) {
+ return 0; // equal
+ }
+
+ n = cmnRoot.firstChild;
+ while (n) {
+ if (n == childA) {
+ return -1; // before
+ }
+
+ if (n == childB) {
+ return 1; // after
+ }
+
+ n = n.nextSibling;
+ }
+ }
+
+ function _setEndPoint(st, n, o) {
+ var ec, sc;
+
+ if (st) {
+ self[START_CONTAINER] = n;
+ self[START_OFFSET] = o;
+ } else {
+ self[END_CONTAINER] = n;
+ self[END_OFFSET] = o;
+ }
+
+ // If one boundary-point of a Range is set to have a root container
+ // other than the current one for the Range, the Range is collapsed to
+ // the new position. This enforces the restriction that both boundary-
+ // points of a Range must have the same root container.
+ ec = self[END_CONTAINER];
+ while (ec.parentNode) {
+ ec = ec.parentNode;
+ }
+
+ sc = self[START_CONTAINER];
+ while (sc.parentNode) {
+ sc = sc.parentNode;
+ }
+
+ if (sc == ec) {
+ // The start position of a Range is guaranteed to never be after the
+ // end position. To enforce this restriction, if the start is set to
+ // be at a position after the end, the Range is collapsed to that
+ // position.
+ if (_compareBoundaryPoints(self[START_CONTAINER], self[START_OFFSET], self[END_CONTAINER], self[END_OFFSET]) > 0) {
+ self.collapse(st);
+ }
+ } else {
+ self.collapse(st);
+ }
+
+ self.collapsed = _isCollapsed();
+ self.commonAncestorContainer = dom.findCommonAncestor(self[START_CONTAINER], self[END_CONTAINER]);
+ }
+
+ function _traverse(how) {
+ var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep;
+
+ if (self[START_CONTAINER] == self[END_CONTAINER]) {
+ return _traverseSameContainer(how);
+ }
+
+ for (c = self[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
+ if (p == self[START_CONTAINER]) {
+ return _traverseCommonStartContainer(c, how);
+ }
+
+ ++endContainerDepth;
+ }
+
+ for (c = self[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
+ if (p == self[END_CONTAINER]) {
+ return _traverseCommonEndContainer(c, how);
+ }
+
+ ++startContainerDepth;
+ }
+
+ depthDiff = startContainerDepth - endContainerDepth;
+
+ startNode = self[START_CONTAINER];
+ while (depthDiff > 0) {
+ startNode = startNode.parentNode;
+ depthDiff--;
+ }
+
+ endNode = self[END_CONTAINER];
+ while (depthDiff < 0) {
+ endNode = endNode.parentNode;
+ depthDiff++;
+ }
+
+ // ascend the ancestor hierarchy until we have a common parent.
+ for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) {
+ startNode = sp;
+ endNode = ep;
+ }
+
+ return _traverseCommonAncestors(startNode, endNode, how);
+ }
+
+ function _traverseSameContainer(how) {
+ var frag, s, sub, n, cnt, sibling, xferNode, start, len;
+
+ if (how != DELETE) {
+ frag = createDocumentFragment();
+ }
+
+ // If selection is empty, just return the fragment
+ if (self[START_OFFSET] == self[END_OFFSET]) {
+ return frag;
+ }
+
+ // Text node needs special case handling
+ if (self[START_CONTAINER].nodeType == 3) { // TEXT_NODE
+ // get the substring
+ s = self[START_CONTAINER].nodeValue;
+ sub = s.substring(self[START_OFFSET], self[END_OFFSET]);
+
+ // set the original text node to its new value
+ if (how != CLONE) {
+ n = self[START_CONTAINER];
+ start = self[START_OFFSET];
+ len = self[END_OFFSET] - self[START_OFFSET];
+
+ if (start === 0 && len >= n.nodeValue.length - 1) {
+ n.parentNode.removeChild(n);
+ } else {
+ n.deleteData(start, len);
+ }
+
+ // Nothing is partially selected, so collapse to start point
+ self.collapse(TRUE);
+ }
+
+ if (how == DELETE) {
+ return;
+ }
+
+ if (sub.length > 0) {
+ frag.appendChild(doc.createTextNode(sub));
+ }
+
+ return frag;
+ }
+
+ // Copy nodes between the start/end offsets.
+ n = _getSelectedNode(self[START_CONTAINER], self[START_OFFSET]);
+ cnt = self[END_OFFSET] - self[START_OFFSET];
+
+ while (n && cnt > 0) {
+ sibling = n.nextSibling;
+ xferNode = _traverseFullySelected(n, how);
+
+ if (frag) {
+ frag.appendChild(xferNode);
+ }
+
+ --cnt;
+ n = sibling;
+ }
+
+ // Nothing is partially selected, so collapse to start point
+ if (how != CLONE) {
+ self.collapse(TRUE);
+ }
+
+ return frag;
+ }
+
+ function _traverseCommonStartContainer(endAncestor, how) {
+ var frag, n, endIdx, cnt, sibling, xferNode;
+
+ if (how != DELETE) {
+ frag = createDocumentFragment();
+ }
+
+ n = _traverseRightBoundary(endAncestor, how);
+
+ if (frag) {
+ frag.appendChild(n);
+ }
+
+ endIdx = nodeIndex(endAncestor);
+ cnt = endIdx - self[START_OFFSET];
+
+ if (cnt <= 0) {
+ // Collapse to just before the endAncestor, which
+ // is partially selected.
+ if (how != CLONE) {
+ self.setEndBefore(endAncestor);
+ self.collapse(FALSE);
+ }
+
+ return frag;
+ }
+
+ n = endAncestor.previousSibling;
+ while (cnt > 0) {
+ sibling = n.previousSibling;
+ xferNode = _traverseFullySelected(n, how);
+
+ if (frag) {
+ frag.insertBefore(xferNode, frag.firstChild);
+ }
+
+ --cnt;
+ n = sibling;
+ }
+
+ // Collapse to just before the endAncestor, which
+ // is partially selected.
+ if (how != CLONE) {
+ self.setEndBefore(endAncestor);
+ self.collapse(FALSE);
+ }
+
+ return frag;
+ }
+
+ function _traverseCommonEndContainer(startAncestor, how) {
+ var frag, startIdx, n, cnt, sibling, xferNode;
+
+ if (how != DELETE) {
+ frag = createDocumentFragment();
+ }
+
+ n = _traverseLeftBoundary(startAncestor, how);
+ if (frag) {
+ frag.appendChild(n);
+ }
+
+ startIdx = nodeIndex(startAncestor);
+ ++startIdx; // Because we already traversed it
+
+ cnt = self[END_OFFSET] - startIdx;
+ n = startAncestor.nextSibling;
+ while (n && cnt > 0) {
+ sibling = n.nextSibling;
+ xferNode = _traverseFullySelected(n, how);
+
+ if (frag) {
+ frag.appendChild(xferNode);
+ }
+
+ --cnt;
+ n = sibling;
+ }
+
+ if (how != CLONE) {
+ self.setStartAfter(startAncestor);
+ self.collapse(TRUE);
+ }
+
+ return frag;
+ }
+
+ function _traverseCommonAncestors(startAncestor, endAncestor, how) {
+ var n, frag, startOffset, endOffset, cnt, sibling, nextSibling;
+
+ if (how != DELETE) {
+ frag = createDocumentFragment();
+ }
+
+ n = _traverseLeftBoundary(startAncestor, how);
+ if (frag) {
+ frag.appendChild(n);
+ }
+
+ startOffset = nodeIndex(startAncestor);
+ endOffset = nodeIndex(endAncestor);
+ ++startOffset;
+
+ cnt = endOffset - startOffset;
+ sibling = startAncestor.nextSibling;
+
+ while (cnt > 0) {
+ nextSibling = sibling.nextSibling;
+ n = _traverseFullySelected(sibling, how);
+
+ if (frag) {
+ frag.appendChild(n);
+ }
+
+ sibling = nextSibling;
+ --cnt;
+ }
+
+ n = _traverseRightBoundary(endAncestor, how);
+
+ if (frag) {
+ frag.appendChild(n);
+ }
+
+ if (how != CLONE) {
+ self.setStartAfter(startAncestor);
+ self.collapse(TRUE);
+ }
+
+ return frag;
+ }
+
+ function _traverseRightBoundary(root, how) {
+ var next = _getSelectedNode(self[END_CONTAINER], self[END_OFFSET] - 1), parent, clonedParent;
+ var prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != self[END_CONTAINER];
+
+ if (next == root) {
+ return _traverseNode(next, isFullySelected, FALSE, how);
+ }
+
+ parent = next.parentNode;
+ clonedParent = _traverseNode(parent, FALSE, FALSE, how);
+
+ while (parent) {
+ while (next) {
+ prevSibling = next.previousSibling;
+ clonedChild = _traverseNode(next, isFullySelected, FALSE, how);
+
+ if (how != DELETE) {
+ clonedParent.insertBefore(clonedChild, clonedParent.firstChild);
+ }
+
+ isFullySelected = TRUE;
+ next = prevSibling;
+ }
+
+ if (parent == root) {
+ return clonedParent;
+ }
+
+ next = parent.previousSibling;
+ parent = parent.parentNode;
+
+ clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how);
+
+ if (how != DELETE) {
+ clonedGrandParent.appendChild(clonedParent);
+ }
+
+ clonedParent = clonedGrandParent;
+ }
+ }
+
+ function _traverseLeftBoundary(root, how) {
+ var next = _getSelectedNode(self[START_CONTAINER], self[START_OFFSET]), isFullySelected = next != self[START_CONTAINER];
+ var parent, clonedParent, nextSibling, clonedChild, clonedGrandParent;
+
+ if (next == root) {
+ return _traverseNode(next, isFullySelected, TRUE, how);
+ }
+
+ parent = next.parentNode;
+ clonedParent = _traverseNode(parent, FALSE, TRUE, how);
+
+ while (parent) {
+ while (next) {
+ nextSibling = next.nextSibling;
+ clonedChild = _traverseNode(next, isFullySelected, TRUE, how);
+
+ if (how != DELETE) {
+ clonedParent.appendChild(clonedChild);
+ }
+
+ isFullySelected = TRUE;
+ next = nextSibling;
+ }
+
+ if (parent == root) {
+ return clonedParent;
+ }
+
+ next = parent.nextSibling;
+ parent = parent.parentNode;
+
+ clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how);
+
+ if (how != DELETE) {
+ clonedGrandParent.appendChild(clonedParent);
+ }
+
+ clonedParent = clonedGrandParent;
+ }
+ }
+
+ function _traverseNode(n, isFullySelected, isLeft, how) {
+ var txtValue, newNodeValue, oldNodeValue, offset, newNode;
+
+ if (isFullySelected) {
+ return _traverseFullySelected(n, how);
+ }
+
+ // TEXT_NODE
+ if (n.nodeType == 3) {
+ txtValue = n.nodeValue;
+
+ if (isLeft) {
+ offset = self[START_OFFSET];
+ newNodeValue = txtValue.substring(offset);
+ oldNodeValue = txtValue.substring(0, offset);
+ } else {
+ offset = self[END_OFFSET];
+ newNodeValue = txtValue.substring(0, offset);
+ oldNodeValue = txtValue.substring(offset);
+ }
+
+ if (how != CLONE) {
+ n.nodeValue = oldNodeValue;
+ }
+
+ if (how == DELETE) {
+ return;
+ }
+
+ newNode = dom.clone(n, FALSE);
+ newNode.nodeValue = newNodeValue;
+
+ return newNode;
+ }
+
+ if (how == DELETE) {
+ return;
+ }
+
+ return dom.clone(n, FALSE);
+ }
+
+ function _traverseFullySelected(n, how) {
+ if (how != DELETE) {
+ return how == CLONE ? dom.clone(n, TRUE) : n;
+ }
+
+ n.parentNode.removeChild(n);
+ }
+
+ function toStringIE() {
+ return dom.create('body', null, cloneContents()).outerText;
+ }
+
+ extend(self, {
+ // Initial states
+ startContainer: doc,
+ startOffset: 0,
+ endContainer: doc,
+ endOffset: 0,
+ collapsed: TRUE,
+ commonAncestorContainer: doc,
+
+ // Range constants
+ START_TO_START: 0,
+ START_TO_END: 1,
+ END_TO_END: 2,
+ END_TO_START: 3,
+
+ // Public methods
+ setStart: setStart,
+ setEnd: setEnd,
+ setStartBefore: setStartBefore,
+ setStartAfter: setStartAfter,
+ setEndBefore: setEndBefore,
+ setEndAfter: setEndAfter,
+ collapse: collapse,
+ selectNode: selectNode,
+ selectNodeContents: selectNodeContents,
+ compareBoundaryPoints: compareBoundaryPoints,
+ deleteContents: deleteContents,
+ extractContents: extractContents,
+ cloneContents: cloneContents,
+ insertNode: insertNode,
+ surroundContents: surroundContents,
+ cloneRange: cloneRange,
+ toStringIE: toStringIE
+ });
+
+ return self;
+ }
+
+ // Older IE versions doesn't let you override toString by it's constructor so we have to stick it in the prototype
+ Range.prototype.toString = function() {
+ return this.toStringIE();
+ };
+
+ return Range;
+});
+
+// Included from: js/tinymce/classes/html/Entities.js
+
+/**
+ * Entities.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*jshint bitwise:false */
+/*eslint no-bitwise:0 */
+
+/**
+ * Entity encoder class.
+ *
+ * @class tinymce.html.Entities
+ * @static
+ * @version 3.4
+ */
+define("tinymce/html/Entities", [
+ "tinymce/util/Tools"
+], function(Tools) {
+ var makeMap = Tools.makeMap;
+
+ var namedEntities, baseEntities, reverseEntities,
+ attrsCharsRegExp = /[&<>\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
+ textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
+ rawCharsRegExp = /[<>&\"\']/g,
+ entityRegExp = /&#([a-z0-9]+);?|&([a-z0-9]+);/gi,
+ asciiMap = {
+ 128: "\u20AC", 130: "\u201A", 131: "\u0192", 132: "\u201E", 133: "\u2026", 134: "\u2020",
+ 135: "\u2021", 136: "\u02C6", 137: "\u2030", 138: "\u0160", 139: "\u2039", 140: "\u0152",
+ 142: "\u017D", 145: "\u2018", 146: "\u2019", 147: "\u201C", 148: "\u201D", 149: "\u2022",
+ 150: "\u2013", 151: "\u2014", 152: "\u02DC", 153: "\u2122", 154: "\u0161", 155: "\u203A",
+ 156: "\u0153", 158: "\u017E", 159: "\u0178"
+ };
+
+ // Raw entities
+ baseEntities = {
+ '\"': '"', // Needs to be escaped since the YUI compressor would otherwise break the code
+ "'": ''',
+ '<': '<',
+ '>': '>',
+ '&': '&',
+ '\u0060': '`'
+ };
+
+ // Reverse lookup table for raw entities
+ reverseEntities = {
+ '<': '<',
+ '>': '>',
+ '&': '&',
+ '"': '"',
+ ''': "'"
+ };
+
+ // Decodes text by using the browser
+ function nativeDecode(text) {
+ var elm;
+
+ elm = document.createElement("div");
+ elm.innerHTML = text;
+
+ return elm.textContent || elm.innerText || text;
+ }
+
+ // Build a two way lookup table for the entities
+ function buildEntitiesLookup(items, radix) {
+ var i, chr, entity, lookup = {};
+
+ if (items) {
+ items = items.split(',');
+ radix = radix || 10;
+
+ // Build entities lookup table
+ for (i = 0; i < items.length; i += 2) {
+ chr = String.fromCharCode(parseInt(items[i], radix));
+
+ // Only add non base entities
+ if (!baseEntities[chr]) {
+ entity = '&' + items[i + 1] + ';';
+ lookup[chr] = entity;
+ lookup[entity] = chr;
+ }
+ }
+
+ return lookup;
+ }
+ }
+
+ // Unpack entities lookup where the numbers are in radix 32 to reduce the size
+ namedEntities = buildEntitiesLookup(
+ '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' +
+ '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' +
+ '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' +
+ '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' +
+ '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' +
+ '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' +
+ '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' +
+ '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' +
+ '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' +
+ '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' +
+ 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' +
+ 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' +
+ 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' +
+ 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' +
+ 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' +
+ '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' +
+ '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' +
+ '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' +
+ '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' +
+ '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' +
+ 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' +
+ 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' +
+ 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' +
+ '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' +
+ '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32);
+
+ var Entities = {
+ /**
+ * Encodes the specified string using raw entities. This means only the required XML base entities will be encoded.
+ *
+ * @method encodeRaw
+ * @param {String} text Text to encode.
+ * @param {Boolean} attr Optional flag to specify if the text is attribute contents.
+ * @return {String} Entity encoded text.
+ */
+ encodeRaw: function(text, attr) {
+ return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
+ return baseEntities[chr] || chr;
+ });
+ },
+
+ /**
+ * Encoded the specified text with both the attributes and text entities. This function will produce larger text contents
+ * since it doesn't know if the context is within a attribute or text node. This was added for compatibility
+ * and is exposed as the DOMUtils.encode function.
+ *
+ * @method encodeAllRaw
+ * @param {String} text Text to encode.
+ * @return {String} Entity encoded text.
+ */
+ encodeAllRaw: function(text) {
+ return ('' + text).replace(rawCharsRegExp, function(chr) {
+ return baseEntities[chr] || chr;
+ });
+ },
+
+ /**
+ * Encodes the specified string using numeric entities. The core entities will be
+ * encoded as named ones but all non lower ascii characters will be encoded into numeric entities.
+ *
+ * @method encodeNumeric
+ * @param {String} text Text to encode.
+ * @param {Boolean} attr Optional flag to specify if the text is attribute contents.
+ * @return {String} Entity encoded text.
+ */
+ encodeNumeric: function(text, attr) {
+ return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
+ // Multi byte sequence convert it to a single entity
+ if (chr.length > 1) {
+ return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';';
+ }
+
+ return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';';
+ });
+ },
+
+ /**
+ * Encodes the specified string using named entities. The core entities will be encoded
+ * as named ones but all non lower ascii characters will be encoded into named entities.
+ *
+ * @method encodeNamed
+ * @param {String} text Text to encode.
+ * @param {Boolean} attr Optional flag to specify if the text is attribute contents.
+ * @param {Object} entities Optional parameter with entities to use.
+ * @return {String} Entity encoded text.
+ */
+ encodeNamed: function(text, attr, entities) {
+ entities = entities || namedEntities;
+
+ return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
+ return baseEntities[chr] || entities[chr] || chr;
+ });
+ },
+
+ /**
+ * Returns an encode function based on the name(s) and it's optional entities.
+ *
+ * @method getEncodeFunc
+ * @param {String} name Comma separated list of encoders for example named,numeric.
+ * @param {String} entities Optional parameter with entities to use instead of the built in set.
+ * @return {function} Encode function to be used.
+ */
+ getEncodeFunc: function(name, entities) {
+ entities = buildEntitiesLookup(entities) || namedEntities;
+
+ function encodeNamedAndNumeric(text, attr) {
+ return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
+ return baseEntities[chr] || entities[chr] || '&#' + chr.charCodeAt(0) + ';' || chr;
+ });
+ }
+
+ function encodeCustomNamed(text, attr) {
+ return Entities.encodeNamed(text, attr, entities);
+ }
+
+ // Replace + with , to be compatible with previous TinyMCE versions
+ name = makeMap(name.replace(/\+/g, ','));
+
+ // Named and numeric encoder
+ if (name.named && name.numeric) {
+ return encodeNamedAndNumeric;
+ }
+
+ // Named encoder
+ if (name.named) {
+ // Custom names
+ if (entities) {
+ return encodeCustomNamed;
+ }
+
+ return Entities.encodeNamed;
+ }
+
+ // Numeric
+ if (name.numeric) {
+ return Entities.encodeNumeric;
+ }
+
+ // Raw encoder
+ return Entities.encodeRaw;
+ },
+
+ /**
+ * Decodes the specified string, this will replace entities with raw UTF characters.
+ *
+ * @method decode
+ * @param {String} text Text to entity decode.
+ * @return {String} Entity decoded string.
+ */
+ decode: function(text) {
+ return text.replace(entityRegExp, function(all, numeric) {
+ if (numeric) {
+ if (numeric.charAt(0).toLowerCase() === 'x') {
+ numeric = parseInt(numeric.substr(1), 16);
+ } else {
+ numeric = parseInt(numeric, 10);
+ }
+
+ // Support upper UTF
+ if (numeric > 0xFFFF) {
+ numeric -= 0x10000;
+
+ return String.fromCharCode(0xD800 + (numeric >> 10), 0xDC00 + (numeric & 0x3FF));
+ }
+
+ return asciiMap[numeric] || String.fromCharCode(numeric);
+ }
+
+ return reverseEntities[all] || namedEntities[all] || nativeDecode(all);
+ });
+ }
+ };
+
+ return Entities;
+});
+
+// Included from: js/tinymce/classes/dom/StyleSheetLoader.js
+
+/**
+ * StyleSheetLoader.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class handles loading of external stylesheets and fires events when these are loaded.
+ *
+ * @class tinymce.dom.StyleSheetLoader
+ * @private
+ */
+define("tinymce/dom/StyleSheetLoader", [
+ "tinymce/util/Tools",
+ "tinymce/util/Delay"
+], function(Tools, Delay) {
+ "use strict";
+
+ return function(document, settings) {
+ var idCount = 0, loadedStates = {}, maxLoadTime;
+
+ settings = settings || {};
+ maxLoadTime = settings.maxLoadTime || 5000;
+
+ function appendToHead(node) {
+ document.getElementsByTagName('head')[0].appendChild(node);
+ }
+
+ /**
+ * Loads the specified css style sheet file and call the loadedCallback once it's finished loading.
+ *
+ * @method load
+ * @param {String} url Url to be loaded.
+ * @param {Function} loadedCallback Callback to be executed when loaded.
+ * @param {Function} errorCallback Callback to be executed when failed loading.
+ */
+ function load(url, loadedCallback, errorCallback) {
+ var link, style, startTime, state;
+
+ function passed() {
+ var callbacks = state.passed, i = callbacks.length;
+
+ while (i--) {
+ callbacks[i]();
+ }
+
+ state.status = 2;
+ state.passed = [];
+ state.failed = [];
+ }
+
+ function failed() {
+ var callbacks = state.failed, i = callbacks.length;
+
+ while (i--) {
+ callbacks[i]();
+ }
+
+ state.status = 3;
+ state.passed = [];
+ state.failed = [];
+ }
+
+ // Sniffs for older WebKit versions that have the link.onload but a broken one
+ function isOldWebKit() {
+ var webKitChunks = navigator.userAgent.match(/WebKit\/(\d*)/);
+ return !!(webKitChunks && webKitChunks[1] < 536);
+ }
+
+ // Calls the waitCallback until the test returns true or the timeout occurs
+ function wait(testCallback, waitCallback) {
+ if (!testCallback()) {
+ // Wait for timeout
+ if ((new Date().getTime()) - startTime < maxLoadTime) {
+ Delay.setTimeout(waitCallback);
+ } else {
+ failed();
+ }
+ }
+ }
+
+ // Workaround for WebKit that doesn't properly support the onload event for link elements
+ // Or WebKit that fires the onload event before the StyleSheet is added to the document
+ function waitForWebKitLinkLoaded() {
+ wait(function() {
+ var styleSheets = document.styleSheets, styleSheet, i = styleSheets.length, owner;
+
+ while (i--) {
+ styleSheet = styleSheets[i];
+ owner = styleSheet.ownerNode ? styleSheet.ownerNode : styleSheet.owningElement;
+ if (owner && owner.id === link.id) {
+ passed();
+ return true;
+ }
+ }
+ }, waitForWebKitLinkLoaded);
+ }
+
+ // Workaround for older Geckos that doesn't have any onload event for StyleSheets
+ function waitForGeckoLinkLoaded() {
+ wait(function() {
+ try {
+ // Accessing the cssRules will throw an exception until the CSS file is loaded
+ var cssRules = style.sheet.cssRules;
+ passed();
+ return !!cssRules;
+ } catch (ex) {
+ // Ignore
+ }
+ }, waitForGeckoLinkLoaded);
+ }
+
+ url = Tools._addCacheSuffix(url);
+
+ if (!loadedStates[url]) {
+ state = {
+ passed: [],
+ failed: []
+ };
+
+ loadedStates[url] = state;
+ } else {
+ state = loadedStates[url];
+ }
+
+ if (loadedCallback) {
+ state.passed.push(loadedCallback);
+ }
+
+ if (errorCallback) {
+ state.failed.push(errorCallback);
+ }
+
+ // Is loading wait for it to pass
+ if (state.status == 1) {
+ return;
+ }
+
+ // Has finished loading and was success
+ if (state.status == 2) {
+ passed();
+ return;
+ }
+
+ // Has finished loading and was a failure
+ if (state.status == 3) {
+ failed();
+ return;
+ }
+
+ // Start loading
+ state.status = 1;
+ link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.type = 'text/css';
+ link.id = 'u' + (idCount++);
+ link.async = false;
+ link.defer = false;
+ startTime = new Date().getTime();
+
+ // Feature detect onload on link element and sniff older webkits since it has an broken onload event
+ if ("onload" in link && !isOldWebKit()) {
+ link.onload = waitForWebKitLinkLoaded;
+ link.onerror = failed;
+ } else {
+ // Sniff for old Firefox that doesn't support the onload event on link elements
+ // TODO: Remove this in the future when everyone uses modern browsers
+ if (navigator.userAgent.indexOf("Firefox") > 0) {
+ style = document.createElement('style');
+ style.textContent = '@import "' + url + '"';
+ waitForGeckoLinkLoaded();
+ appendToHead(style);
+ return;
+ }
+
+ // Use the id owner on older webkits
+ waitForWebKitLinkLoaded();
+ }
+
+ appendToHead(link);
+ link.href = url;
+ }
+
+ this.load = load;
+ };
+});
+
+// Included from: js/tinymce/classes/dom/DOMUtils.js
+
+/**
+ * DOMUtils.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Utility class for various DOM manipulation and retrieval functions.
+ *
+ * @class tinymce.dom.DOMUtils
+ * @example
+ * // Add a class to an element by id in the page
+ * tinymce.DOM.addClass('someid', 'someclass');
+ *
+ * // Add a class to an element by id inside the editor
+ * tinymce.activeEditor.dom.addClass('someid', 'someclass');
+ */
+define("tinymce/dom/DOMUtils", [
+ "tinymce/dom/Sizzle",
+ "tinymce/dom/DomQuery",
+ "tinymce/html/Styles",
+ "tinymce/dom/EventUtils",
+ "tinymce/dom/TreeWalker",
+ "tinymce/dom/Range",
+ "tinymce/html/Entities",
+ "tinymce/Env",
+ "tinymce/util/Tools",
+ "tinymce/dom/StyleSheetLoader"
+], function(Sizzle, $, Styles, EventUtils, TreeWalker, Range, Entities, Env, Tools, StyleSheetLoader) {
+ // Shorten names
+ var each = Tools.each, is = Tools.is, grep = Tools.grep, trim = Tools.trim;
+ var isIE = Env.ie;
+ var simpleSelectorRe = /^([a-z0-9],?)+$/i;
+ var whiteSpaceRegExp = /^[ \t\r\n]*$/;
+
+ function setupAttrHooks(domUtils, settings) {
+ var attrHooks = {}, keepValues = settings.keep_values, keepUrlHook;
+
+ keepUrlHook = {
+ set: function($elm, value, name) {
+ if (settings.url_converter) {
+ value = settings.url_converter.call(settings.url_converter_scope || domUtils, value, name, $elm[0]);
+ }
+
+ $elm.attr('data-mce-' + name, value).attr(name, value);
+ },
+
+ get: function($elm, name) {
+ return $elm.attr('data-mce-' + name) || $elm.attr(name);
+ }
+ };
+
+ attrHooks = {
+ style: {
+ set: function($elm, value) {
+ if (value !== null && typeof value === 'object') {
+ $elm.css(value);
+ return;
+ }
+
+ if (keepValues) {
+ $elm.attr('data-mce-style', value);
+ }
+
+ $elm.attr('style', value);
+ },
+
+ get: function($elm) {
+ var value = $elm.attr('data-mce-style') || $elm.attr('style');
+
+ value = domUtils.serializeStyle(domUtils.parseStyle(value), $elm[0].nodeName);
+
+ return value;
+ }
+ }
+ };
+
+ if (keepValues) {
+ attrHooks.href = attrHooks.src = keepUrlHook;
+ }
+
+ return attrHooks;
+ }
+
+ function updateInternalStyleAttr(domUtils, $elm) {
+ var value = $elm.attr('style');
+
+ value = domUtils.serializeStyle(domUtils.parseStyle(value), $elm[0].nodeName);
+
+ if (!value) {
+ value = null;
+ }
+
+ $elm.attr('data-mce-style', value);
+ }
+
+ function nodeIndex(node, normalized) {
+ var idx = 0, lastNodeType, nodeType;
+
+ if (node) {
+ for (lastNodeType = node.nodeType, node = node.previousSibling; node; node = node.previousSibling) {
+ nodeType = node.nodeType;
+
+ // Normalize text nodes
+ if (normalized && nodeType == 3) {
+ if (nodeType == lastNodeType || !node.nodeValue.length) {
+ continue;
+ }
+ }
+ idx++;
+ lastNodeType = nodeType;
+ }
+ }
+
+ return idx;
+ }
+
+ /**
+ * Constructs a new DOMUtils instance. Consult the Wiki for more details on settings etc for this class.
+ *
+ * @constructor
+ * @method DOMUtils
+ * @param {Document} doc Document reference to bind the utility class to.
+ * @param {settings} settings Optional settings collection.
+ */
+ function DOMUtils(doc, settings) {
+ var self = this, blockElementsMap;
+
+ self.doc = doc;
+ self.win = window;
+ self.files = {};
+ self.counter = 0;
+ self.stdMode = !isIE || doc.documentMode >= 8;
+ self.boxModel = !isIE || doc.compatMode == "CSS1Compat" || self.stdMode;
+ self.styleSheetLoader = new StyleSheetLoader(doc);
+ self.boundEvents = [];
+ self.settings = settings = settings || {};
+ self.schema = settings.schema;
+ self.styles = new Styles({
+ url_converter: settings.url_converter,
+ url_converter_scope: settings.url_converter_scope
+ }, settings.schema);
+
+ self.fixDoc(doc);
+ self.events = settings.ownEvents ? new EventUtils(settings.proxy) : EventUtils.Event;
+ self.attrHooks = setupAttrHooks(self, settings);
+ blockElementsMap = settings.schema ? settings.schema.getBlockElements() : {};
+ self.$ = $.overrideDefaults(function() {
+ return {
+ context: doc,
+ element: self.getRoot()
+ };
+ });
+
+ /**
+ * Returns true/false if the specified element is a block element or not.
+ *
+ * @method isBlock
+ * @param {Node/String} node Element/Node to check.
+ * @return {Boolean} True/False state if the node is a block element or not.
+ */
+ self.isBlock = function(node) {
+ // Fix for #5446
+ if (!node) {
+ return false;
+ }
+
+ // This function is called in module pattern style since it might be executed with the wrong this scope
+ var type = node.nodeType;
+
+ // If it's a node then check the type and use the nodeName
+ if (type) {
+ return !!(type === 1 && blockElementsMap[node.nodeName]);
+ }
+
+ return !!blockElementsMap[node];
+ };
+ }
+
+ DOMUtils.prototype = {
+ $$: function(elm) {
+ if (typeof elm == 'string') {
+ elm = this.get(elm);
+ }
+
+ return this.$(elm);
+ },
+
+ root: null,
+
+ fixDoc: function(doc) {
+ var settings = this.settings, name;
+
+ if (isIE && settings.schema) {
+ // Add missing HTML 4/5 elements to IE
+ ('abbr article aside audio canvas ' +
+ 'details figcaption figure footer ' +
+ 'header hgroup mark menu meter nav ' +
+ 'output progress section summary ' +
+ 'time video').replace(/\w+/g, function(name) {
+ doc.createElement(name);
+ });
+
+ // Create all custom elements
+ for (name in settings.schema.getCustomElements()) {
+ doc.createElement(name);
+ }
+ }
+ },
+
+ clone: function(node, deep) {
+ var self = this, clone, doc;
+
+ // TODO: Add feature detection here in the future
+ if (!isIE || node.nodeType !== 1 || deep) {
+ return node.cloneNode(deep);
+ }
+
+ doc = self.doc;
+
+ // Make a HTML5 safe shallow copy
+ if (!deep) {
+ clone = doc.createElement(node.nodeName);
+
+ // Copy attribs
+ each(self.getAttribs(node), function(attr) {
+ self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName));
+ });
+
+ return clone;
+ }
+
+ return clone.firstChild;
+ },
+
+ /**
+ * Returns the root node of the document. This is normally the body but might be a DIV. Parents like getParent will not
+ * go above the point of this root node.
+ *
+ * @method getRoot
+ * @return {Element} Root element for the utility class.
+ */
+ getRoot: function() {
+ var self = this;
+
+ return self.settings.root_element || self.doc.body;
+ },
+
+ /**
+ * Returns the viewport of the window.
+ *
+ * @method getViewPort
+ * @param {Window} win Optional window to get viewport of.
+ * @return {Object} Viewport object with fields x, y, w and h.
+ */
+ getViewPort: function(win) {
+ var doc, rootElm;
+
+ win = !win ? this.win : win;
+ doc = win.document;
+ rootElm = this.boxModel ? doc.documentElement : doc.body;
+
+ // Returns viewport size excluding scrollbars
+ return {
+ x: win.pageXOffset || rootElm.scrollLeft,
+ y: win.pageYOffset || rootElm.scrollTop,
+ w: win.innerWidth || rootElm.clientWidth,
+ h: win.innerHeight || rootElm.clientHeight
+ };
+ },
+
+ /**
+ * Returns the rectangle for a specific element.
+ *
+ * @method getRect
+ * @param {Element/String} elm Element object or element ID to get rectangle from.
+ * @return {object} Rectangle for specified element object with x, y, w, h fields.
+ */
+ getRect: function(elm) {
+ var self = this, pos, size;
+
+ elm = self.get(elm);
+ pos = self.getPos(elm);
+ size = self.getSize(elm);
+
+ return {
+ x: pos.x, y: pos.y,
+ w: size.w, h: size.h
+ };
+ },
+
+ /**
+ * Returns the size dimensions of the specified element.
+ *
+ * @method getSize
+ * @param {Element/String} elm Element object or element ID to get rectangle from.
+ * @return {object} Rectangle for specified element object with w, h fields.
+ */
+ getSize: function(elm) {
+ var self = this, w, h;
+
+ elm = self.get(elm);
+ w = self.getStyle(elm, 'width');
+ h = self.getStyle(elm, 'height');
+
+ // Non pixel value, then force offset/clientWidth
+ if (w.indexOf('px') === -1) {
+ w = 0;
+ }
+
+ // Non pixel value, then force offset/clientWidth
+ if (h.indexOf('px') === -1) {
+ h = 0;
+ }
+
+ return {
+ w: parseInt(w, 10) || elm.offsetWidth || elm.clientWidth,
+ h: parseInt(h, 10) || elm.offsetHeight || elm.clientHeight
+ };
+ },
+
+ /**
+ * Returns a node by the specified selector function. This function will
+ * loop through all parent nodes and call the specified function for each node.
+ * If the function then returns true indicating that it has found what it was looking for, the loop execution will then end
+ * and the node it found will be returned.
+ *
+ * @method getParent
+ * @param {Node/String} node DOM node to search parents on or ID string.
+ * @param {function} selector Selection function or CSS selector to execute on each node.
+ * @param {Node} root Optional root element, never go below this point.
+ * @return {Node} DOM Node or null if it wasn't found.
+ */
+ getParent: function(node, selector, root) {
+ return this.getParents(node, selector, root, false);
+ },
+
+ /**
+ * Returns a node list of all parents matching the specified selector function or pattern.
+ * If the function then returns true indicating that it has found what it was looking for and that node will be collected.
+ *
+ * @method getParents
+ * @param {Node/String} node DOM node to search parents on or ID string.
+ * @param {function} selector Selection function to execute on each node or CSS pattern.
+ * @param {Node} root Optional root element, never go below this point.
+ * @return {Array} Array of nodes or null if it wasn't found.
+ */
+ getParents: function(node, selector, root, collect) {
+ var self = this, selectorVal, result = [];
+
+ node = self.get(node);
+ collect = collect === undefined;
+
+ // Default root on inline mode
+ root = root || (self.getRoot().nodeName != 'BODY' ? self.getRoot().parentNode : null);
+
+ // Wrap node name as func
+ if (is(selector, 'string')) {
+ selectorVal = selector;
+
+ if (selector === '*') {
+ selector = function(node) {
+ return node.nodeType == 1;
+ };
+ } else {
+ selector = function(node) {
+ return self.is(node, selectorVal);
+ };
+ }
+ }
+
+ while (node) {
+ if (node == root || !node.nodeType || node.nodeType === 9) {
+ break;
+ }
+
+ if (!selector || selector(node)) {
+ if (collect) {
+ result.push(node);
+ } else {
+ return node;
+ }
+ }
+
+ node = node.parentNode;
+ }
+
+ return collect ? result : null;
+ },
+
+ /**
+ * Returns the specified element by ID or the input element if it isn't a string.
+ *
+ * @method get
+ * @param {String/Element} n Element id to look for or element to just pass though.
+ * @return {Element} Element matching the specified id or null if it wasn't found.
+ */
+ get: function(elm) {
+ var name;
+
+ if (elm && this.doc && typeof elm == 'string') {
+ name = elm;
+ elm = this.doc.getElementById(elm);
+
+ // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick
+ if (elm && elm.id !== name) {
+ return this.doc.getElementsByName(name)[1];
+ }
+ }
+
+ return elm;
+ },
+
+ /**
+ * Returns the next node that matches selector or function
+ *
+ * @method getNext
+ * @param {Node} node Node to find siblings from.
+ * @param {String/function} selector Selector CSS expression or function.
+ * @return {Node} Next node item matching the selector or null if it wasn't found.
+ */
+ getNext: function(node, selector) {
+ return this._findSib(node, selector, 'nextSibling');
+ },
+
+ /**
+ * Returns the previous node that matches selector or function
+ *
+ * @method getPrev
+ * @param {Node} node Node to find siblings from.
+ * @param {String/function} selector Selector CSS expression or function.
+ * @return {Node} Previous node item matching the selector or null if it wasn't found.
+ */
+ getPrev: function(node, selector) {
+ return this._findSib(node, selector, 'previousSibling');
+ },
+
+ // #ifndef jquery
+
+ /**
+ * Selects specific elements by a CSS level 3 pattern. For example "div#a1 p.test".
+ * This function is optimized for the most common patterns needed in TinyMCE but it also performs well enough
+ * on more complex patterns.
+ *
+ * @method select
+ * @param {String} selector CSS level 3 pattern to select/find elements by.
+ * @param {Object} scope Optional root element/scope element to search in.
+ * @return {Array} Array with all matched elements.
+ * @example
+ * // Adds a class to all paragraphs in the currently active editor
+ * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass');
+ *
+ * // Adds a class to all spans that have the test class in the currently active editor
+ * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('span.test'), 'someclass')
+ */
+ select: function(selector, scope) {
+ var self = this;
+
+ /*eslint new-cap:0 */
+ return Sizzle(selector, self.get(scope) || self.settings.root_element || self.doc, []);
+ },
+
+ /**
+ * Returns true/false if the specified element matches the specified css pattern.
+ *
+ * @method is
+ * @param {Node/NodeList} elm DOM node to match or an array of nodes to match.
+ * @param {String} selector CSS pattern to match the element against.
+ */
+ is: function(elm, selector) {
+ var i;
+
+ // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance
+ if (elm.length === undefined) {
+ // Simple all selector
+ if (selector === '*') {
+ return elm.nodeType == 1;
+ }
+
+ // Simple selector just elements
+ if (simpleSelectorRe.test(selector)) {
+ selector = selector.toLowerCase().split(/,/);
+ elm = elm.nodeName.toLowerCase();
+
+ for (i = selector.length - 1; i >= 0; i--) {
+ if (selector[i] == elm) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ // Is non element
+ if (elm.nodeType && elm.nodeType != 1) {
+ return false;
+ }
+
+ var elms = elm.nodeType ? [elm] : elm;
+
+ /*eslint new-cap:0 */
+ return Sizzle(selector, elms[0].ownerDocument || elms[0], null, elms).length > 0;
+ },
+
+ // #endif
+
+ /**
+ * Adds the specified element to another element or elements.
+ *
+ * @method add
+ * @param {String/Element/Array} parentElm Element id string, DOM node element or array of ids or elements to add to.
+ * @param {String/Element} name Name of new element to add or existing element to add.
+ * @param {Object} attrs Optional object collection with arguments to add to the new element(s).
+ * @param {String} html Optional inner HTML contents to add for each element.
+ * @param {Boolean} create Optional flag if the element should be created or added.
+ * @return {Element/Array} Element that got created, or an array of created elements if multiple input elements
+ * were passed in.
+ * @example
+ * // Adds a new paragraph to the end of the active editor
+ * tinymce.activeEditor.dom.add(tinymce.activeEditor.getBody(), 'p', {title: 'my title'}, 'Some content');
+ */
+ add: function(parentElm, name, attrs, html, create) {
+ var self = this;
+
+ return this.run(parentElm, function(parentElm) {
+ var newElm;
+
+ newElm = is(name, 'string') ? self.doc.createElement(name) : name;
+ self.setAttribs(newElm, attrs);
+
+ if (html) {
+ if (html.nodeType) {
+ newElm.appendChild(html);
+ } else {
+ self.setHTML(newElm, html);
+ }
+ }
+
+ return !create ? parentElm.appendChild(newElm) : newElm;
+ });
+ },
+
+ /**
+ * Creates a new element.
+ *
+ * @method create
+ * @param {String} name Name of new element.
+ * @param {Object} attrs Optional object name/value collection with element attributes.
+ * @param {String} html Optional HTML string to set as inner HTML of the element.
+ * @return {Element} HTML DOM node element that got created.
+ * @example
+ * // Adds an element where the caret/selection is in the active editor
+ * var el = tinymce.activeEditor.dom.create('div', {id: 'test', 'class': 'myclass'}, 'some content');
+ * tinymce.activeEditor.selection.setNode(el);
+ */
+ create: function(name, attrs, html) {
+ return this.add(this.doc.createElement(name), name, attrs, html, 1);
+ },
+
+ /**
+ * Creates HTML string for element. The element will be closed unless an empty inner HTML string is passed in.
+ *
+ * @method createHTML
+ * @param {String} name Name of new element.
+ * @param {Object} attrs Optional object name/value collection with element attributes.
+ * @param {String} html Optional HTML string to set as inner HTML of the element.
+ * @return {String} String with new HTML element, for example: <a href="#">test</a>.
+ * @example
+ * // Creates a html chunk and inserts it at the current selection/caret location
+ * tinymce.activeEditor.selection.setContent(tinymce.activeEditor.dom.createHTML('a', {href: 'test.html'}, 'some line'));
+ */
+ createHTML: function(name, attrs, html) {
+ var outHtml = '', key;
+
+ outHtml += '<' + name;
+
+ for (key in attrs) {
+ if (attrs.hasOwnProperty(key) && attrs[key] !== null && typeof attrs[key] != 'undefined') {
+ outHtml += ' ' + key + '="' + this.encode(attrs[key]) + '"';
+ }
+ }
+
+ // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime
+ if (typeof html != "undefined") {
+ return outHtml + '>' + html + '</' + name + '>';
+ }
+
+ return outHtml + ' />';
+ },
+
+ /**
+ * Creates a document fragment out of the specified HTML string.
+ *
+ * @method createFragment
+ * @param {String} html Html string to create fragment from.
+ * @return {DocumentFragment} Document fragment node.
+ */
+ createFragment: function(html) {
+ var frag, node, doc = this.doc, container;
+
+ container = doc.createElement("div");
+ frag = doc.createDocumentFragment();
+
+ if (html) {
+ container.innerHTML = html;
+ }
+
+ while ((node = container.firstChild)) {
+ frag.appendChild(node);
+ }
+
+ return frag;
+ },
+
+ /**
+ * Removes/deletes the specified element(s) from the DOM.
+ *
+ * @method remove
+ * @param {String/Element/Array} node ID of element or DOM element object or array containing multiple elements/ids.
+ * @param {Boolean} keepChildren Optional state to keep children or not. If set to true all children will be
+ * placed at the location of the removed element.
+ * @return {Element/Array} HTML DOM element that got removed, or an array of removed elements if multiple input elements
+ * were passed in.
+ * @example
+ * // Removes all paragraphs in the active editor
+ * tinymce.activeEditor.dom.remove(tinymce.activeEditor.dom.select('p'));
+ *
+ * // Removes an element by id in the document
+ * tinymce.DOM.remove('mydiv');
+ */
+ remove: function(node, keepChildren) {
+ node = this.$$(node);
+
+ if (keepChildren) {
+ node.each(function() {
+ var child;
+
+ while ((child = this.firstChild)) {
+ if (child.nodeType == 3 && child.data.length === 0) {
+ this.removeChild(child);
+ } else {
+ this.parentNode.insertBefore(child, this);
+ }
+ }
+ }).remove();
+ } else {
+ node.remove();
+ }
+
+ return node.length > 1 ? node.toArray() : node[0];
+ },
+
+ /**
+ * Sets the CSS style value on a HTML element. The name can be a camelcase string
+ * or the CSS style name like background-color.
+ *
+ * @method setStyle
+ * @param {String/Element/Array} elm HTML element/Array of elements to set CSS style value on.
+ * @param {String} name Name of the style value to set.
+ * @param {String} value Value to set on the style.
+ * @example
+ * // Sets a style value on all paragraphs in the currently active editor
+ * tinymce.activeEditor.dom.setStyle(tinymce.activeEditor.dom.select('p'), 'background-color', 'red');
+ *
+ * // Sets a style value to an element by id in the current document
+ * tinymce.DOM.setStyle('mydiv', 'background-color', 'red');
+ */
+ setStyle: function(elm, name, value) {
+ elm = this.$$(elm).css(name, value);
+
+ if (this.settings.update_styles) {
+ updateInternalStyleAttr(this, elm);
+ }
+ },
+
+ /**
+ * Returns the current style or runtime/computed value of an element.
+ *
+ * @method getStyle
+ * @param {String/Element} elm HTML element or element id string to get style from.
+ * @param {String} name Style name to return.
+ * @param {Boolean} computed Computed style.
+ * @return {String} Current style or computed style value of an element.
+ */
+ getStyle: function(elm, name, computed) {
+ elm = this.$$(elm);
+
+ if (computed) {
+ return elm.css(name);
+ }
+
+ // Camelcase it, if needed
+ name = name.replace(/-(\D)/g, function(a, b) {
+ return b.toUpperCase();
+ });
+
+ if (name == 'float') {
+ name = Env.ie && Env.ie < 12 ? 'styleFloat' : 'cssFloat';
+ }
+
+ return elm[0] && elm[0].style ? elm[0].style[name] : undefined;
+ },
+
+ /**
+ * Sets multiple styles on the specified element(s).
+ *
+ * @method setStyles
+ * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set styles on.
+ * @param {Object} styles Name/Value collection of style items to add to the element(s).
+ * @example
+ * // Sets styles on all paragraphs in the currently active editor
+ * tinymce.activeEditor.dom.setStyles(tinymce.activeEditor.dom.select('p'), {'background-color': 'red', 'color': 'green'});
+ *
+ * // Sets styles to an element by id in the current document
+ * tinymce.DOM.setStyles('mydiv', {'background-color': 'red', 'color': 'green'});
+ */
+ setStyles: function(elm, styles) {
+ elm = this.$$(elm).css(styles);
+
+ if (this.settings.update_styles) {
+ updateInternalStyleAttr(this, elm);
+ }
+ },
+
+ /**
+ * Removes all attributes from an element or elements.
+ *
+ * @method removeAllAttribs
+ * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to remove attributes from.
+ */
+ removeAllAttribs: function(e) {
+ return this.run(e, function(e) {
+ var i, attrs = e.attributes;
+ for (i = attrs.length - 1; i >= 0; i--) {
+ e.removeAttributeNode(attrs.item(i));
+ }
+ });
+ },
+
+ /**
+ * Sets the specified attribute of an element or elements.
+ *
+ * @method setAttrib
+ * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attribute on.
+ * @param {String} name Name of attribute to set.
+ * @param {String} value Value to set on the attribute - if this value is falsy like null, 0 or '' it will remove
+ * the attribute instead.
+ * @example
+ * // Sets class attribute on all paragraphs in the active editor
+ * tinymce.activeEditor.dom.setAttrib(tinymce.activeEditor.dom.select('p'), 'class', 'myclass');
+ *
+ * // Sets class attribute on a specific element in the current page
+ * tinymce.dom.setAttrib('mydiv', 'class', 'myclass');
+ */
+ setAttrib: function(elm, name, value) {
+ var self = this, originalValue, hook, settings = self.settings;
+
+ if (value === '') {
+ value = null;
+ }
+
+ elm = self.$$(elm);
+ originalValue = elm.attr(name);
+
+ if (!elm.length) {
+ return;
+ }
+
+ hook = self.attrHooks[name];
+ if (hook && hook.set) {
+ hook.set(elm, value, name);
+ } else {
+ elm.attr(name, value);
+ }
+
+ if (originalValue != value && settings.onSetAttrib) {
+ settings.onSetAttrib({
+ attrElm: elm,
+ attrName: name,
+ attrValue: value
+ });
+ }
+ },
+
+ /**
+ * Sets two or more specified attributes of an element or elements.
+ *
+ * @method setAttribs
+ * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attributes on.
+ * @param {Object} attrs Name/Value collection of attribute items to add to the element(s).
+ * @example
+ * // Sets class and title attributes on all paragraphs in the active editor
+ * tinymce.activeEditor.dom.setAttribs(tinymce.activeEditor.dom.select('p'), {'class': 'myclass', title: 'some title'});
+ *
+ * // Sets class and title attributes on a specific element in the current page
+ * tinymce.DOM.setAttribs('mydiv', {'class': 'myclass', title: 'some title'});
+ */
+ setAttribs: function(elm, attrs) {
+ var self = this;
+
+ self.$$(elm).each(function(i, node) {
+ each(attrs, function(value, name) {
+ self.setAttrib(node, name, value);
+ });
+ });
+ },
+
+ /**
+ * Returns the specified attribute by name.
+ *
+ * @method getAttrib
+ * @param {String/Element} elm Element string id or DOM element to get attribute from.
+ * @param {String} name Name of attribute to get.
+ * @param {String} defaultVal Optional default value to return if the attribute didn't exist.
+ * @return {String} Attribute value string, default value or null if the attribute wasn't found.
+ */
+ getAttrib: function(elm, name, defaultVal) {
+ var self = this, hook, value;
+
+ elm = self.$$(elm);
+
+ if (elm.length) {
+ hook = self.attrHooks[name];
+
+ if (hook && hook.get) {
+ value = hook.get(elm, name);
+ } else {
+ value = elm.attr(name);
+ }
+ }
+
+ if (typeof value == 'undefined') {
+ value = defaultVal || '';
+ }
+
+ return value;
+ },
+
+ /**
+ * Returns the absolute x, y position of a node. The position will be returned in an object with x, y fields.
+ *
+ * @method getPos
+ * @param {Element/String} elm HTML element or element id to get x, y position from.
+ * @param {Element} rootElm Optional root element to stop calculations at.
+ * @return {object} Absolute position of the specified element object with x, y fields.
+ */
+ getPos: function(elm, rootElm) {
+ var self = this, x = 0, y = 0, offsetParent, doc = self.doc, body = doc.body, pos;
+
+ elm = self.get(elm);
+ rootElm = rootElm || body;
+
+ if (elm) {
+ // Use getBoundingClientRect if it exists since it's faster than looping offset nodes
+ // Fallback to offsetParent calculations if the body isn't static better since it stops at the body root
+ if (rootElm === body && elm.getBoundingClientRect && $(body).css('position') === 'static') {
+ pos = elm.getBoundingClientRect();
+ rootElm = self.boxModel ? doc.documentElement : body;
+
+ // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit
+ // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position
+ x = pos.left + (doc.documentElement.scrollLeft || body.scrollLeft) - rootElm.clientLeft;
+ y = pos.top + (doc.documentElement.scrollTop || body.scrollTop) - rootElm.clientTop;
+
+ return {x: x, y: y};
+ }
+
+ offsetParent = elm;
+ while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) {
+ x += offsetParent.offsetLeft || 0;
+ y += offsetParent.offsetTop || 0;
+ offsetParent = offsetParent.offsetParent;
+ }
+
+ offsetParent = elm.parentNode;
+ while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) {
+ x -= offsetParent.scrollLeft || 0;
+ y -= offsetParent.scrollTop || 0;
+ offsetParent = offsetParent.parentNode;
+ }
+ }
+
+ return {x: x, y: y};
+ },
+
+ /**
+ * Parses the specified style value into an object collection. This parser will also
+ * merge and remove any redundant items that browsers might have added. It will also convert non-hex
+ * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings.
+ *
+ * @method parseStyle
+ * @param {String} cssText Style value to parse, for example: border:1px solid red;.
+ * @return {Object} Object representation of that style, for example: {border: '1px solid red'}
+ */
+ parseStyle: function(cssText) {
+ return this.styles.parse(cssText);
+ },
+
+ /**
+ * Serializes the specified style object into a string.
+ *
+ * @method serializeStyle
+ * @param {Object} styles Object to serialize as string, for example: {border: '1px solid red'}
+ * @param {String} name Optional element name.
+ * @return {String} String representation of the style object, for example: border: 1px solid red.
+ */
+ serializeStyle: function(styles, name) {
+ return this.styles.serialize(styles, name);
+ },
+
+ /**
+ * Adds a style element at the top of the document with the specified cssText content.
+ *
+ * @method addStyle
+ * @param {String} cssText CSS Text style to add to top of head of document.
+ */
+ addStyle: function(cssText) {
+ var self = this, doc = self.doc, head, styleElm;
+
+ // Prevent inline from loading the same styles twice
+ if (self !== DOMUtils.DOM && doc === document) {
+ var addedStyles = DOMUtils.DOM.addedStyles;
+
+ addedStyles = addedStyles || [];
+ if (addedStyles[cssText]) {
+ return;
+ }
+
+ addedStyles[cssText] = true;
+ DOMUtils.DOM.addedStyles = addedStyles;
+ }
+
+ // Create style element if needed
+ styleElm = doc.getElementById('mceDefaultStyles');
+ if (!styleElm) {
+ styleElm = doc.createElement('style');
+ styleElm.id = 'mceDefaultStyles';
+ styleElm.type = 'text/css';
+
+ head = doc.getElementsByTagName('head')[0];
+ if (head.firstChild) {
+ head.insertBefore(styleElm, head.firstChild);
+ } else {
+ head.appendChild(styleElm);
+ }
+ }
+
+ // Append style data to old or new style element
+ if (styleElm.styleSheet) {
+ styleElm.styleSheet.cssText += cssText;
+ } else {
+ styleElm.appendChild(doc.createTextNode(cssText));
+ }
+ },
+
+ /**
+ * Imports/loads the specified CSS file into the document bound to the class.
+ *
+ * @method loadCSS
+ * @param {String} url URL to CSS file to load.
+ * @example
+ * // Loads a CSS file dynamically into the current document
+ * tinymce.DOM.loadCSS('somepath/some.css');
+ *
+ * // Loads a CSS file into the currently active editor instance
+ * tinymce.activeEditor.dom.loadCSS('somepath/some.css');
+ *
+ * // Loads a CSS file into an editor instance by id
+ * tinymce.get('someid').dom.loadCSS('somepath/some.css');
+ *
+ * // Loads multiple CSS files into the current document
+ * tinymce.DOM.loadCSS('somepath/some.css,somepath/someother.css');
+ */
+ loadCSS: function(url) {
+ var self = this, doc = self.doc, head;
+
+ // Prevent inline from loading the same CSS file twice
+ if (self !== DOMUtils.DOM && doc === document) {
+ DOMUtils.DOM.loadCSS(url);
+ return;
+ }
+
+ if (!url) {
+ url = '';
+ }
+
+ head = doc.getElementsByTagName('head')[0];
+
+ each(url.split(','), function(url) {
+ var link;
+
+ url = Tools._addCacheSuffix(url);
+
+ if (self.files[url]) {
+ return;
+ }
+
+ self.files[url] = true;
+ link = self.create('link', {rel: 'stylesheet', href: url});
+
+ // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug
+ // This fix seems to resolve that issue by recalcing the document once a stylesheet finishes loading
+ // It's ugly but it seems to work fine.
+ if (isIE && doc.documentMode && doc.recalc) {
+ link.onload = function() {
+ if (doc.recalc) {
+ doc.recalc();
+ }
+
+ link.onload = null;
+ };
+ }
+
+ head.appendChild(link);
+ });
+ },
+
+ /**
+ * Adds a class to the specified element or elements.
+ *
+ * @method addClass
+ * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs.
+ * @param {String} cls Class name to add to each element.
+ * @return {String/Array} String with new class value or array with new class values for all elements.
+ * @example
+ * // Adds a class to all paragraphs in the active editor
+ * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'myclass');
+ *
+ * // Adds a class to a specific element in the current page
+ * tinymce.DOM.addClass('mydiv', 'myclass');
+ */
+ addClass: function(elm, cls) {
+ this.$$(elm).addClass(cls);
+ },
+
+ /**
+ * Removes a class from the specified element or elements.
+ *
+ * @method removeClass
+ * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs.
+ * @param {String} cls Class name to remove from each element.
+ * @return {String/Array} String of remaining class name(s), or an array of strings if multiple input elements
+ * were passed in.
+ * @example
+ * // Removes a class from all paragraphs in the active editor
+ * tinymce.activeEditor.dom.removeClass(tinymce.activeEditor.dom.select('p'), 'myclass');
+ *
+ * // Removes a class from a specific element in the current page
+ * tinymce.DOM.removeClass('mydiv', 'myclass');
+ */
+ removeClass: function(elm, cls) {
+ this.toggleClass(elm, cls, false);
+ },
+
+ /**
+ * Returns true if the specified element has the specified class.
+ *
+ * @method hasClass
+ * @param {String/Element} elm HTML element or element id string to check CSS class on.
+ * @param {String} cls CSS class to check for.
+ * @return {Boolean} true/false if the specified element has the specified class.
+ */
+ hasClass: function(elm, cls) {
+ return this.$$(elm).hasClass(cls);
+ },
+
+ /**
+ * Toggles the specified class on/off.
+ *
+ * @method toggleClass
+ * @param {Element} elm Element to toggle class on.
+ * @param {[type]} cls Class to toggle on/off.
+ * @param {[type]} state Optional state to set.
+ */
+ toggleClass: function(elm, cls, state) {
+ this.$$(elm).toggleClass(cls, state).each(function() {
+ if (this.className === '') {
+ $(this).attr('class', null);
+ }
+ });
+ },
+
+ /**
+ * Shows the specified element(s) by ID by setting the "display" style.
+ *
+ * @method show
+ * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to show.
+ */
+ show: function(elm) {
+ this.$$(elm).show();
+ },
+
+ /**
+ * Hides the specified element(s) by ID by setting the "display" style.
+ *
+ * @method hide
+ * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to hide.
+ * @example
+ * // Hides an element by id in the document
+ * tinymce.DOM.hide('myid');
+ */
+ hide: function(elm) {
+ this.$$(elm).hide();
+ },
+
+ /**
+ * Returns true/false if the element is hidden or not by checking the "display" style.
+ *
+ * @method isHidden
+ * @param {String/Element} elm Id or element to check display state on.
+ * @return {Boolean} true/false if the element is hidden or not.
+ */
+ isHidden: function(elm) {
+ return this.$$(elm).css('display') == 'none';
+ },
+
+ /**
+ * Returns a unique id. This can be useful when generating elements on the fly.
+ * This method will not check if the element already exists.
+ *
+ * @method uniqueId
+ * @param {String} prefix Optional prefix to add in front of all ids - defaults to "mce_".
+ * @return {String} Unique id.
+ */
+ uniqueId: function(prefix) {
+ return (!prefix ? 'mce_' : prefix) + (this.counter++);
+ },
+
+ /**
+ * Sets the specified HTML content inside the element or elements. The HTML will first be processed. This means
+ * URLs will get converted, hex color values fixed etc. Check processHTML for details.
+ *
+ * @method setHTML
+ * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set HTML inside of.
+ * @param {String} html HTML content to set as inner HTML of the element.
+ * @example
+ * // Sets the inner HTML of all paragraphs in the active editor
+ * tinymce.activeEditor.dom.setHTML(tinymce.activeEditor.dom.select('p'), 'some inner html');
+ *
+ * // Sets the inner HTML of an element by id in the document
+ * tinymce.DOM.setHTML('mydiv', 'some inner html');
+ */
+ setHTML: function(elm, html) {
+ elm = this.$$(elm);
+
+ if (isIE) {
+ elm.each(function(i, target) {
+ if (target.canHaveHTML === false) {
+ return;
+ }
+
+ // Remove all child nodes, IE keeps empty text nodes in DOM
+ while (target.firstChild) {
+ target.removeChild(target.firstChild);
+ }
+
+ try {
+ // IE will remove comments from the beginning
+ // unless you padd the contents with something
+ target.innerHTML = '<br>' + html;
+ target.removeChild(target.firstChild);
+ } catch (ex) {
+ // IE sometimes produces an unknown runtime error on innerHTML if it's a div inside a p
+ $('<div></div>').html('<br>' + html).contents().slice(1).appendTo(target);
+ }
+
+ return html;
+ });
+ } else {
+ elm.html(html);
+ }
+ },
+
+ /**
+ * Returns the outer HTML of an element.
+ *
+ * @method getOuterHTML
+ * @param {String/Element} elm Element ID or element object to get outer HTML from.
+ * @return {String} Outer HTML string.
+ * @example
+ * tinymce.DOM.getOuterHTML(editorElement);
+ * tinymce.activeEditor.getOuterHTML(tinymce.activeEditor.getBody());
+ */
+ getOuterHTML: function(elm) {
+ elm = this.get(elm);
+
+ // Older FF doesn't have outerHTML 3.6 is still used by some orgaizations
+ return elm.nodeType == 1 && "outerHTML" in elm ? elm.outerHTML : $('<div></div>').append($(elm).clone()).html();
+ },
+
+ /**
+ * Sets the specified outer HTML on an element or elements.
+ *
+ * @method setOuterHTML
+ * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set outer HTML on.
+ * @param {Object} html HTML code to set as outer value for the element.
+ * @example
+ * // Sets the outer HTML of all paragraphs in the active editor
+ * tinymce.activeEditor.dom.setOuterHTML(tinymce.activeEditor.dom.select('p'), '<div>some html</div>');
+ *
+ * // Sets the outer HTML of an element by id in the document
+ * tinymce.DOM.setOuterHTML('mydiv', '<div>some html</div>');
+ */
+ setOuterHTML: function(elm, html) {
+ var self = this;
+
+ self.$$(elm).each(function() {
+ try {
+ // Older FF doesn't have outerHTML 3.6 is still used by some organizations
+ if ("outerHTML" in this) {
+ this.outerHTML = html;
+ return;
+ }
+ } catch (ex) {
+ // Ignore
+ }
+
+ // OuterHTML for IE it sometimes produces an "unknown runtime error"
+ self.remove($(this).html(html), true);
+ });
+ },
+
+ /**
+ * Entity decodes a string. This method decodes any HTML entities, such as å.
+ *
+ * @method decode
+ * @param {String} s String to decode entities on.
+ * @return {String} Entity decoded string.
+ */
+ decode: Entities.decode,
+
+ /**
+ * Entity encodes a string. This method encodes the most common entities, such as <>"&.
+ *
+ * @method encode
+ * @param {String} text String to encode with entities.
+ * @return {String} Entity encoded string.
+ */
+ encode: Entities.encodeAllRaw,
+
+ /**
+ * Inserts an element after the reference element.
+ *
+ * @method insertAfter
+ * @param {Element} node Element to insert after the reference.
+ * @param {Element/String/Array} referenceNode Reference element, element id or array of elements to insert after.
+ * @return {Element/Array} Element that got added or an array with elements.
+ */
+ insertAfter: function(node, referenceNode) {
+ referenceNode = this.get(referenceNode);
+
+ return this.run(node, function(node) {
+ var parent, nextSibling;
+
+ parent = referenceNode.parentNode;
+ nextSibling = referenceNode.nextSibling;
+
+ if (nextSibling) {
+ parent.insertBefore(node, nextSibling);
+ } else {
+ parent.appendChild(node);
+ }
+
+ return node;
+ });
+ },
+
+ /**
+ * Replaces the specified element or elements with the new element specified. The new element will
+ * be cloned if multiple input elements are passed in.
+ *
+ * @method replace
+ * @param {Element} newElm New element to replace old ones with.
+ * @param {Element/String/Array} oldElm Element DOM node, element id or array of elements or ids to replace.
+ * @param {Boolean} keepChildren Optional keep children state, if set to true child nodes from the old object will be added
+ * to new ones.
+ */
+ replace: function(newElm, oldElm, keepChildren) {
+ var self = this;
+
+ return self.run(oldElm, function(oldElm) {
+ if (is(oldElm, 'array')) {
+ newElm = newElm.cloneNode(true);
+ }
+
+ if (keepChildren) {
+ each(grep(oldElm.childNodes), function(node) {
+ newElm.appendChild(node);
+ });
+ }
+
+ return oldElm.parentNode.replaceChild(newElm, oldElm);
+ });
+ },
+
+ /**
+ * Renames the specified element and keeps its attributes and children.
+ *
+ * @method rename
+ * @param {Element} elm Element to rename.
+ * @param {String} name Name of the new element.
+ * @return {Element} New element or the old element if it needed renaming.
+ */
+ rename: function(elm, name) {
+ var self = this, newElm;
+
+ if (elm.nodeName != name.toUpperCase()) {
+ // Rename block element
+ newElm = self.create(name);
+
+ // Copy attribs to new block
+ each(self.getAttribs(elm), function(attrNode) {
+ self.setAttrib(newElm, attrNode.nodeName, self.getAttrib(elm, attrNode.nodeName));
+ });
+
+ // Replace block
+ self.replace(newElm, elm, 1);
+ }
+
+ return newElm || elm;
+ },
+
+ /**
+ * Find the common ancestor of two elements. This is a shorter method than using the DOM Range logic.
+ *
+ * @method findCommonAncestor
+ * @param {Element} a Element to find common ancestor of.
+ * @param {Element} b Element to find common ancestor of.
+ * @return {Element} Common ancestor element of the two input elements.
+ */
+ findCommonAncestor: function(a, b) {
+ var ps = a, pe;
+
+ while (ps) {
+ pe = b;
+
+ while (pe && ps != pe) {
+ pe = pe.parentNode;
+ }
+
+ if (ps == pe) {
+ break;
+ }
+
+ ps = ps.parentNode;
+ }
+
+ if (!ps && a.ownerDocument) {
+ return a.ownerDocument.documentElement;
+ }
+
+ return ps;
+ },
+
+ /**
+ * Parses the specified RGB color value and returns a hex version of that color.
+ *
+ * @method toHex
+ * @param {String} rgbVal RGB string value like rgb(1,2,3)
+ * @return {String} Hex version of that RGB value like #FF00FF.
+ */
+ toHex: function(rgbVal) {
+ return this.styles.toHex(Tools.trim(rgbVal));
+ },
+
+ /**
+ * Executes the specified function on the element by id or dom element node or array of elements/id.
+ *
+ * @method run
+ * @param {String/Element/Array} elm ID or DOM element object or array with ids or elements.
+ * @param {function} func Function to execute for each item.
+ * @param {Object} scope Optional scope to execute the function in.
+ * @return {Object/Array} Single object, or an array of objects if multiple input elements were passed in.
+ */
+ run: function(elm, func, scope) {
+ var self = this, result;
+
+ if (typeof elm === 'string') {
+ elm = self.get(elm);
+ }
+
+ if (!elm) {
+ return false;
+ }
+
+ scope = scope || this;
+ if (!elm.nodeType && (elm.length || elm.length === 0)) {
+ result = [];
+
+ each(elm, function(elm, i) {
+ if (elm) {
+ if (typeof elm == 'string') {
+ elm = self.get(elm);
+ }
+
+ result.push(func.call(scope, elm, i));
+ }
+ });
+
+ return result;
+ }
+
+ return func.call(scope, elm);
+ },
+
+ /**
+ * Returns a NodeList with attributes for the element.
+ *
+ * @method getAttribs
+ * @param {HTMLElement/string} elm Element node or string id to get attributes from.
+ * @return {NodeList} NodeList with attributes.
+ */
+ getAttribs: function(elm) {
+ var attrs;
+
+ elm = this.get(elm);
+
+ if (!elm) {
+ return [];
+ }
+
+ if (isIE) {
+ attrs = [];
+
+ // Object will throw exception in IE
+ if (elm.nodeName == 'OBJECT') {
+ return elm.attributes;
+ }
+
+ // IE doesn't keep the selected attribute if you clone option elements
+ if (elm.nodeName === 'OPTION' && this.getAttrib(elm, 'selected')) {
+ attrs.push({specified: 1, nodeName: 'selected'});
+ }
+
+ // It's crazy that this is faster in IE but it's because it returns all attributes all the time
+ var attrRegExp = /<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi;
+ elm.cloneNode(false).outerHTML.replace(attrRegExp, '').replace(/[\w:\-]+/gi, function(a) {
+ attrs.push({specified: 1, nodeName: a});
+ });
+
+ return attrs;
+ }
+
+ return elm.attributes;
+ },
+
+ /**
+ * Returns true/false if the specified node is to be considered empty or not.
+ *
+ * @example
+ * tinymce.DOM.isEmpty(node, {img: true});
+ * @method isEmpty
+ * @param {Object} elements Optional name/value object with elements that are automatically treated as non-empty elements.
+ * @return {Boolean} true/false if the node is empty or not.
+ */
+ isEmpty: function(node, elements) {
+ var self = this, i, attributes, type, walker, name, brCount = 0;
+
+ node = node.firstChild;
+ if (node) {
+ walker = new TreeWalker(node, node.parentNode);
+ elements = elements || (self.schema ? self.schema.getNonEmptyElements() : null);
+
+ do {
+ type = node.nodeType;
+
+ if (type === 1) {
+ // Ignore bogus elements
+ var bogusVal = node.getAttribute('data-mce-bogus');
+ if (bogusVal) {
+ node = walker.next(bogusVal === 'all');
+ continue;
+ }
+
+ // Keep empty elements like <img />
+ name = node.nodeName.toLowerCase();
+ if (elements && elements[name]) {
+ // Ignore single BR elements in blocks like <p><br /></p> or <p><span><br /></span></p>
+ if (name === 'br') {
+ brCount++;
+ node = walker.next();
+ continue;
+ }
+
+ return false;
+ }
+
+ // Keep elements with data-bookmark attributes or name attribute like <a name="1"></a>
+ attributes = self.getAttribs(node);
+ i = attributes.length;
+ while (i--) {
+ name = attributes[i].nodeName;
+ if (name === "name" || name === 'data-mce-bookmark') {
+ return false;
+ }
+ }
+ }
+
+ // Keep comment nodes
+ if (type == 8) {
+ return false;
+ }
+
+ // Keep non whitespace text nodes
+ if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue))) {
+ return false;
+ }
+
+ node = walker.next();
+ } while (node);
+ }
+
+ return brCount <= 1;
+ },
+
+ /**
+ * Creates a new DOM Range object. This will use the native DOM Range API if it's
+ * available. If it's not, it will fall back to the custom TinyMCE implementation.
+ *
+ * @method createRng
+ * @return {DOMRange} DOM Range object.
+ * @example
+ * var rng = tinymce.DOM.createRng();
+ * alert(rng.startContainer + "," + rng.startOffset);
+ */
+ createRng: function() {
+ var doc = this.doc;
+
+ return doc.createRange ? doc.createRange() : new Range(this);
+ },
+
+ /**
+ * Returns the index of the specified node within its parent.
+ *
+ * @method nodeIndex
+ * @param {Node} node Node to look for.
+ * @param {boolean} normalized Optional true/false state if the index is what it would be after a normalization.
+ * @return {Number} Index of the specified node.
+ */
+ nodeIndex: nodeIndex,
+
+ /**
+ * Splits an element into two new elements and places the specified split
+ * element or elements between the new ones. For example splitting the paragraph at the bold element in
+ * this example <p>abc<b>abc</b>123</p> would produce <p>abc</p><b>abc</b><p>123</p>.
+ *
+ * @method split
+ * @param {Element} parentElm Parent element to split.
+ * @param {Element} splitElm Element to split at.
+ * @param {Element} replacementElm Optional replacement element to replace the split element with.
+ * @return {Element} Returns the split element or the replacement element if that is specified.
+ */
+ split: function(parentElm, splitElm, replacementElm) {
+ var self = this, r = self.createRng(), bef, aft, pa;
+
+ // W3C valid browsers tend to leave empty nodes to the left/right side of the contents - this makes sense
+ // but we don't want that in our code since it serves no purpose for the end user
+ // For example splitting this html at the bold element:
+ // <p>text 1<span><b>CHOP</b></span>text 2</p>
+ // would produce:
+ // <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p>
+ // this function will then trim off empty edges and produce:
+ // <p>text 1</p><b>CHOP</b><p>text 2</p>
+ function trimNode(node) {
+ var i, children = node.childNodes, type = node.nodeType;
+
+ function surroundedBySpans(node) {
+ var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN';
+ var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN';
+ return previousIsSpan && nextIsSpan;
+ }
+
+ if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') {
+ return;
+ }
+
+ for (i = children.length - 1; i >= 0; i--) {
+ trimNode(children[i]);
+ }
+
+ if (type != 9) {
+ // Keep non whitespace text nodes
+ if (type == 3 && node.nodeValue.length > 0) {
+ // If parent element isn't a block or there isn't any useful contents for example "<p> </p>"
+ // Also keep text nodes with only spaces if surrounded by spans.
+ // eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b
+ var trimmedLength = trim(node.nodeValue).length;
+ if (!self.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node)) {
+ return;
+ }
+ } else if (type == 1) {
+ // If the only child is a bookmark then move it up
+ children = node.childNodes;
+
+ // TODO fix this complex if
+ if (children.length == 1 && children[0] && children[0].nodeType == 1 &&
+ children[0].getAttribute('data-mce-type') == 'bookmark') {
+ node.parentNode.insertBefore(children[0], node);
+ }
+
+ // Keep non empty elements or img, hr etc
+ if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) {
+ return;
+ }
+ }
+
+ self.remove(node);
+ }
+
+ return node;
+ }
+
+ if (parentElm && splitElm) {
+ // Get before chunk
+ r.setStart(parentElm.parentNode, self.nodeIndex(parentElm));
+ r.setEnd(splitElm.parentNode, self.nodeIndex(splitElm));
+ bef = r.extractContents();
+
+ // Get after chunk
+ r = self.createRng();
+ r.setStart(splitElm.parentNode, self.nodeIndex(splitElm) + 1);
+ r.setEnd(parentElm.parentNode, self.nodeIndex(parentElm) + 1);
+ aft = r.extractContents();
+
+ // Insert before chunk
+ pa = parentElm.parentNode;
+ pa.insertBefore(trimNode(bef), parentElm);
+
+ // Insert middle chunk
+ if (replacementElm) {
+ pa.insertBefore(replacementElm, parentElm);
+ //pa.replaceChild(replacementElm, splitElm);
+ } else {
+ pa.insertBefore(splitElm, parentElm);
+ }
+
+ // Insert after chunk
+ pa.insertBefore(trimNode(aft), parentElm);
+ self.remove(parentElm);
+
+ return replacementElm || splitElm;
+ }
+ },
+
+ /**
+ * Adds an event handler to the specified object.
+ *
+ * @method bind
+ * @param {Element/Document/Window/Array} target Target element to bind events to.
+ * handler to or an array of elements/ids/documents.
+ * @param {String} name Name of event handler to add, for example: click.
+ * @param {function} func Function to execute when the event occurs.
+ * @param {Object} scope Optional scope to execute the function in.
+ * @return {function} Function callback handler the same as the one passed in.
+ */
+ bind: function(target, name, func, scope) {
+ var self = this;
+
+ if (Tools.isArray(target)) {
+ var i = target.length;
+
+ while (i--) {
+ target[i] = self.bind(target[i], name, func, scope);
+ }
+
+ return target;
+ }
+
+ // Collect all window/document events bound by editor instance
+ if (self.settings.collect && (target === self.doc || target === self.win)) {
+ self.boundEvents.push([target, name, func, scope]);
+ }
+
+ return self.events.bind(target, name, func, scope || self);
+ },
+
+ /**
+ * Removes the specified event handler by name and function from an element or collection of elements.
+ *
+ * @method unbind
+ * @param {Element/Document/Window/Array} target Target element to unbind events on.
+ * @param {String} name Event handler name, for example: "click"
+ * @param {function} func Function to remove.
+ * @return {bool/Array} Bool state of true if the handler was removed, or an array of states if multiple input elements
+ * were passed in.
+ */
+ unbind: function(target, name, func) {
+ var self = this, i;
+
+ if (Tools.isArray(target)) {
+ i = target.length;
+
+ while (i--) {
+ target[i] = self.unbind(target[i], name, func);
+ }
+
+ return target;
+ }
+
+ // Remove any bound events matching the input
+ if (self.boundEvents && (target === self.doc || target === self.win)) {
+ i = self.boundEvents.length;
+
+ while (i--) {
+ var item = self.boundEvents[i];
+
+ if (target == item[0] && (!name || name == item[1]) && (!func || func == item[2])) {
+ this.events.unbind(item[0], item[1], item[2]);
+ }
+ }
+ }
+
+ return this.events.unbind(target, name, func);
+ },
+
+ /**
+ * Fires the specified event name with object on target.
+ *
+ * @method fire
+ * @param {Node/Document/Window} target Target element or object to fire event on.
+ * @param {String} name Name of the event to fire.
+ * @param {Object} evt Event object to send.
+ * @return {Event} Event object.
+ */
+ fire: function(target, name, evt) {
+ return this.events.fire(target, name, evt);
+ },
+
+ // Returns the content editable state of a node
+ getContentEditable: function(node) {
+ var contentEditable;
+
+ // Check type
+ if (!node || node.nodeType != 1) {
+ return null;
+ }
+
+ // Check for fake content editable
+ contentEditable = node.getAttribute("data-mce-contenteditable");
+ if (contentEditable && contentEditable !== "inherit") {
+ return contentEditable;
+ }
+
+ // Check for real content editable
+ return node.contentEditable !== "inherit" ? node.contentEditable : null;
+ },
+
+ getContentEditableParent: function(node) {
+ var root = this.getRoot(), state = null;
+
+ for (; node && node !== root; node = node.parentNode) {
+ state = this.getContentEditable(node);
+
+ if (state !== null) {
+ break;
+ }
+ }
+
+ return state;
+ },
+
+ /**
+ * Destroys all internal references to the DOM to solve IE leak issues.
+ *
+ * @method destroy
+ */
+ destroy: function() {
+ var self = this;
+
+ // Unbind all events bound to window/document by editor instance
+ if (self.boundEvents) {
+ var i = self.boundEvents.length;
+
+ while (i--) {
+ var item = self.boundEvents[i];
+ this.events.unbind(item[0], item[1], item[2]);
+ }
+
+ self.boundEvents = null;
+ }
+
+ // Restore sizzle document to window.document
+ // Since the current document might be removed producing "Permission denied" on IE see #6325
+ if (Sizzle.setDocument) {
+ Sizzle.setDocument();
+ }
+
+ self.win = self.doc = self.root = self.events = self.frag = null;
+ },
+
+ isChildOf: function(node, parent) {
+ while (node) {
+ if (parent === node) {
+ return true;
+ }
+
+ node = node.parentNode;
+ }
+
+ return false;
+ },
+
+ // #ifdef debug
+
+ dumpRng: function(r) {
+ return (
+ 'startContainer: ' + r.startContainer.nodeName +
+ ', startOffset: ' + r.startOffset +
+ ', endContainer: ' + r.endContainer.nodeName +
+ ', endOffset: ' + r.endOffset
+ );
+ },
+
+ // #endif
+
+ _findSib: function(node, selector, name) {
+ var self = this, func = selector;
+
+ if (node) {
+ // If expression make a function of it using is
+ if (typeof func == 'string') {
+ func = function(node) {
+ return self.is(node, selector);
+ };
+ }
+
+ // Loop all siblings
+ for (node = node[name]; node; node = node[name]) {
+ if (func(node)) {
+ return node;
+ }
+ }
+ }
+
+ return null;
+ }
+ };
+
+ /**
+ * Instance of DOMUtils for the current document.
+ *
+ * @static
+ * @property DOM
+ * @type tinymce.dom.DOMUtils
+ * @example
+ * // Example of how to add a class to some element by id
+ * tinymce.DOM.addClass('someid', 'someclass');
+ */
+ DOMUtils.DOM = new DOMUtils(document);
+ DOMUtils.nodeIndex = nodeIndex;
+
+ return DOMUtils;
+});
+
+// Included from: js/tinymce/classes/dom/ScriptLoader.js
+
+/**
+ * ScriptLoader.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*globals console*/
+
+/**
+ * This class handles asynchronous/synchronous loading of JavaScript files it will execute callbacks
+ * when various items gets loaded. This class is useful to load external JavaScript files.
+ *
+ * @class tinymce.dom.ScriptLoader
+ * @example
+ * // Load a script from a specific URL using the global script loader
+ * tinymce.ScriptLoader.load('somescript.js');
+ *
+ * // Load a script using a unique instance of the script loader
+ * var scriptLoader = new tinymce.dom.ScriptLoader();
+ *
+ * scriptLoader.load('somescript.js');
+ *
+ * // Load multiple scripts
+ * var scriptLoader = new tinymce.dom.ScriptLoader();
+ *
+ * scriptLoader.add('somescript1.js');
+ * scriptLoader.add('somescript2.js');
+ * scriptLoader.add('somescript3.js');
+ *
+ * scriptLoader.loadQueue(function() {
+ * alert('All scripts are now loaded.');
+ * });
+ */
+define("tinymce/dom/ScriptLoader", [
+ "tinymce/dom/DOMUtils",
+ "tinymce/util/Tools"
+], function(DOMUtils, Tools) {
+ var DOM = DOMUtils.DOM;
+ var each = Tools.each, grep = Tools.grep;
+
+ function ScriptLoader() {
+ var QUEUED = 0,
+ LOADING = 1,
+ LOADED = 2,
+ states = {},
+ queue = [],
+ scriptLoadedCallbacks = {},
+ queueLoadedCallbacks = [],
+ loading = 0,
+ undef;
+
+ /**
+ * Loads a specific script directly without adding it to the load queue.
+ *
+ * @method load
+ * @param {String} url Absolute URL to script to add.
+ * @param {function} callback Optional callback function to execute ones this script gets loaded.
+ */
+ function loadScript(url, callback) {
+ var dom = DOM, elm, id;
+
+ // Execute callback when script is loaded
+ function done() {
+ dom.remove(id);
+
+ if (elm) {
+ elm.onreadystatechange = elm.onload = elm = null;
+ }
+
+ callback();
+ }
+
+ function error() {
+ /*eslint no-console:0 */
+
+ // Report the error so it's easier for people to spot loading errors
+ if (typeof console !== "undefined" && console.log) {
+ console.log("Failed to load: " + url);
+ }
+
+ // We can't mark it as done if there is a load error since
+ // A) We don't want to produce 404 errors on the server and
+ // B) the onerror event won't fire on all browsers.
+ // done();
+ }
+
+ id = dom.uniqueId();
+
+ // Create new script element
+ elm = document.createElement('script');
+ elm.id = id;
+ elm.type = 'text/javascript';
+ elm.src = Tools._addCacheSuffix(url);
+
+ // Seems that onreadystatechange works better on IE 10 onload seems to fire incorrectly
+ if ("onreadystatechange" in elm) {
+ elm.onreadystatechange = function() {
+ if (/loaded|complete/.test(elm.readyState)) {
+ done();
+ }
+ };
+ } else {
+ elm.onload = done;
+ }
+
+ // Add onerror event will get fired on some browsers but not all of them
+ elm.onerror = error;
+
+ // Add script to document
+ (document.getElementsByTagName('head')[0] || document.body).appendChild(elm);
+ }
+
+ /**
+ * Returns true/false if a script has been loaded or not.
+ *
+ * @method isDone
+ * @param {String} url URL to check for.
+ * @return {Boolean} true/false if the URL is loaded.
+ */
+ this.isDone = function(url) {
+ return states[url] == LOADED;
+ };
+
+ /**
+ * Marks a specific script to be loaded. This can be useful if a script got loaded outside
+ * the script loader or to skip it from loading some script.
+ *
+ * @method markDone
+ * @param {string} url Absolute URL to the script to mark as loaded.
+ */
+ this.markDone = function(url) {
+ states[url] = LOADED;
+ };
+
+ /**
+ * Adds a specific script to the load queue of the script loader.
+ *
+ * @method add
+ * @param {String} url Absolute URL to script to add.
+ * @param {function} callback Optional callback function to execute ones this script gets loaded.
+ * @param {Object} scope Optional scope to execute callback in.
+ */
+ this.add = this.load = function(url, callback, scope) {
+ var state = states[url];
+
+ // Add url to load queue
+ if (state == undef) {
+ queue.push(url);
+ states[url] = QUEUED;
+ }
+
+ if (callback) {
+ // Store away callback for later execution
+ if (!scriptLoadedCallbacks[url]) {
+ scriptLoadedCallbacks[url] = [];
+ }
+
+ scriptLoadedCallbacks[url].push({
+ func: callback,
+ scope: scope || this
+ });
+ }
+ };
+
+ this.remove = function(url) {
+ delete states[url];
+ delete scriptLoadedCallbacks[url];
+ };
+
+ /**
+ * Starts the loading of the queue.
+ *
+ * @method loadQueue
+ * @param {function} callback Optional callback to execute when all queued items are loaded.
+ * @param {Object} scope Optional scope to execute the callback in.
+ */
+ this.loadQueue = function(callback, scope) {
+ this.loadScripts(queue, callback, scope);
+ };
+
+ /**
+ * Loads the specified queue of files and executes the callback ones they are loaded.
+ * This method is generally not used outside this class but it might be useful in some scenarios.
+ *
+ * @method loadScripts
+ * @param {Array} scripts Array of queue items to load.
+ * @param {function} callback Optional callback to execute ones all items are loaded.
+ * @param {Object} scope Optional scope to execute callback in.
+ */
+ this.loadScripts = function(scripts, callback, scope) {
+ var loadScripts;
+
+ function execScriptLoadedCallbacks(url) {
+ // Execute URL callback functions
+ each(scriptLoadedCallbacks[url], function(callback) {
+ callback.func.call(callback.scope);
+ });
+
+ scriptLoadedCallbacks[url] = undef;
+ }
+
+ queueLoadedCallbacks.push({
+ func: callback,
+ scope: scope || this
+ });
+
+ loadScripts = function() {
+ var loadingScripts = grep(scripts);
+
+ // Current scripts has been handled
+ scripts.length = 0;
+
+ // Load scripts that needs to be loaded
+ each(loadingScripts, function(url) {
+ // Script is already loaded then execute script callbacks directly
+ if (states[url] == LOADED) {
+ execScriptLoadedCallbacks(url);
+ return;
+ }
+
+ // Is script not loading then start loading it
+ if (states[url] != LOADING) {
+ states[url] = LOADING;
+ loading++;
+
+ loadScript(url, function() {
+ states[url] = LOADED;
+ loading--;
+
+ execScriptLoadedCallbacks(url);
+
+ // Load more scripts if they where added by the recently loaded script
+ loadScripts();
+ });
+ }
+ });
+
+ // No scripts are currently loading then execute all pending queue loaded callbacks
+ if (!loading) {
+ each(queueLoadedCallbacks, function(callback) {
+ callback.func.call(callback.scope);
+ });
+
+ queueLoadedCallbacks.length = 0;
+ }
+ };
+
+ loadScripts();
+ };
+ }
+
+ ScriptLoader.ScriptLoader = new ScriptLoader();
+
+ return ScriptLoader;
+});
+
+// Included from: js/tinymce/classes/AddOnManager.js
+
+/**
+ * AddOnManager.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class handles the loading of themes/plugins or other add-ons and their language packs.
+ *
+ * @class tinymce.AddOnManager
+ */
+define("tinymce/AddOnManager", [
+ "tinymce/dom/ScriptLoader",
+ "tinymce/util/Tools"
+], function(ScriptLoader, Tools) {
+ var each = Tools.each;
+
+ function AddOnManager() {
+ var self = this;
+
+ self.items = [];
+ self.urls = {};
+ self.lookup = {};
+ }
+
+ AddOnManager.prototype = {
+ /**
+ * Returns the specified add on by the short name.
+ *
+ * @method get
+ * @param {String} name Add-on to look for.
+ * @return {tinymce.Theme/tinymce.Plugin} Theme or plugin add-on instance or undefined.
+ */
+ get: function(name) {
+ if (this.lookup[name]) {
+ return this.lookup[name].instance;
+ }
+
+ return undefined;
+ },
+
+ dependencies: function(name) {
+ var result;
+
+ if (this.lookup[name]) {
+ result = this.lookup[name].dependencies;
+ }
+
+ return result || [];
+ },
+
+ /**
+ * Loads a language pack for the specified add-on.
+ *
+ * @method requireLangPack
+ * @param {String} name Short name of the add-on.
+ * @param {String} languages Optional comma or space separated list of languages to check if it matches the name.
+ */
+ requireLangPack: function(name, languages) {
+ var language = AddOnManager.language;
+
+ if (language && AddOnManager.languageLoad !== false) {
+ if (languages) {
+ languages = ',' + languages + ',';
+
+ // Load short form sv.js or long form sv_SE.js
+ if (languages.indexOf(',' + language.substr(0, 2) + ',') != -1) {
+ language = language.substr(0, 2);
+ } else if (languages.indexOf(',' + language + ',') == -1) {
+ return;
+ }
+ }
+
+ ScriptLoader.ScriptLoader.add(this.urls[name] + '/langs/' + language + '.js');
+ }
+ },
+
+ /**
+ * Adds a instance of the add-on by it's short name.
+ *
+ * @method add
+ * @param {String} id Short name/id for the add-on.
+ * @param {tinymce.Theme/tinymce.Plugin} addOn Theme or plugin to add.
+ * @return {tinymce.Theme/tinymce.Plugin} The same theme or plugin instance that got passed in.
+ * @example
+ * // Create a simple plugin
+ * tinymce.create('tinymce.plugins.TestPlugin', {
+ * TestPlugin: function(ed, url) {
+ * ed.on('click', function(e) {
+ * ed.windowManager.alert('Hello World!');
+ * });
+ * }
+ * });
+ *
+ * // Register plugin using the add method
+ * tinymce.PluginManager.add('test', tinymce.plugins.TestPlugin);
+ *
+ * // Initialize TinyMCE
+ * tinymce.init({
+ * ...
+ * plugins: '-test' // Init the plugin but don't try to load it
+ * });
+ */
+ add: function(id, addOn, dependencies) {
+ this.items.push(addOn);
+ this.lookup[id] = {instance: addOn, dependencies: dependencies};
+
+ return addOn;
+ },
+
+ remove: function(name) {
+ delete this.urls[name];
+ delete this.lookup[name];
+ },
+
+ createUrl: function(baseUrl, dep) {
+ if (typeof dep === "object") {
+ return dep;
+ }
+
+ return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix};
+ },
+
+ /**
+ * Add a set of components that will make up the add-on. Using the url of the add-on name as the base url.
+ * This should be used in development mode. A new compressor/javascript munger process will ensure that the
+ * components are put together into the plugin.js file and compressed correctly.
+ *
+ * @method addComponents
+ * @param {String} pluginName name of the plugin to load scripts from (will be used to get the base url for the plugins).
+ * @param {Array} scripts Array containing the names of the scripts to load.
+ */
+ addComponents: function(pluginName, scripts) {
+ var pluginUrl = this.urls[pluginName];
+
+ each(scripts, function(script) {
+ ScriptLoader.ScriptLoader.add(pluginUrl + "/" + script);
+ });
+ },
+
+ /**
+ * Loads an add-on from a specific url.
+ *
+ * @method load
+ * @param {String} name Short name of the add-on that gets loaded.
+ * @param {String} addOnUrl URL to the add-on that will get loaded.
+ * @param {function} callback Optional callback to execute ones the add-on is loaded.
+ * @param {Object} scope Optional scope to execute the callback in.
+ * @example
+ * // Loads a plugin from an external URL
+ * tinymce.PluginManager.load('myplugin', '/some/dir/someplugin/plugin.js');
+ *
+ * // Initialize TinyMCE
+ * tinymce.init({
+ * ...
+ * plugins: '-myplugin' // Don't try to load it again
+ * });
+ */
+ load: function(name, addOnUrl, callback, scope) {
+ var self = this, url = addOnUrl;
+
+ function loadDependencies() {
+ var dependencies = self.dependencies(name);
+
+ each(dependencies, function(dep) {
+ var newUrl = self.createUrl(addOnUrl, dep);
+
+ self.load(newUrl.resource, newUrl, undefined, undefined);
+ });
+
+ if (callback) {
+ if (scope) {
+ callback.call(scope);
+ } else {
+ callback.call(ScriptLoader);
+ }
+ }
+ }
+
+ if (self.urls[name]) {
+ return;
+ }
+
+ if (typeof addOnUrl === "object") {
+ url = addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix;
+ }
+
+ if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) {
+ url = AddOnManager.baseURL + '/' + url;
+ }
+
+ self.urls[name] = url.substring(0, url.lastIndexOf('/'));
+
+ if (self.lookup[name]) {
+ loadDependencies();
+ } else {
+ ScriptLoader.ScriptLoader.add(url, loadDependencies, scope);
+ }
+ }
+ };
+
+ AddOnManager.PluginManager = new AddOnManager();
+ AddOnManager.ThemeManager = new AddOnManager();
+
+ return AddOnManager;
+});
+
+/**
+ * TinyMCE theme class.
+ *
+ * @class tinymce.Theme
+ */
+
+/**
+ * This method is responsible for rendering/generating the overall user interface with toolbars, buttons, iframe containers etc.
+ *
+ * @method renderUI
+ * @param {Object} obj Object parameter containing the targetNode DOM node that will be replaced visually with an editor instance.
+ * @return {Object} an object with items like iframeContainer, editorContainer, sizeContainer, deltaWidth, deltaHeight.
+ */
+
+/**
+ * Plugin base class, this is a pseudo class that describes how a plugin is to be created for TinyMCE. The methods below are all optional.
+ *
+ * @class tinymce.Plugin
+ * @example
+ * tinymce.PluginManager.add('example', function(editor, url) {
+ * // Add a button that opens a window
+ * editor.addButton('example', {
+ * text: 'My button',
+ * icon: false,
+ * onclick: function() {
+ * // Open window
+ * editor.windowManager.open({
+ * title: 'Example plugin',
+ * body: [
+ * {type: 'textbox', name: 'title', label: 'Title'}
+ * ],
+ * onsubmit: function(e) {
+ * // Insert content when the window form is submitted
+ * editor.insertContent('Title: ' + e.data.title);
+ * }
+ * });
+ * }
+ * });
+ *
+ * // Adds a menu item to the tools menu
+ * editor.addMenuItem('example', {
+ * text: 'Example plugin',
+ * context: 'tools',
+ * onclick: function() {
+ * // Open window with a specific url
+ * editor.windowManager.open({
+ * title: 'TinyMCE site',
+ * url: 'http://www.tinymce.com',
+ * width: 800,
+ * height: 600,
+ * buttons: [{
+ * text: 'Close',
+ * onclick: 'close'
+ * }]
+ * });
+ * }
+ * });
+ * });
+ */
+
+// Included from: js/tinymce/classes/dom/NodeType.js
+
+/**
+ * NodeType.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Contains various node validation functions.
+ *
+ * @private
+ * @class tinymce.dom.NodeType
+ */
+define("tinymce/dom/NodeType", [], function() {
+ function isNodeType(type) {
+ return function(node) {
+ return !!node && node.nodeType == type;
+ };
+ }
+
+ var isElement = isNodeType(1);
+
+ function matchNodeNames(names) {
+ names = names.toLowerCase().split(' ');
+
+ return function(node) {
+ var i, name;
+
+ if (node && node.nodeType) {
+ name = node.nodeName.toLowerCase();
+
+ for (i = 0; i < names.length; i++) {
+ if (name === names[i]) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ };
+ }
+
+ function matchStyleValues(name, values) {
+ values = values.toLowerCase().split(' ');
+
+ return function(node) {
+ var i, cssValue;
+
+ if (isElement(node)) {
+ for (i = 0; i < values.length; i++) {
+ cssValue = getComputedStyle(node, null).getPropertyValue(name);
+ if (cssValue === values[i]) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ };
+ }
+
+ function hasPropValue(propName, propValue) {
+ return function(node) {
+ return isElement(node) && node[propName] === propValue;
+ };
+ }
+
+ function hasAttributeValue(attrName, attrValue) {
+ return function(node) {
+ return isElement(node) && node.getAttribute(attrName) === attrValue;
+ };
+ }
+
+ function isBogus(node) {
+ return isElement(node) && node.hasAttribute('data-mce-bogus');
+ }
+
+ function hasContentEditableState(value) {
+ return function(node) {
+ if (isElement(node)) {
+ if (node.contentEditable === value) {
+ return true;
+ }
+
+ if (node.getAttribute('data-mce-contenteditable') === value) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+ }
+
+ return {
+ isText: isNodeType(3),
+ isElement: isElement,
+ isComment: isNodeType(8),
+ isBr: matchNodeNames('br'),
+ isContentEditableTrue: hasContentEditableState('true'),
+ isContentEditableFalse: hasContentEditableState('false'),
+ matchNodeNames: matchNodeNames,
+ hasPropValue: hasPropValue,
+ hasAttributeValue: hasAttributeValue,
+ matchStyleValues: matchStyleValues,
+ isBogus: isBogus
+ };
+});
+
+// Included from: js/tinymce/classes/text/Zwsp.js
+
+/**
+ * Zwsp.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Utility functions for working with zero width space
+ * characters used as character containers etc.
+ *
+ * @private
+ * @class tinymce.text.Zwsp
+ * @example
+ * var isZwsp = Zwsp.isZwsp('\uFEFF');
+ * var abc = Zwsp.trim('a\uFEFFc');
+ */
+define("tinymce/text/Zwsp", [], function() {
+ var ZWSP = '\uFEFF';
+
+ function isZwsp(chr) {
+ return chr == ZWSP;
+ }
+
+ function trim(str) {
+ return str.replace(new RegExp(ZWSP, 'g'), '');
+ }
+
+ return {
+ isZwsp: isZwsp,
+ ZWSP: ZWSP,
+ trim: trim
+ };
+});
+
+// Included from: js/tinymce/classes/caret/CaretContainer.js
+
+/**
+ * CaretContainer.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module handles caret containers. A caret container is a node that
+ * holds the caret for positional purposes.
+ *
+ * @private
+ * @class tinymce.caret.CaretContainer
+ */
+define("tinymce/caret/CaretContainer", [
+ "tinymce/dom/NodeType",
+ "tinymce/text/Zwsp"
+], function(NodeType, Zwsp) {
+ var isElement = NodeType.isElement,
+ isText = NodeType.isText;
+
+ function isCaretContainerBlock(node) {
+ if (isText(node)) {
+ node = node.parentNode;
+ }
+
+ return isElement(node) && node.hasAttribute('data-mce-caret');
+ }
+
+ function isCaretContainerInline(node) {
+ return isText(node) && Zwsp.isZwsp(node.data);
+ }
+
+ function isCaretContainer(node) {
+ return isCaretContainerBlock(node) || isCaretContainerInline(node);
+ }
+
+ function removeNode(node) {
+ var parentNode = node.parentNode;
+ if (parentNode) {
+ parentNode.removeChild(node);
+ }
+ }
+
+ function getNodeValue(node) {
+ try {
+ return node.nodeValue;
+ } catch (ex) {
+ // IE sometimes produces "Invalid argument" on nodes
+ return "";
+ }
+ }
+
+ function setNodeValue(node, text) {
+ if (text.length === 0) {
+ removeNode(node);
+ } else {
+ node.nodeValue = text;
+ }
+ }
+
+ function insertInline(node, before) {
+ var doc, sibling, textNode, parentNode;
+
+ doc = node.ownerDocument;
+ textNode = doc.createTextNode(Zwsp.ZWSP);
+ parentNode = node.parentNode;
+
+ if (!before) {
+ sibling = node.nextSibling;
+ if (isText(sibling)) {
+ if (isCaretContainer(sibling)) {
+ return sibling;
+ }
+
+ if (startsWithCaretContainer(sibling)) {
+ sibling.splitText(1);
+ return sibling;
+ }
+ }
+
+ if (node.nextSibling) {
+ parentNode.insertBefore(textNode, node.nextSibling);
+ } else {
+ parentNode.appendChild(textNode);
+ }
+ } else {
+ sibling = node.previousSibling;
+ if (isText(sibling)) {
+ if (isCaretContainer(sibling)) {
+ return sibling;
+ }
+
+ if (endsWithCaretContainer(sibling)) {
+ return sibling.splitText(sibling.data.length - 1);
+ }
+ }
+
+ parentNode.insertBefore(textNode, node);
+ }
+
+ return textNode;
+ }
+
+ function createBogusBr() {
+ var br = document.createElement('br');
+ br.setAttribute('data-mce-bogus', '1');
+ return br;
+ }
+
+ function insertBlock(blockName, node, before) {
+ var doc, blockNode, parentNode;
+
+ doc = node.ownerDocument;
+ blockNode = doc.createElement(blockName);
+ blockNode.setAttribute('data-mce-caret', before ? 'before' : 'after');
+ blockNode.setAttribute('data-mce-bogus', 'all');
+ blockNode.appendChild(createBogusBr());
+ parentNode = node.parentNode;
+
+ if (!before) {
+ if (node.nextSibling) {
+ parentNode.insertBefore(blockNode, node.nextSibling);
+ } else {
+ parentNode.appendChild(blockNode);
+ }
+ } else {
+ parentNode.insertBefore(blockNode, node);
+ }
+
+ return blockNode;
+ }
+
+ function hasContent(node) {
+ return node.firstChild !== node.lastChild || !NodeType.isBr(node.firstChild);
+ }
+
+ function remove(caretContainerNode) {
+ if (isElement(caretContainerNode) && isCaretContainer(caretContainerNode)) {
+ if (hasContent(caretContainerNode)) {
+ caretContainerNode.removeAttribute('data-mce-caret');
+ } else {
+ removeNode(caretContainerNode);
+ }
+ }
+
+ if (isText(caretContainerNode)) {
+ var text = Zwsp.trim(getNodeValue(caretContainerNode));
+ setNodeValue(caretContainerNode, text);
+ }
+ }
+
+ function startsWithCaretContainer(node) {
+ return isText(node) && node.data[0] == Zwsp.ZWSP;
+ }
+
+ function endsWithCaretContainer(node) {
+ return isText(node) && node.data[node.data.length - 1] == Zwsp.ZWSP;
+ }
+
+ function trimBogusBr(elm) {
+ var brs = elm.getElementsByTagName('br');
+ var lastBr = brs[brs.length - 1];
+ if (NodeType.isBogus(lastBr)) {
+ lastBr.parentNode.removeChild(lastBr);
+ }
+ }
+
+ function showCaretContainerBlock(caretContainer) {
+ if (caretContainer && caretContainer.hasAttribute('data-mce-caret')) {
+ trimBogusBr(caretContainer);
+ caretContainer.removeAttribute('data-mce-caret');
+ caretContainer.removeAttribute('data-mce-bogus');
+ caretContainer.removeAttribute('style');
+ caretContainer.removeAttribute('_moz_abspos');
+ return caretContainer;
+ }
+
+ return null;
+ }
+
+ return {
+ isCaretContainer: isCaretContainer,
+ isCaretContainerBlock: isCaretContainerBlock,
+ isCaretContainerInline: isCaretContainerInline,
+ showCaretContainerBlock: showCaretContainerBlock,
+ insertInline: insertInline,
+ insertBlock: insertBlock,
+ hasContent: hasContent,
+ remove: remove,
+ startsWithCaretContainer: startsWithCaretContainer,
+ endsWithCaretContainer: endsWithCaretContainer
+ };
+});
+
+// Included from: js/tinymce/classes/dom/RangeUtils.js
+
+/**
+ * RangeUtils.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class contains a few utility methods for ranges.
+ *
+ * @class tinymce.dom.RangeUtils
+ */
+define("tinymce/dom/RangeUtils", [
+ "tinymce/util/Tools",
+ "tinymce/dom/TreeWalker",
+ "tinymce/dom/NodeType",
+ "tinymce/dom/Range",
+ "tinymce/caret/CaretContainer"
+], function(Tools, TreeWalker, NodeType, Range, CaretContainer) {
+ var each = Tools.each,
+ isContentEditableTrue = NodeType.isContentEditableTrue,
+ isContentEditableFalse = NodeType.isContentEditableFalse,
+ isCaretContainer = CaretContainer.isCaretContainer;
+
+ function hasCeProperty(node) {
+ return isContentEditableTrue(node) || isContentEditableFalse(node);
+ }
+
+ function getEndChild(container, index) {
+ var childNodes = container.childNodes;
+
+ index--;
+
+ if (index > childNodes.length - 1) {
+ index = childNodes.length - 1;
+ } else if (index < 0) {
+ index = 0;
+ }
+
+ return childNodes[index] || container;
+ }
+
+ function findParent(node, rootNode, predicate) {
+ while (node && node !== rootNode) {
+ if (predicate(node)) {
+ return node;
+ }
+
+ node = node.parentNode;
+ }
+
+ return null;
+ }
+
+ function hasParent(node, rootNode, predicate) {
+ return findParent(node, rootNode, predicate) !== null;
+ }
+
+ function isFormatterCaret(node) {
+ return node.id === '_mce_caret';
+ }
+
+ function isCeFalseCaretContainer(node, rootNode) {
+ return isCaretContainer(node) && hasParent(node, rootNode, isFormatterCaret) === false;
+ }
+
+ function RangeUtils(dom) {
+ /**
+ * Walks the specified range like object and executes the callback for each sibling collection it finds.
+ *
+ * @private
+ * @method walk
+ * @param {Object} rng Range like object.
+ * @param {function} callback Callback function to execute for each sibling collection.
+ */
+ this.walk = function(rng, callback) {
+ var startContainer = rng.startContainer,
+ startOffset = rng.startOffset,
+ endContainer = rng.endContainer,
+ endOffset = rng.endOffset,
+ ancestor, startPoint,
+ endPoint, node, parent, siblings, nodes;
+
+ // Handle table cell selection the table plugin enables
+ // you to fake select table cells and perform formatting actions on them
+ nodes = dom.select('td[data-mce-selected],th[data-mce-selected]');
+ if (nodes.length > 0) {
+ each(nodes, function(node) {
+ callback([node]);
+ });
+
+ return;
+ }
+
+ /**
+ * Excludes start/end text node if they are out side the range
+ *
+ * @private
+ * @param {Array} nodes Nodes to exclude items from.
+ * @return {Array} Array with nodes excluding the start/end container if needed.
+ */
+ function exclude(nodes) {
+ var node;
+
+ // First node is excluded
+ node = nodes[0];
+ if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) {
+ nodes.splice(0, 1);
+ }
+
+ // Last node is excluded
+ node = nodes[nodes.length - 1];
+ if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) {
+ nodes.splice(nodes.length - 1, 1);
+ }
+
+ return nodes;
+ }
+
+ /**
+ * Collects siblings
+ *
+ * @private
+ * @param {Node} node Node to collect siblings from.
+ * @param {String} name Name of the sibling to check for.
+ * @param {Node} end_node
+ * @return {Array} Array of collected siblings.
+ */
+ function collectSiblings(node, name, end_node) {
+ var siblings = [];
+
+ for (; node && node != end_node; node = node[name]) {
+ siblings.push(node);
+ }
+
+ return siblings;
+ }
+
+ /**
+ * Find an end point this is the node just before the common ancestor root.
+ *
+ * @private
+ * @param {Node} node Node to start at.
+ * @param {Node} root Root/ancestor element to stop just before.
+ * @return {Node} Node just before the root element.
+ */
+ function findEndPoint(node, root) {
+ do {
+ if (node.parentNode == root) {
+ return node;
+ }
+
+ node = node.parentNode;
+ } while (node);
+ }
+
+ function walkBoundary(start_node, end_node, next) {
+ var siblingName = next ? 'nextSibling' : 'previousSibling';
+
+ for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) {
+ parent = node.parentNode;
+ siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName);
+
+ if (siblings.length) {
+ if (!next) {
+ siblings.reverse();
+ }
+
+ callback(exclude(siblings));
+ }
+ }
+ }
+
+ // If index based start position then resolve it
+ if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
+ startContainer = startContainer.childNodes[startOffset];
+ }
+
+ // If index based end position then resolve it
+ if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
+ endContainer = getEndChild(endContainer, endOffset);
+ }
+
+ // Same container
+ if (startContainer == endContainer) {
+ return callback(exclude([startContainer]));
+ }
+
+ // Find common ancestor and end points
+ ancestor = dom.findCommonAncestor(startContainer, endContainer);
+
+ // Process left side
+ for (node = startContainer; node; node = node.parentNode) {
+ if (node === endContainer) {
+ return walkBoundary(startContainer, ancestor, true);
+ }
+
+ if (node === ancestor) {
+ break;
+ }
+ }
+
+ // Process right side
+ for (node = endContainer; node; node = node.parentNode) {
+ if (node === startContainer) {
+ return walkBoundary(endContainer, ancestor);
+ }
+
+ if (node === ancestor) {
+ break;
+ }
+ }
+
+ // Find start/end point
+ startPoint = findEndPoint(startContainer, ancestor) || startContainer;
+ endPoint = findEndPoint(endContainer, ancestor) || endContainer;
+
+ // Walk left leaf
+ walkBoundary(startContainer, startPoint, true);
+
+ // Walk the middle from start to end point
+ siblings = collectSiblings(
+ startPoint == startContainer ? startPoint : startPoint.nextSibling,
+ 'nextSibling',
+ endPoint == endContainer ? endPoint.nextSibling : endPoint
+ );
+
+ if (siblings.length) {
+ callback(exclude(siblings));
+ }
+
+ // Walk right leaf
+ walkBoundary(endContainer, endPoint);
+ };
+
+ /**
+ * Splits the specified range at it's start/end points.
+ *
+ * @private
+ * @param {Range/RangeObject} rng Range to split.
+ * @return {Object} Range position object.
+ */
+ this.split = function(rng) {
+ var startContainer = rng.startContainer,
+ startOffset = rng.startOffset,
+ endContainer = rng.endContainer,
+ endOffset = rng.endOffset;
+
+ function splitText(node, offset) {
+ return node.splitText(offset);
+ }
+
+ // Handle single text node
+ if (startContainer == endContainer && startContainer.nodeType == 3) {
+ if (startOffset > 0 && startOffset < startContainer.nodeValue.length) {
+ endContainer = splitText(startContainer, startOffset);
+ startContainer = endContainer.previousSibling;
+
+ if (endOffset > startOffset) {
+ endOffset = endOffset - startOffset;
+ startContainer = endContainer = splitText(endContainer, endOffset).previousSibling;
+ endOffset = endContainer.nodeValue.length;
+ startOffset = 0;
+ } else {
+ endOffset = 0;
+ }
+ }
+ } else {
+ // Split startContainer text node if needed
+ if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) {
+ startContainer = splitText(startContainer, startOffset);
+ startOffset = 0;
+ }
+
+ // Split endContainer text node if needed
+ if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) {
+ endContainer = splitText(endContainer, endOffset).previousSibling;
+ endOffset = endContainer.nodeValue.length;
+ }
+ }
+
+ return {
+ startContainer: startContainer,
+ startOffset: startOffset,
+ endContainer: endContainer,
+ endOffset: endOffset
+ };
+ };
+
+ /**
+ * Normalizes the specified range by finding the closest best suitable caret location.
+ *
+ * @private
+ * @param {Range} rng Range to normalize.
+ * @return {Boolean} True/false if the specified range was normalized or not.
+ */
+ this.normalize = function(rng) {
+ var normalized, collapsed;
+
+ function normalizeEndPoint(start) {
+ var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap;
+ var directionLeft, isAfterNode;
+
+ function isTableCell(node) {
+ return node && /^(TD|TH|CAPTION)$/.test(node.nodeName);
+ }
+
+ function hasBrBeforeAfter(node, left) {
+ var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body);
+
+ while ((node = walker[left ? 'prev' : 'next']())) {
+ if (node.nodeName === "BR") {
+ return true;
+ }
+ }
+ }
+
+ function hasContentEditableFalseParent(node) {
+ while (node && node != body) {
+ if (isContentEditableFalse(node)) {
+ return true;
+ }
+
+ node = node.parentNode;
+ }
+
+ return false;
+ }
+
+ function isPrevNode(node, name) {
+ return node.previousSibling && node.previousSibling.nodeName == name;
+ }
+
+ // Walks the dom left/right to find a suitable text node to move the endpoint into
+ // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG
+ function findTextNodeRelative(left, startNode) {
+ var walker, lastInlineElement, parentBlockContainer;
+
+ startNode = startNode || container;
+ parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body;
+
+ // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680
+ // This: <p><br>|</p> becomes <p>|<br></p>
+ if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) {
+ container = startNode.parentNode;
+ offset = dom.nodeIndex(startNode);
+ normalized = true;
+ return;
+ }
+
+ // Walk left until we hit a text node we can move to or a block/br/img
+ walker = new TreeWalker(startNode, parentBlockContainer);
+ while ((node = walker[left ? 'prev' : 'next']())) {
+ // Break if we hit a non content editable node
+ if (dom.getContentEditableParent(node) === "false" || isCeFalseCaretContainer(node, dom.getRoot())) {
+ return;
+ }
+
+ // Found text node that has a length
+ if (node.nodeType === 3 && node.nodeValue.length > 0) {
+ container = node;
+ offset = left ? node.nodeValue.length : 0;
+ normalized = true;
+ return;
+ }
+
+ // Break if we find a block or a BR/IMG/INPUT etc
+ if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
+ return;
+ }
+
+ lastInlineElement = node;
+ }
+
+ // Only fetch the last inline element when in caret mode for now
+ if (collapsed && lastInlineElement) {
+ container = lastInlineElement;
+ normalized = true;
+ offset = 0;
+ }
+ }
+
+ container = rng[(start ? 'start' : 'end') + 'Container'];
+ offset = rng[(start ? 'start' : 'end') + 'Offset'];
+ isAfterNode = container.nodeType == 1 && offset === container.childNodes.length;
+ nonEmptyElementsMap = dom.schema.getNonEmptyElements();
+ directionLeft = start;
+
+ if (isCaretContainer(container)) {
+ return;
+ }
+
+ if (container.nodeType == 1 && offset > container.childNodes.length - 1) {
+ directionLeft = false;
+ }
+
+ // If the container is a document move it to the body element
+ if (container.nodeType === 9) {
+ container = dom.getRoot();
+ offset = 0;
+ }
+
+ // If the container is body try move it into the closest text node or position
+ if (container === body) {
+ // If start is before/after a image, table etc
+ if (directionLeft) {
+ node = container.childNodes[offset > 0 ? offset - 1 : 0];
+ if (node) {
+ if (isCaretContainer(node)) {
+ return;
+ }
+
+ if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") {
+ return;
+ }
+ }
+ }
+
+ // Resolve the index
+ if (container.hasChildNodes()) {
+ offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1);
+ container = container.childNodes[offset];
+ offset = 0;
+
+ // Don't normalize non collapsed selections like <p>[a</p><table></table>]
+ if (!collapsed && container === body.lastChild && container.nodeName === 'TABLE') {
+ return;
+ }
+
+ if (hasContentEditableFalseParent(container) || isCaretContainer(container)) {
+ return;
+ }
+
+ // Don't walk into elements that doesn't have any child nodes like a IMG
+ if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) {
+ // Walk the DOM to find a text node to place the caret at or a BR
+ node = container;
+ walker = new TreeWalker(container, body);
+
+ do {
+ if (isContentEditableFalse(node) || isCaretContainer(node)) {
+ normalized = false;
+ break;
+ }
+
+ // Found a text node use that position
+ if (node.nodeType === 3 && node.nodeValue.length > 0) {
+ offset = directionLeft ? 0 : node.nodeValue.length;
+ container = node;
+ normalized = true;
+ break;
+ }
+
+ // Found a BR/IMG element that we can place the caret before
+ if (nonEmptyElementsMap[node.nodeName.toLowerCase()] && !isTableCell(node)) {
+ offset = dom.nodeIndex(node);
+ container = node.parentNode;
+
+ // Put caret after image when moving the end point
+ if (node.nodeName == "IMG" && !directionLeft) {
+ offset++;
+ }
+
+ normalized = true;
+ break;
+ }
+ } while ((node = (directionLeft ? walker.next() : walker.prev())));
+ }
+ }
+ }
+
+ // Lean the caret to the left if possible
+ if (collapsed) {
+ // So this: <b>x</b><i>|x</i>
+ // Becomes: <b>x|</b><i>x</i>
+ // Seems that only gecko has issues with this
+ if (container.nodeType === 3 && offset === 0) {
+ findTextNodeRelative(true);
+ }
+
+ // Lean left into empty inline elements when the caret is before a BR
+ // So this: <i><b></b><i>|<br></i>
+ // Becomes: <i><b>|</b><i><br></i>
+ // Seems that only gecko has issues with this.
+ // Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p>
+ if (container.nodeType === 1) {
+ node = container.childNodes[offset];
+
+ // Offset is after the containers last child
+ // then use the previous child for normalization
+ if (!node) {
+ node = container.childNodes[offset - 1];
+ }
+
+ if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') &&
+ !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) {
+ findTextNodeRelative(true, node);
+ }
+ }
+ }
+
+ // Lean the start of the selection right if possible
+ // So this: x[<b>x]</b>
+ // Becomes: x<b>[x]</b>
+ if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) {
+ findTextNodeRelative(false);
+ }
+
+ // Set endpoint if it was normalized
+ if (normalized) {
+ rng['set' + (start ? 'Start' : 'End')](container, offset);
+ }
+ }
+
+ collapsed = rng.collapsed;
+
+ normalizeEndPoint(true);
+
+ if (!collapsed) {
+ normalizeEndPoint();
+ }
+
+ // If it was collapsed then make sure it still is
+ if (normalized && collapsed) {
+ rng.collapse(true);
+ }
+
+ return normalized;
+ };
+ }
+
+ /**
+ * Compares two ranges and checks if they are equal.
+ *
+ * @static
+ * @method compareRanges
+ * @param {DOMRange} rng1 First range to compare.
+ * @param {DOMRange} rng2 First range to compare.
+ * @return {Boolean} true/false if the ranges are equal.
+ */
+ RangeUtils.compareRanges = function(rng1, rng2) {
+ if (rng1 && rng2) {
+ // Compare native IE ranges
+ if (rng1.item || rng1.duplicate) {
+ // Both are control ranges and the selected element matches
+ if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) {
+ return true;
+ }
+
+ // Both are text ranges and the range matches
+ if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) {
+ return true;
+ }
+ } else {
+ // Compare w3c ranges
+ return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Finds the closest selection rect tries to get the range from that.
+ */
+ function findClosestIeRange(clientX, clientY, doc) {
+ var element, rng, rects;
+
+ element = doc.elementFromPoint(clientX, clientY);
+ rng = doc.body.createTextRange();
+
+ if (!element || element.tagName == 'HTML') {
+ element = doc.body;
+ }
+
+ rng.moveToElementText(element);
+ rects = Tools.toArray(rng.getClientRects());
+
+ rects = rects.sort(function(a, b) {
+ a = Math.abs(Math.max(a.top - clientY, a.bottom - clientY));
+ b = Math.abs(Math.max(b.top - clientY, b.bottom - clientY));
+
+ return a - b;
+ });
+
+ if (rects.length > 0) {
+ clientY = (rects[0].bottom + rects[0].top) / 2;
+
+ try {
+ rng.moveToPoint(clientX, clientY);
+ rng.collapse(true);
+
+ return rng;
+ } catch (ex) {
+ // At least we tried
+ }
+ }
+
+ return null;
+ }
+
+ function moveOutOfContentEditableFalse(rng, rootNode) {
+ var parentElement = rng && rng.parentElement ? rng.parentElement() : null;
+ return isContentEditableFalse(findParent(parentElement, rootNode, hasCeProperty)) ? null : rng;
+ }
+
+ /**
+ * Gets the caret range for the given x/y location.
+ *
+ * @static
+ * @method getCaretRangeFromPoint
+ * @param {Number} clientX X coordinate for range
+ * @param {Number} clientY Y coordinate for range
+ * @param {Document} doc Document that x/y are relative to
+ * @returns {Range} caret range
+ */
+ RangeUtils.getCaretRangeFromPoint = function(clientX, clientY, doc) {
+ var rng, point;
+
+ if (doc.caretPositionFromPoint) {
+ point = doc.caretPositionFromPoint(clientX, clientY);
+ rng = doc.createRange();
+ rng.setStart(point.offsetNode, point.offset);
+ rng.collapse(true);
+ } else if (doc.caretRangeFromPoint) {
+ rng = doc.caretRangeFromPoint(clientX, clientY);
+ } else if (doc.body.createTextRange) {
+ rng = doc.body.createTextRange();
+
+ try {
+ rng.moveToPoint(clientX, clientY);
+ rng.collapse(true);
+ } catch (ex) {
+ rng = findClosestIeRange(clientX, clientY, doc);
+ }
+
+ return moveOutOfContentEditableFalse(rng, doc.body);
+ }
+
+ return rng;
+ };
+
+ RangeUtils.getSelectedNode = function(range) {
+ var startContainer = range.startContainer,
+ startOffset = range.startOffset;
+
+ if (startContainer.hasChildNodes() && range.endOffset == startOffset + 1) {
+ return startContainer.childNodes[startOffset];
+ }
+
+ return null;
+ };
+
+ RangeUtils.getNode = function(container, offset) {
+ if (container.nodeType == 1 && container.hasChildNodes()) {
+ if (offset >= container.childNodes.length) {
+ offset = container.childNodes.length - 1;
+ }
+
+ container = container.childNodes[offset];
+ }
+
+ return container;
+ };
+
+ return RangeUtils;
+});
+
+// Included from: js/tinymce/classes/NodeChange.js
+
+/**
+ * NodeChange.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class handles the nodechange event dispatching both manual and through selection change events.
+ *
+ * @class tinymce.NodeChange
+ * @private
+ */
+define("tinymce/NodeChange", [
+ "tinymce/dom/RangeUtils",
+ "tinymce/Env",
+ "tinymce/util/Delay"
+], function(RangeUtils, Env, Delay) {
+ return function(editor) {
+ var lastRng, lastPath = [];
+
+ /**
+ * Returns true/false if the current element path has been changed or not.
+ *
+ * @private
+ * @return {Boolean} True if the element path is the same false if it's not.
+ */
+ function isSameElementPath(startElm) {
+ var i, currentPath;
+
+ currentPath = editor.$(startElm).parentsUntil(editor.getBody()).add(startElm);
+ if (currentPath.length === lastPath.length) {
+ for (i = currentPath.length; i >= 0; i--) {
+ if (currentPath[i] !== lastPath[i]) {
+ break;
+ }
+ }
+
+ if (i === -1) {
+ lastPath = currentPath;
+ return true;
+ }
+ }
+
+ lastPath = currentPath;
+
+ return false;
+ }
+
+ // Gecko doesn't support the "selectionchange" event
+ if (!('onselectionchange' in editor.getDoc())) {
+ editor.on('NodeChange Click MouseUp KeyUp Focus', function(e) {
+ var nativeRng, fakeRng;
+
+ // Since DOM Ranges mutate on modification
+ // of the DOM we need to clone it's contents
+ nativeRng = editor.selection.getRng();
+ fakeRng = {
+ startContainer: nativeRng.startContainer,
+ startOffset: nativeRng.startOffset,
+ endContainer: nativeRng.endContainer,
+ endOffset: nativeRng.endOffset
+ };
+
+ // Always treat nodechange as a selectionchange since applying
+ // formatting to the current range wouldn't update the range but it's parent
+ if (e.type == 'nodechange' || !RangeUtils.compareRanges(fakeRng, lastRng)) {
+ editor.fire('SelectionChange');
+ }
+
+ lastRng = fakeRng;
+ });
+ }
+
+ // IE has a bug where it fires a selectionchange on right click that has a range at the start of the body
+ // When the contextmenu event fires the selection is located at the right location
+ editor.on('contextmenu', function() {
+ editor.fire('SelectionChange');
+ });
+
+ // Selection change is delayed ~200ms on IE when you click inside the current range
+ editor.on('SelectionChange', function() {
+ var startElm = editor.selection.getStart(true);
+
+ // IE 8 will fire a selectionchange event with an incorrect selection
+ // when focusing out of table cells. Click inside cell -> toolbar = Invalid SelectionChange event
+ if (!Env.range && editor.selection.isCollapsed()) {
+ return;
+ }
+
+ if (!isSameElementPath(startElm) && editor.dom.isChildOf(startElm, editor.getBody())) {
+ editor.nodeChanged({selectionChange: true});
+ }
+ });
+
+ // Fire an extra nodeChange on mouseup for compatibility reasons
+ editor.on('MouseUp', function(e) {
+ if (!e.isDefaultPrevented()) {
+ // Delay nodeChanged call for WebKit edge case issue where the range
+ // isn't updated until after you click outside a selected image
+ if (editor.selection.getNode().nodeName == 'IMG') {
+ Delay.setEditorTimeout(editor, function() {
+ editor.nodeChanged();
+ });
+ } else {
+ editor.nodeChanged();
+ }
+ }
+ });
+
+ /**
+ * Dispatches out a onNodeChange event to all observers. This method should be called when you
+ * need to update the UI states or element path etc.
+ *
+ * @method nodeChanged
+ * @param {Object} args Optional args to pass to NodeChange event handlers.
+ */
+ this.nodeChanged = function(args) {
+ var selection = editor.selection, node, parents, root;
+
+ // Fix for bug #1896577 it seems that this can not be fired while the editor is loading
+ if (editor.initialized && selection && !editor.settings.disable_nodechange && !editor.readonly) {
+ // Get start node
+ root = editor.getBody();
+ node = selection.getStart() || root;
+
+ // Make sure the node is within the editor root or is the editor root
+ if (node.ownerDocument != editor.getDoc() || !editor.dom.isChildOf(node, root)) {
+ node = root;
+ }
+
+ // Edge case for <p>|<img></p>
+ if (node.nodeName == 'IMG' && selection.isCollapsed()) {
+ node = node.parentNode;
+ }
+
+ // Get parents and add them to object
+ parents = [];
+ editor.dom.getParent(node, function(node) {
+ if (node === root) {
+ return true;
+ }
+
+ parents.push(node);
+ });
+
+ args = args || {};
+ args.element = node;
+ args.parents = parents;
+
+ editor.fire('NodeChange', args);
+ }
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/html/Node.js
+
+/**
+ * Node.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class is a minimalistic implementation of a DOM like node used by the DomParser class.
+ *
+ * @example
+ * var node = new tinymce.html.Node('strong', 1);
+ * someRoot.append(node);
+ *
+ * @class tinymce.html.Node
+ * @version 3.4
+ */
+define("tinymce/html/Node", [], function() {
+ var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = {
+ '#text': 3,
+ '#comment': 8,
+ '#cdata': 4,
+ '#pi': 7,
+ '#doctype': 10,
+ '#document-fragment': 11
+ };
+
+ // Walks the tree left/right
+ function walk(node, root_node, prev) {
+ var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next';
+
+ // Walk into nodes if it has a start
+ if (node[startName]) {
+ return node[startName];
+ }
+
+ // Return the sibling if it has one
+ if (node !== root_node) {
+ sibling = node[siblingName];
+
+ if (sibling) {
+ return sibling;
+ }
+
+ // Walk up the parents to look for siblings
+ for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) {
+ sibling = parent[siblingName];
+
+ if (sibling) {
+ return sibling;
+ }
+ }
+ }
+ }
+
+ /**
+ * Constructs a new Node instance.
+ *
+ * @constructor
+ * @method Node
+ * @param {String} name Name of the node type.
+ * @param {Number} type Numeric type representing the node.
+ */
+ function Node(name, type) {
+ this.name = name;
+ this.type = type;
+
+ if (type === 1) {
+ this.attributes = [];
+ this.attributes.map = {};
+ }
+ }
+
+ Node.prototype = {
+ /**
+ * Replaces the current node with the specified one.
+ *
+ * @example
+ * someNode.replace(someNewNode);
+ *
+ * @method replace
+ * @param {tinymce.html.Node} node Node to replace the current node with.
+ * @return {tinymce.html.Node} The old node that got replaced.
+ */
+ replace: function(node) {
+ var self = this;
+
+ if (node.parent) {
+ node.remove();
+ }
+
+ self.insert(node, self);
+ self.remove();
+
+ return self;
+ },
+
+ /**
+ * Gets/sets or removes an attribute by name.
+ *
+ * @example
+ * someNode.attr("name", "value"); // Sets an attribute
+ * console.log(someNode.attr("name")); // Gets an attribute
+ * someNode.attr("name", null); // Removes an attribute
+ *
+ * @method attr
+ * @param {String} name Attribute name to set or get.
+ * @param {String} value Optional value to set.
+ * @return {String/tinymce.html.Node} String or undefined on a get operation or the current node on a set operation.
+ */
+ attr: function(name, value) {
+ var self = this, attrs, i, undef;
+
+ if (typeof name !== "string") {
+ for (i in name) {
+ self.attr(i, name[i]);
+ }
+
+ return self;
+ }
+
+ if ((attrs = self.attributes)) {
+ if (value !== undef) {
+ // Remove attribute
+ if (value === null) {
+ if (name in attrs.map) {
+ delete attrs.map[name];
+
+ i = attrs.length;
+ while (i--) {
+ if (attrs[i].name === name) {
+ attrs = attrs.splice(i, 1);
+ return self;
+ }
+ }
+ }
+
+ return self;
+ }
+
+ // Set attribute
+ if (name in attrs.map) {
+ // Set attribute
+ i = attrs.length;
+ while (i--) {
+ if (attrs[i].name === name) {
+ attrs[i].value = value;
+ break;
+ }
+ }
+ } else {
+ attrs.push({name: name, value: value});
+ }
+
+ attrs.map[name] = value;
+
+ return self;
+ }
+
+ return attrs.map[name];
+ }
+ },
+
+ /**
+ * Does a shallow clones the node into a new node. It will also exclude id attributes since
+ * there should only be one id per document.
+ *
+ * @example
+ * var clonedNode = node.clone();
+ *
+ * @method clone
+ * @return {tinymce.html.Node} New copy of the original node.
+ */
+ clone: function() {
+ var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs;
+
+ // Clone element attributes
+ if ((selfAttrs = self.attributes)) {
+ cloneAttrs = [];
+ cloneAttrs.map = {};
+
+ for (i = 0, l = selfAttrs.length; i < l; i++) {
+ selfAttr = selfAttrs[i];
+
+ // Clone everything except id
+ if (selfAttr.name !== 'id') {
+ cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value};
+ cloneAttrs.map[selfAttr.name] = selfAttr.value;
+ }
+ }
+
+ clone.attributes = cloneAttrs;
+ }
+
+ clone.value = self.value;
+ clone.shortEnded = self.shortEnded;
+
+ return clone;
+ },
+
+ /**
+ * Wraps the node in in another node.
+ *
+ * @example
+ * node.wrap(wrapperNode);
+ *
+ * @method wrap
+ */
+ wrap: function(wrapper) {
+ var self = this;
+
+ self.parent.insert(wrapper, self);
+ wrapper.append(self);
+
+ return self;
+ },
+
+ /**
+ * Unwraps the node in other words it removes the node but keeps the children.
+ *
+ * @example
+ * node.unwrap();
+ *
+ * @method unwrap
+ */
+ unwrap: function() {
+ var self = this, node, next;
+
+ for (node = self.firstChild; node;) {
+ next = node.next;
+ self.insert(node, self, true);
+ node = next;
+ }
+
+ self.remove();
+ },
+
+ /**
+ * Removes the node from it's parent.
+ *
+ * @example
+ * node.remove();
+ *
+ * @method remove
+ * @return {tinymce.html.Node} Current node that got removed.
+ */
+ remove: function() {
+ var self = this, parent = self.parent, next = self.next, prev = self.prev;
+
+ if (parent) {
+ if (parent.firstChild === self) {
+ parent.firstChild = next;
+
+ if (next) {
+ next.prev = null;
+ }
+ } else {
+ prev.next = next;
+ }
+
+ if (parent.lastChild === self) {
+ parent.lastChild = prev;
+
+ if (prev) {
+ prev.next = null;
+ }
+ } else {
+ next.prev = prev;
+ }
+
+ self.parent = self.next = self.prev = null;
+ }
+
+ return self;
+ },
+
+ /**
+ * Appends a new node as a child of the current node.
+ *
+ * @example
+ * node.append(someNode);
+ *
+ * @method append
+ * @param {tinymce.html.Node} node Node to append as a child of the current one.
+ * @return {tinymce.html.Node} The node that got appended.
+ */
+ append: function(node) {
+ var self = this, last;
+
+ if (node.parent) {
+ node.remove();
+ }
+
+ last = self.lastChild;
+ if (last) {
+ last.next = node;
+ node.prev = last;
+ self.lastChild = node;
+ } else {
+ self.lastChild = self.firstChild = node;
+ }
+
+ node.parent = self;
+
+ return node;
+ },
+
+ /**
+ * Inserts a node at a specific position as a child of the current node.
+ *
+ * @example
+ * parentNode.insert(newChildNode, oldChildNode);
+ *
+ * @method insert
+ * @param {tinymce.html.Node} node Node to insert as a child of the current node.
+ * @param {tinymce.html.Node} ref_node Reference node to set node before/after.
+ * @param {Boolean} before Optional state to insert the node before the reference node.
+ * @return {tinymce.html.Node} The node that got inserted.
+ */
+ insert: function(node, ref_node, before) {
+ var parent;
+
+ if (node.parent) {
+ node.remove();
+ }
+
+ parent = ref_node.parent || this;
+
+ if (before) {
+ if (ref_node === parent.firstChild) {
+ parent.firstChild = node;
+ } else {
+ ref_node.prev.next = node;
+ }
+
+ node.prev = ref_node.prev;
+ node.next = ref_node;
+ ref_node.prev = node;
+ } else {
+ if (ref_node === parent.lastChild) {
+ parent.lastChild = node;
+ } else {
+ ref_node.next.prev = node;
+ }
+
+ node.next = ref_node.next;
+ node.prev = ref_node;
+ ref_node.next = node;
+ }
+
+ node.parent = parent;
+
+ return node;
+ },
+
+ /**
+ * Get all children by name.
+ *
+ * @method getAll
+ * @param {String} name Name of the child nodes to collect.
+ * @return {Array} Array with child nodes matchin the specified name.
+ */
+ getAll: function(name) {
+ var self = this, node, collection = [];
+
+ for (node = self.firstChild; node; node = walk(node, self)) {
+ if (node.name === name) {
+ collection.push(node);
+ }
+ }
+
+ return collection;
+ },
+
+ /**
+ * Removes all children of the current node.
+ *
+ * @method empty
+ * @return {tinymce.html.Node} The current node that got cleared.
+ */
+ empty: function() {
+ var self = this, nodes, i, node;
+
+ // Remove all children
+ if (self.firstChild) {
+ nodes = [];
+
+ // Collect the children
+ for (node = self.firstChild; node; node = walk(node, self)) {
+ nodes.push(node);
+ }
+
+ // Remove the children
+ i = nodes.length;
+ while (i--) {
+ node = nodes[i];
+ node.parent = node.firstChild = node.lastChild = node.next = node.prev = null;
+ }
+ }
+
+ self.firstChild = self.lastChild = null;
+
+ return self;
+ },
+
+ /**
+ * Returns true/false if the node is to be considered empty or not.
+ *
+ * @example
+ * node.isEmpty({img: true});
+ * @method isEmpty
+ * @param {Object} elements Name/value object with elements that are automatically treated as non empty elements.
+ * @return {Boolean} true/false if the node is empty or not.
+ */
+ isEmpty: function(elements) {
+ var self = this, node = self.firstChild, i, name;
+
+ if (node) {
+ do {
+ if (node.type === 1) {
+ // Ignore bogus elements
+ if (node.attributes.map['data-mce-bogus']) {
+ continue;
+ }
+
+ // Keep empty elements like <img />
+ if (elements[node.name]) {
+ return false;
+ }
+
+ // Keep bookmark nodes and name attribute like <a name="1"></a>
+ i = node.attributes.length;
+ while (i--) {
+ name = node.attributes[i].name;
+ if (name === "name" || name.indexOf('data-mce-bookmark') === 0) {
+ return false;
+ }
+ }
+ }
+
+ // Keep comments
+ if (node.type === 8) {
+ return false;
+ }
+
+ // Keep non whitespace text nodes
+ if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) {
+ return false;
+ }
+ } while ((node = walk(node, self)));
+ }
+
+ return true;
+ },
+
+ /**
+ * Walks to the next or previous node and returns that node or null if it wasn't found.
+ *
+ * @method walk
+ * @param {Boolean} prev Optional previous node state defaults to false.
+ * @return {tinymce.html.Node} Node that is next to or previous of the current node.
+ */
+ walk: function(prev) {
+ return walk(this, null, prev);
+ }
+ };
+
+ /**
+ * Creates a node of a specific type.
+ *
+ * @static
+ * @method create
+ * @param {String} name Name of the node type to create for example "b" or "#text".
+ * @param {Object} attrs Name/value collection of attributes that will be applied to elements.
+ */
+ Node.create = function(name, attrs) {
+ var node, attrName;
+
+ // Create node
+ node = new Node(name, typeLookup[name] || 1);
+
+ // Add attributes if needed
+ if (attrs) {
+ for (attrName in attrs) {
+ node.attr(attrName, attrs[attrName]);
+ }
+ }
+
+ return node;
+ };
+
+ return Node;
+});
+
+// Included from: js/tinymce/classes/html/Schema.js
+
+/**
+ * Schema.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Schema validator class.
+ *
+ * @class tinymce.html.Schema
+ * @example
+ * if (tinymce.activeEditor.schema.isValidChild('p', 'span'))
+ * alert('span is valid child of p.');
+ *
+ * if (tinymce.activeEditor.schema.getElementRule('p'))
+ * alert('P is a valid element.');
+ *
+ * @class tinymce.html.Schema
+ * @version 3.4
+ */
+define("tinymce/html/Schema", [
+ "tinymce/util/Tools"
+], function(Tools) {
+ var mapCache = {}, dummyObj = {};
+ var makeMap = Tools.makeMap, each = Tools.each, extend = Tools.extend, explode = Tools.explode, inArray = Tools.inArray;
+
+ function split(items, delim) {
+ items = Tools.trim(items);
+ return items ? items.split(delim || ' ') : [];
+ }
+
+ /**
+ * Builds a schema lookup table
+ *
+ * @private
+ * @param {String} type html4, html5 or html5-strict schema type.
+ * @return {Object} Schema lookup table.
+ */
+ function compileSchema(type) {
+ var schema = {}, globalAttributes, blockContent;
+ var phrasingContent, flowContent, html4BlockContent, html4PhrasingContent;
+
+ function add(name, attributes, children) {
+ var ni, attributesOrder, element;
+
+ function arrayToMap(array, obj) {
+ var map = {}, i, l;
+
+ for (i = 0, l = array.length; i < l; i++) {
+ map[array[i]] = obj || {};
+ }
+
+ return map;
+ }
+
+ children = children || [];
+ attributes = attributes || "";
+
+ if (typeof children === "string") {
+ children = split(children);
+ }
+
+ name = split(name);
+ ni = name.length;
+ while (ni--) {
+ attributesOrder = split([globalAttributes, attributes].join(' '));
+
+ element = {
+ attributes: arrayToMap(attributesOrder),
+ attributesOrder: attributesOrder,
+ children: arrayToMap(children, dummyObj)
+ };
+
+ schema[name[ni]] = element;
+ }
+ }
+
+ function addAttrs(name, attributes) {
+ var ni, schemaItem, i, l;
+
+ name = split(name);
+ ni = name.length;
+ attributes = split(attributes);
+ while (ni--) {
+ schemaItem = schema[name[ni]];
+ for (i = 0, l = attributes.length; i < l; i++) {
+ schemaItem.attributes[attributes[i]] = {};
+ schemaItem.attributesOrder.push(attributes[i]);
+ }
+ }
+ }
+
+ // Use cached schema
+ if (mapCache[type]) {
+ return mapCache[type];
+ }
+
+ // Attributes present on all elements
+ globalAttributes = "id accesskey class dir lang style tabindex title";
+
+ // Event attributes can be opt-in/opt-out
+ /*eventAttributes = split("onabort onblur oncancel oncanplay oncanplaythrough onchange onclick onclose oncontextmenu oncuechange " +
+ "ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended " +
+ "onerror onfocus oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart " +
+ "onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onpause onplay onplaying onprogress onratechange " +
+ "onreset onscroll onseeked onseeking onseeking onselect onshow onstalled onsubmit onsuspend ontimeupdate onvolumechange " +
+ "onwaiting"
+ );*/
+
+ // Block content elements
+ blockContent =
+ "address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul";
+
+ // Phrasing content elements from the HTML5 spec (inline)
+ phrasingContent =
+ "a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd " +
+ "label map noscript object q s samp script select small span strong sub sup " +
+ "textarea u var #text #comment"
+ ;
+
+ // Add HTML5 items to globalAttributes, blockContent, phrasingContent
+ if (type != "html4") {
+ globalAttributes += " contenteditable contextmenu draggable dropzone " +
+ "hidden spellcheck translate";
+ blockContent += " article aside details dialog figure header footer hgroup section nav";
+ phrasingContent += " audio canvas command datalist mark meter output picture " +
+ "progress time wbr video ruby bdi keygen";
+ }
+
+ // Add HTML4 elements unless it's html5-strict
+ if (type != "html5-strict") {
+ globalAttributes += " xml:lang";
+
+ html4PhrasingContent = "acronym applet basefont big font strike tt";
+ phrasingContent = [phrasingContent, html4PhrasingContent].join(' ');
+
+ each(split(html4PhrasingContent), function(name) {
+ add(name, "", phrasingContent);
+ });
+
+ html4BlockContent = "center dir isindex noframes";
+ blockContent = [blockContent, html4BlockContent].join(' ');
+
+ // Flow content elements from the HTML5 spec (block+inline)
+ flowContent = [blockContent, phrasingContent].join(' ');
+
+ each(split(html4BlockContent), function(name) {
+ add(name, "", flowContent);
+ });
+ }
+
+ // Flow content elements from the HTML5 spec (block+inline)
+ flowContent = flowContent || [blockContent, phrasingContent].join(" ");
+
+ // HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement
+ // Schema items <element name>, <specific attributes>, <children ..>
+ add("html", "manifest", "head body");
+ add("head", "", "base command link meta noscript script style title");
+ add("title hr noscript br");
+ add("base", "href target");
+ add("link", "href rel media hreflang type sizes hreflang");
+ add("meta", "name http-equiv content charset");
+ add("style", "media type scoped");
+ add("script", "src async defer type charset");
+ add("body", "onafterprint onbeforeprint onbeforeunload onblur onerror onfocus " +
+ "onhashchange onload onmessage onoffline ononline onpagehide onpageshow " +
+ "onpopstate onresize onscroll onstorage onunload", flowContent);
+ add("address dt dd div caption", "", flowContent);
+ add("h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn", "", phrasingContent);
+ add("blockquote", "cite", flowContent);
+ add("ol", "reversed start type", "li");
+ add("ul", "", "li");
+ add("li", "value", flowContent);
+ add("dl", "", "dt dd");
+ add("a", "href target rel media hreflang type", phrasingContent);
+ add("q", "cite", phrasingContent);
+ add("ins del", "cite datetime", flowContent);
+ add("img", "src sizes srcset alt usemap ismap width height");
+ add("iframe", "src name width height", flowContent);
+ add("embed", "src type width height");
+ add("object", "data type typemustmatch name usemap form width height", [flowContent, "param"].join(' '));
+ add("param", "name value");
+ add("map", "name", [flowContent, "area"].join(' '));
+ add("area", "alt coords shape href target rel media hreflang type");
+ add("table", "border", "caption colgroup thead tfoot tbody tr" + (type == "html4" ? " col" : ""));
+ add("colgroup", "span", "col");
+ add("col", "span");
+ add("tbody thead tfoot", "", "tr");
+ add("tr", "", "td th");
+ add("td", "colspan rowspan headers", flowContent);
+ add("th", "colspan rowspan headers scope abbr", flowContent);
+ add("form", "accept-charset action autocomplete enctype method name novalidate target", flowContent);
+ add("fieldset", "disabled form name", [flowContent, "legend"].join(' '));
+ add("label", "form for", phrasingContent);
+ add("input", "accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate " +
+ "formtarget height list max maxlength min multiple name pattern readonly required size src step type value width"
+ );
+ add("button", "disabled form formaction formenctype formmethod formnovalidate formtarget name type value",
+ type == "html4" ? flowContent : phrasingContent);
+ add("select", "disabled form multiple name required size", "option optgroup");
+ add("optgroup", "disabled label", "option");
+ add("option", "disabled label selected value");
+ add("textarea", "cols dirname disabled form maxlength name readonly required rows wrap");
+ add("menu", "type label", [flowContent, "li"].join(' '));
+ add("noscript", "", flowContent);
+
+ // Extend with HTML5 elements
+ if (type != "html4") {
+ add("wbr");
+ add("ruby", "", [phrasingContent, "rt rp"].join(' '));
+ add("figcaption", "", flowContent);
+ add("mark rt rp summary bdi", "", phrasingContent);
+ add("canvas", "width height", flowContent);
+ add("video", "src crossorigin poster preload autoplay mediagroup loop " +
+ "muted controls width height buffered", [flowContent, "track source"].join(' '));
+ add("audio", "src crossorigin preload autoplay mediagroup loop muted controls " +
+ "buffered volume", [flowContent, "track source"].join(' '));
+ add("picture", "", "img source");
+ add("source", "src srcset type media sizes");
+ add("track", "kind src srclang label default");
+ add("datalist", "", [phrasingContent, "option"].join(' '));
+ add("article section nav aside header footer", "", flowContent);
+ add("hgroup", "", "h1 h2 h3 h4 h5 h6");
+ add("figure", "", [flowContent, "figcaption"].join(' '));
+ add("time", "datetime", phrasingContent);
+ add("dialog", "open", flowContent);
+ add("command", "type label icon disabled checked radiogroup command");
+ add("output", "for form name", phrasingContent);
+ add("progress", "value max", phrasingContent);
+ add("meter", "value min max low high optimum", phrasingContent);
+ add("details", "open", [flowContent, "summary"].join(' '));
+ add("keygen", "autofocus challenge disabled form keytype name");
+ }
+
+ // Extend with HTML4 attributes unless it's html5-strict
+ if (type != "html5-strict") {
+ addAttrs("script", "language xml:space");
+ addAttrs("style", "xml:space");
+ addAttrs("object", "declare classid code codebase codetype archive standby align border hspace vspace");
+ addAttrs("embed", "align name hspace vspace");
+ addAttrs("param", "valuetype type");
+ addAttrs("a", "charset name rev shape coords");
+ addAttrs("br", "clear");
+ addAttrs("applet", "codebase archive code object alt name width height align hspace vspace");
+ addAttrs("img", "name longdesc align border hspace vspace");
+ addAttrs("iframe", "longdesc frameborder marginwidth marginheight scrolling align");
+ addAttrs("font basefont", "size color face");
+ addAttrs("input", "usemap align");
+ addAttrs("select", "onchange");
+ addAttrs("textarea");
+ addAttrs("h1 h2 h3 h4 h5 h6 div p legend caption", "align");
+ addAttrs("ul", "type compact");
+ addAttrs("li", "type");
+ addAttrs("ol dl menu dir", "compact");
+ addAttrs("pre", "width xml:space");
+ addAttrs("hr", "align noshade size width");
+ addAttrs("isindex", "prompt");
+ addAttrs("table", "summary width frame rules cellspacing cellpadding align bgcolor");
+ addAttrs("col", "width align char charoff valign");
+ addAttrs("colgroup", "width align char charoff valign");
+ addAttrs("thead", "align char charoff valign");
+ addAttrs("tr", "align char charoff valign bgcolor");
+ addAttrs("th", "axis align char charoff valign nowrap bgcolor width height");
+ addAttrs("form", "accept");
+ addAttrs("td", "abbr axis scope align char charoff valign nowrap bgcolor width height");
+ addAttrs("tfoot", "align char charoff valign");
+ addAttrs("tbody", "align char charoff valign");
+ addAttrs("area", "nohref");
+ addAttrs("body", "background bgcolor text link vlink alink");
+ }
+
+ // Extend with HTML5 attributes unless it's html4
+ if (type != "html4") {
+ addAttrs("input button select textarea", "autofocus");
+ addAttrs("input textarea", "placeholder");
+ addAttrs("a", "download");
+ addAttrs("link script img", "crossorigin");
+ addAttrs("iframe", "sandbox seamless allowfullscreen"); // Excluded: srcdoc
+ }
+
+ // Special: iframe, ruby, video, audio, label
+
+ // Delete children of the same name from it's parent
+ // For example: form can't have a child of the name form
+ each(split('a form meter progress dfn'), function(name) {
+ if (schema[name]) {
+ delete schema[name].children[name];
+ }
+ });
+
+ // Delete header, footer, sectioning and heading content descendants
+ /*each('dt th address', function(name) {
+ delete schema[name].children[name];
+ });*/
+
+ // Caption can't have tables
+ delete schema.caption.children.table;
+
+ // Delete scripts by default due to possible XSS
+ delete schema.script;
+
+ // TODO: LI:s can only have value if parent is OL
+
+ // TODO: Handle transparent elements
+ // a ins del canvas map
+
+ mapCache[type] = schema;
+
+ return schema;
+ }
+
+ function compileElementMap(value, mode) {
+ var styles;
+
+ if (value) {
+ styles = {};
+
+ if (typeof value == 'string') {
+ value = {
+ '*': value
+ };
+ }
+
+ // Convert styles into a rule list
+ each(value, function(value, key) {
+ styles[key] = styles[key.toUpperCase()] = mode == 'map' ? makeMap(value, /[, ]/) : explode(value, /[, ]/);
+ });
+ }
+
+ return styles;
+ }
+
+ /**
+ * Constructs a new Schema instance.
+ *
+ * @constructor
+ * @method Schema
+ * @param {Object} settings Name/value settings object.
+ */
+ return function(settings) {
+ var self = this, elements = {}, children = {}, patternElements = [], validStyles, invalidStyles, schemaItems;
+ var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, validClasses;
+ var blockElementsMap, nonEmptyElementsMap, moveCaretBeforeOnEnterElementsMap, textBlockElementsMap, textInlineElementsMap;
+ var customElementsMap = {}, specialElements = {};
+
+ // Creates an lookup table map object for the specified option or the default value
+ function createLookupTable(option, default_value, extendWith) {
+ var value = settings[option];
+
+ if (!value) {
+ // Get cached default map or make it if needed
+ value = mapCache[option];
+
+ if (!value) {
+ value = makeMap(default_value, ' ', makeMap(default_value.toUpperCase(), ' '));
+ value = extend(value, extendWith);
+
+ mapCache[option] = value;
+ }
+ } else {
+ // Create custom map
+ value = makeMap(value, /[, ]/, makeMap(value.toUpperCase(), /[, ]/));
+ }
+
+ return value;
+ }
+
+ settings = settings || {};
+ schemaItems = compileSchema(settings.schema);
+
+ // Allow all elements and attributes if verify_html is set to false
+ if (settings.verify_html === false) {
+ settings.valid_elements = '*[*]';
+ }
+
+ validStyles = compileElementMap(settings.valid_styles);
+ invalidStyles = compileElementMap(settings.invalid_styles, 'map');
+ validClasses = compileElementMap(settings.valid_classes, 'map');
+
+ // Setup map objects
+ whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script noscript style textarea video audio iframe object');
+ selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr');
+ shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link ' +
+ 'meta param embed source wbr track');
+ boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize ' +
+ 'noshade nowrap readonly selected autoplay loop controls');
+ nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object script', shortEndedElementsMap);
+ moveCaretBeforeOnEnterElementsMap = createLookupTable('move_caret_before_on_enter_elements', 'table', nonEmptyElementsMap);
+ textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' +
+ 'blockquote center dir fieldset header footer article section hgroup aside nav figure');
+ blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' +
+ 'th tr td li ol ul caption dl dt dd noscript menu isindex option ' +
+ 'datalist select optgroup figcaption', textBlockElementsMap);
+ textInlineElementsMap = createLookupTable('text_inline_elements', 'span strong b em i font strike u var cite ' +
+ 'dfn code mark q sup sub samp');
+
+ each((settings.special || 'script noscript style textarea').split(' '), function(name) {
+ specialElements[name] = new RegExp('<\/' + name + '[^>]*>', 'gi');
+ });
+
+ // Converts a wildcard expression string to a regexp for example *a will become /.*a/.
+ function patternToRegExp(str) {
+ return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$');
+ }
+
+ // Parses the specified valid_elements string and adds to the current rules
+ // This function is a bit hard to read since it's heavily optimized for speed
+ function addValidElements(validElements) {
+ var ei, el, ai, al, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder,
+ prefix, outputName, globalAttributes, globalAttributesOrder, key, value,
+ elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/,
+ attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/,
+ hasPatternsRegExp = /[*?+]/;
+
+ if (validElements) {
+ // Split valid elements into an array with rules
+ validElements = split(validElements, ',');
+
+ if (elements['@']) {
+ globalAttributes = elements['@'].attributes;
+ globalAttributesOrder = elements['@'].attributesOrder;
+ }
+
+ // Loop all rules
+ for (ei = 0, el = validElements.length; ei < el; ei++) {
+ // Parse element rule
+ matches = elementRuleRegExp.exec(validElements[ei]);
+ if (matches) {
+ // Setup local names for matches
+ prefix = matches[1];
+ elementName = matches[2];
+ outputName = matches[3];
+ attrData = matches[5];
+
+ // Create new attributes and attributesOrder
+ attributes = {};
+ attributesOrder = [];
+
+ // Create the new element
+ element = {
+ attributes: attributes,
+ attributesOrder: attributesOrder
+ };
+
+ // Padd empty elements prefix
+ if (prefix === '#') {
+ element.paddEmpty = true;
+ }
+
+ // Remove empty elements prefix
+ if (prefix === '-') {
+ element.removeEmpty = true;
+ }
+
+ if (matches[4] === '!') {
+ element.removeEmptyAttrs = true;
+ }
+
+ // Copy attributes from global rule into current rule
+ if (globalAttributes) {
+ for (key in globalAttributes) {
+ attributes[key] = globalAttributes[key];
+ }
+
+ attributesOrder.push.apply(attributesOrder, globalAttributesOrder);
+ }
+
+ // Attributes defined
+ if (attrData) {
+ attrData = split(attrData, '|');
+ for (ai = 0, al = attrData.length; ai < al; ai++) {
+ matches = attrRuleRegExp.exec(attrData[ai]);
+ if (matches) {
+ attr = {};
+ attrType = matches[1];
+ attrName = matches[2].replace(/::/g, ':');
+ prefix = matches[3];
+ value = matches[4];
+
+ // Required
+ if (attrType === '!') {
+ element.attributesRequired = element.attributesRequired || [];
+ element.attributesRequired.push(attrName);
+ attr.required = true;
+ }
+
+ // Denied from global
+ if (attrType === '-') {
+ delete attributes[attrName];
+ attributesOrder.splice(inArray(attributesOrder, attrName), 1);
+ continue;
+ }
+
+ // Default value
+ if (prefix) {
+ // Default value
+ if (prefix === '=') {
+ element.attributesDefault = element.attributesDefault || [];
+ element.attributesDefault.push({name: attrName, value: value});
+ attr.defaultValue = value;
+ }
+
+ // Forced value
+ if (prefix === ':') {
+ element.attributesForced = element.attributesForced || [];
+ element.attributesForced.push({name: attrName, value: value});
+ attr.forcedValue = value;
+ }
+
+ // Required values
+ if (prefix === '<') {
+ attr.validValues = makeMap(value, '?');
+ }
+ }
+
+ // Check for attribute patterns
+ if (hasPatternsRegExp.test(attrName)) {
+ element.attributePatterns = element.attributePatterns || [];
+ attr.pattern = patternToRegExp(attrName);
+ element.attributePatterns.push(attr);
+ } else {
+ // Add attribute to order list if it doesn't already exist
+ if (!attributes[attrName]) {
+ attributesOrder.push(attrName);
+ }
+
+ attributes[attrName] = attr;
+ }
+ }
+ }
+ }
+
+ // Global rule, store away these for later usage
+ if (!globalAttributes && elementName == '@') {
+ globalAttributes = attributes;
+ globalAttributesOrder = attributesOrder;
+ }
+
+ // Handle substitute elements such as b/strong
+ if (outputName) {
+ element.outputName = elementName;
+ elements[outputName] = element;
+ }
+
+ // Add pattern or exact element
+ if (hasPatternsRegExp.test(elementName)) {
+ element.pattern = patternToRegExp(elementName);
+ patternElements.push(element);
+ } else {
+ elements[elementName] = element;
+ }
+ }
+ }
+ }
+ }
+
+ function setValidElements(validElements) {
+ elements = {};
+ patternElements = [];
+
+ addValidElements(validElements);
+
+ each(schemaItems, function(element, name) {
+ children[name] = element.children;
+ });
+ }
+
+ // Adds custom non HTML elements to the schema
+ function addCustomElements(customElements) {
+ var customElementRegExp = /^(~)?(.+)$/;
+
+ if (customElements) {
+ // Flush cached items since we are altering the default maps
+ mapCache.text_block_elements = mapCache.block_elements = null;
+
+ each(split(customElements, ','), function(rule) {
+ var matches = customElementRegExp.exec(rule),
+ inline = matches[1] === '~',
+ cloneName = inline ? 'span' : 'div',
+ name = matches[2];
+
+ children[name] = children[cloneName];
+ customElementsMap[name] = cloneName;
+
+ // If it's not marked as inline then add it to valid block elements
+ if (!inline) {
+ blockElementsMap[name.toUpperCase()] = {};
+ blockElementsMap[name] = {};
+ }
+
+ // Add elements clone if needed
+ if (!elements[name]) {
+ var customRule = elements[cloneName];
+
+ customRule = extend({}, customRule);
+ delete customRule.removeEmptyAttrs;
+ delete customRule.removeEmpty;
+
+ elements[name] = customRule;
+ }
+
+ // Add custom elements at span/div positions
+ each(children, function(element, elmName) {
+ if (element[cloneName]) {
+ children[elmName] = element = extend({}, children[elmName]);
+ element[name] = element[cloneName];
+ }
+ });
+ });
+ }
+ }
+
+ // Adds valid children to the schema object
+ function addValidChildren(validChildren) {
+ var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/;
+
+ // Invalidate the schema cache if the schema is mutated
+ mapCache[settings.schema] = null;
+
+ if (validChildren) {
+ each(split(validChildren, ','), function(rule) {
+ var matches = childRuleRegExp.exec(rule), parent, prefix;
+
+ if (matches) {
+ prefix = matches[1];
+
+ // Add/remove items from default
+ if (prefix) {
+ parent = children[matches[2]];
+ } else {
+ parent = children[matches[2]] = {'#comment': {}};
+ }
+
+ parent = children[matches[2]];
+
+ each(split(matches[3], '|'), function(child) {
+ if (prefix === '-') {
+ delete parent[child];
+ } else {
+ parent[child] = {};
+ }
+ });
+ }
+ });
+ }
+ }
+
+ function getElementRule(name) {
+ var element = elements[name], i;
+
+ // Exact match found
+ if (element) {
+ return element;
+ }
+
+ // No exact match then try the patterns
+ i = patternElements.length;
+ while (i--) {
+ element = patternElements[i];
+
+ if (element.pattern.test(name)) {
+ return element;
+ }
+ }
+ }
+
+ if (!settings.valid_elements) {
+ // No valid elements defined then clone the elements from the schema spec
+ each(schemaItems, function(element, name) {
+ elements[name] = {
+ attributes: element.attributes,
+ attributesOrder: element.attributesOrder
+ };
+
+ children[name] = element.children;
+ });
+
+ // Switch these on HTML4
+ if (settings.schema != "html5") {
+ each(split('strong/b em/i'), function(item) {
+ item = split(item, '/');
+ elements[item[1]].outputName = item[0];
+ });
+ }
+
+ // Add default alt attribute for images, removed since alt="" is treated as presentational.
+ // elements.img.attributesDefault = [{name: 'alt', value: ''}];
+
+ // Remove these if they are empty by default
+ each(split('ol ul sub sup blockquote span font a table tbody tr strong em b i'), function(name) {
+ if (elements[name]) {
+ elements[name].removeEmpty = true;
+ }
+ });
+
+ // Padd these by default
+ each(split('p h1 h2 h3 h4 h5 h6 th td pre div address caption'), function(name) {
+ elements[name].paddEmpty = true;
+ });
+
+ // Remove these if they have no attributes
+ each(split('span'), function(name) {
+ elements[name].removeEmptyAttrs = true;
+ });
+
+ // Remove these by default
+ // TODO: Reenable in 4.1
+ /*each(split('script style'), function(name) {
+ delete elements[name];
+ });*/
+ } else {
+ setValidElements(settings.valid_elements);
+ }
+
+ addCustomElements(settings.custom_elements);
+ addValidChildren(settings.valid_children);
+ addValidElements(settings.extended_valid_elements);
+
+ // Todo: Remove this when we fix list handling to be valid
+ addValidChildren('+ol[ul|ol],+ul[ul|ol]');
+
+
+ // Some elements are not valid by themselves - require parents
+ each({
+ dd: 'dl',
+ dt: 'dl',
+ li: 'ul ol',
+ td: 'tr',
+ th: 'tr',
+ tr: 'tbody thead tfoot',
+ tbody: 'table',
+ thead: 'table',
+ tfoot: 'table',
+ legend: 'fieldset',
+ area: 'map',
+ param: 'video audio object'
+ }, function(parents, item) {
+ if (elements[item]) {
+ elements[item].parentsRequired = split(parents);
+ }
+ });
+
+
+ // Delete invalid elements
+ if (settings.invalid_elements) {
+ each(explode(settings.invalid_elements), function(item) {
+ if (elements[item]) {
+ delete elements[item];
+ }
+ });
+ }
+
+ // If the user didn't allow span only allow internal spans
+ if (!getElementRule('span')) {
+ addValidElements('span[!data-mce-type|*]');
+ }
+
+ /**
+ * Name/value map object with valid parents and children to those parents.
+ *
+ * @example
+ * children = {
+ * div:{p:{}, h1:{}}
+ * };
+ * @field children
+ * @type Object
+ */
+ self.children = children;
+
+ /**
+ * Name/value map object with valid styles for each element.
+ *
+ * @method getValidStyles
+ * @type Object
+ */
+ self.getValidStyles = function() {
+ return validStyles;
+ };
+
+ /**
+ * Name/value map object with valid styles for each element.
+ *
+ * @method getInvalidStyles
+ * @type Object
+ */
+ self.getInvalidStyles = function() {
+ return invalidStyles;
+ };
+
+ /**
+ * Name/value map object with valid classes for each element.
+ *
+ * @method getValidClasses
+ * @type Object
+ */
+ self.getValidClasses = function() {
+ return validClasses;
+ };
+
+ /**
+ * Returns a map with boolean attributes.
+ *
+ * @method getBoolAttrs
+ * @return {Object} Name/value lookup map for boolean attributes.
+ */
+ self.getBoolAttrs = function() {
+ return boolAttrMap;
+ };
+
+ /**
+ * Returns a map with block elements.
+ *
+ * @method getBlockElements
+ * @return {Object} Name/value lookup map for block elements.
+ */
+ self.getBlockElements = function() {
+ return blockElementsMap;
+ };
+
+ /**
+ * Returns a map with text block elements. Such as: p,h1-h6,div,address
+ *
+ * @method getTextBlockElements
+ * @return {Object} Name/value lookup map for block elements.
+ */
+ self.getTextBlockElements = function() {
+ return textBlockElementsMap;
+ };
+
+ /**
+ * Returns a map of inline text format nodes for example strong/span or ins.
+ *
+ * @method getTextInlineElements
+ * @return {Object} Name/value lookup map for text format elements.
+ */
+ self.getTextInlineElements = function() {
+ return textInlineElementsMap;
+ };
+
+ /**
+ * Returns a map with short ended elements such as BR or IMG.
+ *
+ * @method getShortEndedElements
+ * @return {Object} Name/value lookup map for short ended elements.
+ */
+ self.getShortEndedElements = function() {
+ return shortEndedElementsMap;
+ };
+
+ /**
+ * Returns a map with self closing tags such as <li>.
+ *
+ * @method getSelfClosingElements
+ * @return {Object} Name/value lookup map for self closing tags elements.
+ */
+ self.getSelfClosingElements = function() {
+ return selfClosingElementsMap;
+ };
+
+ /**
+ * Returns a map with elements that should be treated as contents regardless if it has text
+ * content in them or not such as TD, VIDEO or IMG.
+ *
+ * @method getNonEmptyElements
+ * @return {Object} Name/value lookup map for non empty elements.
+ */
+ self.getNonEmptyElements = function() {
+ return nonEmptyElementsMap;
+ };
+
+ /**
+ * Returns a map with elements that the caret should be moved in front of after enter is
+ * pressed
+ *
+ * @method getMoveCaretBeforeOnEnterElements
+ * @return {Object} Name/value lookup map for elements to place the caret in front of.
+ */
+ self.getMoveCaretBeforeOnEnterElements = function() {
+ return moveCaretBeforeOnEnterElementsMap;
+ };
+
+ /**
+ * Returns a map with elements where white space is to be preserved like PRE or SCRIPT.
+ *
+ * @method getWhiteSpaceElements
+ * @return {Object} Name/value lookup map for white space elements.
+ */
+ self.getWhiteSpaceElements = function() {
+ return whiteSpaceElementsMap;
+ };
+
+ /**
+ * Returns a map with special elements. These are elements that needs to be parsed
+ * in a special way such as script, style, textarea etc. The map object values
+ * are regexps used to find the end of the element.
+ *
+ * @method getSpecialElements
+ * @return {Object} Name/value lookup map for special elements.
+ */
+ self.getSpecialElements = function() {
+ return specialElements;
+ };
+
+ /**
+ * Returns true/false if the specified element and it's child is valid or not
+ * according to the schema.
+ *
+ * @method isValidChild
+ * @param {String} name Element name to check for.
+ * @param {String} child Element child to verify.
+ * @return {Boolean} True/false if the element is a valid child of the specified parent.
+ */
+ self.isValidChild = function(name, child) {
+ var parent = children[name];
+
+ return !!(parent && parent[child]);
+ };
+
+ /**
+ * Returns true/false if the specified element name and optional attribute is
+ * valid according to the schema.
+ *
+ * @method isValid
+ * @param {String} name Name of element to check.
+ * @param {String} attr Optional attribute name to check for.
+ * @return {Boolean} True/false if the element and attribute is valid.
+ */
+ self.isValid = function(name, attr) {
+ var attrPatterns, i, rule = getElementRule(name);
+
+ // Check if it's a valid element
+ if (rule) {
+ if (attr) {
+ // Check if attribute name exists
+ if (rule.attributes[attr]) {
+ return true;
+ }
+
+ // Check if attribute matches a regexp pattern
+ attrPatterns = rule.attributePatterns;
+ if (attrPatterns) {
+ i = attrPatterns.length;
+ while (i--) {
+ if (attrPatterns[i].pattern.test(name)) {
+ return true;
+ }
+ }
+ }
+ } else {
+ return true;
+ }
+ }
+
+ // No match
+ return false;
+ };
+
+ /**
+ * Returns true/false if the specified element is valid or not
+ * according to the schema.
+ *
+ * @method getElementRule
+ * @param {String} name Element name to check for.
+ * @return {Object} Element object or undefined if the element isn't valid.
+ */
+ self.getElementRule = getElementRule;
+
+ /**
+ * Returns an map object of all custom elements.
+ *
+ * @method getCustomElements
+ * @return {Object} Name/value map object of all custom elements.
+ */
+ self.getCustomElements = function() {
+ return customElementsMap;
+ };
+
+ /**
+ * Parses a valid elements string and adds it to the schema. The valid elements
+ * format is for example "element[attr=default|otherattr]".
+ * Existing rules will be replaced with the ones specified, so this extends the schema.
+ *
+ * @method addValidElements
+ * @param {String} valid_elements String in the valid elements format to be parsed.
+ */
+ self.addValidElements = addValidElements;
+
+ /**
+ * Parses a valid elements string and sets it to the schema. The valid elements
+ * format is for example "element[attr=default|otherattr]".
+ * Existing rules will be replaced with the ones specified, so this extends the schema.
+ *
+ * @method setValidElements
+ * @param {String} valid_elements String in the valid elements format to be parsed.
+ */
+ self.setValidElements = setValidElements;
+
+ /**
+ * Adds custom non HTML elements to the schema.
+ *
+ * @method addCustomElements
+ * @param {String} custom_elements Comma separated list of custom elements to add.
+ */
+ self.addCustomElements = addCustomElements;
+
+ /**
+ * Parses a valid children string and adds them to the schema structure. The valid children
+ * format is for example: "element[child1|child2]".
+ *
+ * @method addValidChildren
+ * @param {String} valid_children Valid children elements string to parse
+ */
+ self.addValidChildren = addValidChildren;
+
+ self.elements = elements;
+ };
+});
+
+// Included from: js/tinymce/classes/html/SaxParser.js
+
+/**
+ * SaxParser.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*eslint max-depth:[2, 9] */
+
+/**
+ * This class parses HTML code using pure JavaScript and executes various events for each item it finds. It will
+ * always execute the events in the right order for tag soup code like <b><p></b></p>. It will also remove elements
+ * and attributes that doesn't fit the schema if the validate setting is enabled.
+ *
+ * @example
+ * var parser = new tinymce.html.SaxParser({
+ * validate: true,
+ *
+ * comment: function(text) {
+ * console.log('Comment:', text);
+ * },
+ *
+ * cdata: function(text) {
+ * console.log('CDATA:', text);
+ * },
+ *
+ * text: function(text, raw) {
+ * console.log('Text:', text, 'Raw:', raw);
+ * },
+ *
+ * start: function(name, attrs, empty) {
+ * console.log('Start:', name, attrs, empty);
+ * },
+ *
+ * end: function(name) {
+ * console.log('End:', name);
+ * },
+ *
+ * pi: function(name, text) {
+ * console.log('PI:', name, text);
+ * },
+ *
+ * doctype: function(text) {
+ * console.log('DocType:', text);
+ * }
+ * }, schema);
+ * @class tinymce.html.SaxParser
+ * @version 3.4
+ */
+define("tinymce/html/SaxParser", [
+ "tinymce/html/Schema",
+ "tinymce/html/Entities",
+ "tinymce/util/Tools"
+], function(Schema, Entities, Tools) {
+ var each = Tools.each;
+
+ /**
+ * Returns the index of the end tag for a specific start tag. This can be
+ * used to skip all children of a parent element from being processed.
+ *
+ * @private
+ * @method findEndTag
+ * @param {tinymce.html.Schema} schema Schema instance to use to match short ended elements.
+ * @param {String} html HTML string to find the end tag in.
+ * @param {Number} startIndex Indext to start searching at should be after the start tag.
+ * @return {Number} Index of the end tag.
+ */
+ function findEndTag(schema, html, startIndex) {
+ var count = 1, index, matches, tokenRegExp, shortEndedElements;
+
+ shortEndedElements = schema.getShortEndedElements();
+ tokenRegExp = /<([!?\/])?([A-Za-z0-9\-_\:\.]+)((?:\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\/|\s+)>/g;
+ tokenRegExp.lastIndex = index = startIndex;
+
+ while ((matches = tokenRegExp.exec(html))) {
+ index = tokenRegExp.lastIndex;
+
+ if (matches[1] === '/') { // End element
+ count--;
+ } else if (!matches[1]) { // Start element
+ if (matches[2] in shortEndedElements) {
+ continue;
+ }
+
+ count++;
+ }
+
+ if (count === 0) {
+ break;
+ }
+ }
+
+ return index;
+ }
+
+ /**
+ * Constructs a new SaxParser instance.
+ *
+ * @constructor
+ * @method SaxParser
+ * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks.
+ * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing.
+ */
+ function SaxParser(settings, schema) {
+ var self = this;
+
+ function noop() {}
+
+ settings = settings || {};
+ self.schema = schema = schema || new Schema();
+
+ if (settings.fix_self_closing !== false) {
+ settings.fix_self_closing = true;
+ }
+
+ // Add handler functions from settings and setup default handlers
+ each('comment cdata text start end pi doctype'.split(' '), function(name) {
+ if (name) {
+ self[name] = settings[name] || noop;
+ }
+ });
+
+ /**
+ * Parses the specified HTML string and executes the callbacks for each item it finds.
+ *
+ * @example
+ * new SaxParser({...}).parse('<b>text</b>');
+ * @method parse
+ * @param {String} html Html string to sax parse.
+ */
+ self.parse = function(html) {
+ var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name;
+ var isInternalElement, removeInternalElements, shortEndedElements, fillAttrsMap, isShortEnded;
+ var validate, elementRule, isValidElement, attr, attribsValue, validAttributesMap, validAttributePatterns;
+ var attributesRequired, attributesDefault, attributesForced;
+ var anyAttributesRequired, selfClosing, tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0;
+ var decode = Entities.decode, fixSelfClosing, filteredUrlAttrs = Tools.makeMap('src,href,data,background,formaction,poster');
+ var scriptUriRegExp = /((java|vb)script|mhtml):/i, dataUriRegExp = /^data:/i;
+
+ function processEndTag(name) {
+ var pos, i;
+
+ // Find position of parent of the same type
+ pos = stack.length;
+ while (pos--) {
+ if (stack[pos].name === name) {
+ break;
+ }
+ }
+
+ // Found parent
+ if (pos >= 0) {
+ // Close all the open elements
+ for (i = stack.length - 1; i >= pos; i--) {
+ name = stack[i];
+
+ if (name.valid) {
+ self.end(name.name);
+ }
+ }
+
+ // Remove the open elements from the stack
+ stack.length = pos;
+ }
+ }
+
+ function parseAttribute(match, name, value, val2, val3) {
+ var attrRule, i, trimRegExp = /[\s\u0000-\u001F]+/g;
+
+ name = name.toLowerCase();
+ value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute
+
+ // Validate name and value pass through all data- attributes
+ if (validate && !isInternalElement && name.indexOf('data-') !== 0) {
+ attrRule = validAttributesMap[name];
+
+ // Find rule by pattern matching
+ if (!attrRule && validAttributePatterns) {
+ i = validAttributePatterns.length;
+ while (i--) {
+ attrRule = validAttributePatterns[i];
+ if (attrRule.pattern.test(name)) {
+ break;
+ }
+ }
+
+ // No rule matched
+ if (i === -1) {
+ attrRule = null;
+ }
+ }
+
+ // No attribute rule found
+ if (!attrRule) {
+ return;
+ }
+
+ // Validate value
+ if (attrRule.validValues && !(value in attrRule.validValues)) {
+ return;
+ }
+ }
+
+ // Block any javascript: urls or non image data uris
+ if (filteredUrlAttrs[name] && !settings.allow_script_urls) {
+ var uri = value.replace(trimRegExp, '');
+
+ try {
+ // Might throw malformed URI sequence
+ uri = decodeURIComponent(uri);
+ } catch (ex) {
+ // Fallback to non UTF-8 decoder
+ uri = unescape(uri);
+ }
+
+ if (scriptUriRegExp.test(uri)) {
+ return;
+ }
+
+ if (!settings.allow_html_data_urls && dataUriRegExp.test(uri) && !/^data:image\//i.test(uri)) {
+ return;
+ }
+ }
+
+ // Add attribute to list and map
+ attrList.map[name] = value;
+ attrList.push({
+ name: name,
+ value: value
+ });
+ }
+
+ // Precompile RegExps and map objects
+ tokenRegExp = new RegExp('<(?:' +
+ '(?:!--([\\w\\W]*?)-->)|' + // Comment
+ '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA
+ '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE
+ '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI
+ '(?:\\/([^>]+)>)|' + // End element
+ '(?:([A-Za-z0-9\\-_\\:\\.]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element
+ ')', 'g');
+
+ attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g;
+
+ // Setup lookup tables for empty elements and boolean attributes
+ shortEndedElements = schema.getShortEndedElements();
+ selfClosing = settings.self_closing_elements || schema.getSelfClosingElements();
+ fillAttrsMap = schema.getBoolAttrs();
+ validate = settings.validate;
+ removeInternalElements = settings.remove_internals;
+ fixSelfClosing = settings.fix_self_closing;
+ specialElements = schema.getSpecialElements();
+
+ while ((matches = tokenRegExp.exec(html))) {
+ // Text
+ if (index < matches.index) {
+ self.text(decode(html.substr(index, matches.index - index)));
+ }
+
+ if ((value = matches[6])) { // End element
+ value = value.toLowerCase();
+
+ // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
+ if (value.charAt(0) === ':') {
+ value = value.substr(1);
+ }
+
+ processEndTag(value);
+ } else if ((value = matches[7])) { // Start element
+ value = value.toLowerCase();
+
+ // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
+ if (value.charAt(0) === ':') {
+ value = value.substr(1);
+ }
+
+ isShortEnded = value in shortEndedElements;
+
+ // Is self closing tag for example an <li> after an open <li>
+ if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) {
+ processEndTag(value);
+ }
+
+ // Validate element
+ if (!validate || (elementRule = schema.getElementRule(value))) {
+ isValidElement = true;
+
+ // Grab attributes map and patters when validation is enabled
+ if (validate) {
+ validAttributesMap = elementRule.attributes;
+ validAttributePatterns = elementRule.attributePatterns;
+ }
+
+ // Parse attributes
+ if ((attribsValue = matches[8])) {
+ isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element
+
+ // If the element has internal attributes then remove it if we are told to do so
+ if (isInternalElement && removeInternalElements) {
+ isValidElement = false;
+ }
+
+ attrList = [];
+ attrList.map = {};
+
+ attribsValue.replace(attrRegExp, parseAttribute);
+ } else {
+ attrList = [];
+ attrList.map = {};
+ }
+
+ // Process attributes if validation is enabled
+ if (validate && !isInternalElement) {
+ attributesRequired = elementRule.attributesRequired;
+ attributesDefault = elementRule.attributesDefault;
+ attributesForced = elementRule.attributesForced;
+ anyAttributesRequired = elementRule.removeEmptyAttrs;
+
+ // Check if any attribute exists
+ if (anyAttributesRequired && !attrList.length) {
+ isValidElement = false;
+ }
+
+ // Handle forced attributes
+ if (attributesForced) {
+ i = attributesForced.length;
+ while (i--) {
+ attr = attributesForced[i];
+ name = attr.name;
+ attrValue = attr.value;
+
+ if (attrValue === '{$uid}') {
+ attrValue = 'mce_' + idCount++;
+ }
+
+ attrList.map[name] = attrValue;
+ attrList.push({name: name, value: attrValue});
+ }
+ }
+
+ // Handle default attributes
+ if (attributesDefault) {
+ i = attributesDefault.length;
+ while (i--) {
+ attr = attributesDefault[i];
+ name = attr.name;
+
+ if (!(name in attrList.map)) {
+ attrValue = attr.value;
+
+ if (attrValue === '{$uid}') {
+ attrValue = 'mce_' + idCount++;
+ }
+
+ attrList.map[name] = attrValue;
+ attrList.push({name: name, value: attrValue});
+ }
+ }
+ }
+
+ // Handle required attributes
+ if (attributesRequired) {
+ i = attributesRequired.length;
+ while (i--) {
+ if (attributesRequired[i] in attrList.map) {
+ break;
+ }
+ }
+
+ // None of the required attributes where found
+ if (i === -1) {
+ isValidElement = false;
+ }
+ }
+
+ // Invalidate element if it's marked as bogus
+ if ((attr = attrList.map['data-mce-bogus'])) {
+ if (attr === 'all') {
+ index = findEndTag(schema, html, tokenRegExp.lastIndex);
+ tokenRegExp.lastIndex = index;
+ continue;
+ }
+
+ isValidElement = false;
+ }
+ }
+
+ if (isValidElement) {
+ self.start(value, attrList, isShortEnded);
+ }
+ } else {
+ isValidElement = false;
+ }
+
+ // Treat script, noscript and style a bit different since they may include code that looks like elements
+ if ((endRegExp = specialElements[value])) {
+ endRegExp.lastIndex = index = matches.index + matches[0].length;
+
+ if ((matches = endRegExp.exec(html))) {
+ if (isValidElement) {
+ text = html.substr(index, matches.index - index);
+ }
+
+ index = matches.index + matches[0].length;
+ } else {
+ text = html.substr(index);
+ index = html.length;
+ }
+
+ if (isValidElement) {
+ if (text.length > 0) {
+ self.text(text, true);
+ }
+
+ self.end(value);
+ }
+
+ tokenRegExp.lastIndex = index;
+ continue;
+ }
+
+ // Push value on to stack
+ if (!isShortEnded) {
+ if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1) {
+ stack.push({name: value, valid: isValidElement});
+ } else if (isValidElement) {
+ self.end(value);
+ }
+ }
+ } else if ((value = matches[1])) { // Comment
+ // Padd comment value to avoid browsers from parsing invalid comments as HTML
+ if (value.charAt(0) === '>') {
+ value = ' ' + value;
+ }
+
+ if (!settings.allow_conditional_comments && value.substr(0, 3).toLowerCase() === '[if') {
+ value = ' ' + value;
+ }
+
+ self.comment(value);
+ } else if ((value = matches[2])) { // CDATA
+ self.cdata(value);
+ } else if ((value = matches[3])) { // DOCTYPE
+ self.doctype(value);
+ } else if ((value = matches[4])) { // PI
+ self.pi(value, matches[5]);
+ }
+
+ index = matches.index + matches[0].length;
+ }
+
+ // Text
+ if (index < html.length) {
+ self.text(decode(html.substr(index)));
+ }
+
+ // Close any open elements
+ for (i = stack.length - 1; i >= 0; i--) {
+ value = stack[i];
+
+ if (value.valid) {
+ self.end(value.name);
+ }
+ }
+ };
+ }
+
+ SaxParser.findEndTag = findEndTag;
+
+ return SaxParser;
+});
+
+// Included from: js/tinymce/classes/html/DomParser.js
+
+/**
+ * DomParser.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class parses HTML code into a DOM like structure of nodes it will remove redundant whitespace and make
+ * sure that the node tree is valid according to the specified schema.
+ * So for example: <p>a<p>b</p>c</p> will become <p>a</p><p>b</p><p>c</p>
+ *
+ * @example
+ * var parser = new tinymce.html.DomParser({validate: true}, schema);
+ * var rootNode = parser.parse('<h1>content</h1>');
+ *
+ * @class tinymce.html.DomParser
+ * @version 3.4
+ */
+define("tinymce/html/DomParser", [
+ "tinymce/html/Node",
+ "tinymce/html/Schema",
+ "tinymce/html/SaxParser",
+ "tinymce/util/Tools"
+], function(Node, Schema, SaxParser, Tools) {
+ var makeMap = Tools.makeMap, each = Tools.each, explode = Tools.explode, extend = Tools.extend;
+
+ /**
+ * Constructs a new DomParser instance.
+ *
+ * @constructor
+ * @method DomParser
+ * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks.
+ * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing.
+ */
+ return function(settings, schema) {
+ var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {};
+
+ settings = settings || {};
+ settings.validate = "validate" in settings ? settings.validate : true;
+ settings.root_name = settings.root_name || 'body';
+ self.schema = schema = schema || new Schema();
+
+ function fixInvalidChildren(nodes) {
+ var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i;
+ var nonEmptyElements, nonSplitableElements, textBlockElements, specialElements, sibling, nextNode;
+
+ nonSplitableElements = makeMap('tr,td,th,tbody,thead,tfoot,table');
+ nonEmptyElements = schema.getNonEmptyElements();
+ textBlockElements = schema.getTextBlockElements();
+ specialElements = schema.getSpecialElements();
+
+ for (ni = 0; ni < nodes.length; ni++) {
+ node = nodes[ni];
+
+ // Already removed or fixed
+ if (!node.parent || node.fixed) {
+ continue;
+ }
+
+ // If the invalid element is a text block and the text block is within a parent LI element
+ // Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office
+ if (textBlockElements[node.name] && node.parent.name == 'li') {
+ // Move sibling text blocks after LI element
+ sibling = node.next;
+ while (sibling) {
+ if (textBlockElements[sibling.name]) {
+ sibling.name = 'li';
+ sibling.fixed = true;
+ node.parent.insert(sibling, node.parent);
+ } else {
+ break;
+ }
+
+ sibling = sibling.next;
+ }
+
+ // Unwrap current text block
+ node.unwrap(node);
+ continue;
+ }
+
+ // Get list of all parent nodes until we find a valid parent to stick the child into
+ parents = [node];
+ for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) &&
+ !nonSplitableElements[parent.name]; parent = parent.parent) {
+ parents.push(parent);
+ }
+
+ // Found a suitable parent
+ if (parent && parents.length > 1) {
+ // Reverse the array since it makes looping easier
+ parents.reverse();
+
+ // Clone the related parent and insert that after the moved node
+ newParent = currentNode = self.filterNode(parents[0].clone());
+
+ // Start cloning and moving children on the left side of the target node
+ for (i = 0; i < parents.length - 1; i++) {
+ if (schema.isValidChild(currentNode.name, parents[i].name)) {
+ tempNode = self.filterNode(parents[i].clone());
+ currentNode.append(tempNode);
+ } else {
+ tempNode = currentNode;
+ }
+
+ for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1];) {
+ nextNode = childNode.next;
+ tempNode.append(childNode);
+ childNode = nextNode;
+ }
+
+ currentNode = tempNode;
+ }
+
+ if (!newParent.isEmpty(nonEmptyElements)) {
+ parent.insert(newParent, parents[0], true);
+ parent.insert(node, newParent);
+ } else {
+ parent.insert(node, parents[0], true);
+ }
+
+ // Check if the element is empty by looking through it's contents and special treatment for <p><br /></p>
+ parent = parents[0];
+ if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') {
+ parent.empty().remove();
+ }
+ } else if (node.parent) {
+ // If it's an LI try to find a UL/OL for it or wrap it
+ if (node.name === 'li') {
+ sibling = node.prev;
+ if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
+ sibling.append(node);
+ continue;
+ }
+
+ sibling = node.next;
+ if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
+ sibling.insert(node, sibling.firstChild, true);
+ continue;
+ }
+
+ node.wrap(self.filterNode(new Node('ul', 1)));
+ continue;
+ }
+
+ // Try wrapping the element in a DIV
+ if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) {
+ node.wrap(self.filterNode(new Node('div', 1)));
+ } else {
+ // We failed wrapping it, then remove or unwrap it
+ if (specialElements[node.name]) {
+ node.empty().remove();
+ } else {
+ node.unwrap();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Runs the specified node though the element and attributes filters.
+ *
+ * @method filterNode
+ * @param {tinymce.html.Node} Node the node to run filters on.
+ * @return {tinymce.html.Node} The passed in node.
+ */
+ self.filterNode = function(node) {
+ var i, name, list;
+
+ // Run element filters
+ if (name in nodeFilters) {
+ list = matchedNodes[name];
+
+ if (list) {
+ list.push(node);
+ } else {
+ matchedNodes[name] = [node];
+ }
+ }
+
+ // Run attribute filters
+ i = attributeFilters.length;
+ while (i--) {
+ name = attributeFilters[i].name;
+
+ if (name in node.attributes.map) {
+ list = matchedAttributes[name];
+
+ if (list) {
+ list.push(node);
+ } else {
+ matchedAttributes[name] = [node];
+ }
+ }
+ }
+
+ return node;
+ };
+
+ /**
+ * Adds a node filter function to the parser, the parser will collect the specified nodes by name
+ * and then execute the callback ones it has finished parsing the document.
+ *
+ * @example
+ * parser.addNodeFilter('p,h1', function(nodes, name) {
+ * for (var i = 0; i < nodes.length; i++) {
+ * console.log(nodes[i].name);
+ * }
+ * });
+ * @method addNodeFilter
+ * @method {String} name Comma separated list of nodes to collect.
+ * @param {function} callback Callback function to execute once it has collected nodes.
+ */
+ self.addNodeFilter = function(name, callback) {
+ each(explode(name), function(name) {
+ var list = nodeFilters[name];
+
+ if (!list) {
+ nodeFilters[name] = list = [];
+ }
+
+ list.push(callback);
+ });
+ };
+
+ /**
+ * Adds a attribute filter function to the parser, the parser will collect nodes that has the specified attributes
+ * and then execute the callback ones it has finished parsing the document.
+ *
+ * @example
+ * parser.addAttributeFilter('src,href', function(nodes, name) {
+ * for (var i = 0; i < nodes.length; i++) {
+ * console.log(nodes[i].name);
+ * }
+ * });
+ * @method addAttributeFilter
+ * @method {String} name Comma separated list of nodes to collect.
+ * @param {function} callback Callback function to execute once it has collected nodes.
+ */
+ self.addAttributeFilter = function(name, callback) {
+ each(explode(name), function(name) {
+ var i;
+
+ for (i = 0; i < attributeFilters.length; i++) {
+ if (attributeFilters[i].name === name) {
+ attributeFilters[i].callbacks.push(callback);
+ return;
+ }
+ }
+
+ attributeFilters.push({name: name, callbacks: [callback]});
+ });
+ };
+
+ /**
+ * Parses the specified HTML string into a DOM like node tree and returns the result.
+ *
+ * @example
+ * var rootNode = new DomParser({...}).parse('<b>text</b>');
+ * @method parse
+ * @param {String} html Html string to sax parse.
+ * @param {Object} args Optional args object that gets passed to all filter functions.
+ * @return {tinymce.html.Node} Root node containing the tree.
+ */
+ self.parse = function(html, args) {
+ var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate;
+ var blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement;
+ var endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements;
+ var children, nonEmptyElements, rootBlockName;
+
+ args = args || {};
+ matchedNodes = {};
+ matchedAttributes = {};
+ blockElements = extend(makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements());
+ nonEmptyElements = schema.getNonEmptyElements();
+ children = schema.children;
+ validate = settings.validate;
+ rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block;
+
+ whiteSpaceElements = schema.getWhiteSpaceElements();
+ startWhiteSpaceRegExp = /^[ \t\r\n]+/;
+ endWhiteSpaceRegExp = /[ \t\r\n]+$/;
+ allWhiteSpaceRegExp = /[ \t\r\n]+/g;
+ isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/;
+
+ function addRootBlocks() {
+ var node = rootNode.firstChild, next, rootBlockNode;
+
+ // Removes whitespace at beginning and end of block so:
+ // <p> x </p> -> <p>x</p>
+ function trim(rootBlockNode) {
+ if (rootBlockNode) {
+ node = rootBlockNode.firstChild;
+ if (node && node.type == 3) {
+ node.value = node.value.replace(startWhiteSpaceRegExp, '');
+ }
+
+ node = rootBlockNode.lastChild;
+ if (node && node.type == 3) {
+ node.value = node.value.replace(endWhiteSpaceRegExp, '');
+ }
+ }
+ }
+
+ // Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root
+ if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) {
+ return;
+ }
+
+ while (node) {
+ next = node.next;
+
+ if (node.type == 3 || (node.type == 1 && node.name !== 'p' &&
+ !blockElements[node.name] && !node.attr('data-mce-type'))) {
+ if (!rootBlockNode) {
+ // Create a new root block element
+ rootBlockNode = createNode(rootBlockName, 1);
+ rootBlockNode.attr(settings.forced_root_block_attrs);
+ rootNode.insert(rootBlockNode, node);
+ rootBlockNode.append(node);
+ } else {
+ rootBlockNode.append(node);
+ }
+ } else {
+ trim(rootBlockNode);
+ rootBlockNode = null;
+ }
+
+ node = next;
+ }
+
+ trim(rootBlockNode);
+ }
+
+ function createNode(name, type) {
+ var node = new Node(name, type), list;
+
+ if (name in nodeFilters) {
+ list = matchedNodes[name];
+
+ if (list) {
+ list.push(node);
+ } else {
+ matchedNodes[name] = [node];
+ }
+ }
+
+ return node;
+ }
+
+ function removeWhitespaceBefore(node) {
+ var textNode, textNodeNext, textVal, sibling, blockElements = schema.getBlockElements();
+
+ for (textNode = node.prev; textNode && textNode.type === 3;) {
+ textVal = textNode.value.replace(endWhiteSpaceRegExp, '');
+
+ // Found a text node with non whitespace then trim that and break
+ if (textVal.length > 0) {
+ textNode.value = textVal;
+ return;
+ }
+
+ textNodeNext = textNode.next;
+
+ // Fix for bug #7543 where bogus nodes would produce empty
+ // text nodes and these would be removed if a nested list was before it
+ if (textNodeNext) {
+ if (textNodeNext.type == 3 && textNodeNext.value.length) {
+ textNode = textNode.prev;
+ continue;
+ }
+
+ if (!blockElements[textNodeNext.name] && textNodeNext.name != 'script' && textNodeNext.name != 'style') {
+ textNode = textNode.prev;
+ continue;
+ }
+ }
+
+ sibling = textNode.prev;
+ textNode.remove();
+ textNode = sibling;
+ }
+ }
+
+ function cloneAndExcludeBlocks(input) {
+ var name, output = {};
+
+ for (name in input) {
+ if (name !== 'li' && name != 'p') {
+ output[name] = input[name];
+ }
+ }
+
+ return output;
+ }
+
+ parser = new SaxParser({
+ validate: validate,
+ allow_script_urls: settings.allow_script_urls,
+ allow_conditional_comments: settings.allow_conditional_comments,
+
+ // Exclude P and LI from DOM parsing since it's treated better by the DOM parser
+ self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()),
+
+ cdata: function(text) {
+ node.append(createNode('#cdata', 4)).value = text;
+ },
+
+ text: function(text, raw) {
+ var textNode;
+
+ // Trim all redundant whitespace on non white space elements
+ if (!isInWhiteSpacePreservedElement) {
+ text = text.replace(allWhiteSpaceRegExp, ' ');
+
+ if (node.lastChild && blockElements[node.lastChild.name]) {
+ text = text.replace(startWhiteSpaceRegExp, '');
+ }
+ }
+
+ // Do we need to create the node
+ if (text.length !== 0) {
+ textNode = createNode('#text', 3);
+ textNode.raw = !!raw;
+ node.append(textNode).value = text;
+ }
+ },
+
+ comment: function(text) {
+ node.append(createNode('#comment', 8)).value = text;
+ },
+
+ pi: function(name, text) {
+ node.append(createNode(name, 7)).value = text;
+ removeWhitespaceBefore(node);
+ },
+
+ doctype: function(text) {
+ var newNode;
+
+ newNode = node.append(createNode('#doctype', 10));
+ newNode.value = text;
+ removeWhitespaceBefore(node);
+ },
+
+ start: function(name, attrs, empty) {
+ var newNode, attrFiltersLen, elementRule, attrName, parent;
+
+ elementRule = validate ? schema.getElementRule(name) : {};
+ if (elementRule) {
+ newNode = createNode(elementRule.outputName || name, 1);
+ newNode.attributes = attrs;
+ newNode.shortEnded = empty;
+
+ node.append(newNode);
+
+ // Check if node is valid child of the parent node is the child is
+ // unknown we don't collect it since it's probably a custom element
+ parent = children[node.name];
+ if (parent && children[newNode.name] && !parent[newNode.name]) {
+ invalidChildren.push(newNode);
+ }
+
+ attrFiltersLen = attributeFilters.length;
+ while (attrFiltersLen--) {
+ attrName = attributeFilters[attrFiltersLen].name;
+
+ if (attrName in attrs.map) {
+ list = matchedAttributes[attrName];
+
+ if (list) {
+ list.push(newNode);
+ } else {
+ matchedAttributes[attrName] = [newNode];
+ }
+ }
+ }
+
+ // Trim whitespace before block
+ if (blockElements[name]) {
+ removeWhitespaceBefore(newNode);
+ }
+
+ // Change current node if the element wasn't empty i.e not <br /> or <img />
+ if (!empty) {
+ node = newNode;
+ }
+
+ // Check if we are inside a whitespace preserved element
+ if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
+ isInWhiteSpacePreservedElement = true;
+ }
+ }
+ },
+
+ end: function(name) {
+ var textNode, elementRule, text, sibling, tempNode;
+
+ elementRule = validate ? schema.getElementRule(name) : {};
+ if (elementRule) {
+ if (blockElements[name]) {
+ if (!isInWhiteSpacePreservedElement) {
+ // Trim whitespace of the first node in a block
+ textNode = node.firstChild;
+ if (textNode && textNode.type === 3) {
+ text = textNode.value.replace(startWhiteSpaceRegExp, '');
+
+ // Any characters left after trim or should we remove it
+ if (text.length > 0) {
+ textNode.value = text;
+ textNode = textNode.next;
+ } else {
+ sibling = textNode.next;
+ textNode.remove();
+ textNode = sibling;
+
+ // Remove any pure whitespace siblings
+ while (textNode && textNode.type === 3) {
+ text = textNode.value;
+ sibling = textNode.next;
+
+ if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
+ textNode.remove();
+ textNode = sibling;
+ }
+
+ textNode = sibling;
+ }
+ }
+ }
+
+ // Trim whitespace of the last node in a block
+ textNode = node.lastChild;
+ if (textNode && textNode.type === 3) {
+ text = textNode.value.replace(endWhiteSpaceRegExp, '');
+
+ // Any characters left after trim or should we remove it
+ if (text.length > 0) {
+ textNode.value = text;
+ textNode = textNode.prev;
+ } else {
+ sibling = textNode.prev;
+ textNode.remove();
+ textNode = sibling;
+
+ // Remove any pure whitespace siblings
+ while (textNode && textNode.type === 3) {
+ text = textNode.value;
+ sibling = textNode.prev;
+
+ if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
+ textNode.remove();
+ textNode = sibling;
+ }
+
+ textNode = sibling;
+ }
+ }
+ }
+ }
+
+ // Trim start white space
+ // Removed due to: #5424
+ /*textNode = node.prev;
+ if (textNode && textNode.type === 3) {
+ text = textNode.value.replace(startWhiteSpaceRegExp, '');
+
+ if (text.length > 0)
+ textNode.value = text;
+ else
+ textNode.remove();
+ }*/
+ }
+
+ // Check if we exited a whitespace preserved element
+ if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
+ isInWhiteSpacePreservedElement = false;
+ }
+
+ // Handle empty nodes
+ if (elementRule.removeEmpty || elementRule.paddEmpty) {
+ if (node.isEmpty(nonEmptyElements)) {
+ if (elementRule.paddEmpty) {
+ node.empty().append(new Node('#text', '3')).value = '\u00a0';
+ } else {
+ // Leave nodes that have a name like <a name="name">
+ if (!node.attributes.map.name && !node.attributes.map.id) {
+ tempNode = node.parent;
+
+ if (blockElements[node.name]) {
+ node.empty().remove();
+ } else {
+ node.unwrap();
+ }
+
+ node = tempNode;
+ return;
+ }
+ }
+ }
+ }
+
+ node = node.parent;
+ }
+ }
+ }, schema);
+
+ rootNode = node = new Node(args.context || settings.root_name, 11);
+
+ parser.parse(html);
+
+ // Fix invalid children or report invalid children in a contextual parsing
+ if (validate && invalidChildren.length) {
+ if (!args.context) {
+ fixInvalidChildren(invalidChildren);
+ } else {
+ args.invalid = true;
+ }
+ }
+
+ // Wrap nodes in the root into block elements if the root is body
+ if (rootBlockName && (rootNode.name == 'body' || args.isRootContent)) {
+ addRootBlocks();
+ }
+
+ // Run filters only when the contents is valid
+ if (!args.invalid) {
+ // Run node filters
+ for (name in matchedNodes) {
+ list = nodeFilters[name];
+ nodes = matchedNodes[name];
+
+ // Remove already removed children
+ fi = nodes.length;
+ while (fi--) {
+ if (!nodes[fi].parent) {
+ nodes.splice(fi, 1);
+ }
+ }
+
+ for (i = 0, l = list.length; i < l; i++) {
+ list[i](nodes, name, args);
+ }
+ }
+
+ // Run attribute filters
+ for (i = 0, l = attributeFilters.length; i < l; i++) {
+ list = attributeFilters[i];
+
+ if (list.name in matchedAttributes) {
+ nodes = matchedAttributes[list.name];
+
+ // Remove already removed children
+ fi = nodes.length;
+ while (fi--) {
+ if (!nodes[fi].parent) {
+ nodes.splice(fi, 1);
+ }
+ }
+
+ for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) {
+ list.callbacks[fi](nodes, list.name, args);
+ }
+ }
+ }
+ }
+
+ return rootNode;
+ };
+
+ // Remove <br> at end of block elements Gecko and WebKit injects BR elements to
+ // make it possible to place the caret inside empty blocks. This logic tries to remove
+ // these elements and keep br elements that where intended to be there intact
+ if (settings.remove_trailing_brs) {
+ self.addNodeFilter('br', function(nodes) {
+ var i, l = nodes.length, node, blockElements = extend({}, schema.getBlockElements());
+ var nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName;
+ var elementRule, textNode;
+
+ // Remove brs from body element as well
+ blockElements.body = 1;
+
+ // Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p>
+ for (i = 0; i < l; i++) {
+ node = nodes[i];
+ parent = node.parent;
+
+ if (blockElements[node.parent.name] && node === parent.lastChild) {
+ // Loop all nodes to the left of the current node and check for other BR elements
+ // excluding bookmarks since they are invisible
+ prev = node.prev;
+ while (prev) {
+ prevName = prev.name;
+
+ // Ignore bookmarks
+ if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') {
+ // Found a non BR element
+ if (prevName !== "br") {
+ break;
+ }
+
+ // Found another br it's a <br><br> structure then don't remove anything
+ if (prevName === 'br') {
+ node = null;
+ break;
+ }
+ }
+
+ prev = prev.prev;
+ }
+
+ if (node) {
+ node.remove();
+
+ // Is the parent to be considered empty after we removed the BR
+ if (parent.isEmpty(nonEmptyElements)) {
+ elementRule = schema.getElementRule(parent.name);
+
+ // Remove or padd the element depending on schema rule
+ if (elementRule) {
+ if (elementRule.removeEmpty) {
+ parent.remove();
+ } else if (elementRule.paddEmpty) {
+ parent.empty().append(new Node('#text', 3)).value = '\u00a0';
+ }
+ }
+ }
+ }
+ } else {
+ // Replaces BR elements inside inline elements like <p><b><i><br></i></b></p>
+ // so they become <p><b><i> </i></b></p>
+ lastParent = node;
+ while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) {
+ lastParent = parent;
+
+ if (blockElements[parent.name]) {
+ break;
+ }
+
+ parent = parent.parent;
+ }
+
+ if (lastParent === parent) {
+ textNode = new Node('#text', 3);
+ textNode.value = '\u00a0';
+ node.replace(textNode);
+ }
+ }
+ }
+ });
+ }
+
+ if (!settings.allow_unsafe_link_target) {
+ self.addAttributeFilter('href', function(nodes) {
+ var i = nodes.length, node, rel;
+ var rules = 'noopener noreferrer';
+
+ function addTargetRules(rel) {
+ rel = removeTargetRules(rel);
+ return rel ? [rel, rules].join(' ') : rules;
+ }
+
+ function removeTargetRules(rel) {
+ var regExp = new RegExp('(' + rules.replace(' ', '|') + ')', 'g');
+ if (rel) {
+ rel = Tools.trim(rel.replace(regExp, ''));
+ }
+ return rel ? rel : null;
+ }
+
+ function toggleTargetRules(rel, isUnsafe) {
+ return isUnsafe ? addTargetRules(rel) : removeTargetRules(rel);
+ }
+
+ while (i--) {
+ node = nodes[i];
+ rel = node.attr('rel');
+ if (node.name === 'a') {
+ node.attr('rel', toggleTargetRules(rel, node.attr('target') == '_blank'));
+ }
+ }
+ });
+ }
+
+ // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included.
+ if (!settings.allow_html_in_named_anchor) {
+ self.addAttributeFilter('id,name', function(nodes) {
+ var i = nodes.length, sibling, prevSibling, parent, node;
+
+ while (i--) {
+ node = nodes[i];
+ if (node.name === 'a' && node.firstChild && !node.attr('href')) {
+ parent = node.parent;
+
+ // Move children after current node
+ sibling = node.lastChild;
+ do {
+ prevSibling = sibling.prev;
+ parent.insert(sibling, node);
+ sibling = prevSibling;
+ } while (sibling);
+ }
+ }
+ });
+ }
+
+ if (settings.validate && schema.getValidClasses()) {
+ self.addAttributeFilter('class', function(nodes) {
+ var i = nodes.length, node, classList, ci, className, classValue;
+ var validClasses = schema.getValidClasses(), validClassesMap, valid;
+
+ while (i--) {
+ node = nodes[i];
+ classList = node.attr('class').split(' ');
+ classValue = '';
+
+ for (ci = 0; ci < classList.length; ci++) {
+ className = classList[ci];
+ valid = false;
+
+ validClassesMap = validClasses['*'];
+ if (validClassesMap && validClassesMap[className]) {
+ valid = true;
+ }
+
+ validClassesMap = validClasses[node.name];
+ if (!valid && validClassesMap && validClassesMap[className]) {
+ valid = true;
+ }
+
+ if (valid) {
+ if (classValue) {
+ classValue += ' ';
+ }
+
+ classValue += className;
+ }
+ }
+
+ if (!classValue.length) {
+ classValue = null;
+ }
+
+ node.attr('class', classValue);
+ }
+ });
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/html/Writer.js
+
+/**
+ * Writer.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class is used to write HTML tags out it can be used with the Serializer or the SaxParser.
+ *
+ * @class tinymce.html.Writer
+ * @example
+ * var writer = new tinymce.html.Writer({indent: true});
+ * var parser = new tinymce.html.SaxParser(writer).parse('<p><br></p>');
+ * console.log(writer.getContent());
+ *
+ * @class tinymce.html.Writer
+ * @version 3.4
+ */
+define("tinymce/html/Writer", [
+ "tinymce/html/Entities",
+ "tinymce/util/Tools"
+], function(Entities, Tools) {
+ var makeMap = Tools.makeMap;
+
+ /**
+ * Constructs a new Writer instance.
+ *
+ * @constructor
+ * @method Writer
+ * @param {Object} settings Name/value settings object.
+ */
+ return function(settings) {
+ var html = [], indent, indentBefore, indentAfter, encode, htmlOutput;
+
+ settings = settings || {};
+ indent = settings.indent;
+ indentBefore = makeMap(settings.indent_before || '');
+ indentAfter = makeMap(settings.indent_after || '');
+ encode = Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities);
+ htmlOutput = settings.element_format == "html";
+
+ return {
+ /**
+ * Writes the a start element such as <p id="a">.
+ *
+ * @method start
+ * @param {String} name Name of the element.
+ * @param {Array} attrs Optional attribute array or undefined if it hasn't any.
+ * @param {Boolean} empty Optional empty state if the tag should end like <br />.
+ */
+ start: function(name, attrs, empty) {
+ var i, l, attr, value;
+
+ if (indent && indentBefore[name] && html.length > 0) {
+ value = html[html.length - 1];
+
+ if (value.length > 0 && value !== '\n') {
+ html.push('\n');
+ }
+ }
+
+ html.push('<', name);
+
+ if (attrs) {
+ for (i = 0, l = attrs.length; i < l; i++) {
+ attr = attrs[i];
+ html.push(' ', attr.name, '="', encode(attr.value, true), '"');
+ }
+ }
+
+ if (!empty || htmlOutput) {
+ html[html.length] = '>';
+ } else {
+ html[html.length] = ' />';
+ }
+
+ if (empty && indent && indentAfter[name] && html.length > 0) {
+ value = html[html.length - 1];
+
+ if (value.length > 0 && value !== '\n') {
+ html.push('\n');
+ }
+ }
+ },
+
+ /**
+ * Writes the a end element such as </p>.
+ *
+ * @method end
+ * @param {String} name Name of the element.
+ */
+ end: function(name) {
+ var value;
+
+ /*if (indent && indentBefore[name] && html.length > 0) {
+ value = html[html.length - 1];
+
+ if (value.length > 0 && value !== '\n')
+ html.push('\n');
+ }*/
+
+ html.push('</', name, '>');
+
+ if (indent && indentAfter[name] && html.length > 0) {
+ value = html[html.length - 1];
+
+ if (value.length > 0 && value !== '\n') {
+ html.push('\n');
+ }
+ }
+ },
+
+ /**
+ * Writes a text node.
+ *
+ * @method text
+ * @param {String} text String to write out.
+ * @param {Boolean} raw Optional raw state if true the contents wont get encoded.
+ */
+ text: function(text, raw) {
+ if (text.length > 0) {
+ html[html.length] = raw ? text : encode(text);
+ }
+ },
+
+ /**
+ * Writes a cdata node such as <![CDATA[data]]>.
+ *
+ * @method cdata
+ * @param {String} text String to write out inside the cdata.
+ */
+ cdata: function(text) {
+ html.push('<![CDATA[', text, ']]>');
+ },
+
+ /**
+ * Writes a comment node such as <!-- Comment -->.
+ *
+ * @method cdata
+ * @param {String} text String to write out inside the comment.
+ */
+ comment: function(text) {
+ html.push('<!--', text, '-->');
+ },
+
+ /**
+ * Writes a PI node such as <?xml attr="value" ?>.
+ *
+ * @method pi
+ * @param {String} name Name of the pi.
+ * @param {String} text String to write out inside the pi.
+ */
+ pi: function(name, text) {
+ if (text) {
+ html.push('<?', name, ' ', encode(text), '?>');
+ } else {
+ html.push('<?', name, '?>');
+ }
+
+ if (indent) {
+ html.push('\n');
+ }
+ },
+
+ /**
+ * Writes a doctype node such as <!DOCTYPE data>.
+ *
+ * @method doctype
+ * @param {String} text String to write out inside the doctype.
+ */
+ doctype: function(text) {
+ html.push('<!DOCTYPE', text, '>', indent ? '\n' : '');
+ },
+
+ /**
+ * Resets the internal buffer if one wants to reuse the writer.
+ *
+ * @method reset
+ */
+ reset: function() {
+ html.length = 0;
+ },
+
+ /**
+ * Returns the contents that got serialized.
+ *
+ * @method getContent
+ * @return {String} HTML contents that got written down.
+ */
+ getContent: function() {
+ return html.join('').replace(/\n$/, '');
+ }
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/html/Serializer.js
+
+/**
+ * Serializer.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class is used to serialize down the DOM tree into a string using a Writer instance.
+ *
+ *
+ * @example
+ * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>'));
+ * @class tinymce.html.Serializer
+ * @version 3.4
+ */
+define("tinymce/html/Serializer", [
+ "tinymce/html/Writer",
+ "tinymce/html/Schema"
+], function(Writer, Schema) {
+ /**
+ * Constructs a new Serializer instance.
+ *
+ * @constructor
+ * @method Serializer
+ * @param {Object} settings Name/value settings object.
+ * @param {tinymce.html.Schema} schema Schema instance to use.
+ */
+ return function(settings, schema) {
+ var self = this, writer = new Writer(settings);
+
+ settings = settings || {};
+ settings.validate = "validate" in settings ? settings.validate : true;
+
+ self.schema = schema = schema || new Schema();
+ self.writer = writer;
+
+ /**
+ * Serializes the specified node into a string.
+ *
+ * @example
+ * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>'));
+ * @method serialize
+ * @param {tinymce.html.Node} node Node instance to serialize.
+ * @return {String} String with HTML based on DOM tree.
+ */
+ self.serialize = function(node) {
+ var handlers, validate;
+
+ validate = settings.validate;
+
+ handlers = {
+ // #text
+ 3: function(node) {
+ writer.text(node.value, node.raw);
+ },
+
+ // #comment
+ 8: function(node) {
+ writer.comment(node.value);
+ },
+
+ // Processing instruction
+ 7: function(node) {
+ writer.pi(node.name, node.value);
+ },
+
+ // Doctype
+ 10: function(node) {
+ writer.doctype(node.value);
+ },
+
+ // CDATA
+ 4: function(node) {
+ writer.cdata(node.value);
+ },
+
+ // Document fragment
+ 11: function(node) {
+ if ((node = node.firstChild)) {
+ do {
+ walk(node);
+ } while ((node = node.next));
+ }
+ }
+ };
+
+ writer.reset();
+
+ function walk(node) {
+ var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule;
+
+ if (!handler) {
+ name = node.name;
+ isEmpty = node.shortEnded;
+ attrs = node.attributes;
+
+ // Sort attributes
+ if (validate && attrs && attrs.length > 1) {
+ sortedAttrs = [];
+ sortedAttrs.map = {};
+
+ elementRule = schema.getElementRule(node.name);
+ if (elementRule) {
+ for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) {
+ attrName = elementRule.attributesOrder[i];
+
+ if (attrName in attrs.map) {
+ attrValue = attrs.map[attrName];
+ sortedAttrs.map[attrName] = attrValue;
+ sortedAttrs.push({name: attrName, value: attrValue});
+ }
+ }
+
+ for (i = 0, l = attrs.length; i < l; i++) {
+ attrName = attrs[i].name;
+
+ if (!(attrName in sortedAttrs.map)) {
+ attrValue = attrs.map[attrName];
+ sortedAttrs.map[attrName] = attrValue;
+ sortedAttrs.push({name: attrName, value: attrValue});
+ }
+ }
+
+ attrs = sortedAttrs;
+ }
+ }
+
+ writer.start(node.name, attrs, isEmpty);
+
+ if (!isEmpty) {
+ if ((node = node.firstChild)) {
+ do {
+ walk(node);
+ } while ((node = node.next));
+ }
+
+ writer.end(name);
+ }
+ } else {
+ handler(node);
+ }
+ }
+
+ // Serialize element and treat all non elements as fragments
+ if (node.type == 1 && !settings.inner) {
+ walk(node);
+ } else {
+ handlers[11](node);
+ }
+
+ return writer.getContent();
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/dom/Serializer.js
+
+/**
+ * Serializer.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class is used to serialize DOM trees into a string. Consult the TinyMCE Wiki API for
+ * more details and examples on how to use this class.
+ *
+ * @class tinymce.dom.Serializer
+ */
+define("tinymce/dom/Serializer", [
+ "tinymce/dom/DOMUtils",
+ "tinymce/html/DomParser",
+ "tinymce/html/SaxParser",
+ "tinymce/html/Entities",
+ "tinymce/html/Serializer",
+ "tinymce/html/Node",
+ "tinymce/html/Schema",
+ "tinymce/Env",
+ "tinymce/util/Tools",
+ "tinymce/text/Zwsp"
+], function(DOMUtils, DomParser, SaxParser, Entities, Serializer, Node, Schema, Env, Tools, Zwsp) {
+ var each = Tools.each, trim = Tools.trim;
+ var DOM = DOMUtils.DOM;
+
+ /**
+ * IE 11 has a fantastic bug where it will produce two trailing BR elements to iframe bodies when
+ * the iframe is hidden by display: none on a parent container. The DOM is actually out of sync
+ * with innerHTML in this case. It's like IE adds shadow DOM BR elements that appears on innerHTML
+ * but not as the lastChild of the body. So this fix simply removes the last two
+ * BR elements at the end of the document.
+ *
+ * Example of what happens: <body>text</body> becomes <body>text<br><br></body>
+ */
+ function trimTrailingBr(rootNode) {
+ var brNode1, brNode2;
+
+ function isBr(node) {
+ return node && node.name === 'br';
+ }
+
+ brNode1 = rootNode.lastChild;
+ if (isBr(brNode1)) {
+ brNode2 = brNode1.prev;
+
+ if (isBr(brNode2)) {
+ brNode1.remove();
+ brNode2.remove();
+ }
+ }
+ }
+
+ /**
+ * Constructs a new DOM serializer class.
+ *
+ * @constructor
+ * @method Serializer
+ * @param {Object} settings Serializer settings object.
+ * @param {tinymce.Editor} editor Optional editor to bind events to and get schema/dom from.
+ */
+ return function(settings, editor) {
+ var dom, schema, htmlParser, tempAttrs = ["data-mce-selected"];
+
+ if (editor) {
+ dom = editor.dom;
+ schema = editor.schema;
+ }
+
+ function trimHtml(html) {
+ var trimContentRegExp = new RegExp([
+ '<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\\/span>', // Trim bogus spans like caret containers
+ '\\s?(' + tempAttrs.join('|') + ')="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected
+ ].join('|'), 'gi');
+
+ html = Zwsp.trim(html.replace(trimContentRegExp, ''));
+
+ return html;
+ }
+
+ function trimContent(html) {
+ var content = html;
+ var bogusAllRegExp = /<(\w+) [^>]*data-mce-bogus="all"[^>]*>/g;
+ var endTagIndex, index, matchLength, matches, shortEndedElements, schema = editor.schema;
+
+ content = trimHtml(content);
+ shortEndedElements = schema.getShortEndedElements();
+
+ // Remove all bogus elements marked with "all"
+ while ((matches = bogusAllRegExp.exec(content))) {
+ index = bogusAllRegExp.lastIndex;
+ matchLength = matches[0].length;
+
+ if (shortEndedElements[matches[1]]) {
+ endTagIndex = index;
+ } else {
+ endTagIndex = SaxParser.findEndTag(schema, content, index);
+ }
+
+ content = content.substring(0, index - matchLength) + content.substring(endTagIndex);
+ bogusAllRegExp.lastIndex = index - matchLength;
+ }
+
+ return trim(content);
+ }
+
+ /**
+ * Returns a trimmed version of the editor contents to be used for the undo level. This
+ * will remove any data-mce-bogus="all" marked elements since these are used for UI it will also
+ * remove the data-mce-selected attributes used for selection of objects and caret containers.
+ * It will keep all data-mce-bogus="1" elements since these can be used to place the caret etc and will
+ * be removed by the serialization logic when you save.
+ *
+ * @private
+ * @return {String} HTML contents of the editor excluding some internal bogus elements.
+ */
+ function getTrimmedContent() {
+ return trimContent(editor.getBody().innerHTML);
+ }
+
+ function addTempAttr(name) {
+ if (Tools.inArray(tempAttrs, name) === -1) {
+ htmlParser.addAttributeFilter(name, function(nodes, name) {
+ var i = nodes.length;
+
+ while (i--) {
+ nodes[i].attr(name, null);
+ }
+ });
+
+ tempAttrs.push(name);
+ }
+ }
+
+ // Default DOM and Schema if they are undefined
+ dom = dom || DOM;
+ schema = schema || new Schema(settings);
+ settings.entity_encoding = settings.entity_encoding || 'named';
+ settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true;
+
+ htmlParser = new DomParser(settings, schema);
+
+ // Convert tabindex back to elements when serializing contents
+ htmlParser.addAttributeFilter('data-mce-tabindex', function(nodes, name) {
+ var i = nodes.length, node;
+
+ while (i--) {
+ node = nodes[i];
+ node.attr('tabindex', node.attributes.map['data-mce-tabindex']);
+ node.attr(name, null);
+ }
+ });
+
+ // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed
+ htmlParser.addAttributeFilter('src,href,style', function(nodes, name) {
+ var i = nodes.length, node, value, internalName = 'data-mce-' + name;
+ var urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef;
+
+ while (i--) {
+ node = nodes[i];
+
+ value = node.attributes.map[internalName];
+ if (value !== undef) {
+ // Set external name to internal value and remove internal
+ node.attr(name, value.length > 0 ? value : null);
+ node.attr(internalName, null);
+ } else {
+ // No internal attribute found then convert the value we have in the DOM
+ value = node.attributes.map[name];
+
+ if (name === "style") {
+ value = dom.serializeStyle(dom.parseStyle(value), node.name);
+ } else if (urlConverter) {
+ value = urlConverter.call(urlConverterScope, value, name, node.name);
+ }
+
+ node.attr(name, value.length > 0 ? value : null);
+ }
+ }
+ });
+
+ // Remove internal classes mceItem<..> or mceSelected
+ htmlParser.addAttributeFilter('class', function(nodes) {
+ var i = nodes.length, node, value;
+
+ while (i--) {
+ node = nodes[i];
+ value = node.attr('class');
+
+ if (value) {
+ value = node.attr('class').replace(/(?:^|\s)mce-item-\w+(?!\S)/g, '');
+ node.attr('class', value.length > 0 ? value : null);
+ }
+ }
+ });
+
+ // Remove bookmark elements
+ htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) {
+ var i = nodes.length, node;
+
+ while (i--) {
+ node = nodes[i];
+
+ if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) {
+ node.remove();
+ }
+ }
+ });
+
+ htmlParser.addNodeFilter('noscript', function(nodes) {
+ var i = nodes.length, node;
+
+ while (i--) {
+ node = nodes[i].firstChild;
+
+ if (node) {
+ node.value = Entities.decode(node.value);
+ }
+ }
+ });
+
+ // Force script into CDATA sections and remove the mce- prefix also add comments around styles
+ htmlParser.addNodeFilter('script,style', function(nodes, name) {
+ var i = nodes.length, node, value, type;
+
+ function trim(value) {
+ /*jshint maxlen:255 */
+ /*eslint max-len:0 */
+ return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n')
+ .replace(/^[\r\n]*|[\r\n]*$/g, '')
+ .replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '')
+ .replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, '');
+ }
+
+ while (i--) {
+ node = nodes[i];
+ value = node.firstChild ? node.firstChild.value : '';
+
+ if (name === "script") {
+ // Remove mce- prefix from script elements and remove default type since the user specified
+ // a script element without type attribute
+ type = node.attr('type');
+ if (type) {
+ node.attr('type', type == 'mce-no/type' ? null : type.replace(/^mce\-/, ''));
+ }
+
+ if (value.length > 0) {
+ node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>';
+ }
+ } else {
+ if (value.length > 0) {
+ node.firstChild.value = '<!--\n' + trim(value) + '\n-->';
+ }
+ }
+ }
+ });
+
+ // Convert comments to cdata and handle protected comments
+ htmlParser.addNodeFilter('#comment', function(nodes) {
+ var i = nodes.length, node;
+
+ while (i--) {
+ node = nodes[i];
+
+ if (node.value.indexOf('[CDATA[') === 0) {
+ node.name = '#cdata';
+ node.type = 4;
+ node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, '');
+ } else if (node.value.indexOf('mce:protected ') === 0) {
+ node.name = "#text";
+ node.type = 3;
+ node.raw = true;
+ node.value = unescape(node.value).substr(14);
+ }
+ }
+ });
+
+ htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) {
+ var i = nodes.length, node;
+
+ while (i--) {
+ node = nodes[i];
+ if (node.type === 7) {
+ node.remove();
+ } else if (node.type === 1) {
+ if (name === "input" && !("type" in node.attributes.map)) {
+ node.attr('type', 'text');
+ }
+ }
+ }
+ });
+
+ // Fix list elements, TODO: Replace this later
+ if (settings.fix_list_elements) {
+ htmlParser.addNodeFilter('ul,ol', function(nodes) {
+ var i = nodes.length, node, parentNode;
+
+ while (i--) {
+ node = nodes[i];
+ parentNode = node.parent;
+
+ if (parentNode.name === 'ul' || parentNode.name === 'ol') {
+ if (node.prev && node.prev.name === 'li') {
+ node.prev.append(node);
+ }
+ }
+ }
+ });
+ }
+
+ // Remove internal data attributes
+ htmlParser.addAttributeFilter(
+ 'data-mce-src,data-mce-href,data-mce-style,' +
+ 'data-mce-selected,data-mce-expando,' +
+ 'data-mce-type,data-mce-resize',
+
+ function(nodes, name) {
+ var i = nodes.length;
+
+ while (i--) {
+ nodes[i].attr(name, null);
+ }
+ }
+ );
+
+ // Return public methods
+ return {
+ /**
+ * Schema instance that was used to when the Serializer was constructed.
+ *
+ * @field {tinymce.html.Schema} schema
+ */
+ schema: schema,
+
+ /**
+ * Adds a node filter function to the parser used by the serializer, the parser will collect the specified nodes by name
+ * and then execute the callback ones it has finished parsing the document.
+ *
+ * @example
+ * parser.addNodeFilter('p,h1', function(nodes, name) {
+ * for (var i = 0; i < nodes.length; i++) {
+ * console.log(nodes[i].name);
+ * }
+ * });
+ * @method addNodeFilter
+ * @method {String} name Comma separated list of nodes to collect.
+ * @param {function} callback Callback function to execute once it has collected nodes.
+ */
+ addNodeFilter: htmlParser.addNodeFilter,
+
+ /**
+ * Adds a attribute filter function to the parser used by the serializer, the parser will
+ * collect nodes that has the specified attributes
+ * and then execute the callback ones it has finished parsing the document.
+ *
+ * @example
+ * parser.addAttributeFilter('src,href', function(nodes, name) {
+ * for (var i = 0; i < nodes.length; i++) {
+ * console.log(nodes[i].name);
+ * }
+ * });
+ * @method addAttributeFilter
+ * @method {String} name Comma separated list of nodes to collect.
+ * @param {function} callback Callback function to execute once it has collected nodes.
+ */
+ addAttributeFilter: htmlParser.addAttributeFilter,
+
+ /**
+ * Serializes the specified browser DOM node into a HTML string.
+ *
+ * @method serialize
+ * @param {DOMNode} node DOM node to serialize.
+ * @param {Object} args Arguments option that gets passed to event handlers.
+ */
+ serialize: function(node, args) {
+ var self = this, impl, doc, oldDoc, htmlSerializer, content, rootNode;
+
+ // Explorer won't clone contents of script and style and the
+ // selected index of select elements are cleared on a clone operation.
+ if (Env.ie && dom.select('script,style,select,map').length > 0) {
+ content = node.innerHTML;
+ node = node.cloneNode(false);
+ dom.setHTML(node, content);
+ } else {
+ node = node.cloneNode(true);
+ }
+
+ // Nodes needs to be attached to something in WebKit/Opera
+ // This fix will make DOM ranges and make Sizzle happy!
+ impl = document.implementation;
+ if (impl.createHTMLDocument) {
+ // Create an empty HTML document
+ doc = impl.createHTMLDocument("");
+
+ // Add the element or it's children if it's a body element to the new document
+ each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) {
+ doc.body.appendChild(doc.importNode(node, true));
+ });
+
+ // Grab first child or body element for serialization
+ if (node.nodeName != 'BODY') {
+ node = doc.body.firstChild;
+ } else {
+ node = doc.body;
+ }
+
+ // set the new document in DOMUtils so createElement etc works
+ oldDoc = dom.doc;
+ dom.doc = doc;
+ }
+
+ args = args || {};
+ args.format = args.format || 'html';
+
+ // Don't wrap content if we want selected html
+ if (args.selection) {
+ args.forced_root_block = '';
+ }
+
+ // Pre process
+ if (!args.no_events) {
+ args.node = node;
+ self.onPreProcess(args);
+ }
+
+ // Parse HTML
+ rootNode = htmlParser.parse(trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)), args);
+ trimTrailingBr(rootNode);
+
+ // Serialize HTML
+ htmlSerializer = new Serializer(settings, schema);
+ args.content = htmlSerializer.serialize(rootNode);
+
+ // Replace all BOM characters for now until we can find a better solution
+ if (!args.cleanup) {
+ args.content = Zwsp.trim(args.content);
+ args.content = args.content.replace(/\uFEFF/g, '');
+ }
+
+ // Post process
+ if (!args.no_events) {
+ self.onPostProcess(args);
+ }
+
+ // Restore the old document if it was changed
+ if (oldDoc) {
+ dom.doc = oldDoc;
+ }
+
+ args.node = null;
+
+ return args.content;
+ },
+
+ /**
+ * Adds valid elements rules to the serializers schema instance this enables you to specify things
+ * like what elements should be outputted and what attributes specific elements might have.
+ * Consult the Wiki for more details on this format.
+ *
+ * @method addRules
+ * @param {String} rules Valid elements rules string to add to schema.
+ */
+ addRules: function(rules) {
+ schema.addValidElements(rules);
+ },
+
+ /**
+ * Sets the valid elements rules to the serializers schema instance this enables you to specify things
+ * like what elements should be outputted and what attributes specific elements might have.
+ * Consult the Wiki for more details on this format.
+ *
+ * @method setRules
+ * @param {String} rules Valid elements rules string.
+ */
+ setRules: function(rules) {
+ schema.setValidElements(rules);
+ },
+
+ onPreProcess: function(args) {
+ if (editor) {
+ editor.fire('PreProcess', args);
+ }
+ },
+
+ onPostProcess: function(args) {
+ if (editor) {
+ editor.fire('PostProcess', args);
+ }
+ },
+
+ /**
+ * Adds a temporary internal attribute these attributes will get removed on undo and
+ * when getting contents out of the editor.
+ *
+ * @method addTempAttr
+ * @param {String} name string
+ */
+ addTempAttr: addTempAttr,
+
+ // Internal
+ trimHtml: trimHtml,
+ getTrimmedContent: getTrimmedContent,
+ trimContent: trimContent
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/dom/TridentSelection.js
+
+/**
+ * TridentSelection.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Selection class for old explorer versions. This one fakes the
+ * native selection object available on modern browsers.
+ *
+ * @private
+ * @class tinymce.dom.TridentSelection
+ */
+define("tinymce/dom/TridentSelection", [], function() {
+ function Selection(selection) {
+ var self = this, dom = selection.dom, FALSE = false;
+
+ function getPosition(rng, start) {
+ var checkRng, startIndex = 0, endIndex, inside,
+ children, child, offset, index, position = -1, parent;
+
+ // Setup test range, collapse it and get the parent
+ checkRng = rng.duplicate();
+ checkRng.collapse(start);
+ parent = checkRng.parentElement();
+
+ // Check if the selection is within the right document
+ if (parent.ownerDocument !== selection.dom.doc) {
+ return;
+ }
+
+ // IE will report non editable elements as it's parent so look for an editable one
+ while (parent.contentEditable === "false") {
+ parent = parent.parentNode;
+ }
+
+ // If parent doesn't have any children then return that we are inside the element
+ if (!parent.hasChildNodes()) {
+ return {node: parent, inside: 1};
+ }
+
+ // Setup node list and endIndex
+ children = parent.children;
+ endIndex = children.length - 1;
+
+ // Perform a binary search for the position
+ while (startIndex <= endIndex) {
+ index = Math.floor((startIndex + endIndex) / 2);
+
+ // Move selection to node and compare the ranges
+ child = children[index];
+ checkRng.moveToElementText(child);
+ position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng);
+
+ // Before/after or an exact match
+ if (position > 0) {
+ endIndex = index - 1;
+ } else if (position < 0) {
+ startIndex = index + 1;
+ } else {
+ return {node: child};
+ }
+ }
+
+ // Check if child position is before or we didn't find a position
+ if (position < 0) {
+ // No element child was found use the parent element and the offset inside that
+ if (!child) {
+ checkRng.moveToElementText(parent);
+ checkRng.collapse(true);
+ child = parent;
+ inside = true;
+ } else {
+ checkRng.collapse(false);
+ }
+
+ // Walk character by character in text node until we hit the selected range endpoint,
+ // hit the end of document or parent isn't the right one
+ // We need to walk char by char since rng.text or rng.htmlText will trim line endings
+ offset = 0;
+ while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) {
+ if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) {
+ break;
+ }
+
+ offset++;
+ }
+ } else {
+ // Child position is after the selection endpoint
+ checkRng.collapse(true);
+
+ // Walk character by character in text node until we hit the selected range endpoint, hit
+ // the end of document or parent isn't the right one
+ offset = 0;
+ while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) {
+ if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) {
+ break;
+ }
+
+ offset++;
+ }
+ }
+
+ return {node: child, position: position, offset: offset, inside: inside};
+ }
+
+ // Returns a W3C DOM compatible range object by using the IE Range API
+ function getRange() {
+ var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark;
+
+ // If selection is outside the current document just return an empty range
+ element = ieRange.item ? ieRange.item(0) : ieRange.parentElement();
+ if (element.ownerDocument != dom.doc) {
+ return domRange;
+ }
+
+ collapsed = selection.isCollapsed();
+
+ // Handle control selection
+ if (ieRange.item) {
+ domRange.setStart(element.parentNode, dom.nodeIndex(element));
+ domRange.setEnd(domRange.startContainer, domRange.startOffset + 1);
+
+ return domRange;
+ }
+
+ function findEndPoint(start) {
+ var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue;
+
+ container = endPoint.node;
+ offset = endPoint.offset;
+
+ if (endPoint.inside && !container.hasChildNodes()) {
+ domRange[start ? 'setStart' : 'setEnd'](container, 0);
+ return;
+ }
+
+ if (offset === undef) {
+ domRange[start ? 'setStartBefore' : 'setEndAfter'](container);
+ return;
+ }
+
+ if (endPoint.position < 0) {
+ sibling = endPoint.inside ? container.firstChild : container.nextSibling;
+
+ if (!sibling) {
+ domRange[start ? 'setStartAfter' : 'setEndAfter'](container);
+ return;
+ }
+
+ if (!offset) {
+ if (sibling.nodeType == 3) {
+ domRange[start ? 'setStart' : 'setEnd'](sibling, 0);
+ } else {
+ domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling);
+ }
+
+ return;
+ }
+
+ // Find the text node and offset
+ while (sibling) {
+ if (sibling.nodeType == 3) {
+ nodeValue = sibling.nodeValue;
+ textNodeOffset += nodeValue.length;
+
+ // We are at or passed the position we where looking for
+ if (textNodeOffset >= offset) {
+ container = sibling;
+ textNodeOffset -= offset;
+ textNodeOffset = nodeValue.length - textNodeOffset;
+ break;
+ }
+ }
+
+ sibling = sibling.nextSibling;
+ }
+ } else {
+ // Find the text node and offset
+ sibling = container.previousSibling;
+
+ if (!sibling) {
+ return domRange[start ? 'setStartBefore' : 'setEndBefore'](container);
+ }
+
+ // If there isn't any text to loop then use the first position
+ if (!offset) {
+ if (container.nodeType == 3) {
+ domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length);
+ } else {
+ domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling);
+ }
+
+ return;
+ }
+
+ while (sibling) {
+ if (sibling.nodeType == 3) {
+ textNodeOffset += sibling.nodeValue.length;
+
+ // We are at or passed the position we where looking for
+ if (textNodeOffset >= offset) {
+ container = sibling;
+ textNodeOffset -= offset;
+ break;
+ }
+ }
+
+ sibling = sibling.previousSibling;
+ }
+ }
+
+ domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset);
+ }
+
+ try {
+ // Find start point
+ findEndPoint(true);
+
+ // Find end point if needed
+ if (!collapsed) {
+ findEndPoint();
+ }
+ } catch (ex) {
+ // IE has a nasty bug where text nodes might throw "invalid argument" when you
+ // access the nodeValue or other properties of text nodes. This seems to happen when
+ // text nodes are split into two nodes by a delete/backspace call.
+ // So let us detect and try to fix it.
+ if (ex.number == -2147024809) {
+ // Get the current selection
+ bookmark = self.getBookmark(2);
+
+ // Get start element
+ tmpRange = ieRange.duplicate();
+ tmpRange.collapse(true);
+ element = tmpRange.parentElement();
+
+ // Get end element
+ if (!collapsed) {
+ tmpRange = ieRange.duplicate();
+ tmpRange.collapse(false);
+ element2 = tmpRange.parentElement();
+ element2.innerHTML = element2.innerHTML;
+ }
+
+ // Remove the broken elements
+ element.innerHTML = element.innerHTML;
+
+ // Restore the selection
+ self.moveToBookmark(bookmark);
+
+ // Since the range has moved we need to re-get it
+ ieRange = selection.getRng();
+
+ // Find start point
+ findEndPoint(true);
+
+ // Find end point if needed
+ if (!collapsed) {
+ findEndPoint();
+ }
+ } else {
+ throw ex; // Throw other errors
+ }
+ }
+
+ return domRange;
+ }
+
+ this.getBookmark = function(type) {
+ var rng = selection.getRng(), bookmark = {};
+
+ function getIndexes(node) {
+ var parent, root, children, i, indexes = [];
+
+ parent = node.parentNode;
+ root = dom.getRoot().parentNode;
+
+ while (parent != root && parent.nodeType !== 9) {
+ children = parent.children;
+
+ i = children.length;
+ while (i--) {
+ if (node === children[i]) {
+ indexes.push(i);
+ break;
+ }
+ }
+
+ node = parent;
+ parent = parent.parentNode;
+ }
+
+ return indexes;
+ }
+
+ function getBookmarkEndPoint(start) {
+ var position;
+
+ position = getPosition(rng, start);
+ if (position) {
+ return {
+ position: position.position,
+ offset: position.offset,
+ indexes: getIndexes(position.node),
+ inside: position.inside
+ };
+ }
+ }
+
+ // Non ubstructive bookmark
+ if (type === 2) {
+ // Handle text selection
+ if (!rng.item) {
+ bookmark.start = getBookmarkEndPoint(true);
+
+ if (!selection.isCollapsed()) {
+ bookmark.end = getBookmarkEndPoint();
+ }
+ } else {
+ bookmark.start = {ctrl: true, indexes: getIndexes(rng.item(0))};
+ }
+ }
+
+ return bookmark;
+ };
+
+ this.moveToBookmark = function(bookmark) {
+ var rng, body = dom.doc.body;
+
+ function resolveIndexes(indexes) {
+ var node, i, idx, children;
+
+ node = dom.getRoot();
+ for (i = indexes.length - 1; i >= 0; i--) {
+ children = node.children;
+ idx = indexes[i];
+
+ if (idx <= children.length - 1) {
+ node = children[idx];
+ }
+ }
+
+ return node;
+ }
+
+ function setBookmarkEndPoint(start) {
+ var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef, offset;
+
+ if (endPoint) {
+ moveLeft = endPoint.position > 0;
+
+ moveRng = body.createTextRange();
+ moveRng.moveToElementText(resolveIndexes(endPoint.indexes));
+
+ offset = endPoint.offset;
+ if (offset !== undef) {
+ moveRng.collapse(endPoint.inside || moveLeft);
+ moveRng.moveStart('character', moveLeft ? -offset : offset);
+ } else {
+ moveRng.collapse(start);
+ }
+
+ rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng);
+
+ if (start) {
+ rng.collapse(true);
+ }
+ }
+ }
+
+ if (bookmark.start) {
+ if (bookmark.start.ctrl) {
+ rng = body.createControlRange();
+ rng.addElement(resolveIndexes(bookmark.start.indexes));
+ rng.select();
+ } else {
+ rng = body.createTextRange();
+ setBookmarkEndPoint(true);
+ setBookmarkEndPoint();
+ rng.select();
+ }
+ }
+ };
+
+ this.addRange = function(rng) {
+ var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling,
+ doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm;
+
+ function setEndPoint(start) {
+ var container, offset, marker, tmpRng, nodes;
+
+ marker = dom.create('a');
+ container = start ? startContainer : endContainer;
+ offset = start ? startOffset : endOffset;
+ tmpRng = ieRng.duplicate();
+
+ if (container == doc || container == doc.documentElement) {
+ container = body;
+ offset = 0;
+ }
+
+ if (container.nodeType == 3) {
+ container.parentNode.insertBefore(marker, container);
+ tmpRng.moveToElementText(marker);
+ tmpRng.moveStart('character', offset);
+ dom.remove(marker);
+ ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
+ } else {
+ nodes = container.childNodes;
+
+ if (nodes.length) {
+ if (offset >= nodes.length) {
+ dom.insertAfter(marker, nodes[nodes.length - 1]);
+ } else {
+ container.insertBefore(marker, nodes[offset]);
+ }
+
+ tmpRng.moveToElementText(marker);
+ } else if (container.canHaveHTML) {
+ // Empty node selection for example <div>|</div>
+ // Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open
+ container.innerHTML = '<span></span>';
+ marker = container.firstChild;
+ tmpRng.moveToElementText(marker);
+ tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason
+ }
+
+ ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
+ dom.remove(marker);
+ }
+ }
+
+ // Setup some shorter versions
+ startContainer = rng.startContainer;
+ startOffset = rng.startOffset;
+ endContainer = rng.endContainer;
+ endOffset = rng.endOffset;
+ ieRng = body.createTextRange();
+
+ // If single element selection then try making a control selection out of it
+ if (startContainer == endContainer && startContainer.nodeType == 1) {
+ // Trick to place the caret inside an empty block element like <p></p>
+ if (startOffset == endOffset && !startContainer.hasChildNodes()) {
+ if (startContainer.canHaveHTML) {
+ // Check if previous sibling is an empty block if it is then we need to render it
+ // IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236
+ // Example this: <p></p><p>|</p> would become this: <p>|</p><p></p>
+ sibling = startContainer.previousSibling;
+ if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) {
+ sibling.innerHTML = '';
+ } else {
+ sibling = null;
+ }
+
+ startContainer.innerHTML = '<span></span><span></span>';
+ ieRng.moveToElementText(startContainer.lastChild);
+ ieRng.select();
+ dom.doc.selection.clear();
+ startContainer.innerHTML = '';
+
+ if (sibling) {
+ sibling.innerHTML = '';
+ }
+ return;
+ }
+
+ startOffset = dom.nodeIndex(startContainer);
+ startContainer = startContainer.parentNode;
+ }
+
+ if (startOffset == endOffset - 1) {
+ try {
+ ctrlElm = startContainer.childNodes[startOffset];
+ ctrlRng = body.createControlRange();
+ ctrlRng.addElement(ctrlElm);
+ ctrlRng.select();
+
+ // Check if the range produced is on the correct element and is a control range
+ // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398
+ nativeRng = selection.getRng();
+ if (nativeRng.item && ctrlElm === nativeRng.item(0)) {
+ return;
+ }
+ } catch (ex) {
+ // Ignore
+ }
+ }
+ }
+
+ // Set start/end point of selection
+ setEndPoint(true);
+ setEndPoint();
+
+ // Select the new range and scroll it into view
+ ieRng.select();
+ };
+
+ // Expose range method
+ this.getRangeAt = getRange;
+ }
+
+ return Selection;
+});
+
+// Included from: js/tinymce/classes/util/VK.js
+
+/**
+ * VK.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This file exposes a set of the common KeyCodes for use. Please grow it as needed.
+ */
+define("tinymce/util/VK", [
+ "tinymce/Env"
+], function(Env) {
+ return {
+ BACKSPACE: 8,
+ DELETE: 46,
+ DOWN: 40,
+ ENTER: 13,
+ LEFT: 37,
+ RIGHT: 39,
+ SPACEBAR: 32,
+ TAB: 9,
+ UP: 38,
+
+ modifierPressed: function(e) {
+ return e.shiftKey || e.ctrlKey || e.altKey || this.metaKeyPressed(e);
+ },
+
+ metaKeyPressed: function(e) {
+ // Check if ctrl or meta key is pressed. Edge case for AltGr on Windows where it produces ctrlKey+altKey states
+ return (Env.mac ? e.metaKey : e.ctrlKey && !e.altKey);
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/dom/ControlSelection.js
+
+/**
+ * ControlSelection.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class handles control selection of elements. Controls are elements
+ * that can be resized and needs to be selected as a whole. It adds custom resize handles
+ * to all browser engines that support properly disabling the built in resize logic.
+ *
+ * @class tinymce.dom.ControlSelection
+ */
+define("tinymce/dom/ControlSelection", [
+ "tinymce/util/VK",
+ "tinymce/util/Tools",
+ "tinymce/util/Delay",
+ "tinymce/Env",
+ "tinymce/dom/NodeType"
+], function(VK, Tools, Delay, Env, NodeType) {
+ var isContentEditableFalse = NodeType.isContentEditableFalse;
+ var isContentEditableTrue = NodeType.isContentEditableTrue;
+
+ function getContentEditableRoot(root, node) {
+ while (node && node != root) {
+ if (isContentEditableTrue(node) || isContentEditableFalse(node)) {
+ return node;
+ }
+
+ node = node.parentNode;
+ }
+
+ return null;
+ }
+
+ return function(selection, editor) {
+ var dom = editor.dom, each = Tools.each;
+ var selectedElm, selectedElmGhost, resizeHelper, resizeHandles, selectedHandle, lastMouseDownEvent;
+ var startX, startY, selectedElmX, selectedElmY, startW, startH, ratio, resizeStarted;
+ var width, height, editableDoc = editor.getDoc(), rootDocument = document, isIE = Env.ie && Env.ie < 11;
+ var abs = Math.abs, round = Math.round, rootElement = editor.getBody(), startScrollWidth, startScrollHeight;
+
+ // Details about each resize handle how to scale etc
+ resizeHandles = {
+ // Name: x multiplier, y multiplier, delta size x, delta size y
+ /*n: [0.5, 0, 0, -1],
+ e: [1, 0.5, 1, 0],
+ s: [0.5, 1, 0, 1],
+ w: [0, 0.5, -1, 0],*/
+ nw: [0, 0, -1, -1],
+ ne: [1, 0, 1, -1],
+ se: [1, 1, 1, 1],
+ sw: [0, 1, -1, 1]
+ };
+
+ // Add CSS for resize handles, cloned element and selected
+ var rootClass = '.mce-content-body';
+ editor.contentStyles.push(
+ rootClass + ' div.mce-resizehandle {' +
+ 'position: absolute;' +
+ 'border: 1px solid black;' +
+ 'box-sizing: box-sizing;' +
+ 'background: #FFF;' +
+ 'width: 7px;' +
+ 'height: 7px;' +
+ 'z-index: 10000' +
+ '}' +
+ rootClass + ' .mce-resizehandle:hover {' +
+ 'background: #000' +
+ '}' +
+ rootClass + ' img[data-mce-selected],' + rootClass + ' hr[data-mce-selected] {' +
+ 'outline: 1px solid black;' +
+ 'resize: none' + // Have been talks about implementing this in browsers
+ '}' +
+ rootClass + ' .mce-clonedresizable {' +
+ 'position: absolute;' +
+ (Env.gecko ? '' : 'outline: 1px dashed black;') + // Gecko produces trails while resizing
+ 'opacity: .5;' +
+ 'filter: alpha(opacity=50);' +
+ 'z-index: 10000' +
+ '}' +
+ rootClass + ' .mce-resize-helper {' +
+ 'background: #555;' +
+ 'background: rgba(0,0,0,0.75);' +
+ 'border-radius: 3px;' +
+ 'border: 1px;' +
+ 'color: white;' +
+ 'display: none;' +
+ 'font-family: sans-serif;' +
+ 'font-size: 12px;' +
+ 'white-space: nowrap;' +
+ 'line-height: 14px;' +
+ 'margin: 5px 10px;' +
+ 'padding: 5px;' +
+ 'position: absolute;' +
+ 'z-index: 10001' +
+ '}'
+ );
+
+ function isResizable(elm) {
+ var selector = editor.settings.object_resizing;
+
+ if (selector === false || Env.iOS) {
+ return false;
+ }
+
+ if (typeof selector != 'string') {
+ selector = 'table,img,div';
+ }
+
+ if (elm.getAttribute('data-mce-resize') === 'false') {
+ return false;
+ }
+
+ if (elm == editor.getBody()) {
+ return false;
+ }
+
+ return editor.dom.is(elm, selector);
+ }
+
+ function resizeGhostElement(e) {
+ var deltaX, deltaY, proportional;
+ var resizeHelperX, resizeHelperY;
+
+ // Calc new width/height
+ deltaX = e.screenX - startX;
+ deltaY = e.screenY - startY;
+
+ // Calc new size
+ width = deltaX * selectedHandle[2] + startW;
+ height = deltaY * selectedHandle[3] + startH;
+
+ // Never scale down lower than 5 pixels
+ width = width < 5 ? 5 : width;
+ height = height < 5 ? 5 : height;
+
+ if (selectedElm.nodeName == "IMG" && editor.settings.resize_img_proportional !== false) {
+ proportional = !VK.modifierPressed(e);
+ } else {
+ proportional = VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0);
+ }
+
+ // Constrain proportions
+ if (proportional) {
+ if (abs(deltaX) > abs(deltaY)) {
+ height = round(width * ratio);
+ width = round(height / ratio);
+ } else {
+ width = round(height / ratio);
+ height = round(width * ratio);
+ }
+ }
+
+ // Update ghost size
+ dom.setStyles(selectedElmGhost, {
+ width: width,
+ height: height
+ });
+
+ // Update resize helper position
+ resizeHelperX = selectedHandle.startPos.x + deltaX;
+ resizeHelperY = selectedHandle.startPos.y + deltaY;
+ resizeHelperX = resizeHelperX > 0 ? resizeHelperX : 0;
+ resizeHelperY = resizeHelperY > 0 ? resizeHelperY : 0;
+
+ dom.setStyles(resizeHelper, {
+ left: resizeHelperX,
+ top: resizeHelperY,
+ display: 'block'
+ });
+
+ resizeHelper.innerHTML = width + ' × ' + height;
+
+ // Update ghost X position if needed
+ if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) {
+ dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width));
+ }
+
+ // Update ghost Y position if needed
+ if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) {
+ dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height));
+ }
+
+ // Calculate how must overflow we got
+ deltaX = rootElement.scrollWidth - startScrollWidth;
+ deltaY = rootElement.scrollHeight - startScrollHeight;
+
+ // Re-position the resize helper based on the overflow
+ if (deltaX + deltaY !== 0) {
+ dom.setStyles(resizeHelper, {
+ left: resizeHelperX - deltaX,
+ top: resizeHelperY - deltaY
+ });
+ }
+
+ if (!resizeStarted) {
+ editor.fire('ObjectResizeStart', {target: selectedElm, width: startW, height: startH});
+ resizeStarted = true;
+ }
+ }
+
+ function endGhostResize() {
+ resizeStarted = false;
+
+ function setSizeProp(name, value) {
+ if (value) {
+ // Resize by using style or attribute
+ if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) {
+ dom.setStyle(selectedElm, name, value);
+ } else {
+ dom.setAttrib(selectedElm, name, value);
+ }
+ }
+ }
+
+ // Set width/height properties
+ setSizeProp('width', width);
+ setSizeProp('height', height);
+
+ dom.unbind(editableDoc, 'mousemove', resizeGhostElement);
+ dom.unbind(editableDoc, 'mouseup', endGhostResize);
+
+ if (rootDocument != editableDoc) {
+ dom.unbind(rootDocument, 'mousemove', resizeGhostElement);
+ dom.unbind(rootDocument, 'mouseup', endGhostResize);
+ }
+
+ // Remove ghost/helper and update resize handle positions
+ dom.remove(selectedElmGhost);
+ dom.remove(resizeHelper);
+
+ if (!isIE || selectedElm.nodeName == "TABLE") {
+ showResizeRect(selectedElm);
+ }
+
+ editor.fire('ObjectResized', {target: selectedElm, width: width, height: height});
+ dom.setAttrib(selectedElm, 'style', dom.getAttrib(selectedElm, 'style'));
+ editor.nodeChanged();
+ }
+
+ function showResizeRect(targetElm, mouseDownHandleName, mouseDownEvent) {
+ var position, targetWidth, targetHeight, e, rect;
+
+ hideResizeRect();
+ unbindResizeHandleEvents();
+
+ // Get position and size of target
+ position = dom.getPos(targetElm, rootElement);
+ selectedElmX = position.x;
+ selectedElmY = position.y;
+ rect = targetElm.getBoundingClientRect(); // Fix for Gecko offsetHeight for table with caption
+ targetWidth = rect.width || (rect.right - rect.left);
+ targetHeight = rect.height || (rect.bottom - rect.top);
+
+ // Reset width/height if user selects a new image/table
+ if (selectedElm != targetElm) {
+ detachResizeStartListener();
+ selectedElm = targetElm;
+ width = height = 0;
+ }
+
+ // Makes it possible to disable resizing
+ e = editor.fire('ObjectSelected', {target: targetElm});
+
+ if (isResizable(targetElm) && !e.isDefaultPrevented()) {
+ each(resizeHandles, function(handle, name) {
+ var handleElm;
+
+ function startDrag(e) {
+ startX = e.screenX;
+ startY = e.screenY;
+ startW = selectedElm.clientWidth;
+ startH = selectedElm.clientHeight;
+ ratio = startH / startW;
+ selectedHandle = handle;
+
+ handle.startPos = {
+ x: targetWidth * handle[0] + selectedElmX,
+ y: targetHeight * handle[1] + selectedElmY
+ };
+
+ startScrollWidth = rootElement.scrollWidth;
+ startScrollHeight = rootElement.scrollHeight;
+
+ selectedElmGhost = selectedElm.cloneNode(true);
+ dom.addClass(selectedElmGhost, 'mce-clonedresizable');
+ dom.setAttrib(selectedElmGhost, 'data-mce-bogus', 'all');
+ selectedElmGhost.contentEditable = false; // Hides IE move layer cursor
+ selectedElmGhost.unSelectabe = true;
+ dom.setStyles(selectedElmGhost, {
+ left: selectedElmX,
+ top: selectedElmY,
+ margin: 0
+ });
+
+ selectedElmGhost.removeAttribute('data-mce-selected');
+ rootElement.appendChild(selectedElmGhost);
+
+ dom.bind(editableDoc, 'mousemove', resizeGhostElement);
+ dom.bind(editableDoc, 'mouseup', endGhostResize);
+
+ if (rootDocument != editableDoc) {
+ dom.bind(rootDocument, 'mousemove', resizeGhostElement);
+ dom.bind(rootDocument, 'mouseup', endGhostResize);
+ }
+
+ resizeHelper = dom.add(rootElement, 'div', {
+ 'class': 'mce-resize-helper',
+ 'data-mce-bogus': 'all'
+ }, startW + ' × ' + startH);
+ }
+
+ if (mouseDownHandleName) {
+ // Drag started by IE native resizestart
+ if (name == mouseDownHandleName) {
+ startDrag(mouseDownEvent);
+ }
+
+ return;
+ }
+
+ // Get existing or render resize handle
+ handleElm = dom.get('mceResizeHandle' + name);
+ if (handleElm) {
+ dom.remove(handleElm);
+ }
+
+ handleElm = dom.add(rootElement, 'div', {
+ id: 'mceResizeHandle' + name,
+ 'data-mce-bogus': 'all',
+ 'class': 'mce-resizehandle',
+ unselectable: true,
+ style: 'cursor:' + name + '-resize; margin:0; padding:0'
+ });
+
+ // Hides IE move layer cursor
+ // If we set it on Chrome we get this wounderful bug: #6725
+ if (Env.ie) {
+ handleElm.contentEditable = false;
+ }
+
+ dom.bind(handleElm, 'mousedown', function(e) {
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ startDrag(e);
+ });
+
+ handle.elm = handleElm;
+
+ // Position element
+ dom.setStyles(handleElm, {
+ left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2),
+ top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2)
+ });
+ });
+ } else {
+ hideResizeRect();
+ }
+
+ selectedElm.setAttribute('data-mce-selected', '1');
+ }
+
+ function hideResizeRect() {
+ var name, handleElm;
+
+ unbindResizeHandleEvents();
+
+ if (selectedElm) {
+ selectedElm.removeAttribute('data-mce-selected');
+ }
+
+ for (name in resizeHandles) {
+ handleElm = dom.get('mceResizeHandle' + name);
+ if (handleElm) {
+ dom.unbind(handleElm);
+ dom.remove(handleElm);
+ }
+ }
+ }
+
+ function updateResizeRect(e) {
+ var startElm, controlElm;
+
+ function isChildOrEqual(node, parent) {
+ if (node) {
+ do {
+ if (node === parent) {
+ return true;
+ }
+ } while ((node = node.parentNode));
+ }
+ }
+
+ // Ignore all events while resizing or if the editor instance was removed
+ if (resizeStarted || editor.removed) {
+ return;
+ }
+
+ // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v
+ each(dom.select('img[data-mce-selected],hr[data-mce-selected]'), function(img) {
+ img.removeAttribute('data-mce-selected');
+ });
+
+ controlElm = e.type == 'mousedown' ? e.target : selection.getNode();
+ controlElm = dom.$(controlElm).closest(isIE ? 'table' : 'table,img,hr')[0];
+
+ if (isChildOrEqual(controlElm, rootElement)) {
+ disableGeckoResize();
+ startElm = selection.getStart(true);
+
+ if (isChildOrEqual(startElm, controlElm) && isChildOrEqual(selection.getEnd(true), controlElm)) {
+ if (!isIE || (controlElm != startElm && startElm.nodeName !== 'IMG')) {
+ showResizeRect(controlElm);
+ return;
+ }
+ }
+ }
+
+ hideResizeRect();
+ }
+
+ function attachEvent(elm, name, func) {
+ if (elm && elm.attachEvent) {
+ elm.attachEvent('on' + name, func);
+ }
+ }
+
+ function detachEvent(elm, name, func) {
+ if (elm && elm.detachEvent) {
+ elm.detachEvent('on' + name, func);
+ }
+ }
+
+ function resizeNativeStart(e) {
+ var target = e.srcElement, pos, name, corner, cornerX, cornerY, relativeX, relativeY;
+
+ pos = target.getBoundingClientRect();
+ relativeX = lastMouseDownEvent.clientX - pos.left;
+ relativeY = lastMouseDownEvent.clientY - pos.top;
+
+ // Figure out what corner we are draging on
+ for (name in resizeHandles) {
+ corner = resizeHandles[name];
+
+ cornerX = target.offsetWidth * corner[0];
+ cornerY = target.offsetHeight * corner[1];
+
+ if (abs(cornerX - relativeX) < 8 && abs(cornerY - relativeY) < 8) {
+ selectedHandle = corner;
+ break;
+ }
+ }
+
+ // Remove native selection and let the magic begin
+ resizeStarted = true;
+ editor.fire('ObjectResizeStart', {
+ target: selectedElm,
+ width: selectedElm.clientWidth,
+ height: selectedElm.clientHeight
+ });
+ editor.getDoc().selection.empty();
+ showResizeRect(target, name, lastMouseDownEvent);
+ }
+
+ function preventDefault(e) {
+ if (e.preventDefault) {
+ e.preventDefault();
+ } else {
+ e.returnValue = false; // IE
+ }
+ }
+
+ function isWithinContentEditableFalse(elm) {
+ return isContentEditableFalse(getContentEditableRoot(editor.getBody(), elm));
+ }
+
+ function nativeControlSelect(e) {
+ var target = e.srcElement;
+
+ if (isWithinContentEditableFalse(target)) {
+ preventDefault(e);
+ return;
+ }
+
+ if (target != selectedElm) {
+ editor.fire('ObjectSelected', {target: target});
+ detachResizeStartListener();
+
+ if (target.id.indexOf('mceResizeHandle') === 0) {
+ e.returnValue = false;
+ return;
+ }
+
+ if (target.nodeName == 'IMG' || target.nodeName == 'TABLE') {
+ hideResizeRect();
+ selectedElm = target;
+ attachEvent(target, 'resizestart', resizeNativeStart);
+ }
+ }
+ }
+
+ function detachResizeStartListener() {
+ detachEvent(selectedElm, 'resizestart', resizeNativeStart);
+ }
+
+ function unbindResizeHandleEvents() {
+ for (var name in resizeHandles) {
+ var handle = resizeHandles[name];
+
+ if (handle.elm) {
+ dom.unbind(handle.elm);
+ delete handle.elm;
+ }
+ }
+ }
+
+ function disableGeckoResize() {
+ try {
+ // Disable object resizing on Gecko
+ editor.getDoc().execCommand('enableObjectResizing', false, false);
+ } catch (ex) {
+ // Ignore
+ }
+ }
+
+ function controlSelect(elm) {
+ var ctrlRng;
+
+ if (!isIE) {
+ return;
+ }
+
+ ctrlRng = editableDoc.body.createControlRange();
+
+ try {
+ ctrlRng.addElement(elm);
+ ctrlRng.select();
+ return true;
+ } catch (ex) {
+ // Ignore since the element can't be control selected for example a P tag
+ }
+ }
+
+ editor.on('init', function() {
+ if (isIE) {
+ // Hide the resize rect on resize and reselect the image
+ editor.on('ObjectResized', function(e) {
+ if (e.target.nodeName != 'TABLE') {
+ hideResizeRect();
+ controlSelect(e.target);
+ }
+ });
+
+ attachEvent(rootElement, 'controlselect', nativeControlSelect);
+
+ editor.on('mousedown', function(e) {
+ lastMouseDownEvent = e;
+ });
+ } else {
+ disableGeckoResize();
+
+ // Sniff sniff, hard to feature detect this stuff
+ if (Env.ie >= 11) {
+ // Needs to be mousedown for drag/drop to work on IE 11
+ // Needs to be click on Edge to properly select images
+ editor.on('mousedown click', function(e) {
+ var target = e.target, nodeName = target.nodeName;
+
+ if (!resizeStarted && /^(TABLE|IMG|HR)$/.test(nodeName) && !isWithinContentEditableFalse(target)) {
+ editor.selection.select(target, nodeName == 'TABLE');
+
+ // Only fire once since nodeChange is expensive
+ if (e.type == 'mousedown') {
+ editor.nodeChanged();
+ }
+ }
+ });
+
+ editor.dom.bind(rootElement, 'mscontrolselect', function(e) {
+ function delayedSelect(node) {
+ Delay.setEditorTimeout(editor, function() {
+ editor.selection.select(node);
+ });
+ }
+
+ if (isWithinContentEditableFalse(e.target)) {
+ e.preventDefault();
+ delayedSelect(e.target);
+ return;
+ }
+
+ if (/^(TABLE|IMG|HR)$/.test(e.target.nodeName)) {
+ e.preventDefault();
+
+ // This moves the selection from being a control selection to a text like selection like in WebKit #6753
+ // TODO: Fix this the day IE works like other browsers without this nasty native ugly control selections.
+ if (e.target.tagName == 'IMG') {
+ delayedSelect(e.target);
+ }
+ }
+ });
+ }
+ }
+
+ var throttledUpdateResizeRect = Delay.throttle(function(e) {
+ if (!editor.composing) {
+ updateResizeRect(e);
+ }
+ });
+
+ editor.on('nodechange ResizeEditor ResizeWindow drop', throttledUpdateResizeRect);
+
+ // Update resize rect while typing in a table
+ editor.on('keyup compositionend', function(e) {
+ // Don't update the resize rect while composing since it blows away the IME see: #2710
+ if (selectedElm && selectedElm.nodeName == "TABLE") {
+ throttledUpdateResizeRect(e);
+ }
+ });
+
+ editor.on('hide blur', hideResizeRect);
+
+ // Hide rect on focusout since it would float on top of windows otherwise
+ //editor.on('focusout', hideResizeRect);
+ });
+
+ editor.on('remove', unbindResizeHandleEvents);
+
+ function destroy() {
+ selectedElm = selectedElmGhost = null;
+
+ if (isIE) {
+ detachResizeStartListener();
+ detachEvent(rootElement, 'controlselect', nativeControlSelect);
+ }
+ }
+
+ return {
+ isResizable: isResizable,
+ showResizeRect: showResizeRect,
+ hideResizeRect: hideResizeRect,
+ updateResizeRect: updateResizeRect,
+ controlSelect: controlSelect,
+ destroy: destroy
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/util/Fun.js
+
+/**
+ * Fun.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Functional utility class.
+ *
+ * @private
+ * @class tinymce.util.Fun
+ */
+define("tinymce/util/Fun", [], function() {
+ var slice = [].slice;
+
+ function constant(value) {
+ return function() {
+ return value;
+ };
+ }
+
+ function negate(predicate) {
+ return function(x) {
+ return !predicate(x);
+ };
+ }
+
+ function compose(f, g) {
+ return function(x) {
+ return f(g(x));
+ };
+ }
+
+ function or() {
+ var args = slice.call(arguments);
+
+ return function(x) {
+ for (var i = 0; i < args.length; i++) {
+ if (args[i](x)) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+ }
+
+ function and() {
+ var args = slice.call(arguments);
+
+ return function(x) {
+ for (var i = 0; i < args.length; i++) {
+ if (!args[i](x)) {
+ return false;
+ }
+ }
+
+ return true;
+ };
+ }
+
+ function curry(fn) {
+ var args = slice.call(arguments);
+
+ if (args.length - 1 >= fn.length) {
+ return fn.apply(this, args.slice(1));
+ }
+
+ return function() {
+ var tempArgs = args.concat([].slice.call(arguments));
+ return curry.apply(this, tempArgs);
+ };
+ }
+
+ function noop() {
+ }
+
+ return {
+ constant: constant,
+ negate: negate,
+ and: and,
+ or: or,
+ curry: curry,
+ compose: compose,
+ noop: noop
+ };
+});
+
+// Included from: js/tinymce/classes/caret/CaretCandidate.js
+
+/**
+ * CaretCandidate.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module contains logic for handling caret candidates. A caret candidate is
+ * for example text nodes, images, input elements, cE=false elements etc.
+ *
+ * @private
+ * @class tinymce.caret.CaretCandidate
+ */
+define("tinymce/caret/CaretCandidate", [
+ "tinymce/dom/NodeType",
+ "tinymce/util/Arr",
+ "tinymce/caret/CaretContainer"
+], function(NodeType, Arr, CaretContainer) {
+ var isContentEditableTrue = NodeType.isContentEditableTrue,
+ isContentEditableFalse = NodeType.isContentEditableFalse,
+ isBr = NodeType.isBr,
+ isText = NodeType.isText,
+ isInvalidTextElement = NodeType.matchNodeNames('script style textarea'),
+ isAtomicInline = NodeType.matchNodeNames('img input textarea hr iframe video audio object'),
+ isTable = NodeType.matchNodeNames('table'),
+ isCaretContainer = CaretContainer.isCaretContainer;
+
+ function isCaretCandidate(node) {
+ if (isCaretContainer(node)) {
+ return false;
+ }
+
+ if (isText(node)) {
+ if (isInvalidTextElement(node.parentNode)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ return isAtomicInline(node) || isBr(node) || isTable(node) || isContentEditableFalse(node);
+ }
+
+ function isInEditable(node, rootNode) {
+ for (node = node.parentNode; node && node != rootNode; node = node.parentNode) {
+ if (isContentEditableFalse(node)) {
+ return false;
+ }
+
+ if (isContentEditableTrue(node)) {
+ return true;
+ }
+ }
+
+ return true;
+ }
+
+ function isAtomicContentEditableFalse(node) {
+ if (!isContentEditableFalse(node)) {
+ return false;
+ }
+
+ return Arr.reduce(node.getElementsByTagName('*'), function(result, elm) {
+ return result || isContentEditableTrue(elm);
+ }, false) !== true;
+ }
+
+ function isAtomic(node) {
+ return isAtomicInline(node) || isAtomicContentEditableFalse(node);
+ }
+
+ function isEditableCaretCandidate(node, rootNode) {
+ return isCaretCandidate(node) && isInEditable(node, rootNode);
+ }
+
+ return {
+ isCaretCandidate: isCaretCandidate,
+ isInEditable: isInEditable,
+ isAtomic: isAtomic,
+ isEditableCaretCandidate: isEditableCaretCandidate
+ };
+});
+
+// Included from: js/tinymce/classes/geom/ClientRect.js
+
+/**
+ * ClientRect.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Utility functions for working with client rects.
+ *
+ * @private
+ * @class tinymce.geom.ClientRect
+ */
+define("tinymce/geom/ClientRect", [], function() {
+ var round = Math.round;
+
+ function clone(rect) {
+ if (!rect) {
+ return {left: 0, top: 0, bottom: 0, right: 0, width: 0, height: 0};
+ }
+
+ return {
+ left: round(rect.left),
+ top: round(rect.top),
+ bottom: round(rect.bottom),
+ right: round(rect.right),
+ width: round(rect.width),
+ height: round(rect.height)
+ };
+ }
+
+ function collapse(clientRect, toStart) {
+ clientRect = clone(clientRect);
+
+ if (toStart) {
+ clientRect.right = clientRect.left;
+ } else {
+ clientRect.left = clientRect.left + clientRect.width;
+ clientRect.right = clientRect.left;
+ }
+
+ clientRect.width = 0;
+
+ return clientRect;
+ }
+
+ function isEqual(rect1, rect2) {
+ return (
+ rect1.left === rect2.left &&
+ rect1.top === rect2.top &&
+ rect1.bottom === rect2.bottom &&
+ rect1.right === rect2.right
+ );
+ }
+
+ function isValidOverflow(overflowY, clientRect1, clientRect2) {
+ return overflowY >= 0 && overflowY <= Math.min(clientRect1.height, clientRect2.height) / 2;
+
+ }
+
+ function isAbove(clientRect1, clientRect2) {
+ if (clientRect1.bottom < clientRect2.top) {
+ return true;
+ }
+
+ if (clientRect1.top > clientRect2.bottom) {
+ return false;
+ }
+
+ return isValidOverflow(clientRect2.top - clientRect1.bottom, clientRect1, clientRect2);
+ }
+
+ function isBelow(clientRect1, clientRect2) {
+ if (clientRect1.top > clientRect2.bottom) {
+ return true;
+ }
+
+ if (clientRect1.bottom < clientRect2.top) {
+ return false;
+ }
+
+ return isValidOverflow(clientRect2.bottom - clientRect1.top, clientRect1, clientRect2);
+ }
+
+ function isLeft(clientRect1, clientRect2) {
+ return clientRect1.left < clientRect2.left;
+ }
+
+ function isRight(clientRect1, clientRect2) {
+ return clientRect1.right > clientRect2.right;
+ }
+
+ function compare(clientRect1, clientRect2) {
+ if (isAbove(clientRect1, clientRect2)) {
+ return -1;
+ }
+
+ if (isBelow(clientRect1, clientRect2)) {
+ return 1;
+ }
+
+ if (isLeft(clientRect1, clientRect2)) {
+ return -1;
+ }
+
+ if (isRight(clientRect1, clientRect2)) {
+ return 1;
+ }
+
+ return 0;
+ }
+
+ function containsXY(clientRect, clientX, clientY) {
+ return (
+ clientX >= clientRect.left &&
+ clientX <= clientRect.right &&
+ clientY >= clientRect.top &&
+ clientY <= clientRect.bottom
+ );
+ }
+
+ return {
+ clone: clone,
+ collapse: collapse,
+ isEqual: isEqual,
+ isAbove: isAbove,
+ isBelow: isBelow,
+ isLeft: isLeft,
+ isRight: isRight,
+ compare: compare,
+ containsXY: containsXY
+ };
+});
+
+// Included from: js/tinymce/classes/text/ExtendingChar.js
+
+/**
+ * ExtendingChar.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class contains logic for detecting extending characters.
+ *
+ * @private
+ * @class tinymce.text.ExtendingChar
+ * @example
+ * var isExtending = ExtendingChar.isExtendingChar('a');
+ */
+define("tinymce/text/ExtendingChar", [], function() {
+ // Generated from: http://www.unicode.org/Public/UNIDATA/DerivedCoreProperties.txt
+ // Only includes the characters in that fit into UCS-2 16 bit
+ var extendingChars = new RegExp(
+ "[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A" +
+ "\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0" +
+ "\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08E3-\u0902\u093A\u093C" +
+ "\u0941-\u0948\u094D\u0951-\u0957\u0962-\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09CD\u09D7\u09E2-\u09E3" +
+ "\u0A01-\u0A02\u0A3C\u0A41-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A51\u0A70-\u0A71\u0A75\u0A81-\u0A82\u0ABC" +
+ "\u0AC1-\u0AC5\u0AC7-\u0AC8\u0ACD\u0AE2-\u0AE3\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B4D\u0B56\u0B57" +
+ "\u0B62-\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56" +
+ "\u0C62-\u0C63\u0C81\u0CBC\u0CBF\u0CC2\u0CC6\u0CCC-\u0CCD\u0CD5-\u0CD6\u0CE2-\u0CE3\u0D01\u0D3E\u0D41-\u0D44" +
+ "\u0D4D\u0D57\u0D62-\u0D63\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9" +
+ "\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86-\u0F87\u0F8D-\u0F97" +
+ "\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039-\u103A\u103D-\u103E\u1058-\u1059\u105E-\u1060\u1071-\u1074" +
+ "\u1082\u1085-\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B4-\u17B5" +
+ "\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u18A9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193B\u1A17-\u1A18" +
+ "\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABD\u1ABE\u1B00-\u1B03\u1B34" +
+ "\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80-\u1B81\u1BA2-\u1BA5\u1BA8-\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8-\u1BE9" +
+ "\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8-\u1CF9" +
+ "\u1DC0-\u1DF5\u1DFC-\u1DFF\u200C-\u200D\u20D0-\u20DC\u20DD-\u20E0\u20E1\u20E2-\u20E4\u20E5-\u20F0\u2CEF-\u2CF1" +
+ "\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u302E-\u302F\u3099-\u309A\uA66F\uA670-\uA672\uA674-\uA67D\uA69E-\uA69F\uA6F0-\uA6F1" +
+ "\uA802\uA806\uA80B\uA825-\uA826\uA8C4\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC" +
+ "\uA9E5\uAA29-\uAA2E\uAA31-\uAA32\uAA35-\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7-\uAAB8\uAABE-\uAABF\uAAC1" +
+ "\uAAEC-\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E-\uFF9F]"
+ );
+
+ function isExtendingChar(ch) {
+ return typeof ch == "string" && ch.charCodeAt(0) >= 768 && extendingChars.test(ch);
+ }
+
+ return {
+ isExtendingChar: isExtendingChar
+ };
+});
+
+// Included from: js/tinymce/classes/caret/CaretPosition.js
+
+/**
+ * CaretPosition.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module contains logic for creating caret positions within a document a caretposition
+ * is similar to a DOMRange object but it doesn't have two endpoints and is also more lightweight
+ * since it's now updated live when the DOM changes.
+ *
+ * @private
+ * @class tinymce.caret.CaretPosition
+ * @example
+ * var caretPos1 = new CaretPosition(container, offset);
+ * var caretPos2 = CaretPosition.fromRangeStart(someRange);
+ */
+define("tinymce/caret/CaretPosition", [
+ "tinymce/util/Fun",
+ "tinymce/dom/NodeType",
+ "tinymce/dom/DOMUtils",
+ "tinymce/dom/RangeUtils",
+ "tinymce/caret/CaretCandidate",
+ "tinymce/geom/ClientRect",
+ "tinymce/text/ExtendingChar"
+], function(Fun, NodeType, DOMUtils, RangeUtils, CaretCandidate, ClientRect, ExtendingChar) {
+ var isElement = NodeType.isElement,
+ isCaretCandidate = CaretCandidate.isCaretCandidate,
+ isBlock = NodeType.matchStyleValues('display', 'block table'),
+ isFloated = NodeType.matchStyleValues('float', 'left right'),
+ isValidElementCaretCandidate = Fun.and(isElement, isCaretCandidate, Fun.negate(isFloated)),
+ isNotPre = Fun.negate(NodeType.matchStyleValues('white-space', 'pre pre-line pre-wrap')),
+ isText = NodeType.isText,
+ isBr = NodeType.isBr,
+ nodeIndex = DOMUtils.nodeIndex,
+ resolveIndex = RangeUtils.getNode;
+
+ function createRange(doc) {
+ return "createRange" in doc ? doc.createRange() : DOMUtils.DOM.createRng();
+ }
+
+ function isWhiteSpace(chr) {
+ return chr && /[\r\n\t ]/.test(chr);
+ }
+
+ function isHiddenWhiteSpaceRange(range) {
+ var container = range.startContainer,
+ offset = range.startOffset,
+ text;
+
+ if (isWhiteSpace(range.toString()) && isNotPre(container.parentNode)) {
+ text = container.data;
+
+ if (isWhiteSpace(text[offset - 1]) || isWhiteSpace(text[offset + 1])) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function getCaretPositionClientRects(caretPosition) {
+ var clientRects = [], beforeNode, node;
+
+ // Hack for older WebKit versions that doesn't
+ // support getBoundingClientRect on BR elements
+ function getBrClientRect(brNode) {
+ var doc = brNode.ownerDocument,
+ rng = createRange(doc),
+ nbsp = doc.createTextNode('\u00a0'),
+ parentNode = brNode.parentNode,
+ clientRect;
+
+ parentNode.insertBefore(nbsp, brNode);
+ rng.setStart(nbsp, 0);
+ rng.setEnd(nbsp, 1);
+ clientRect = ClientRect.clone(rng.getBoundingClientRect());
+ parentNode.removeChild(nbsp);
+
+ return clientRect;
+ }
+
+ function getBoundingClientRect(item) {
+ var clientRect, clientRects;
+
+ clientRects = item.getClientRects();
+ if (clientRects.length > 0) {
+ clientRect = ClientRect.clone(clientRects[0]);
+ } else {
+ clientRect = ClientRect.clone(item.getBoundingClientRect());
+ }
+
+ if (isBr(item) && clientRect.left === 0) {
+ return getBrClientRect(item);
+ }
+
+ return clientRect;
+ }
+
+ function collapseAndInflateWidth(clientRect, toStart) {
+ clientRect = ClientRect.collapse(clientRect, toStart);
+ clientRect.width = 1;
+ clientRect.right = clientRect.left + 1;
+
+ return clientRect;
+ }
+
+ function addUniqueAndValidRect(clientRect) {
+ if (clientRect.height === 0) {
+ return;
+ }
+
+ if (clientRects.length > 0) {
+ if (ClientRect.isEqual(clientRect, clientRects[clientRects.length - 1])) {
+ return;
+ }
+ }
+
+ clientRects.push(clientRect);
+ }
+
+ function addCharacterOffset(container, offset) {
+ var range = createRange(container.ownerDocument);
+
+ if (offset < container.data.length) {
+ if (ExtendingChar.isExtendingChar(container.data[offset])) {
+ return clientRects;
+ }
+
+ // WebKit returns two client rects for a position after an extending
+ // character a\uxxx|b so expand on "b" and collapse to start of "b" box
+ if (ExtendingChar.isExtendingChar(container.data[offset - 1])) {
+ range.setStart(container, offset);
+ range.setEnd(container, offset + 1);
+
+ if (!isHiddenWhiteSpaceRange(range)) {
+ addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), false));
+ return clientRects;
+ }
+ }
+ }
+
+ if (offset > 0) {
+ range.setStart(container, offset - 1);
+ range.setEnd(container, offset);
+
+ if (!isHiddenWhiteSpaceRange(range)) {
+ addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), false));
+ }
+ }
+
+ if (offset < container.data.length) {
+ range.setStart(container, offset);
+ range.setEnd(container, offset + 1);
+
+ if (!isHiddenWhiteSpaceRange(range)) {
+ addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), true));
+ }
+ }
+ }
+
+ if (isText(caretPosition.container())) {
+ addCharacterOffset(caretPosition.container(), caretPosition.offset());
+ return clientRects;
+ }
+
+ if (isElement(caretPosition.container())) {
+ if (caretPosition.isAtEnd()) {
+ node = resolveIndex(caretPosition.container(), caretPosition.offset());
+ if (isText(node)) {
+ addCharacterOffset(node, node.data.length);
+ }
+
+ if (isValidElementCaretCandidate(node) && !isBr(node)) {
+ addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), false));
+ }
+ } else {
+ node = resolveIndex(caretPosition.container(), caretPosition.offset());
+ if (isText(node)) {
+ addCharacterOffset(node, 0);
+ }
+
+ if (isValidElementCaretCandidate(node) && caretPosition.isAtEnd()) {
+ addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), false));
+ return clientRects;
+ }
+
+ beforeNode = resolveIndex(caretPosition.container(), caretPosition.offset() - 1);
+ if (isValidElementCaretCandidate(beforeNode) && !isBr(beforeNode)) {
+ if (isBlock(beforeNode) || isBlock(node) || !isValidElementCaretCandidate(node)) {
+ addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(beforeNode), false));
+ }
+ }
+
+ if (isValidElementCaretCandidate(node)) {
+ addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), true));
+ }
+ }
+ }
+
+ return clientRects;
+ }
+
+ /**
+ * Represents a location within the document by a container and an offset.
+ *
+ * @constructor
+ * @param {Node} container Container node.
+ * @param {Number} offset Offset within that container node.
+ * @param {Array} clientRects Optional client rects array for the position.
+ */
+ function CaretPosition(container, offset, clientRects) {
+ function isAtStart() {
+ if (isText(container)) {
+ return offset === 0;
+ }
+
+ return offset === 0;
+ }
+
+ function isAtEnd() {
+ if (isText(container)) {
+ return offset >= container.data.length;
+ }
+
+ return offset >= container.childNodes.length;
+ }
+
+ function toRange() {
+ var range;
+
+ range = createRange(container.ownerDocument);
+ range.setStart(container, offset);
+ range.setEnd(container, offset);
+
+ return range;
+ }
+
+ function getClientRects() {
+ if (!clientRects) {
+ clientRects = getCaretPositionClientRects(new CaretPosition(container, offset));
+ }
+
+ return clientRects;
+ }
+
+ function isVisible() {
+ return getClientRects().length > 0;
+ }
+
+ function isEqual(caretPosition) {
+ return caretPosition && container === caretPosition.container() && offset === caretPosition.offset();
+ }
+
+ function getNode(before) {
+ return resolveIndex(container, before ? offset - 1 : offset);
+ }
+
+ return {
+ /**
+ * Returns the container node.
+ *
+ * @method container
+ * @return {Node} Container node.
+ */
+ container: Fun.constant(container),
+
+ /**
+ * Returns the offset within the container node.
+ *
+ * @method offset
+ * @return {Number} Offset within the container node.
+ */
+ offset: Fun.constant(offset),
+
+ /**
+ * Returns a range out of a the caret position.
+ *
+ * @method toRange
+ * @return {DOMRange} range for the caret position.
+ */
+ toRange: toRange,
+
+ /**
+ * Returns the client rects for the caret position. Might be multiple rects between
+ * block elements.
+ *
+ * @method getClientRects
+ * @return {Array} Array of client rects.
+ */
+ getClientRects: getClientRects,
+
+ /**
+ * Returns true if the caret location is visible/displayed on screen.
+ *
+ * @method isVisible
+ * @return {Boolean} true/false if the position is visible or not.
+ */
+ isVisible: isVisible,
+
+ /**
+ * Returns true if the caret location is at the beginning of text node or container.
+ *
+ * @method isVisible
+ * @return {Boolean} true/false if the position is at the beginning.
+ */
+ isAtStart: isAtStart,
+
+ /**
+ * Returns true if the caret location is at the end of text node or container.
+ *
+ * @method isVisible
+ * @return {Boolean} true/false if the position is at the end.
+ */
+ isAtEnd: isAtEnd,
+
+ /**
+ * Compares the caret position to another caret position. This will only compare the
+ * container and offset not it's visual position.
+ *
+ * @method isEqual
+ * @param {tinymce.caret.CaretPosition} caretPosition Caret position to compare with.
+ * @return {Boolean} true if the caret positions are equal.
+ */
+ isEqual: isEqual,
+
+ /**
+ * Returns the closest resolved node from a node index. That means if you have an offset after the
+ * last node in a container it will return that last node.
+ *
+ * @method getNode
+ * @return {Node} Node that is closest to the index.
+ */
+ getNode: getNode
+ };
+ }
+
+ /**
+ * Creates a caret position from the start of a range.
+ *
+ * @method fromRangeStart
+ * @param {DOMRange} range DOM Range to create caret position from.
+ * @return {tinymce.caret.CaretPosition} Caret position from the start of DOM range.
+ */
+ CaretPosition.fromRangeStart = function(range) {
+ return new CaretPosition(range.startContainer, range.startOffset);
+ };
+
+ /**
+ * Creates a caret position from the end of a range.
+ *
+ * @method fromRangeEnd
+ * @param {DOMRange} range DOM Range to create caret position from.
+ * @return {tinymce.caret.CaretPosition} Caret position from the end of DOM range.
+ */
+ CaretPosition.fromRangeEnd = function(range) {
+ return new CaretPosition(range.endContainer, range.endOffset);
+ };
+
+ /**
+ * Creates a caret position from a node and places the offset after it.
+ *
+ * @method after
+ * @param {Node} node Node to get caret position from.
+ * @return {tinymce.caret.CaretPosition} Caret position from the node.
+ */
+ CaretPosition.after = function(node) {
+ return new CaretPosition(node.parentNode, nodeIndex(node) + 1);
+ };
+
+ /**
+ * Creates a caret position from a node and places the offset before it.
+ *
+ * @method before
+ * @param {Node} node Node to get caret position from.
+ * @return {tinymce.caret.CaretPosition} Caret position from the node.
+ */
+ CaretPosition.before = function(node) {
+ return new CaretPosition(node.parentNode, nodeIndex(node));
+ };
+
+ return CaretPosition;
+});
+
+// Included from: js/tinymce/classes/caret/CaretBookmark.js
+
+/**
+ * CaretBookmark.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module creates or resolves xpath like string representation of a CaretPositions.
+ *
+ * The format is a / separated list of chunks with:
+ * <element|text()>[index|after|before]
+ *
+ * For example:
+ * p[0]/b[0]/text()[0],1 = <p><b>a|c</b></p>
+ * p[0]/img[0],before = <p>|<img></p>
+ * p[0]/img[0],after = <p><img>|</p>
+ *
+ * @private
+ * @static
+ * @class tinymce.caret.CaretBookmark
+ * @example
+ * var bookmark = CaretBookmark.create(rootElm, CaretPosition.before(rootElm.firstChild));
+ * var caretPosition = CaretBookmark.resolve(bookmark);
+ */
+define('tinymce/caret/CaretBookmark', [
+ 'tinymce/dom/NodeType',
+ 'tinymce/dom/DOMUtils',
+ 'tinymce/util/Fun',
+ 'tinymce/util/Arr',
+ 'tinymce/caret/CaretPosition'
+], function(NodeType, DomUtils, Fun, Arr, CaretPosition) {
+ var isText = NodeType.isText,
+ isBogus = NodeType.isBogus,
+ nodeIndex = DomUtils.nodeIndex;
+
+ function normalizedParent(node) {
+ var parentNode = node.parentNode;
+
+ if (isBogus(parentNode)) {
+ return normalizedParent(parentNode);
+ }
+
+ return parentNode;
+ }
+
+ function getChildNodes(node) {
+ if (!node) {
+ return [];
+ }
+
+ return Arr.reduce(node.childNodes, function(result, node) {
+ if (isBogus(node) && node.nodeName != 'BR') {
+ result = result.concat(getChildNodes(node));
+ } else {
+ result.push(node);
+ }
+
+ return result;
+ }, []);
+ }
+
+ function normalizedTextOffset(textNode, offset) {
+ while ((textNode = textNode.previousSibling)) {
+ if (!isText(textNode)) {
+ break;
+ }
+
+ offset += textNode.data.length;
+ }
+
+ return offset;
+ }
+
+ function equal(targetValue) {
+ return function(value) {
+ return targetValue === value;
+ };
+ }
+
+ function normalizedNodeIndex(node) {
+ var nodes, index, numTextFragments;
+
+ nodes = getChildNodes(normalizedParent(node));
+ index = Arr.findIndex(nodes, equal(node), node);
+ nodes = nodes.slice(0, index + 1);
+ numTextFragments = Arr.reduce(nodes, function(result, node, i) {
+ if (isText(node) && isText(nodes[i - 1])) {
+ result++;
+ }
+
+ return result;
+ }, 0);
+
+ nodes = Arr.filter(nodes, NodeType.matchNodeNames(node.nodeName));
+ index = Arr.findIndex(nodes, equal(node), node);
+
+ return index - numTextFragments;
+ }
+
+ function createPathItem(node) {
+ var name;
+
+ if (isText(node)) {
+ name = 'text()';
+ } else {
+ name = node.nodeName.toLowerCase();
+ }
+
+ return name + '[' + normalizedNodeIndex(node) + ']';
+ }
+
+ function parentsUntil(rootNode, node, predicate) {
+ var parents = [];
+
+ for (node = node.parentNode; node != rootNode; node = node.parentNode) {
+ if (predicate && predicate(node)) {
+ break;
+ }
+
+ parents.push(node);
+ }
+
+ return parents;
+ }
+
+ function create(rootNode, caretPosition) {
+ var container, offset, path = [],
+ outputOffset, childNodes, parents;
+
+ container = caretPosition.container();
+ offset = caretPosition.offset();
+
+ if (isText(container)) {
+ outputOffset = normalizedTextOffset(container, offset);
+ } else {
+ childNodes = container.childNodes;
+ if (offset >= childNodes.length) {
+ outputOffset = 'after';
+ offset = childNodes.length - 1;
+ } else {
+ outputOffset = 'before';
+ }
+
+ container = childNodes[offset];
+ }
+
+ path.push(createPathItem(container));
+ parents = parentsUntil(rootNode, container);
+ parents = Arr.filter(parents, Fun.negate(NodeType.isBogus));
+ path = path.concat(Arr.map(parents, function(node) {
+ return createPathItem(node);
+ }));
+
+ return path.reverse().join('/') + ',' + outputOffset;
+ }
+
+ function resolvePathItem(node, name, index) {
+ var nodes = getChildNodes(node);
+
+ nodes = Arr.filter(nodes, function(node, index) {
+ return !isText(node) || !isText(nodes[index - 1]);
+ });
+
+ nodes = Arr.filter(nodes, NodeType.matchNodeNames(name));
+ return nodes[index];
+ }
+
+ function findTextPosition(container, offset) {
+ var node = container, targetOffset = 0, dataLen;
+
+ while (isText(node)) {
+ dataLen = node.data.length;
+
+ if (offset >= targetOffset && offset <= targetOffset + dataLen) {
+ container = node;
+ offset = offset - targetOffset;
+ break;
+ }
+
+ if (!isText(node.nextSibling)) {
+ container = node;
+ offset = dataLen;
+ break;
+ }
+
+ targetOffset += dataLen;
+ node = node.nextSibling;
+ }
+
+ if (offset > container.data.length) {
+ offset = container.data.length;
+ }
+
+ return new CaretPosition(container, offset);
+ }
+
+ function resolve(rootNode, path) {
+ var parts, container, offset;
+
+ if (!path) {
+ return null;
+ }
+
+ parts = path.split(',');
+ path = parts[0].split('/');
+ offset = parts.length > 1 ? parts[1] : 'before';
+
+ container = Arr.reduce(path, function(result, value) {
+ value = /([\w\-\(\)]+)\[([0-9]+)\]/.exec(value);
+ if (!value) {
+ return null;
+ }
+
+ if (value[1] === 'text()') {
+ value[1] = '#text';
+ }
+
+ return resolvePathItem(result, value[1], parseInt(value[2], 10));
+ }, rootNode);
+
+ if (!container) {
+ return null;
+ }
+
+ if (!isText(container)) {
+ if (offset === 'after') {
+ offset = nodeIndex(container) + 1;
+ } else {
+ offset = nodeIndex(container);
+ }
+
+ return new CaretPosition(container.parentNode, offset);
+ }
+
+ return findTextPosition(container, parseInt(offset, 10));
+ }
+
+ return {
+ /**
+ * Create a xpath bookmark location for the specified caret position.
+ *
+ * @method create
+ * @param {Node} rootNode Root node to create bookmark within.
+ * @param {tinymce.caret.CaretPosition} caretPosition Caret position within the root node.
+ * @return {String} String xpath like location of caret position.
+ */
+ create: create,
+
+ /**
+ * Resolves a xpath like bookmark location to the a caret position.
+ *
+ * @method resolve
+ * @param {Node} rootNode Root node to resolve xpath bookmark within.
+ * @param {String} bookmark Bookmark string to resolve.
+ * @return {tinymce.caret.CaretPosition} Caret position resolved from xpath like bookmark.
+ */
+ resolve: resolve
+ };
+});
+
+// Included from: js/tinymce/classes/dom/BookmarkManager.js
+
+/**
+ * BookmarkManager.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class handles selection bookmarks.
+ *
+ * @class tinymce.dom.BookmarkManager
+ */
+define("tinymce/dom/BookmarkManager", [
+ "tinymce/Env",
+ "tinymce/util/Tools",
+ "tinymce/caret/CaretContainer",
+ "tinymce/caret/CaretBookmark",
+ "tinymce/caret/CaretPosition",
+ "tinymce/dom/NodeType",
+ "tinymce/dom/RangeUtils"
+], function(Env, Tools, CaretContainer, CaretBookmark, CaretPosition, NodeType, RangeUtils) {
+ var isContentEditableFalse = NodeType.isContentEditableFalse;
+
+ /**
+ * Constructs a new BookmarkManager instance for a specific selection instance.
+ *
+ * @constructor
+ * @method BookmarkManager
+ * @param {tinymce.dom.Selection} selection Selection instance to handle bookmarks for.
+ */
+ function BookmarkManager(selection) {
+ var dom = selection.dom;
+
+ /**
+ * Returns a bookmark location for the current selection. This bookmark object
+ * can then be used to restore the selection after some content modification to the document.
+ *
+ * @method getBookmark
+ * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
+ * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
+ * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
+ * @example
+ * // Stores a bookmark of the current selection
+ * var bm = tinymce.activeEditor.selection.getBookmark();
+ *
+ * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
+ *
+ * // Restore the selection bookmark
+ * tinymce.activeEditor.selection.moveToBookmark(bm);
+ */
+ this.getBookmark = function(type, normalized) {
+ var rng, rng2, id, collapsed, name, element, chr = '', styles;
+
+ function findIndex(name, element) {
+ var count = 0;
+
+ Tools.each(dom.select(name), function(node) {
+ if (node.getAttribute('data-mce-bogus') === 'all') {
+ return;
+ }
+
+ if (node == element) {
+ return false;
+ }
+
+ count++;
+ });
+
+ return count;
+ }
+
+ function normalizeTableCellSelection(rng) {
+ function moveEndPoint(start) {
+ var container, offset, childNodes, prefix = start ? 'start' : 'end';
+
+ container = rng[prefix + 'Container'];
+ offset = rng[prefix + 'Offset'];
+
+ if (container.nodeType == 1 && container.nodeName == "TR") {
+ childNodes = container.childNodes;
+ container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)];
+ if (container) {
+ offset = start ? 0 : container.childNodes.length;
+ rng['set' + (start ? 'Start' : 'End')](container, offset);
+ }
+ }
+ }
+
+ moveEndPoint(true);
+ moveEndPoint();
+
+ return rng;
+ }
+
+ function getLocation(rng) {
+ var root = dom.getRoot(), bookmark = {};
+
+ function getPoint(rng, start) {
+ var container = rng[start ? 'startContainer' : 'endContainer'],
+ offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0;
+
+ if (container.nodeType == 3) {
+ if (normalized) {
+ for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) {
+ offset += node.nodeValue.length;
+ }
+ }
+
+ point.push(offset);
+ } else {
+ childNodes = container.childNodes;
+
+ if (offset >= childNodes.length && childNodes.length) {
+ after = 1;
+ offset = Math.max(0, childNodes.length - 1);
+ }
+
+ point.push(dom.nodeIndex(childNodes[offset], normalized) + after);
+ }
+
+ for (; container && container != root; container = container.parentNode) {
+ point.push(dom.nodeIndex(container, normalized));
+ }
+
+ return point;
+ }
+
+ bookmark.start = getPoint(rng, true);
+
+ if (!selection.isCollapsed()) {
+ bookmark.end = getPoint(rng);
+ }
+
+ return bookmark;
+ }
+
+ function findAdjacentContentEditableFalseElm(rng) {
+ function findSibling(node, offset) {
+ var sibling;
+
+ if (NodeType.isElement(node)) {
+ node = RangeUtils.getNode(node, offset);
+ if (isContentEditableFalse(node)) {
+ return node;
+ }
+ }
+
+ if (CaretContainer.isCaretContainer(node)) {
+ if (NodeType.isText(node) && CaretContainer.isCaretContainerBlock(node)) {
+ node = node.parentNode;
+ }
+
+ sibling = node.previousSibling;
+ if (isContentEditableFalse(sibling)) {
+ return sibling;
+ }
+
+ sibling = node.nextSibling;
+ if (isContentEditableFalse(sibling)) {
+ return sibling;
+ }
+ }
+ }
+
+ return findSibling(rng.startContainer, rng.startOffset) || findSibling(rng.endContainer, rng.endOffset);
+ }
+
+ if (type == 2) {
+ element = selection.getNode();
+ name = element ? element.nodeName : null;
+ rng = selection.getRng();
+
+ if (isContentEditableFalse(element) || name == 'IMG') {
+ return {name: name, index: findIndex(name, element)};
+ }
+
+ if (selection.tridentSel) {
+ return selection.tridentSel.getBookmark(type);
+ }
+
+ element = findAdjacentContentEditableFalseElm(rng);
+ if (element) {
+ name = element.tagName;
+ return {name: name, index: findIndex(name, element)};
+ }
+
+ return getLocation(rng);
+ }
+
+ if (type == 3) {
+ rng = selection.getRng();
+
+ return {
+ start: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeStart(rng)),
+ end: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeEnd(rng))
+ };
+ }
+
+ // Handle simple range
+ if (type) {
+ return {rng: selection.getRng()};
+ }
+
+ rng = selection.getRng();
+ id = dom.uniqueId();
+ collapsed = selection.isCollapsed();
+ styles = 'overflow:hidden;line-height:0px';
+
+ // Explorer method
+ if (rng.duplicate || rng.item) {
+ // Text selection
+ if (!rng.item) {
+ rng2 = rng.duplicate();
+
+ try {
+ // Insert start marker
+ rng.collapse();
+ rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>');
+
+ // Insert end marker
+ if (!collapsed) {
+ rng2.collapse(false);
+
+ // Detect the empty space after block elements in IE and move the
+ // end back one character <p></p>] becomes <p>]</p>
+ rng.moveToElementText(rng2.parentElement());
+ if (rng.compareEndPoints('StartToEnd', rng2) === 0) {
+ rng2.move('character', -1);
+ }
+
+ rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>');
+ }
+ } catch (ex) {
+ // IE might throw unspecified error so lets ignore it
+ return null;
+ }
+ } else {
+ // Control selection
+ element = rng.item(0);
+ name = element.nodeName;
+
+ return {name: name, index: findIndex(name, element)};
+ }
+ } else {
+ element = selection.getNode();
+ name = element.nodeName;
+ if (name == 'IMG') {
+ return {name: name, index: findIndex(name, element)};
+ }
+
+ // W3C method
+ rng2 = normalizeTableCellSelection(rng.cloneRange());
+
+ // Insert end marker
+ if (!collapsed) {
+ rng2.collapse(false);
+ rng2.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_end', style: styles}, chr));
+ }
+
+ rng = normalizeTableCellSelection(rng);
+ rng.collapse(true);
+ rng.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_start', style: styles}, chr));
+ }
+
+ selection.moveToBookmark({id: id, keep: 1});
+
+ return {id: id};
+ };
+
+ /**
+ * Restores the selection to the specified bookmark.
+ *
+ * @method moveToBookmark
+ * @param {Object} bookmark Bookmark to restore selection from.
+ * @return {Boolean} true/false if it was successful or not.
+ * @example
+ * // Stores a bookmark of the current selection
+ * var bm = tinymce.activeEditor.selection.getBookmark();
+ *
+ * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
+ *
+ * // Restore the selection bookmark
+ * tinymce.activeEditor.selection.moveToBookmark(bm);
+ */
+ this.moveToBookmark = function(bookmark) {
+ var rng, root, startContainer, endContainer, startOffset, endOffset;
+
+ function setEndPoint(start) {
+ var point = bookmark[start ? 'start' : 'end'], i, node, offset, children;
+
+ if (point) {
+ offset = point[0];
+
+ // Find container node
+ for (node = root, i = point.length - 1; i >= 1; i--) {
+ children = node.childNodes;
+
+ if (point[i] > children.length - 1) {
+ return;
+ }
+
+ node = children[point[i]];
+ }
+
+ // Move text offset to best suitable location
+ if (node.nodeType === 3) {
+ offset = Math.min(point[0], node.nodeValue.length);
+ }
+
+ // Move element offset to best suitable location
+ if (node.nodeType === 1) {
+ offset = Math.min(point[0], node.childNodes.length);
+ }
+
+ // Set offset within container node
+ if (start) {
+ rng.setStart(node, offset);
+ } else {
+ rng.setEnd(node, offset);
+ }
+ }
+
+ return true;
+ }
+
+ function restoreEndPoint(suffix) {
+ var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep;
+
+ if (marker) {
+ node = marker.parentNode;
+
+ if (suffix == 'start') {
+ if (!keep) {
+ idx = dom.nodeIndex(marker);
+ } else {
+ node = marker.firstChild;
+ idx = 1;
+ }
+
+ startContainer = endContainer = node;
+ startOffset = endOffset = idx;
+ } else {
+ if (!keep) {
+ idx = dom.nodeIndex(marker);
+ } else {
+ node = marker.firstChild;
+ idx = 1;
+ }
+
+ endContainer = node;
+ endOffset = idx;
+ }
+
+ if (!keep) {
+ prev = marker.previousSibling;
+ next = marker.nextSibling;
+
+ // Remove all marker text nodes
+ Tools.each(Tools.grep(marker.childNodes), function(node) {
+ if (node.nodeType == 3) {
+ node.nodeValue = node.nodeValue.replace(/\uFEFF/g, '');
+ }
+ });
+
+ // Remove marker but keep children if for example contents where inserted into the marker
+ // Also remove duplicated instances of the marker for example by a
+ // split operation or by WebKit auto split on paste feature
+ while ((marker = dom.get(bookmark.id + '_' + suffix))) {
+ dom.remove(marker, 1);
+ }
+
+ // If siblings are text nodes then merge them unless it's Opera since it some how removes the node
+ // and we are sniffing since adding a lot of detection code for a browser with 3% of the market
+ // isn't worth the effort. Sorry, Opera but it's just a fact
+ if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !Env.opera) {
+ idx = prev.nodeValue.length;
+ prev.appendData(next.nodeValue);
+ dom.remove(next);
+
+ if (suffix == 'start') {
+ startContainer = endContainer = prev;
+ startOffset = endOffset = idx;
+ } else {
+ endContainer = prev;
+ endOffset = idx;
+ }
+ }
+ }
+ }
+ }
+
+ function addBogus(node) {
+ // Adds a bogus BR element for empty block elements
+ if (dom.isBlock(node) && !node.innerHTML && !Env.ie) {
+ node.innerHTML = '<br data-mce-bogus="1" />';
+ }
+
+ return node;
+ }
+
+ function resolveCaretPositionBookmark() {
+ var rng, pos;
+
+ rng = dom.createRng();
+ pos = CaretBookmark.resolve(dom.getRoot(), bookmark.start);
+ rng.setStart(pos.container(), pos.offset());
+
+ pos = CaretBookmark.resolve(dom.getRoot(), bookmark.end);
+ rng.setEnd(pos.container(), pos.offset());
+
+ return rng;
+ }
+
+ if (bookmark) {
+ if (Tools.isArray(bookmark.start)) {
+ rng = dom.createRng();
+ root = dom.getRoot();
+
+ if (selection.tridentSel) {
+ return selection.tridentSel.moveToBookmark(bookmark);
+ }
+
+ if (setEndPoint(true) && setEndPoint()) {
+ selection.setRng(rng);
+ }
+ } else if (typeof bookmark.start == 'string') {
+ selection.setRng(resolveCaretPositionBookmark(bookmark));
+ } else if (bookmark.id) {
+ // Restore start/end points
+ restoreEndPoint('start');
+ restoreEndPoint('end');
+
+ if (startContainer) {
+ rng = dom.createRng();
+ rng.setStart(addBogus(startContainer), startOffset);
+ rng.setEnd(addBogus(endContainer), endOffset);
+ selection.setRng(rng);
+ }
+ } else if (bookmark.name) {
+ selection.select(dom.select(bookmark.name)[bookmark.index]);
+ } else if (bookmark.rng) {
+ selection.setRng(bookmark.rng);
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns true/false if the specified node is a bookmark node or not.
+ *
+ * @static
+ * @method isBookmarkNode
+ * @param {DOMNode} node DOM Node to check if it's a bookmark node or not.
+ * @return {Boolean} true/false if the node is a bookmark node or not.
+ */
+ BookmarkManager.isBookmarkNode = function(node) {
+ return node && node.tagName === 'SPAN' && node.getAttribute('data-mce-type') === 'bookmark';
+ };
+
+ return BookmarkManager;
+});
+
+// Included from: js/tinymce/classes/dom/Selection.js
+
+/**
+ * Selection.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class handles text and control selection it's an crossbrowser utility class.
+ * Consult the TinyMCE Wiki API for more details and examples on how to use this class.
+ *
+ * @class tinymce.dom.Selection
+ * @example
+ * // Getting the currently selected node for the active editor
+ * alert(tinymce.activeEditor.selection.getNode().nodeName);
+ */
+define("tinymce/dom/Selection", [
+ "tinymce/dom/TreeWalker",
+ "tinymce/dom/TridentSelection",
+ "tinymce/dom/ControlSelection",
+ "tinymce/dom/RangeUtils",
+ "tinymce/dom/BookmarkManager",
+ "tinymce/dom/NodeType",
+ "tinymce/Env",
+ "tinymce/util/Tools",
+ "tinymce/caret/CaretPosition"
+], function(TreeWalker, TridentSelection, ControlSelection, RangeUtils, BookmarkManager, NodeType, Env, Tools, CaretPosition) {
+ var each = Tools.each, trim = Tools.trim;
+ var isIE = Env.ie;
+
+ /**
+ * Constructs a new selection instance.
+ *
+ * @constructor
+ * @method Selection
+ * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference.
+ * @param {Window} win Window to bind the selection object to.
+ * @param {tinymce.Editor} editor Editor instance of the selection.
+ * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent.
+ */
+ function Selection(dom, win, serializer, editor) {
+ var self = this;
+
+ self.dom = dom;
+ self.win = win;
+ self.serializer = serializer;
+ self.editor = editor;
+ self.bookmarkManager = new BookmarkManager(self);
+ self.controlSelection = new ControlSelection(self, editor);
+
+ // No W3C Range support
+ if (!self.win.getSelection) {
+ self.tridentSel = new TridentSelection(self);
+ }
+ }
+
+ Selection.prototype = {
+ /**
+ * Move the selection cursor range to the specified node and offset.
+ * If there is no node specified it will move it to the first suitable location within the body.
+ *
+ * @method setCursorLocation
+ * @param {Node} node Optional node to put the cursor in.
+ * @param {Number} offset Optional offset from the start of the node to put the cursor at.
+ */
+ setCursorLocation: function(node, offset) {
+ var self = this, rng = self.dom.createRng();
+
+ if (!node) {
+ self._moveEndPoint(rng, self.editor.getBody(), true);
+ self.setRng(rng);
+ } else {
+ rng.setStart(node, offset);
+ rng.setEnd(node, offset);
+ self.setRng(rng);
+ self.collapse(false);
+ }
+ },
+
+ /**
+ * Returns the selected contents using the DOM serializer passed in to this class.
+ *
+ * @method getContent
+ * @param {Object} args Optional settings class with for example output format text or html.
+ * @return {String} Selected contents in for example HTML format.
+ * @example
+ * // Alerts the currently selected contents
+ * alert(tinymce.activeEditor.selection.getContent());
+ *
+ * // Alerts the currently selected contents as plain text
+ * alert(tinymce.activeEditor.selection.getContent({format: 'text'}));
+ */
+ getContent: function(args) {
+ var self = this, rng = self.getRng(), tmpElm = self.dom.create("body");
+ var se = self.getSel(), whiteSpaceBefore, whiteSpaceAfter, fragment;
+
+ args = args || {};
+ whiteSpaceBefore = whiteSpaceAfter = '';
+ args.get = true;
+ args.format = args.format || 'html';
+ args.selection = true;
+ self.editor.fire('BeforeGetContent', args);
+
+ if (args.format == 'text') {
+ return self.isCollapsed() ? '' : (rng.text || (se.toString ? se.toString() : ''));
+ }
+
+ if (rng.cloneContents) {
+ fragment = rng.cloneContents();
+
+ if (fragment) {
+ tmpElm.appendChild(fragment);
+ }
+ } else if (rng.item !== undefined || rng.htmlText !== undefined) {
+ // IE will produce invalid markup if elements are present that
+ // it doesn't understand like custom elements or HTML5 elements.
+ // Adding a BR in front of the contents and then remoiving it seems to fix it though.
+ tmpElm.innerHTML = '<br>' + (rng.item ? rng.item(0).outerHTML : rng.htmlText);
+ tmpElm.removeChild(tmpElm.firstChild);
+ } else {
+ tmpElm.innerHTML = rng.toString();
+ }
+
+ // Keep whitespace before and after
+ if (/^\s/.test(tmpElm.innerHTML)) {
+ whiteSpaceBefore = ' ';
+ }
+
+ if (/\s+$/.test(tmpElm.innerHTML)) {
+ whiteSpaceAfter = ' ';
+ }
+
+ args.getInner = true;
+
+ args.content = self.isCollapsed() ? '' : whiteSpaceBefore + self.serializer.serialize(tmpElm, args) + whiteSpaceAfter;
+ self.editor.fire('GetContent', args);
+
+ return args.content;
+ },
+
+ /**
+ * Sets the current selection to the specified content. If any contents is selected it will be replaced
+ * with the contents passed in to this function. If there is no selection the contents will be inserted
+ * where the caret is placed in the editor/page.
+ *
+ * @method setContent
+ * @param {String} content HTML contents to set could also be other formats depending on settings.
+ * @param {Object} args Optional settings object with for example data format.
+ * @example
+ * // Inserts some HTML contents at the current selection
+ * tinymce.activeEditor.selection.setContent('<strong>Some contents</strong>');
+ */
+ setContent: function(content, args) {
+ var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp;
+
+ args = args || {format: 'html'};
+ args.set = true;
+ args.selection = true;
+ args.content = content;
+
+ // Dispatch before set content event
+ if (!args.no_events) {
+ self.editor.fire('BeforeSetContent', args);
+ }
+
+ content = args.content;
+
+ if (rng.insertNode) {
+ // Make caret marker since insertNode places the caret in the beginning of text after insert
+ content += '<span id="__caret">_</span>';
+
+ // Delete and insert new node
+ if (rng.startContainer == doc && rng.endContainer == doc) {
+ // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents
+ doc.body.innerHTML = content;
+ } else {
+ rng.deleteContents();
+
+ if (doc.body.childNodes.length === 0) {
+ doc.body.innerHTML = content;
+ } else {
+ // createContextualFragment doesn't exists in IE 9 DOMRanges
+ if (rng.createContextualFragment) {
+ rng.insertNode(rng.createContextualFragment(content));
+ } else {
+ // Fake createContextualFragment call in IE 9
+ frag = doc.createDocumentFragment();
+ temp = doc.createElement('div');
+
+ frag.appendChild(temp);
+ temp.outerHTML = content;
+
+ rng.insertNode(frag);
+ }
+ }
+ }
+
+ // Move to caret marker
+ caretNode = self.dom.get('__caret');
+
+ // Make sure we wrap it compleatly, Opera fails with a simple select call
+ rng = doc.createRange();
+ rng.setStartBefore(caretNode);
+ rng.setEndBefore(caretNode);
+ self.setRng(rng);
+
+ // Remove the caret position
+ self.dom.remove('__caret');
+
+ try {
+ self.setRng(rng);
+ } catch (ex) {
+ // Might fail on Opera for some odd reason
+ }
+ } else {
+ if (rng.item) {
+ // Delete content and get caret text selection
+ doc.execCommand('Delete', false, null);
+ rng = self.getRng();
+ }
+
+ // Explorer removes spaces from the beginning of pasted contents
+ if (/^\s+/.test(content)) {
+ rng.pasteHTML('<span id="__mce_tmp">_</span>' + content);
+ self.dom.remove('__mce_tmp');
+ } else {
+ rng.pasteHTML(content);
+ }
+ }
+
+ // Dispatch set content event
+ if (!args.no_events) {
+ self.editor.fire('SetContent', args);
+ }
+ },
+
+ /**
+ * Returns the start element of a selection range. If the start is in a text
+ * node the parent element will be returned.
+ *
+ * @method getStart
+ * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element.
+ * @return {Element} Start element of selection range.
+ */
+ getStart: function(real) {
+ var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node;
+
+ if (rng.duplicate || rng.item) {
+ // Control selection, return first item
+ if (rng.item) {
+ return rng.item(0);
+ }
+
+ // Get start element
+ checkRng = rng.duplicate();
+ checkRng.collapse(1);
+ startElement = checkRng.parentElement();
+ if (startElement.ownerDocument !== self.dom.doc) {
+ startElement = self.dom.getRoot();
+ }
+
+ // Check if range parent is inside the start element, then return the inner parent element
+ // This will fix issues when a single element is selected, IE would otherwise return the wrong start element
+ parentElement = node = rng.parentElement();
+ while ((node = node.parentNode)) {
+ if (node == startElement) {
+ startElement = parentElement;
+ break;
+ }
+ }
+
+ return startElement;
+ }
+
+ startElement = rng.startContainer;
+
+ if (startElement.nodeType == 1 && startElement.hasChildNodes()) {
+ if (!real || !rng.collapsed) {
+ startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)];
+ }
+ }
+
+ if (startElement && startElement.nodeType == 3) {
+ return startElement.parentNode;
+ }
+
+ return startElement;
+ },
+
+ /**
+ * Returns the end element of a selection range. If the end is in a text
+ * node the parent element will be returned.
+ *
+ * @method getEnd
+ * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element.
+ * @return {Element} End element of selection range.
+ */
+ getEnd: function(real) {
+ var self = this, rng = self.getRng(), endElement, endOffset;
+
+ if (rng.duplicate || rng.item) {
+ if (rng.item) {
+ return rng.item(0);
+ }
+
+ rng = rng.duplicate();
+ rng.collapse(0);
+ endElement = rng.parentElement();
+ if (endElement.ownerDocument !== self.dom.doc) {
+ endElement = self.dom.getRoot();
+ }
+
+ if (endElement && endElement.nodeName == 'BODY') {
+ return endElement.lastChild || endElement;
+ }
+
+ return endElement;
+ }
+
+ endElement = rng.endContainer;
+ endOffset = rng.endOffset;
+
+ if (endElement.nodeType == 1 && endElement.hasChildNodes()) {
+ if (!real || !rng.collapsed) {
+ endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset];
+ }
+ }
+
+ if (endElement && endElement.nodeType == 3) {
+ return endElement.parentNode;
+ }
+
+ return endElement;
+ },
+
+ /**
+ * Returns a bookmark location for the current selection. This bookmark object
+ * can then be used to restore the selection after some content modification to the document.
+ *
+ * @method getBookmark
+ * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
+ * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
+ * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
+ * @example
+ * // Stores a bookmark of the current selection
+ * var bm = tinymce.activeEditor.selection.getBookmark();
+ *
+ * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
+ *
+ * // Restore the selection bookmark
+ * tinymce.activeEditor.selection.moveToBookmark(bm);
+ */
+ getBookmark: function(type, normalized) {
+ return this.bookmarkManager.getBookmark(type, normalized);
+ },
+
+ /**
+ * Restores the selection to the specified bookmark.
+ *
+ * @method moveToBookmark
+ * @param {Object} bookmark Bookmark to restore selection from.
+ * @return {Boolean} true/false if it was successful or not.
+ * @example
+ * // Stores a bookmark of the current selection
+ * var bm = tinymce.activeEditor.selection.getBookmark();
+ *
+ * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
+ *
+ * // Restore the selection bookmark
+ * tinymce.activeEditor.selection.moveToBookmark(bm);
+ */
+ moveToBookmark: function(bookmark) {
+ return this.bookmarkManager.moveToBookmark(bookmark);
+ },
+
+ /**
+ * Selects the specified element. This will place the start and end of the selection range around the element.
+ *
+ * @method select
+ * @param {Element} node HTML DOM element to select.
+ * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser.
+ * @return {Element} Selected element the same element as the one that got passed in.
+ * @example
+ * // Select the first paragraph in the active editor
+ * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]);
+ */
+ select: function(node, content) {
+ var self = this, dom = self.dom, rng = dom.createRng(), idx;
+
+ // Clear stored range set by FocusManager
+ self.lastFocusBookmark = null;
+
+ if (node) {
+ if (!content && self.controlSelection.controlSelect(node)) {
+ return;
+ }
+
+ idx = dom.nodeIndex(node);
+ rng.setStart(node.parentNode, idx);
+ rng.setEnd(node.parentNode, idx + 1);
+
+ // Find first/last text node or BR element
+ if (content) {
+ self._moveEndPoint(rng, node, true);
+ self._moveEndPoint(rng, node);
+ }
+
+ self.setRng(rng);
+ }
+
+ return node;
+ },
+
+ /**
+ * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
+ *
+ * @method isCollapsed
+ * @return {Boolean} true/false state if the selection range is collapsed or not.
+ * Collapsed means if it's a caret or a larger selection.
+ */
+ isCollapsed: function() {
+ var self = this, rng = self.getRng(), sel = self.getSel();
+
+ if (!rng || rng.item) {
+ return false;
+ }
+
+ if (rng.compareEndPoints) {
+ return rng.compareEndPoints('StartToEnd', rng) === 0;
+ }
+
+ return !sel || rng.collapsed;
+ },
+
+ /**
+ * Collapse the selection to start or end of range.
+ *
+ * @method collapse
+ * @param {Boolean} toStart Optional boolean state if to collapse to end or not. Defaults to false.
+ */
+ collapse: function(toStart) {
+ var self = this, rng = self.getRng(), node;
+
+ // Control range on IE
+ if (rng.item) {
+ node = rng.item(0);
+ rng = self.win.document.body.createTextRange();
+ rng.moveToElementText(node);
+ }
+
+ rng.collapse(!!toStart);
+ self.setRng(rng);
+ },
+
+ /**
+ * Returns the browsers internal selection object.
+ *
+ * @method getSel
+ * @return {Selection} Internal browser selection object.
+ */
+ getSel: function() {
+ var win = this.win;
+
+ return win.getSelection ? win.getSelection() : win.document.selection;
+ },
+
+ /**
+ * Returns the browsers internal range object.
+ *
+ * @method getRng
+ * @param {Boolean} w3c Forces a compatible W3C range on IE.
+ * @return {Range} Internal browser range object.
+ * @see http://www.quirksmode.org/dom/range_intro.html
+ * @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/
+ */
+ getRng: function(w3c) {
+ var self = this, selection, rng, elm, doc, ieRng, evt;
+
+ function tryCompareBoundaryPoints(how, sourceRange, destinationRange) {
+ try {
+ return sourceRange.compareBoundaryPoints(how, destinationRange);
+ } catch (ex) {
+ // Gecko throws wrong document exception if the range points
+ // to nodes that where removed from the dom #6690
+ // Browsers should mutate existing DOMRange instances so that they always point
+ // to something in the document this is not the case in Gecko works fine in IE/WebKit/Blink
+ // For performance reasons just return -1
+ return -1;
+ }
+ }
+
+ if (!self.win) {
+ return null;
+ }
+
+ doc = self.win.document;
+
+ if (typeof doc === 'undefined' || doc === null) {
+ return null;
+ }
+
+ // Use last rng passed from FocusManager if it's available this enables
+ // calls to editor.selection.getStart() to work when caret focus is lost on IE
+ if (!w3c && self.lastFocusBookmark) {
+ var bookmark = self.lastFocusBookmark;
+
+ // Convert bookmark to range IE 11 fix
+ if (bookmark.startContainer) {
+ rng = doc.createRange();
+ rng.setStart(bookmark.startContainer, bookmark.startOffset);
+ rng.setEnd(bookmark.endContainer, bookmark.endOffset);
+ } else {
+ rng = bookmark;
+ }
+
+ return rng;
+ }
+
+ // Found tridentSel object then we need to use that one
+ if (w3c && self.tridentSel) {
+ return self.tridentSel.getRangeAt(0);
+ }
+
+ try {
+ if ((selection = self.getSel())) {
+ if (selection.rangeCount > 0) {
+ rng = selection.getRangeAt(0);
+ } else {
+ rng = selection.createRange ? selection.createRange() : doc.createRange();
+ }
+ }
+ } catch (ex) {
+ // IE throws unspecified error here if TinyMCE is placed in a frame/iframe
+ }
+
+ evt = self.editor.fire('GetSelectionRange', {range: rng});
+ if (evt.range !== rng) {
+ return evt.range;
+ }
+
+ // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet
+ // IE 11 doesn't support the selection object so we check for that as well
+ if (isIE && rng && rng.setStart && doc.selection) {
+ try {
+ // IE will sometimes throw an exception here
+ ieRng = doc.selection.createRange();
+ } catch (ex) {
+ // Ignore
+ }
+
+ if (ieRng && ieRng.item) {
+ elm = ieRng.item(0);
+ rng = doc.createRange();
+ rng.setStartBefore(elm);
+ rng.setEndAfter(elm);
+ }
+ }
+
+ // No range found then create an empty one
+ // This can occur when the editor is placed in a hidden container element on Gecko
+ // Or on IE when there was an exception
+ if (!rng) {
+ rng = doc.createRange ? doc.createRange() : doc.body.createTextRange();
+ }
+
+ // If range is at start of document then move it to start of body
+ if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) {
+ elm = self.dom.getRoot();
+ rng.setStart(elm, 0);
+ rng.setEnd(elm, 0);
+ }
+
+ if (self.selectedRange && self.explicitRange) {
+ if (tryCompareBoundaryPoints(rng.START_TO_START, rng, self.selectedRange) === 0 &&
+ tryCompareBoundaryPoints(rng.END_TO_END, rng, self.selectedRange) === 0) {
+ // Safari, Opera and Chrome only ever select text which causes the range to change.
+ // This lets us use the originally set range if the selection hasn't been changed by the user.
+ rng = self.explicitRange;
+ } else {
+ self.selectedRange = null;
+ self.explicitRange = null;
+ }
+ }
+
+ return rng;
+ },
+
+ /**
+ * Changes the selection to the specified DOM range.
+ *
+ * @method setRng
+ * @param {Range} rng Range to select.
+ * @param {Boolean} forward Optional boolean if the selection is forwards or backwards.
+ */
+ setRng: function(rng, forward) {
+ var self = this, sel, node, evt;
+
+ if (!rng) {
+ return;
+ }
+
+ // Is IE specific range
+ if (rng.select) {
+ self.explicitRange = null;
+
+ try {
+ rng.select();
+ } catch (ex) {
+ // Needed for some odd IE bug #1843306
+ }
+
+ return;
+ }
+
+ if (!self.tridentSel) {
+ sel = self.getSel();
+
+ evt = self.editor.fire('SetSelectionRange', {range: rng});
+ rng = evt.range;
+
+ if (sel) {
+ self.explicitRange = rng;
+
+ try {
+ sel.removeAllRanges();
+ sel.addRange(rng);
+ } catch (ex) {
+ // IE might throw errors here if the editor is within a hidden container and selection is changed
+ }
+
+ // Forward is set to false and we have an extend function
+ if (forward === false && sel.extend) {
+ sel.collapse(rng.endContainer, rng.endOffset);
+ sel.extend(rng.startContainer, rng.startOffset);
+ }
+
+ // adding range isn't always successful so we need to check range count otherwise an exception can occur
+ self.selectedRange = sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
+ }
+
+ // WebKit egde case selecting images works better using setBaseAndExtent
+ if (!rng.collapsed && rng.startContainer == rng.endContainer && sel.setBaseAndExtent && !Env.ie) {
+ if (rng.endOffset - rng.startOffset < 2) {
+ if (rng.startContainer.hasChildNodes()) {
+ node = rng.startContainer.childNodes[rng.startOffset];
+ if (node && node.tagName == 'IMG') {
+ self.getSel().setBaseAndExtent(node, 0, node, 1);
+ }
+ }
+ }
+ }
+
+ self.editor.fire('AfterSetSelectionRange', {range: rng});
+ } else {
+ // Is W3C Range fake range on IE
+ if (rng.cloneRange) {
+ try {
+ self.tridentSel.addRange(rng);
+ } catch (ex) {
+ //IE9 throws an error here if called before selection is placed in the editor
+ }
+ }
+ }
+ },
+
+ /**
+ * Sets the current selection to the specified DOM element.
+ *
+ * @method setNode
+ * @param {Element} elm Element to set as the contents of the selection.
+ * @return {Element} Returns the element that got passed in.
+ * @example
+ * // Inserts a DOM node at current selection/caret location
+ * tinymce.activeEditor.selection.setNode(tinymce.activeEditor.dom.create('img', {src: 'some.gif', title: 'some title'}));
+ */
+ setNode: function(elm) {
+ var self = this;
+
+ self.setContent(self.dom.getOuterHTML(elm));
+
+ return elm;
+ },
+
+ /**
+ * Returns the currently selected element or the common ancestor element for both start and end of the selection.
+ *
+ * @method getNode
+ * @return {Element} Currently selected element or common ancestor element.
+ * @example
+ * // Alerts the currently selected elements node name
+ * alert(tinymce.activeEditor.selection.getNode().nodeName);
+ */
+ getNode: function() {
+ var self = this, rng = self.getRng(), elm;
+ var startContainer, endContainer, startOffset, endOffset, root = self.dom.getRoot();
+
+ function skipEmptyTextNodes(node, forwards) {
+ var orig = node;
+
+ while (node && node.nodeType === 3 && node.length === 0) {
+ node = forwards ? node.nextSibling : node.previousSibling;
+ }
+
+ return node || orig;
+ }
+
+ // Range maybe lost after the editor is made visible again
+ if (!rng) {
+ return root;
+ }
+
+ startContainer = rng.startContainer;
+ endContainer = rng.endContainer;
+ startOffset = rng.startOffset;
+ endOffset = rng.endOffset;
+
+ if (rng.setStart) {
+ elm = rng.commonAncestorContainer;
+
+ // Handle selection a image or other control like element such as anchors
+ if (!rng.collapsed) {
+ if (startContainer == endContainer) {
+ if (endOffset - startOffset < 2) {
+ if (startContainer.hasChildNodes()) {
+ elm = startContainer.childNodes[startOffset];
+ }
+ }
+ }
+
+ // If the anchor node is a element instead of a text node then return this element
+ //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1)
+ // return sel.anchorNode.childNodes[sel.anchorOffset];
+
+ // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent.
+ // This happens when you double click an underlined word in FireFox.
+ if (startContainer.nodeType === 3 && endContainer.nodeType === 3) {
+ if (startContainer.length === startOffset) {
+ startContainer = skipEmptyTextNodes(startContainer.nextSibling, true);
+ } else {
+ startContainer = startContainer.parentNode;
+ }
+
+ if (endOffset === 0) {
+ endContainer = skipEmptyTextNodes(endContainer.previousSibling, false);
+ } else {
+ endContainer = endContainer.parentNode;
+ }
+
+ if (startContainer && startContainer === endContainer) {
+ return startContainer;
+ }
+ }
+ }
+
+ if (elm && elm.nodeType == 3) {
+ return elm.parentNode;
+ }
+
+ return elm;
+ }
+
+ elm = rng.item ? rng.item(0) : rng.parentElement();
+
+ // IE 7 might return elements outside the iframe
+ if (elm.ownerDocument !== self.win.document) {
+ elm = root;
+ }
+
+ return elm;
+ },
+
+ getSelectedBlocks: function(startElm, endElm) {
+ var self = this, dom = self.dom, node, root, selectedBlocks = [];
+
+ root = dom.getRoot();
+ startElm = dom.getParent(startElm || self.getStart(), dom.isBlock);
+ endElm = dom.getParent(endElm || self.getEnd(), dom.isBlock);
+
+ if (startElm && startElm != root) {
+ selectedBlocks.push(startElm);
+ }
+
+ if (startElm && endElm && startElm != endElm) {
+ node = startElm;
+
+ var walker = new TreeWalker(startElm, root);
+ while ((node = walker.next()) && node != endElm) {
+ if (dom.isBlock(node)) {
+ selectedBlocks.push(node);
+ }
+ }
+ }
+
+ if (endElm && startElm != endElm && endElm != root) {
+ selectedBlocks.push(endElm);
+ }
+
+ return selectedBlocks;
+ },
+
+ isForward: function() {
+ var dom = this.dom, sel = this.getSel(), anchorRange, focusRange;
+
+ // No support for selection direction then always return true
+ if (!sel || !sel.anchorNode || !sel.focusNode) {
+ return true;
+ }
+
+ anchorRange = dom.createRng();
+ anchorRange.setStart(sel.anchorNode, sel.anchorOffset);
+ anchorRange.collapse(true);
+
+ focusRange = dom.createRng();
+ focusRange.setStart(sel.focusNode, sel.focusOffset);
+ focusRange.collapse(true);
+
+ return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0;
+ },
+
+ normalize: function() {
+ var self = this, rng = self.getRng();
+
+ if (Env.range && new RangeUtils(self.dom).normalize(rng)) {
+ self.setRng(rng, self.isForward());
+ }
+
+ return rng;
+ },
+
+ /**
+ * Executes callback when the current selection starts/stops matching the specified selector. The current
+ * state will be passed to the callback as it's first argument.
+ *
+ * @method selectorChanged
+ * @param {String} selector CSS selector to check for.
+ * @param {function} callback Callback with state and args when the selector is matches or not.
+ */
+ selectorChanged: function(selector, callback) {
+ var self = this, currentSelectors;
+
+ if (!self.selectorChangedData) {
+ self.selectorChangedData = {};
+ currentSelectors = {};
+
+ self.editor.on('NodeChange', function(e) {
+ var node = e.element, dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {};
+
+ // Check for new matching selectors
+ each(self.selectorChangedData, function(callbacks, selector) {
+ each(parents, function(node) {
+ if (dom.is(node, selector)) {
+ if (!currentSelectors[selector]) {
+ // Execute callbacks
+ each(callbacks, function(callback) {
+ callback(true, {node: node, selector: selector, parents: parents});
+ });
+
+ currentSelectors[selector] = callbacks;
+ }
+
+ matchedSelectors[selector] = callbacks;
+ return false;
+ }
+ });
+ });
+
+ // Check if current selectors still match
+ each(currentSelectors, function(callbacks, selector) {
+ if (!matchedSelectors[selector]) {
+ delete currentSelectors[selector];
+
+ each(callbacks, function(callback) {
+ callback(false, {node: node, selector: selector, parents: parents});
+ });
+ }
+ });
+ });
+ }
+
+ // Add selector listeners
+ if (!self.selectorChangedData[selector]) {
+ self.selectorChangedData[selector] = [];
+ }
+
+ self.selectorChangedData[selector].push(callback);
+
+ return self;
+ },
+
+ getScrollContainer: function() {
+ var scrollContainer, node = this.dom.getRoot();
+
+ while (node && node.nodeName != 'BODY') {
+ if (node.scrollHeight > node.clientHeight) {
+ scrollContainer = node;
+ break;
+ }
+
+ node = node.parentNode;
+ }
+
+ return scrollContainer;
+ },
+
+ scrollIntoView: function(elm, alignToTop) {
+ var y, viewPort, self = this, dom = self.dom, root = dom.getRoot(), viewPortY, viewPortH, offsetY = 0;
+
+ function getPos(elm) {
+ var x = 0, y = 0;
+
+ var offsetParent = elm;
+ while (offsetParent && offsetParent.nodeType) {
+ x += offsetParent.offsetLeft || 0;
+ y += offsetParent.offsetTop || 0;
+ offsetParent = offsetParent.offsetParent;
+ }
+
+ return {x: x, y: y};
+ }
+
+ if (!NodeType.isElement(elm)) {
+ return;
+ }
+
+ if (alignToTop === false) {
+ offsetY = elm.offsetHeight;
+ }
+
+ if (root.nodeName != 'BODY') {
+ var scrollContainer = self.getScrollContainer();
+ if (scrollContainer) {
+ y = getPos(elm).y - getPos(scrollContainer).y + offsetY;
+ viewPortH = scrollContainer.clientHeight;
+ viewPortY = scrollContainer.scrollTop;
+ if (y < viewPortY || y + 25 > viewPortY + viewPortH) {
+ scrollContainer.scrollTop = y < viewPortY ? y : y - viewPortH + 25;
+ }
+
+ return;
+ }
+ }
+
+ viewPort = dom.getViewPort(self.editor.getWin());
+ y = dom.getPos(elm).y + offsetY;
+ viewPortY = viewPort.y;
+ viewPortH = viewPort.h;
+ if (y < viewPort.y || y + 25 > viewPortY + viewPortH) {
+ self.editor.getWin().scrollTo(0, y < viewPortY ? y : y - viewPortH + 25);
+ }
+ },
+
+ placeCaretAt: function(clientX, clientY) {
+ this.setRng(RangeUtils.getCaretRangeFromPoint(clientX, clientY, this.editor.getDoc()));
+ },
+
+ _moveEndPoint: function(rng, node, start) {
+ var root = node, walker = new TreeWalker(node, root);
+ var nonEmptyElementsMap = this.dom.schema.getNonEmptyElements();
+
+ do {
+ // Text node
+ if (node.nodeType == 3 && trim(node.nodeValue).length !== 0) {
+ if (start) {
+ rng.setStart(node, 0);
+ } else {
+ rng.setEnd(node, node.nodeValue.length);
+ }
+
+ return;
+ }
+
+ // BR/IMG/INPUT elements but not table cells
+ if (nonEmptyElementsMap[node.nodeName] && !/^(TD|TH)$/.test(node.nodeName)) {
+ if (start) {
+ rng.setStartBefore(node);
+ } else {
+ if (node.nodeName == 'BR') {
+ rng.setEndBefore(node);
+ } else {
+ rng.setEndAfter(node);
+ }
+ }
+
+ return;
+ }
+
+ // Found empty text block old IE can place the selection inside those
+ if (Env.ie && Env.ie < 11 && this.dom.isBlock(node) && this.dom.isEmpty(node)) {
+ if (start) {
+ rng.setStart(node, 0);
+ } else {
+ rng.setEnd(node, 0);
+ }
+
+ return;
+ }
+ } while ((node = (start ? walker.next() : walker.prev())));
+
+ // Failed to find any text node or other suitable location then move to the root of body
+ if (root.nodeName == 'BODY') {
+ if (start) {
+ rng.setStart(root, 0);
+ } else {
+ rng.setEnd(root, root.childNodes.length);
+ }
+ }
+ },
+
+ getBoundingClientRect: function() {
+ var rng = this.getRng();
+ return rng.collapsed ? CaretPosition.fromRangeStart(rng).getClientRects()[0] : rng.getBoundingClientRect();
+ },
+
+ destroy: function() {
+ this.win = null;
+ this.controlSelection.destroy();
+ }
+ };
+
+ return Selection;
+});
+
+// Included from: js/tinymce/classes/dom/ElementUtils.js
+
+/**
+ * ElementUtils.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Utility class for various element specific functions.
+ *
+ * @private
+ * @class tinymce.dom.ElementUtils
+ */
+define("tinymce/dom/ElementUtils", [
+ "tinymce/dom/BookmarkManager",
+ "tinymce/util/Tools"
+], function(BookmarkManager, Tools) {
+ var each = Tools.each;
+
+ function ElementUtils(dom) {
+ /**
+ * Compares two nodes and checks if it's attributes and styles matches.
+ * This doesn't compare classes as items since their order is significant.
+ *
+ * @method compare
+ * @param {Node} node1 First node to compare with.
+ * @param {Node} node2 Second node to compare with.
+ * @return {boolean} True/false if the nodes are the same or not.
+ */
+ this.compare = function(node1, node2) {
+ // Not the same name
+ if (node1.nodeName != node2.nodeName) {
+ return false;
+ }
+
+ /**
+ * Returns all the nodes attributes excluding internal ones, styles and classes.
+ *
+ * @private
+ * @param {Node} node Node to get attributes from.
+ * @return {Object} Name/value object with attributes and attribute values.
+ */
+ function getAttribs(node) {
+ var attribs = {};
+
+ each(dom.getAttribs(node), function(attr) {
+ var name = attr.nodeName.toLowerCase();
+
+ // Don't compare internal attributes or style
+ if (name.indexOf('_') !== 0 && name !== 'style' && name.indexOf('data-') !== 0) {
+ attribs[name] = dom.getAttrib(node, name);
+ }
+ });
+
+ return attribs;
+ }
+
+ /**
+ * Compares two objects checks if it's key + value exists in the other one.
+ *
+ * @private
+ * @param {Object} obj1 First object to compare.
+ * @param {Object} obj2 Second object to compare.
+ * @return {boolean} True/false if the objects matches or not.
+ */
+ function compareObjects(obj1, obj2) {
+ var value, name;
+
+ for (name in obj1) {
+ // Obj1 has item obj2 doesn't have
+ if (obj1.hasOwnProperty(name)) {
+ value = obj2[name];
+
+ // Obj2 doesn't have obj1 item
+ if (typeof value == "undefined") {
+ return false;
+ }
+
+ // Obj2 item has a different value
+ if (obj1[name] != value) {
+ return false;
+ }
+
+ // Delete similar value
+ delete obj2[name];
+ }
+ }
+
+ // Check if obj 2 has something obj 1 doesn't have
+ for (name in obj2) {
+ // Obj2 has item obj1 doesn't have
+ if (obj2.hasOwnProperty(name)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ // Attribs are not the same
+ if (!compareObjects(getAttribs(node1), getAttribs(node2))) {
+ return false;
+ }
+
+ // Styles are not the same
+ if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) {
+ return false;
+ }
+
+ return !BookmarkManager.isBookmarkNode(node1) && !BookmarkManager.isBookmarkNode(node2);
+ };
+ }
+
+ return ElementUtils;
+});
+
+// Included from: js/tinymce/classes/fmt/Preview.js
+
+/**
+ * Preview.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Internal class for generating previews styles for formats.
+ *
+ * Example:
+ * Preview.getCssText(editor, 'bold');
+ *
+ * @private
+ * @class tinymce.fmt.Preview
+ */
+define("tinymce/fmt/Preview", [
+ "tinymce/dom/DOMUtils",
+ "tinymce/util/Tools",
+ "tinymce/html/Schema"
+], function(DOMUtils, Tools, Schema) {
+ var each = Tools.each;
+ var dom = DOMUtils.DOM;
+
+ function parsedSelectorToHtml(ancestry, editor) {
+ var elm, item, fragment;
+ var schema = editor && editor.schema || new Schema({});
+
+ function decorate(elm, item) {
+ if (item.classes.length) {
+ dom.addClass(elm, item.classes.join(' '));
+ }
+ dom.setAttribs(elm, item.attrs);
+ }
+
+ function createElement(sItem) {
+ var elm;
+
+ item = typeof sItem === 'string' ? {
+ name: sItem,
+ classes: [],
+ attrs: {}
+ } : sItem;
+
+ elm = dom.create(item.name);
+ decorate(elm, item);
+ return elm;
+ }
+
+ function getRequiredParent(elm, candidate) {
+ var name = typeof elm !== 'string' ? elm.nodeName.toLowerCase() : elm;
+ var elmRule = schema.getElementRule(name);
+ var parentsRequired = elmRule.parentsRequired;
+
+ if (parentsRequired && parentsRequired.length) {
+ return candidate && Tools.inArray(parentsRequired, candidate) !== -1 ? candidate : parentsRequired[0];
+ } else {
+ return false;
+ }
+ }
+
+ function wrapInHtml(elm, ancestry, siblings) {
+ var parent, parentCandidate, parentRequired;
+ var ancestor = ancestry.length && ancestry[0];
+ var ancestorName = ancestor && ancestor.name;
+
+ parentRequired = getRequiredParent(elm, ancestorName);
+
+ if (parentRequired) {
+ if (ancestorName == parentRequired) {
+ parentCandidate = ancestry[0];
+ ancestry = ancestry.slice(1);
+ } else {
+ parentCandidate = parentRequired;
+ }
+ } else if (ancestor) {
+ parentCandidate = ancestry[0];
+ ancestry = ancestry.slice(1);
+ } else if (!siblings) {
+ return elm;
+ }
+
+ if (parentCandidate) {
+ parent = createElement(parentCandidate);
+ parent.appendChild(elm);
+ }
+
+ if (siblings) {
+ if (!parent) {
+ // if no more ancestry, wrap in generic div
+ parent = dom.create('div');
+ parent.appendChild(elm);
+ }
+
+ Tools.each(siblings, function(sibling) {
+ var siblingElm = createElement(sibling);
+ parent.insertBefore(siblingElm, elm);
+ });
+ }
+
+ return wrapInHtml(parent, ancestry, parentCandidate && parentCandidate.siblings);
+ }
+
+ if (ancestry && ancestry.length) {
+ item = ancestry[0];
+ elm = createElement(item);
+ fragment = dom.create('div');
+ fragment.appendChild(wrapInHtml(elm, ancestry.slice(1), item.siblings));
+ return fragment;
+ } else {
+ return '';
+ }
+ }
+
+
+ function selectorToHtml(selector, editor) {
+ return parsedSelectorToHtml(parseSelector(selector, editor));
+ }
+
+
+ function parseSelectorItem(item) {
+ var tagName;
+ var obj = {
+ classes: [],
+ attrs: {}
+ };
+
+ item = obj.selector = Tools.trim(item);
+
+ if (item !== '*') {
+ // matching IDs, CLASSes, ATTRIBUTES and PSEUDOs
+ tagName = item.replace(/(?:([#\.]|::?)([\w\-]+)|(\[)([^\]]+)\]?)/g, function($0, $1, $2, $3, $4) {
+ switch ($1) {
+ case '#':
+ obj.attrs.id = $2;
+ break;
+
+ case '.':
+ obj.classes.push($2);
+ break;
+
+ case ':':
+ if (Tools.inArray('checked disabled enabled read-only required'.split(' '), $2) !== -1) {
+ obj.attrs[$2] = $2;
+ }
+ break;
+ }
+
+ // atribute matched
+ if ($3 == '[') {
+ var m = $4.match(/([\w\-]+)(?:\=\"([^\"]+))?/);
+ if (m) {
+ obj.attrs[m[1]] = m[2];
+ }
+ }
+
+ return '';
+ });
+ }
+
+ obj.name = tagName || 'div';
+ return obj;
+ }
+
+
+ function parseSelector(selector) {
+ if (!selector || typeof selector !== 'string') {
+ return [];
+ }
+
+ // take into account only first one
+ selector = selector.split(/\s*,\s*/)[0];
+
+ // tighten
+ selector = selector.replace(/\s*(~\+|~|\+|>)\s*/g, '$1');
+
+ // split either on > or on space, but not the one inside brackets
+ return Tools.map(selector.split(/(?:>|\s+(?![^\[\]]+\]))/), function(item) {
+ // process each sibling selector separately
+ var siblings = Tools.map(item.split(/(?:~\+|~|\+)/), parseSelectorItem);
+ var obj = siblings.pop(); // the last one is our real target
+
+ if (siblings.length) {
+ obj.siblings = siblings;
+ }
+ return obj;
+ }).reverse();
+ }
+
+
+ function getCssText(editor, format) {
+ var name, previewFrag, previewElm, items;
+ var previewCss = '', parentFontSize, previewStyles;
+
+ previewStyles = editor.settings.preview_styles;
+
+ // No preview forced
+ if (previewStyles === false) {
+ return '';
+ }
+
+ // Default preview
+ if (typeof previewStyles !== 'string') {
+ previewStyles = 'font-family font-size font-weight font-style text-decoration ' +
+ 'text-transform color background-color border border-radius outline text-shadow';
+ }
+
+ // Removes any variables since these can't be previewed
+ function removeVars(val) {
+ return val.replace(/%(\w+)/g, '');
+ }
+
+ // Create block/inline element to use for preview
+ if (typeof format == "string") {
+ format = editor.formatter.get(format);
+ if (!format) {
+ return;
+ }
+
+ format = format[0];
+ }
+
+ // Format specific preview override
+ // TODO: This should probably be further reduced by the previewStyles option
+ if ('preview' in format) {
+ previewStyles = format.preview;
+ if (previewStyles === false) {
+ return '';
+ }
+ }
+
+ name = format.block || format.inline || 'span';
+
+ items = parseSelector(format.selector);
+ if (items.length) {
+ if (!items[0].name) { // e.g. something like ul > .someClass was provided
+ items[0].name = name;
+ }
+ name = format.selector;
+ previewFrag = parsedSelectorToHtml(items);
+ } else {
+ previewFrag = parsedSelectorToHtml([name]);
+ }
+
+ previewElm = dom.select(name, previewFrag)[0] || previewFrag.firstChild;
+
+ // Add format styles to preview element
+ each(format.styles, function(value, name) {
+ value = removeVars(value);
+
+ if (value) {
+ dom.setStyle(previewElm, name, value);
+ }
+ });
+
+ // Add attributes to preview element
+ each(format.attributes, function(value, name) {
+ value = removeVars(value);
+
+ if (value) {
+ dom.setAttrib(previewElm, name, value);
+ }
+ });
+
+ // Add classes to preview element
+ each(format.classes, function(value) {
+ value = removeVars(value);
+
+ if (!dom.hasClass(previewElm, value)) {
+ dom.addClass(previewElm, value);
+ }
+ });
+
+ editor.fire('PreviewFormats');
+
+ // Add the previewElm outside the visual area
+ dom.setStyles(previewFrag, {position: 'absolute', left: -0xFFFF});
+ editor.getBody().appendChild(previewFrag);
+
+ // Get parent container font size so we can compute px values out of em/% for older IE:s
+ parentFontSize = dom.getStyle(editor.getBody(), 'fontSize', true);
+ parentFontSize = /px$/.test(parentFontSize) ? parseInt(parentFontSize, 10) : 0;
+
+ each(previewStyles.split(' '), function(name) {
+ var value = dom.getStyle(previewElm, name, true);
+
+ // If background is transparent then check if the body has a background color we can use
+ if (name == 'background-color' && /transparent|rgba\s*\([^)]+,\s*0\)/.test(value)) {
+ value = dom.getStyle(editor.getBody(), name, true);
+
+ // Ignore white since it's the default color, not the nicest fix
+ // TODO: Fix this by detecting runtime style
+ if (dom.toHex(value).toLowerCase() == '#ffffff') {
+ return;
+ }
+ }
+
+ if (name == 'color') {
+ // Ignore black since it's the default color, not the nicest fix
+ // TODO: Fix this by detecting runtime style
+ if (dom.toHex(value).toLowerCase() == '#000000') {
+ return;
+ }
+ }
+
+ // Old IE won't calculate the font size so we need to do that manually
+ if (name == 'font-size') {
+ if (/em|%$/.test(value)) {
+ if (parentFontSize === 0) {
+ return;
+ }
+
+ // Convert font size from em/% to px
+ value = parseFloat(value, 10) / (/%$/.test(value) ? 100 : 1);
+ value = (value * parentFontSize) + 'px';
+ }
+ }
+
+ if (name == "border" && value) {
+ previewCss += 'padding:0 2px;';
+ }
+
+ previewCss += name + ':' + value + ';';
+ });
+
+ editor.fire('AfterPreviewFormats');
+
+ //previewCss += 'line-height:normal';
+
+ dom.remove(previewFrag);
+
+ return previewCss;
+ }
+
+ return {
+ getCssText: getCssText,
+ parseSelector: parseSelector,
+ selectorToHtml: selectorToHtml
+ };
+});
+
+// Included from: js/tinymce/classes/fmt/Hooks.js
+
+/**
+ * Hooks.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Internal class for overriding formatting.
+ *
+ * @private
+ * @class tinymce.fmt.Hooks
+ */
+define("tinymce/fmt/Hooks", [
+ "tinymce/util/Arr",
+ "tinymce/dom/NodeType",
+ "tinymce/dom/DomQuery"
+], function(Arr, NodeType, $) {
+ var postProcessHooks = {}, filter = Arr.filter, each = Arr.each;
+
+ function addPostProcessHook(name, hook) {
+ var hooks = postProcessHooks[name];
+
+ if (!hooks) {
+ postProcessHooks[name] = hooks = [];
+ }
+
+ postProcessHooks[name].push(hook);
+ }
+
+ function postProcess(name, editor) {
+ each(postProcessHooks[name], function(hook) {
+ hook(editor);
+ });
+ }
+
+ addPostProcessHook("pre", function(editor) {
+ var rng = editor.selection.getRng(), isPre, blocks;
+
+ function hasPreSibling(pre) {
+ return isPre(pre.previousSibling) && Arr.indexOf(blocks, pre.previousSibling) != -1;
+ }
+
+ function joinPre(pre1, pre2) {
+ $(pre2).remove();
+ $(pre1).append('<br><br>').append(pre2.childNodes);
+ }
+
+ isPre = NodeType.matchNodeNames('pre');
+
+ if (!rng.collapsed) {
+ blocks = editor.selection.getSelectedBlocks();
+
+ each(filter(filter(blocks, isPre), hasPreSibling), function(pre) {
+ joinPre(pre.previousSibling, pre);
+ });
+ }
+ });
+
+ return {
+ postProcess: postProcess
+ };
+});
+
+// Included from: js/tinymce/classes/Formatter.js
+
+/**
+ * Formatter.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Text formatter engine class. This class is used to apply formats like bold, italic, font size
+ * etc to the current selection or specific nodes. This engine was built to replace the browser's
+ * default formatting logic for execCommand due to its inconsistent and buggy behavior.
+ *
+ * @class tinymce.Formatter
+ * @example
+ * tinymce.activeEditor.formatter.register('mycustomformat', {
+ * inline: 'span',
+ * styles: {color: '#ff0000'}
+ * });
+ *
+ * tinymce.activeEditor.formatter.apply('mycustomformat');
+ */
+define("tinymce/Formatter", [
+ "tinymce/dom/TreeWalker",
+ "tinymce/dom/RangeUtils",
+ "tinymce/dom/BookmarkManager",
+ "tinymce/dom/ElementUtils",
+ "tinymce/util/Tools",
+ "tinymce/fmt/Preview",
+ "tinymce/fmt/Hooks"
+], function(TreeWalker, RangeUtils, BookmarkManager, ElementUtils, Tools, Preview, Hooks) {
+ /**
+ * Constructs a new formatter instance.
+ *
+ * @constructor Formatter
+ * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to.
+ */
+ return function(ed) {
+ var formats = {},
+ dom = ed.dom,
+ selection = ed.selection,
+ rangeUtils = new RangeUtils(dom),
+ isValid = ed.schema.isValidChild,
+ isBlock = dom.isBlock,
+ forcedRootBlock = ed.settings.forced_root_block,
+ nodeIndex = dom.nodeIndex,
+ INVISIBLE_CHAR = '\uFEFF',
+ MCE_ATTR_RE = /^(src|href|style)$/,
+ FALSE = false,
+ TRUE = true,
+ formatChangeData,
+ undef,
+ getContentEditable = dom.getContentEditable,
+ disableCaretContainer,
+ markCaretContainersBogus,
+ isBookmarkNode = BookmarkManager.isBookmarkNode;
+
+ var each = Tools.each,
+ grep = Tools.grep,
+ walk = Tools.walk,
+ extend = Tools.extend;
+
+ function isTextBlock(name) {
+ if (name.nodeType) {
+ name = name.nodeName;
+ }
+
+ return !!ed.schema.getTextBlockElements()[name.toLowerCase()];
+ }
+
+ function isTableCell(node) {
+ return /^(TH|TD)$/.test(node.nodeName);
+ }
+
+ function isInlineBlock(node) {
+ return node && /^(IMG)$/.test(node.nodeName);
+ }
+
+ function getParents(node, selector) {
+ return dom.getParents(node, selector, dom.getRoot());
+ }
+
+ function isCaretNode(node) {
+ return node.nodeType === 1 && node.id === '_mce_caret';
+ }
+
+ function defaultFormats() {
+ register({
+ valigntop: [
+ {selector: 'td,th', styles: {'verticalAlign': 'top'}}
+ ],
+
+ valignmiddle: [
+ {selector: 'td,th', styles: {'verticalAlign': 'middle'}}
+ ],
+
+ valignbottom: [
+ {selector: 'td,th', styles: {'verticalAlign': 'bottom'}}
+ ],
+
+ alignleft: [
+ {
+ selector: 'figure.image',
+ collapsed: false,
+ classes: 'align-left',
+ ceFalseOverride: true,
+ preview: 'font-family font-size'
+ },
+ {
+ selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
+ styles: {
+ textAlign: 'left'
+ },
+ inherit: false,
+ preview: false,
+ defaultBlock: 'div'
+ },
+ {selector: 'img,table', collapsed: false, styles: {'float': 'left'}, preview: 'font-family font-size'}
+ ],
+
+ aligncenter: [
+ {
+ selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
+ styles: {
+ textAlign: 'center'
+ },
+ inherit: false,
+ preview: false,
+ defaultBlock: 'div'
+ },
+ {
+ selector: 'figure.image',
+ collapsed: false,
+ classes: 'align-center',
+ ceFalseOverride: true,
+ preview: 'font-family font-size'
+ },
+ {
+ selector: 'img',
+ collapsed: false,
+ styles: {
+ display: 'block',
+ marginLeft: 'auto',
+ marginRight: 'auto'
+ },
+ preview: false
+ },
+ {
+ selector: 'table',
+ collapsed: false,
+ styles: {
+ marginLeft: 'auto',
+ marginRight: 'auto'
+ },
+ preview: 'font-family font-size'
+ }
+ ],
+
+ alignright: [
+ {
+ selector: 'figure.image',
+ collapsed: false,
+ classes: 'align-right',
+ ceFalseOverride: true,
+ preview: 'font-family font-size'
+ },
+ {
+ selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
+ styles: {
+ textAlign: 'right'
+ },
+ inherit: false,
+ preview: 'font-family font-size',
+ defaultBlock: 'div'
+ },
+ {
+ selector: 'img,table',
+ collapsed: false,
+ styles: {
+ 'float': 'right'
+ },
+ preview: 'font-family font-size'
+ }
+ ],
+
+ alignjustify: [
+ {
+ selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
+ styles: {
+ textAlign: 'justify'
+ },
+ inherit: false,
+ defaultBlock: 'div',
+ preview: 'font-family font-size'
+ }
+ ],
+
+ bold: [
+ {inline: 'strong', remove: 'all'},
+ {inline: 'span', styles: {fontWeight: 'bold'}},
+ {inline: 'b', remove: 'all'}
+ ],
+
+ italic: [
+ {inline: 'em', remove: 'all'},
+ {inline: 'span', styles: {fontStyle: 'italic'}},
+ {inline: 'i', remove: 'all'}
+ ],
+
+ underline: [
+ {inline: 'span', styles: {textDecoration: 'underline'}, exact: true},
+ {inline: 'u', remove: 'all'}
+ ],
+
+ strikethrough: [
+ {inline: 'span', styles: {textDecoration: 'line-through'}, exact: true},
+ {inline: 'strike', remove: 'all'}
+ ],
+
+ forecolor: {inline: 'span', styles: {color: '%value'}, links: true, remove_similar: true},
+ hilitecolor: {inline: 'span', styles: {backgroundColor: '%value'}, links: true, remove_similar: true},
+ fontname: {inline: 'span', styles: {fontFamily: '%value'}},
+ fontsize: {inline: 'span', styles: {fontSize: '%value'}},
+ fontsize_class: {inline: 'span', attributes: {'class': '%value'}},
+ blockquote: {block: 'blockquote', wrapper: 1, remove: 'all'},
+ subscript: {inline: 'sub'},
+ superscript: {inline: 'sup'},
+ code: {inline: 'code'},
+
+ link: {inline: 'a', selector: 'a', remove: 'all', split: true, deep: true,
+ onmatch: function() {
+ return true;
+ },
+
+ onformat: function(elm, fmt, vars) {
+ each(vars, function(value, key) {
+ dom.setAttrib(elm, key, value);
+ });
+ }
+ },
+
+ removeformat: [
+ {
+ selector: 'b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q,del,ins',
+ remove: 'all',
+ split: true,
+ expand: false,
+ block_expand: true,
+ deep: true
+ },
+ {selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true},
+ {selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true}
+ ]
+ });
+
+ // Register default block formats
+ each('p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp'.split(/\s/), function(name) {
+ register(name, {block: name, remove: 'all'});
+ });
+
+ // Register user defined formats
+ register(ed.settings.formats);
+ }
+
+ function addKeyboardShortcuts() {
+ // Add some inline shortcuts
+ ed.addShortcut('meta+b', 'bold_desc', 'Bold');
+ ed.addShortcut('meta+i', 'italic_desc', 'Italic');
+ ed.addShortcut('meta+u', 'underline_desc', 'Underline');
+
+ // BlockFormat shortcuts keys
+ for (var i = 1; i <= 6; i++) {
+ ed.addShortcut('access+' + i, '', ['FormatBlock', false, 'h' + i]);
+ }
+
+ ed.addShortcut('access+7', '', ['FormatBlock', false, 'p']);
+ ed.addShortcut('access+8', '', ['FormatBlock', false, 'div']);
+ ed.addShortcut('access+9', '', ['FormatBlock', false, 'address']);
+ }
+
+ // Public functions
+
+ /**
+ * Returns the format by name or all formats if no name is specified.
+ *
+ * @method get
+ * @param {String} name Optional name to retrieve by.
+ * @return {Array/Object} Array/Object with all registered formats or a specific format.
+ */
+ function get(name) {
+ return name ? formats[name] : formats;
+ }
+
+ /**
+ * Registers a specific format by name.
+ *
+ * @method register
+ * @param {Object/String} name Name of the format for example "bold".
+ * @param {Object/Array} format Optional format object or array of format variants
+ * can only be omitted if the first arg is an object.
+ */
+ function register(name, format) {
+ if (name) {
+ if (typeof name !== 'string') {
+ each(name, function(format, name) {
+ register(name, format);
+ });
+ } else {
+ // Force format into array and add it to internal collection
+ format = format.length ? format : [format];
+
+ each(format, function(format) {
+ // Set deep to false by default on selector formats this to avoid removing
+ // alignment on images inside paragraphs when alignment is changed on paragraphs
+ if (format.deep === undef) {
+ format.deep = !format.selector;
+ }
+
+ // Default to true
+ if (format.split === undef) {
+ format.split = !format.selector || format.inline;
+ }
+
+ // Default to true
+ if (format.remove === undef && format.selector && !format.inline) {
+ format.remove = 'none';
+ }
+
+ // Mark format as a mixed format inline + block level
+ if (format.selector && format.inline) {
+ format.mixed = true;
+ format.block_expand = true;
+ }
+
+ // Split classes if needed
+ if (typeof format.classes === 'string') {
+ format.classes = format.classes.split(/\s+/);
+ }
+ });
+
+ formats[name] = format;
+ }
+ }
+ }
+
+ /**
+ * Unregister a specific format by name.
+ *
+ * @method unregister
+ * @param {String} name Name of the format for example "bold".
+ */
+ function unregister(name) {
+ if (name && formats[name]) {
+ delete formats[name];
+ }
+
+ return formats;
+ }
+
+ function matchesUnInheritedFormatSelector(node, name) {
+ var formatList = get(name);
+
+ if (formatList) {
+ for (var i = 0; i < formatList.length; i++) {
+ if (formatList[i].inherit === false && dom.is(node, formatList[i].selector)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ function getTextDecoration(node) {
+ var decoration;
+
+ ed.dom.getParent(node, function(n) {
+ decoration = ed.dom.getStyle(n, 'text-decoration');
+ return decoration && decoration !== 'none';
+ });
+
+ return decoration;
+ }
+
+ function processUnderlineAndColor(node) {
+ var textDecoration;
+ if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) {
+ textDecoration = getTextDecoration(node.parentNode);
+ if (ed.dom.getStyle(node, 'color') && textDecoration) {
+ ed.dom.setStyle(node, 'text-decoration', textDecoration);
+ } else if (ed.dom.getStyle(node, 'text-decoration') === textDecoration) {
+ ed.dom.setStyle(node, 'text-decoration', null);
+ }
+ }
+ }
+
+ /**
+ * Applies the specified format to the current selection or specified node.
+ *
+ * @method apply
+ * @param {String} name Name of format to apply.
+ * @param {Object} vars Optional list of variables to replace within format before applying it.
+ * @param {Node} node Optional node to apply the format to defaults to current selection.
+ */
+ function apply(name, vars, node) {
+ var formatList = get(name), format = formatList[0], bookmark, rng, isCollapsed = !node && selection.isCollapsed();
+
+ function setElementFormat(elm, fmt) {
+ fmt = fmt || format;
+
+ if (elm) {
+ if (fmt.onformat) {
+ fmt.onformat(elm, fmt, vars, node);
+ }
+
+ each(fmt.styles, function(value, name) {
+ dom.setStyle(elm, name, replaceVars(value, vars));
+ });
+
+ // Needed for the WebKit span spam bug
+ // TODO: Remove this once WebKit/Blink fixes this
+ if (fmt.styles) {
+ var styleVal = dom.getAttrib(elm, 'style');
+
+ if (styleVal) {
+ elm.setAttribute('data-mce-style', styleVal);
+ }
+ }
+
+ each(fmt.attributes, function(value, name) {
+ dom.setAttrib(elm, name, replaceVars(value, vars));
+ });
+
+ each(fmt.classes, function(value) {
+ value = replaceVars(value, vars);
+
+ if (!dom.hasClass(elm, value)) {
+ dom.addClass(elm, value);
+ }
+ });
+ }
+ }
+
+ function applyNodeStyle(formatList, node) {
+ var found = false;
+
+ if (!format.selector) {
+ return false;
+ }
+
+ // Look for matching formats
+ each(formatList, function(format) {
+ // Check collapsed state if it exists
+ if ('collapsed' in format && format.collapsed !== isCollapsed) {
+ return;
+ }
+
+ if (dom.is(node, format.selector) && !isCaretNode(node)) {
+ setElementFormat(node, format);
+ found = true;
+ return false;
+ }
+ });
+
+ return found;
+ }
+
+ // This converts: <p>[a</p><p>]b</p> -> <p>[a]</p><p>b</p>
+ function adjustSelectionToVisibleSelection() {
+ function findSelectionEnd(start, end) {
+ var walker = new TreeWalker(end);
+ for (node = walker.prev2(); node; node = walker.prev2()) {
+ if (node.nodeType == 3 && node.data.length > 0) {
+ return node;
+ }
+
+ if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') {
+ return node;
+ }
+ }
+ }
+
+ // Adjust selection so that a end container with a end offset of zero is not included in the selection
+ // as this isn't visible to the user.
+ var rng = ed.selection.getRng();
+ var start = rng.startContainer;
+ var end = rng.endContainer;
+
+ if (start != end && rng.endOffset === 0) {
+ var newEnd = findSelectionEnd(start, end);
+ var endOffset = newEnd.nodeType == 3 ? newEnd.data.length : newEnd.childNodes.length;
+
+ rng.setEnd(newEnd, endOffset);
+ }
+
+ return rng;
+ }
+
+ function applyRngStyle(rng, bookmark, node_specific) {
+ var newWrappers = [], wrapName, wrapElm, contentEditable = true;
+
+ // Setup wrapper element
+ wrapName = format.inline || format.block;
+ wrapElm = dom.create(wrapName);
+ setElementFormat(wrapElm);
+
+ rangeUtils.walk(rng, function(nodes) {
+ var currentWrapElm;
+
+ /**
+ * Process a list of nodes wrap them.
+ */
+ function process(node) {
+ var nodeName, parentName, hasContentEditableState, lastContentEditable;
+
+ lastContentEditable = contentEditable;
+ nodeName = node.nodeName.toLowerCase();
+ parentName = node.parentNode.nodeName.toLowerCase();
+
+ // Node has a contentEditable value
+ if (node.nodeType === 1 && getContentEditable(node)) {
+ lastContentEditable = contentEditable;
+ contentEditable = getContentEditable(node) === "true";
+ hasContentEditableState = true; // We don't want to wrap the container only it's children
+ }
+
+ // Stop wrapping on br elements
+ if (isEq(nodeName, 'br')) {
+ currentWrapElm = 0;
+
+ // Remove any br elements when we wrap things
+ if (format.block) {
+ dom.remove(node);
+ }
+
+ return;
+ }
+
+ // If node is wrapper type
+ if (format.wrapper && matchNode(node, name, vars)) {
+ currentWrapElm = 0;
+ return;
+ }
+
+ // Can we rename the block
+ // TODO: Break this if up, too complex
+ if (contentEditable && !hasContentEditableState && format.block &&
+ !format.wrapper && isTextBlock(nodeName) && isValid(parentName, wrapName)) {
+ node = dom.rename(node, wrapName);
+ setElementFormat(node);
+ newWrappers.push(node);
+ currentWrapElm = 0;
+ return;
+ }
+
+ // Handle selector patterns
+ if (format.selector) {
+ var found = applyNodeStyle(formatList, node);
+
+ // Continue processing if a selector match wasn't found and a inline element is defined
+ if (!format.inline || found) {
+ currentWrapElm = 0;
+ return;
+ }
+ }
+
+ // Is it valid to wrap this item
+ // TODO: Break this if up, too complex
+ if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) &&
+ !(!node_specific && node.nodeType === 3 &&
+ node.nodeValue.length === 1 &&
+ node.nodeValue.charCodeAt(0) === 65279) &&
+ !isCaretNode(node) &&
+ (!format.inline || !isBlock(node))) {
+ // Start wrapping
+ if (!currentWrapElm) {
+ // Wrap the node
+ currentWrapElm = dom.clone(wrapElm, FALSE);
+ node.parentNode.insertBefore(currentWrapElm, node);
+ newWrappers.push(currentWrapElm);
+ }
+
+ currentWrapElm.appendChild(node);
+ } else {
+ // Start a new wrapper for possible children
+ currentWrapElm = 0;
+
+ each(grep(node.childNodes), process);
+
+ if (hasContentEditableState) {
+ contentEditable = lastContentEditable; // Restore last contentEditable state from stack
+ }
+
+ // End the last wrapper
+ currentWrapElm = 0;
+ }
+ }
+
+ // Process siblings from range
+ each(nodes, process);
+ });
+
+ // Apply formats to links as well to get the color of the underline to change as well
+ if (format.links === true) {
+ each(newWrappers, function(node) {
+ function process(node) {
+ if (node.nodeName === 'A') {
+ setElementFormat(node, format);
+ }
+
+ each(grep(node.childNodes), process);
+ }
+
+ process(node);
+ });
+ }
+
+ // Cleanup
+ each(newWrappers, function(node) {
+ var childCount;
+
+ function getChildCount(node) {
+ var count = 0;
+
+ each(node.childNodes, function(node) {
+ if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) {
+ count++;
+ }
+ });
+
+ return count;
+ }
+
+ function mergeStyles(node) {
+ var child, clone;
+
+ each(node.childNodes, function(node) {
+ if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) {
+ child = node;
+ return FALSE; // break loop
+ }
+ });
+
+ // If child was found and of the same type as the current node
+ if (child && !isBookmarkNode(child) && matchName(child, format)) {
+ clone = dom.clone(child, FALSE);
+ setElementFormat(clone);
+
+ dom.replace(clone, node, TRUE);
+ dom.remove(child, 1);
+ }
+
+ return clone || node;
+ }
+
+ childCount = getChildCount(node);
+
+ // Remove empty nodes but only if there is multiple wrappers and they are not block
+ // elements so never remove single <h1></h1> since that would remove the
+ // current empty block element where the caret is at
+ if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) {
+ dom.remove(node, 1);
+ return;
+ }
+
+ if (format.inline || format.wrapper) {
+ // Merges the current node with it's children of similar type to reduce the number of elements
+ if (!format.exact && childCount === 1) {
+ node = mergeStyles(node);
+ }
+
+ // Remove/merge children
+ each(formatList, function(format) {
+ // Merge all children of similar type will move styles from child to parent
+ // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span>
+ // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span>
+ each(dom.select(format.inline, node), function(child) {
+ if (isBookmarkNode(child)) {
+ return;
+ }
+
+ removeFormat(format, vars, child, format.exact ? child : null);
+ });
+ });
+
+ // Remove child if direct parent is of same type
+ if (matchNode(node.parentNode, name, vars)) {
+ dom.remove(node, 1);
+ node = 0;
+ return TRUE;
+ }
+
+ // Look for parent with similar style format
+ if (format.merge_with_parents) {
+ dom.getParent(node.parentNode, function(parent) {
+ if (matchNode(parent, name, vars)) {
+ dom.remove(node, 1);
+ node = 0;
+ return TRUE;
+ }
+ });
+ }
+
+ // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b>
+ if (node && format.merge_siblings !== false) {
+ node = mergeSiblings(getNonWhiteSpaceSibling(node), node);
+ node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE));
+ }
+ }
+ });
+ }
+
+ if (getContentEditable(selection.getNode()) === "false") {
+ node = selection.getNode();
+ for (var i = 0, l = formatList.length; i < l; i++) {
+ if (formatList[i].ceFalseOverride && dom.is(node, formatList[i].selector)) {
+ setElementFormat(node, formatList[i]);
+ return;
+ }
+ }
+
+ return;
+ }
+
+ if (format) {
+ if (node) {
+ if (node.nodeType) {
+ if (!applyNodeStyle(formatList, node)) {
+ rng = dom.createRng();
+ rng.setStartBefore(node);
+ rng.setEndAfter(node);
+ applyRngStyle(expandRng(rng, formatList), null, true);
+ }
+ } else {
+ applyRngStyle(node, null, true);
+ }
+ } else {
+ if (!isCollapsed || !format.inline || dom.select('td[data-mce-selected],th[data-mce-selected]').length) {
+ // Obtain selection node before selection is unselected by applyRngStyle()
+ var curSelNode = ed.selection.getNode();
+
+ // If the formats have a default block and we can't find a parent block then
+ // start wrapping it with a DIV this is for forced_root_blocks: false
+ // It's kind of a hack but people should be using the default block type P since all desktop editors work that way
+ if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) {
+ apply(formatList[0].defaultBlock);
+ }
+
+ // Apply formatting to selection
+ ed.selection.setRng(adjustSelectionToVisibleSelection());
+ bookmark = selection.getBookmark();
+ applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark);
+
+ // Colored nodes should be underlined so that the color of the underline matches the text color.
+ if (format.styles && (format.styles.color || format.styles.textDecoration)) {
+ walk(curSelNode, processUnderlineAndColor, 'childNodes');
+ processUnderlineAndColor(curSelNode);
+ }
+
+ selection.moveToBookmark(bookmark);
+ moveStart(selection.getRng(TRUE));
+ ed.nodeChanged();
+ } else {
+ performCaretAction('apply', name, vars);
+ }
+ }
+
+ Hooks.postProcess(name, ed);
+ }
+ }
+
+ /**
+ * Removes the specified format from the current selection or specified node.
+ *
+ * @method remove
+ * @param {String} name Name of format to remove.
+ * @param {Object} vars Optional list of variables to replace within format before removing it.
+ * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection.
+ */
+ function remove(name, vars, node, similar) {
+ var formatList = get(name), format = formatList[0], bookmark, rng, contentEditable = true;
+
+ // Merges the styles for each node
+ function process(node) {
+ var children, i, l, lastContentEditable, hasContentEditableState;
+
+ // Node has a contentEditable value
+ if (node.nodeType === 1 && getContentEditable(node)) {
+ lastContentEditable = contentEditable;
+ contentEditable = getContentEditable(node) === "true";
+ hasContentEditableState = true; // We don't want to wrap the container only it's children
+ }
+
+ // Grab the children first since the nodelist might be changed
+ children = grep(node.childNodes);
+
+ // Process current node
+ if (contentEditable && !hasContentEditableState) {
+ for (i = 0, l = formatList.length; i < l; i++) {
+ if (removeFormat(formatList[i], vars, node, node)) {
+ break;
+ }
+ }
+ }
+
+ // Process the children
+ if (format.deep) {
+ if (children.length) {
+ for (i = 0, l = children.length; i < l; i++) {
+ process(children[i]);
+ }
+
+ if (hasContentEditableState) {
+ contentEditable = lastContentEditable; // Restore last contentEditable state from stack
+ }
+ }
+ }
+ }
+
+ function findFormatRoot(container) {
+ var formatRoot;
+
+ // Find format root
+ each(getParents(container.parentNode).reverse(), function(parent) {
+ var format;
+
+ // Find format root element
+ if (!formatRoot && parent.id != '_start' && parent.id != '_end') {
+ // Is the node matching the format we are looking for
+ format = matchNode(parent, name, vars, similar);
+ if (format && format.split !== false) {
+ formatRoot = parent;
+ }
+ }
+ });
+
+ return formatRoot;
+ }
+
+ function wrapAndSplit(formatRoot, container, target, split) {
+ var parent, clone, lastClone, firstClone, i, formatRootParent;
+
+ // Format root found then clone formats and split it
+ if (formatRoot) {
+ formatRootParent = formatRoot.parentNode;
+
+ for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) {
+ clone = dom.clone(parent, FALSE);
+
+ for (i = 0; i < formatList.length; i++) {
+ if (removeFormat(formatList[i], vars, clone, clone)) {
+ clone = 0;
+ break;
+ }
+ }
+
+ // Build wrapper node
+ if (clone) {
+ if (lastClone) {
+ clone.appendChild(lastClone);
+ }
+
+ if (!firstClone) {
+ firstClone = clone;
+ }
+
+ lastClone = clone;
+ }
+ }
+
+ // Never split block elements if the format is mixed
+ if (split && (!format.mixed || !isBlock(formatRoot))) {
+ container = dom.split(formatRoot, container);
+ }
+
+ // Wrap container in cloned formats
+ if (lastClone) {
+ target.parentNode.insertBefore(lastClone, target);
+ firstClone.appendChild(target);
+ }
+ }
+
+ return container;
+ }
+
+ function splitToFormatRoot(container) {
+ return wrapAndSplit(findFormatRoot(container), container, container, true);
+ }
+
+ function unwrap(start) {
+ var node = dom.get(start ? '_start' : '_end'),
+ out = node[start ? 'firstChild' : 'lastChild'];
+
+ // If the end is placed within the start the result will be removed
+ // So this checks if the out node is a bookmark node if it is it
+ // checks for another more suitable node
+ if (isBookmarkNode(out)) {
+ out = out[start ? 'firstChild' : 'lastChild'];
+ }
+
+ // Since dom.remove removes empty text nodes then we need to try to find a better node
+ if (out.nodeType == 3 && out.data.length === 0) {
+ out = start ? node.previousSibling || node.nextSibling : node.nextSibling || node.previousSibling;
+ }
+
+ dom.remove(node, true);
+
+ return out;
+ }
+
+ function removeRngStyle(rng) {
+ var startContainer, endContainer;
+ var commonAncestorContainer = rng.commonAncestorContainer;
+
+ rng = expandRng(rng, formatList, TRUE);
+
+ if (format.split) {
+ startContainer = getContainer(rng, TRUE);
+ endContainer = getContainer(rng);
+
+ if (startContainer != endContainer) {
+ // WebKit will render the table incorrectly if we wrap a TH or TD in a SPAN
+ // so let's see if we can use the first child instead
+ // This will happen if you triple click a table cell and use remove formatting
+ if (/^(TR|TH|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) {
+ if (startContainer.nodeName == "TR") {
+ startContainer = startContainer.firstChild.firstChild || startContainer;
+ } else {
+ startContainer = startContainer.firstChild || startContainer;
+ }
+ }
+
+ // Try to adjust endContainer as well if cells on the same row were selected - bug #6410
+ if (commonAncestorContainer &&
+ /^T(HEAD|BODY|FOOT|R)$/.test(commonAncestorContainer.nodeName) &&
+ isTableCell(endContainer) && endContainer.firstChild) {
+ endContainer = endContainer.firstChild || endContainer;
+ }
+
+ if (dom.isChildOf(startContainer, endContainer) && !isBlock(endContainer) &&
+ !isTableCell(startContainer) && !isTableCell(endContainer)) {
+ startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'});
+ splitToFormatRoot(startContainer);
+ startContainer = unwrap(TRUE);
+ return;
+ }
+
+ // Wrap start/end nodes in span element since these might be cloned/moved
+ startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'});
+ endContainer = wrap(endContainer, 'span', {id: '_end', 'data-mce-type': 'bookmark'});
+
+ // Split start/end
+ splitToFormatRoot(startContainer);
+ splitToFormatRoot(endContainer);
+
+ // Unwrap start/end to get real elements again
+ startContainer = unwrap(TRUE);
+ endContainer = unwrap();
+ } else {
+ startContainer = endContainer = splitToFormatRoot(startContainer);
+ }
+
+ // Update range positions since they might have changed after the split operations
+ rng.startContainer = startContainer.parentNode ? startContainer.parentNode : startContainer;
+ rng.startOffset = nodeIndex(startContainer);
+ rng.endContainer = endContainer.parentNode ? endContainer.parentNode : endContainer;
+ rng.endOffset = nodeIndex(endContainer) + 1;
+ }
+
+ // Remove items between start/end
+ rangeUtils.walk(rng, function(nodes) {
+ each(nodes, function(node) {
+ process(node);
+
+ // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined.
+ if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' &&
+ node.parentNode && getTextDecoration(node.parentNode) === 'underline') {
+ removeFormat({
+ 'deep': false,
+ 'exact': true,
+ 'inline': 'span',
+ 'styles': {
+ 'textDecoration': 'underline'
+ }
+ }, null, node);
+ }
+ });
+ });
+ }
+
+ // Handle node
+ if (node) {
+ if (node.nodeType) {
+ rng = dom.createRng();
+ rng.setStartBefore(node);
+ rng.setEndAfter(node);
+ removeRngStyle(rng);
+ } else {
+ removeRngStyle(node);
+ }
+
+ return;
+ }
+
+ if (getContentEditable(selection.getNode()) === "false") {
+ node = selection.getNode();
+ for (var i = 0, l = formatList.length; i < l; i++) {
+ if (formatList[i].ceFalseOverride) {
+ if (removeFormat(formatList[i], vars, node, node)) {
+ break;
+ }
+ }
+ }
+
+ return;
+ }
+
+ if (!selection.isCollapsed() || !format.inline || dom.select('td[data-mce-selected],th[data-mce-selected]').length) {
+ bookmark = selection.getBookmark();
+ removeRngStyle(selection.getRng(TRUE));
+ selection.moveToBookmark(bookmark);
+
+ // Check if start element still has formatting then we are at: "<b>text|</b>text"
+ // and need to move the start into the next text node
+ if (format.inline && match(name, vars, selection.getStart())) {
+ moveStart(selection.getRng(true));
+ }
+
+ ed.nodeChanged();
+ } else {
+ performCaretAction('remove', name, vars, similar);
+ }
+ }
+
+ /**
+ * Toggles the specified format on/off.
+ *
+ * @method toggle
+ * @param {String} name Name of format to apply/remove.
+ * @param {Object} vars Optional list of variables to replace within format before applying/removing it.
+ * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection.
+ */
+ function toggle(name, vars, node) {
+ var fmt = get(name);
+
+ if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) {
+ remove(name, vars, node);
+ } else {
+ apply(name, vars, node);
+ }
+ }
+
+ /**
+ * Return true/false if the specified node has the specified format.
+ *
+ * @method matchNode
+ * @param {Node} node Node to check the format on.
+ * @param {String} name Format name to check.
+ * @param {Object} vars Optional list of variables to replace before checking it.
+ * @param {Boolean} similar Match format that has similar properties.
+ * @return {Object} Returns the format object it matches or undefined if it doesn't match.
+ */
+ function matchNode(node, name, vars, similar) {
+ var formatList = get(name), format, i, classes;
+
+ function matchItems(node, format, item_name) {
+ var key, value, items = format[item_name], i;
+
+ // Custom match
+ if (format.onmatch) {
+ return format.onmatch(node, format, item_name);
+ }
+
+ // Check all items
+ if (items) {
+ // Non indexed object
+ if (items.length === undef) {
+ for (key in items) {
+ if (items.hasOwnProperty(key)) {
+ if (item_name === 'attributes') {
+ value = dom.getAttrib(node, key);
+ } else {
+ value = getStyle(node, key);
+ }
+
+ if (similar && !value && !format.exact) {
+ return;
+ }
+
+ if ((!similar || format.exact) && !isEq(value, normalizeStyleValue(replaceVars(items[key], vars), key))) {
+ return;
+ }
+ }
+ }
+ } else {
+ // Only one match needed for indexed arrays
+ for (i = 0; i < items.length; i++) {
+ if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) {
+ return format;
+ }
+ }
+ }
+ }
+
+ return format;
+ }
+
+ if (formatList && node) {
+ // Check each format in list
+ for (i = 0; i < formatList.length; i++) {
+ format = formatList[i];
+
+ // Name name, attributes, styles and classes
+ if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) {
+ // Match classes
+ if ((classes = format.classes)) {
+ for (i = 0; i < classes.length; i++) {
+ if (!dom.hasClass(node, classes[i])) {
+ return;
+ }
+ }
+ }
+
+ return format;
+ }
+ }
+ }
+ }
+
+ /**
+ * Matches the current selection or specified node against the specified format name.
+ *
+ * @method match
+ * @param {String} name Name of format to match.
+ * @param {Object} vars Optional list of variables to replace before checking it.
+ * @param {Node} node Optional node to check.
+ * @return {boolean} true/false if the specified selection/node matches the format.
+ */
+ function match(name, vars, node) {
+ var startNode;
+
+ function matchParents(node) {
+ var root = dom.getRoot();
+
+ if (node === root) {
+ return false;
+ }
+
+ // Find first node with similar format settings
+ node = dom.getParent(node, function(node) {
+ if (matchesUnInheritedFormatSelector(node, name)) {
+ return true;
+ }
+
+ return node.parentNode === root || !!matchNode(node, name, vars, true);
+ });
+
+ // Do an exact check on the similar format element
+ return matchNode(node, name, vars);
+ }
+
+ // Check specified node
+ if (node) {
+ return matchParents(node);
+ }
+
+ // Check selected node
+ node = selection.getNode();
+ if (matchParents(node)) {
+ return TRUE;
+ }
+
+ // Check start node if it's different
+ startNode = selection.getStart();
+ if (startNode != node) {
+ if (matchParents(startNode)) {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+ }
+
+ /**
+ * Matches the current selection against the array of formats and returns a new array with matching formats.
+ *
+ * @method matchAll
+ * @param {Array} names Name of format to match.
+ * @param {Object} vars Optional list of variables to replace before checking it.
+ * @return {Array} Array with matched formats.
+ */
+ function matchAll(names, vars) {
+ var startElement, matchedFormatNames = [], checkedMap = {};
+
+ // Check start of selection for formats
+ startElement = selection.getStart();
+ dom.getParent(startElement, function(node) {
+ var i, name;
+
+ for (i = 0; i < names.length; i++) {
+ name = names[i];
+
+ if (!checkedMap[name] && matchNode(node, name, vars)) {
+ checkedMap[name] = true;
+ matchedFormatNames.push(name);
+ }
+ }
+ }, dom.getRoot());
+
+ return matchedFormatNames;
+ }
+
+ /**
+ * Returns true/false if the specified format can be applied to the current selection or not. It
+ * will currently only check the state for selector formats, it returns true on all other format types.
+ *
+ * @method canApply
+ * @param {String} name Name of format to check.
+ * @return {boolean} true/false if the specified format can be applied to the current selection/node.
+ */
+ function canApply(name) {
+ var formatList = get(name), startNode, parents, i, x, selector;
+
+ if (formatList) {
+ startNode = selection.getStart();
+ parents = getParents(startNode);
+
+ for (x = formatList.length - 1; x >= 0; x--) {
+ selector = formatList[x].selector;
+
+ // Format is not selector based then always return TRUE
+ // Is it has a defaultBlock then it's likely it can be applied for example align on a non block element line
+ if (!selector || formatList[x].defaultBlock) {
+ return TRUE;
+ }
+
+ for (i = parents.length - 1; i >= 0; i--) {
+ if (dom.is(parents[i], selector)) {
+ return TRUE;
+ }
+ }
+ }
+ }
+
+ return FALSE;
+ }
+
+ /**
+ * Executes the specified callback when the current selection matches the formats or not.
+ *
+ * @method formatChanged
+ * @param {String} formats Comma separated list of formats to check for.
+ * @param {function} callback Callback with state and args when the format is changed/toggled on/off.
+ * @param {Boolean} similar True/false state if the match should handle similar or exact formats.
+ */
+ function formatChanged(formats, callback, similar) {
+ var currentFormats;
+
+ // Setup format node change logic
+ if (!formatChangeData) {
+ formatChangeData = {};
+ currentFormats = {};
+
+ ed.on('NodeChange', function(e) {
+ var parents = getParents(e.element), matchedFormats = {};
+
+ // Ignore bogus nodes like the <a> tag created by moveStart()
+ parents = Tools.grep(parents, function(node) {
+ return node.nodeType == 1 && !node.getAttribute('data-mce-bogus');
+ });
+
+ // Check for new formats
+ each(formatChangeData, function(callbacks, format) {
+ each(parents, function(node) {
+ if (matchNode(node, format, {}, callbacks.similar)) {
+ if (!currentFormats[format]) {
+ // Execute callbacks
+ each(callbacks, function(callback) {
+ callback(true, {node: node, format: format, parents: parents});
+ });
+
+ currentFormats[format] = callbacks;
+ }
+
+ matchedFormats[format] = callbacks;
+ return false;
+ }
+
+ if (matchesUnInheritedFormatSelector(node, format)) {
+ return false;
+ }
+ });
+ });
+
+ // Check if current formats still match
+ each(currentFormats, function(callbacks, format) {
+ if (!matchedFormats[format]) {
+ delete currentFormats[format];
+
+ each(callbacks, function(callback) {
+ callback(false, {node: e.element, format: format, parents: parents});
+ });
+ }
+ });
+ });
+ }
+
+ // Add format listeners
+ each(formats.split(','), function(format) {
+ if (!formatChangeData[format]) {
+ formatChangeData[format] = [];
+ formatChangeData[format].similar = similar;
+ }
+
+ formatChangeData[format].push(callback);
+ });
+
+ return this;
+ }
+
+ /**
+ * Returns a preview css text for the specified format.
+ *
+ * @method getCssText
+ * @param {String/Object} format Format to generate preview css text for.
+ * @return {String} Css text for the specified format.
+ * @example
+ * var cssText1 = editor.formatter.getCssText('bold');
+ * var cssText2 = editor.formatter.getCssText({inline: 'b'});
+ */
+ function getCssText(format) {
+ return Preview.getCssText(ed, format);
+ }
+
+ // Expose to public
+ extend(this, {
+ get: get,
+ register: register,
+ unregister: unregister,
+ apply: apply,
+ remove: remove,
+ toggle: toggle,
+ match: match,
+ matchAll: matchAll,
+ matchNode: matchNode,
+ canApply: canApply,
+ formatChanged: formatChanged,
+ getCssText: getCssText
+ });
+
+ // Initialize
+ defaultFormats();
+ addKeyboardShortcuts();
+ ed.on('BeforeGetContent', function(e) {
+ if (markCaretContainersBogus && e.format != 'raw') {
+ markCaretContainersBogus();
+ }
+ });
+ ed.on('mouseup keydown', function(e) {
+ if (disableCaretContainer) {
+ disableCaretContainer(e);
+ }
+ });
+
+ // Private functions
+
+ /**
+ * Checks if the specified nodes name matches the format inline/block or selector.
+ *
+ * @private
+ * @param {Node} node Node to match against the specified format.
+ * @param {Object} format Format object o match with.
+ * @return {boolean} true/false if the format matches.
+ */
+ function matchName(node, format) {
+ // Check for inline match
+ if (isEq(node, format.inline)) {
+ return TRUE;
+ }
+
+ // Check for block match
+ if (isEq(node, format.block)) {
+ return TRUE;
+ }
+
+ // Check for selector match
+ if (format.selector) {
+ return node.nodeType == 1 && dom.is(node, format.selector);
+ }
+ }
+
+ /**
+ * Compares two string/nodes regardless of their case.
+ *
+ * @private
+ * @param {String/Node} str1 Node or string to compare.
+ * @param {String/Node} str2 Node or string to compare.
+ * @return {boolean} True/false if they match.
+ */
+ function isEq(str1, str2) {
+ str1 = str1 || '';
+ str2 = str2 || '';
+
+ str1 = '' + (str1.nodeName || str1);
+ str2 = '' + (str2.nodeName || str2);
+
+ return str1.toLowerCase() == str2.toLowerCase();
+ }
+
+ /**
+ * Returns the style by name on the specified node. This method modifies the style
+ * contents to make it more easy to match. This will resolve a few browser issues.
+ *
+ * @private
+ * @param {Node} node to get style from.
+ * @param {String} name Style name to get.
+ * @return {String} Style item value.
+ */
+ function getStyle(node, name) {
+ return normalizeStyleValue(dom.getStyle(node, name), name);
+ }
+
+ /**
+ * Normalize style value by name. This method modifies the style contents
+ * to make it more easy to match. This will resolve a few browser issues.
+ *
+ * @private
+ * @param {String} value Value to get style from.
+ * @param {String} name Style name to get.
+ * @return {String} Style item value.
+ */
+ function normalizeStyleValue(value, name) {
+ // Force the format to hex
+ if (name == 'color' || name == 'backgroundColor') {
+ value = dom.toHex(value);
+ }
+
+ // Opera will return bold as 700
+ if (name == 'fontWeight' && value == 700) {
+ value = 'bold';
+ }
+
+ // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font"
+ if (name == 'fontFamily') {
+ value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ',');
+ }
+
+ return '' + value;
+ }
+
+ /**
+ * Replaces variables in the value. The variable format is %var.
+ *
+ * @private
+ * @param {String} value Value to replace variables in.
+ * @param {Object} vars Name/value array with variables to replace.
+ * @return {String} New value with replaced variables.
+ */
+ function replaceVars(value, vars) {
+ if (typeof value != "string") {
+ value = value(vars);
+ } else if (vars) {
+ value = value.replace(/%(\w+)/g, function(str, name) {
+ return vars[name] || str;
+ });
+ }
+
+ return value;
+ }
+
+ function isWhiteSpaceNode(node) {
+ return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue);
+ }
+
+ function wrap(node, name, attrs) {
+ var wrapper = dom.create(name, attrs);
+
+ node.parentNode.insertBefore(wrapper, node);
+ wrapper.appendChild(node);
+
+ return wrapper;
+ }
+
+ /**
+ * Expands the specified range like object to depending on format.
+ *
+ * For example on block formats it will move the start/end position
+ * to the beginning of the current block.
+ *
+ * @private
+ * @param {Object} rng Range like object.
+ * @param {Array} format Array with formats to expand by.
+ * @param {Boolean} remove
+ * @return {Object} Expanded range like object.
+ */
+ function expandRng(rng, format, remove) {
+ var lastIdx, leaf, endPoint,
+ startContainer = rng.startContainer,
+ startOffset = rng.startOffset,
+ endContainer = rng.endContainer,
+ endOffset = rng.endOffset;
+
+ // This function walks up the tree if there is no siblings before/after the node
+ function findParentContainer(start) {
+ var container, parent, sibling, siblingName, root;
+
+ container = parent = start ? startContainer : endContainer;
+ siblingName = start ? 'previousSibling' : 'nextSibling';
+ root = dom.getRoot();
+
+ function isBogusBr(node) {
+ return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling;
+ }
+
+ // If it's a text node and the offset is inside the text
+ if (container.nodeType == 3 && !isWhiteSpaceNode(container)) {
+ if (start ? startOffset > 0 : endOffset < container.nodeValue.length) {
+ return container;
+ }
+ }
+
+ /*eslint no-constant-condition:0 */
+ while (true) {
+ // Stop expanding on block elements
+ if (!format[0].block_expand && isBlock(parent)) {
+ return parent;
+ }
+
+ // Walk left/right
+ for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) {
+ if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) {
+ return parent;
+ }
+ }
+
+ // Check if we can move up are we at root level or body level
+ if (parent == root || parent.parentNode == root) {
+ container = parent;
+ break;
+ }
+
+ parent = parent.parentNode;
+ }
+
+ return container;
+ }
+
+ // This function walks down the tree to find the leaf at the selection.
+ // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node.
+ function findLeaf(node, offset) {
+ if (offset === undef) {
+ offset = node.nodeType === 3 ? node.length : node.childNodes.length;
+ }
+
+ while (node && node.hasChildNodes()) {
+ node = node.childNodes[offset];
+ if (node) {
+ offset = node.nodeType === 3 ? node.length : node.childNodes.length;
+ }
+ }
+ return {node: node, offset: offset};
+ }
+
+ // If index based start position then resolve it
+ if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
+ lastIdx = startContainer.childNodes.length - 1;
+ startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset];
+
+ if (startContainer.nodeType == 3) {
+ startOffset = 0;
+ }
+ }
+
+ // If index based end position then resolve it
+ if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
+ lastIdx = endContainer.childNodes.length - 1;
+ endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1];
+
+ if (endContainer.nodeType == 3) {
+ endOffset = endContainer.nodeValue.length;
+ }
+ }
+
+ // Expands the node to the closes contentEditable false element if it exists
+ function findParentContentEditable(node) {
+ var parent = node;
+
+ while (parent) {
+ if (parent.nodeType === 1 && getContentEditable(parent)) {
+ return getContentEditable(parent) === "false" ? parent : node;
+ }
+
+ parent = parent.parentNode;
+ }
+
+ return node;
+ }
+
+ function findWordEndPoint(container, offset, start) {
+ var walker, node, pos, lastTextNode;
+
+ function findSpace(node, offset) {
+ var pos, pos2, str = node.nodeValue;
+
+ if (typeof offset == "undefined") {
+ offset = start ? str.length : 0;
+ }
+
+ if (start) {
+ pos = str.lastIndexOf(' ', offset);
+ pos2 = str.lastIndexOf('\u00a0', offset);
+ pos = pos > pos2 ? pos : pos2;
+
+ // Include the space on remove to avoid tag soup
+ if (pos !== -1 && !remove) {
+ pos++;
+ }
+ } else {
+ pos = str.indexOf(' ', offset);
+ pos2 = str.indexOf('\u00a0', offset);
+ pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2;
+ }
+
+ return pos;
+ }
+
+ if (container.nodeType === 3) {
+ pos = findSpace(container, offset);
+
+ if (pos !== -1) {
+ return {container: container, offset: pos};
+ }
+
+ lastTextNode = container;
+ }
+
+ // Walk the nodes inside the block
+ walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody());
+ while ((node = walker[start ? 'prev' : 'next']())) {
+ if (node.nodeType === 3) {
+ lastTextNode = node;
+ pos = findSpace(node);
+
+ if (pos !== -1) {
+ return {container: node, offset: pos};
+ }
+ } else if (isBlock(node)) {
+ break;
+ }
+ }
+
+ if (lastTextNode) {
+ if (start) {
+ offset = 0;
+ } else {
+ offset = lastTextNode.length;
+ }
+
+ return {container: lastTextNode, offset: offset};
+ }
+ }
+
+ function findSelectorEndPoint(container, sibling_name) {
+ var parents, i, y, curFormat;
+
+ if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name]) {
+ container = container[sibling_name];
+ }
+
+ parents = getParents(container);
+ for (i = 0; i < parents.length; i++) {
+ for (y = 0; y < format.length; y++) {
+ curFormat = format[y];
+
+ // If collapsed state is set then skip formats that doesn't match that
+ if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) {
+ continue;
+ }
+
+ if (dom.is(parents[i], curFormat.selector)) {
+ return parents[i];
+ }
+ }
+ }
+
+ return container;
+ }
+
+ function findBlockEndPoint(container, sibling_name) {
+ var node, root = dom.getRoot();
+
+ // Expand to block of similar type
+ if (!format[0].wrapper) {
+ node = dom.getParent(container, format[0].block, root);
+ }
+
+ // Expand to first wrappable block element or any block element
+ if (!node) {
+ node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, function(node) {
+ // Fixes #6183 where it would expand to editable parent element in inline mode
+ return node != root && isTextBlock(node);
+ });
+ }
+
+ // Exclude inner lists from wrapping
+ if (node && format[0].wrapper) {
+ node = getParents(node, 'ul,ol').reverse()[0] || node;
+ }
+
+ // Didn't find a block element look for first/last wrappable element
+ if (!node) {
+ node = container;
+
+ while (node[sibling_name] && !isBlock(node[sibling_name])) {
+ node = node[sibling_name];
+
+ // Break on BR but include it will be removed later on
+ // we can't remove it now since we need to check if it can be wrapped
+ if (isEq(node, 'br')) {
+ break;
+ }
+ }
+ }
+
+ return node || container;
+ }
+
+ // Expand to closest contentEditable element
+ startContainer = findParentContentEditable(startContainer);
+ endContainer = findParentContentEditable(endContainer);
+
+ // Exclude bookmark nodes if possible
+ if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) {
+ startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode;
+ startContainer = startContainer.nextSibling || startContainer;
+
+ if (startContainer.nodeType == 3) {
+ startOffset = 0;
+ }
+ }
+
+ if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) {
+ endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode;
+ endContainer = endContainer.previousSibling || endContainer;
+
+ if (endContainer.nodeType == 3) {
+ endOffset = endContainer.length;
+ }
+ }
+
+ if (format[0].inline) {
+ if (rng.collapsed) {
+ // Expand left to closest word boundary
+ endPoint = findWordEndPoint(startContainer, startOffset, true);
+ if (endPoint) {
+ startContainer = endPoint.container;
+ startOffset = endPoint.offset;
+ }
+
+ // Expand right to closest word boundary
+ endPoint = findWordEndPoint(endContainer, endOffset);
+ if (endPoint) {
+ endContainer = endPoint.container;
+ endOffset = endPoint.offset;
+ }
+ }
+
+ // Avoid applying formatting to a trailing space.
+ leaf = findLeaf(endContainer, endOffset);
+ if (leaf.node) {
+ while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) {
+ leaf = findLeaf(leaf.node.previousSibling);
+ }
+
+ if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 &&
+ leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') {
+
+ if (leaf.offset > 1) {
+ endContainer = leaf.node;
+ endContainer.splitText(leaf.offset - 1);
+ }
+ }
+ }
+ }
+
+ // Move start/end point up the tree if the leaves are sharp and if we are in different containers
+ // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>!
+ // This will reduce the number of wrapper elements that needs to be created
+ // Move start point up the tree
+ if (format[0].inline || format[0].block_expand) {
+ if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) {
+ startContainer = findParentContainer(true);
+ }
+
+ if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) {
+ endContainer = findParentContainer();
+ }
+ }
+
+ // Expand start/end container to matching selector
+ if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) {
+ // Find new startContainer/endContainer if there is better one
+ startContainer = findSelectorEndPoint(startContainer, 'previousSibling');
+ endContainer = findSelectorEndPoint(endContainer, 'nextSibling');
+ }
+
+ // Expand start/end container to matching block element or text node
+ if (format[0].block || format[0].selector) {
+ // Find new startContainer/endContainer if there is better one
+ startContainer = findBlockEndPoint(startContainer, 'previousSibling');
+ endContainer = findBlockEndPoint(endContainer, 'nextSibling');
+
+ // Non block element then try to expand up the leaf
+ if (format[0].block) {
+ if (!isBlock(startContainer)) {
+ startContainer = findParentContainer(true);
+ }
+
+ if (!isBlock(endContainer)) {
+ endContainer = findParentContainer();
+ }
+ }
+ }
+
+ // Setup index for startContainer
+ if (startContainer.nodeType == 1) {
+ startOffset = nodeIndex(startContainer);
+ startContainer = startContainer.parentNode;
+ }
+
+ // Setup index for endContainer
+ if (endContainer.nodeType == 1) {
+ endOffset = nodeIndex(endContainer) + 1;
+ endContainer = endContainer.parentNode;
+ }
+
+ // Return new range like object
+ return {
+ startContainer: startContainer,
+ startOffset: startOffset,
+ endContainer: endContainer,
+ endOffset: endOffset
+ };
+ }
+
+ function isColorFormatAndAnchor(node, format) {
+ return format.links && node.tagName == 'A';
+ }
+
+ /**
+ * Removes the specified format for the specified node. It will also remove the node if it doesn't have
+ * any attributes if the format specifies it to do so.
+ *
+ * @private
+ * @param {Object} format Format object with items to remove from node.
+ * @param {Object} vars Name/value object with variables to apply to format.
+ * @param {Node} node Node to remove the format styles on.
+ * @param {Node} compare_node Optional compare node, if specified the styles will be compared to that node.
+ * @return {Boolean} True/false if the node was removed or not.
+ */
+ function removeFormat(format, vars, node, compare_node) {
+ var i, attrs, stylesModified;
+
+ // Check if node matches format
+ if (!matchName(node, format) && !isColorFormatAndAnchor(node, format)) {
+ return FALSE;
+ }
+
+ // Should we compare with format attribs and styles
+ if (format.remove != 'all') {
+ // Remove styles
+ each(format.styles, function(value, name) {
+ value = normalizeStyleValue(replaceVars(value, vars), name);
+
+ // Indexed array
+ if (typeof name === 'number') {
+ name = value;
+ compare_node = 0;
+ }
+
+ if (format.remove_similar || (!compare_node || isEq(getStyle(compare_node, name), value))) {
+ dom.setStyle(node, name, '');
+ }
+
+ stylesModified = 1;
+ });
+
+ // Remove style attribute if it's empty
+ if (stylesModified && dom.getAttrib(node, 'style') === '') {
+ node.removeAttribute('style');
+ node.removeAttribute('data-mce-style');
+ }
+
+ // Remove attributes
+ each(format.attributes, function(value, name) {
+ var valueOut;
+
+ value = replaceVars(value, vars);
+
+ // Indexed array
+ if (typeof name === 'number') {
+ name = value;
+ compare_node = 0;
+ }
+
+ if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) {
+ // Keep internal classes
+ if (name == 'class') {
+ value = dom.getAttrib(node, name);
+ if (value) {
+ // Build new class value where everything is removed except the internal prefixed classes
+ valueOut = '';
+ each(value.split(/\s+/), function(cls) {
+ if (/mce\-\w+/.test(cls)) {
+ valueOut += (valueOut ? ' ' : '') + cls;
+ }
+ });
+
+ // We got some internal classes left
+ if (valueOut) {
+ dom.setAttrib(node, name, valueOut);
+ return;
+ }
+ }
+ }
+
+ // IE6 has a bug where the attribute doesn't get removed correctly
+ if (name == "class") {
+ node.removeAttribute('className');
+ }
+
+ // Remove mce prefixed attributes
+ if (MCE_ATTR_RE.test(name)) {
+ node.removeAttribute('data-mce-' + name);
+ }
+
+ node.removeAttribute(name);
+ }
+ });
+
+ // Remove classes
+ each(format.classes, function(value) {
+ value = replaceVars(value, vars);
+
+ if (!compare_node || dom.hasClass(compare_node, value)) {
+ dom.removeClass(node, value);
+ }
+ });
+
+ // Check for non internal attributes
+ attrs = dom.getAttribs(node);
+ for (i = 0; i < attrs.length; i++) {
+ var attrName = attrs[i].nodeName;
+ if (attrName.indexOf('_') !== 0 && attrName.indexOf('data-') !== 0) {
+ return FALSE;
+ }
+ }
+ }
+
+ // Remove the inline child if it's empty for example <b> or <span>
+ if (format.remove != 'none') {
+ removeNode(node, format);
+ return TRUE;
+ }
+ }
+
+ /**
+ * Removes the node and wrap it's children in paragraphs before doing so or
+ * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled.
+ *
+ * If the div in the node below gets removed:
+ * text<div>text</div>text
+ *
+ * Output becomes:
+ * text<div><br />text<br /></div>text
+ *
+ * So when the div is removed the result is:
+ * text<br />text<br />text
+ *
+ * @private
+ * @param {Node} node Node to remove + apply BR/P elements to.
+ * @param {Object} format Format rule.
+ * @return {Node} Input node.
+ */
+ function removeNode(node, format) {
+ var parentNode = node.parentNode, rootBlockElm;
+
+ function find(node, next, inc) {
+ node = getNonWhiteSpaceSibling(node, next, inc);
+
+ return !node || (node.nodeName == 'BR' || isBlock(node));
+ }
+
+ if (format.block) {
+ if (!forcedRootBlock) {
+ // Append BR elements if needed before we remove the block
+ if (isBlock(node) && !isBlock(parentNode)) {
+ if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) {
+ node.insertBefore(dom.create('br'), node.firstChild);
+ }
+
+ if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) {
+ node.appendChild(dom.create('br'));
+ }
+ }
+ } else {
+ // Wrap the block in a forcedRootBlock if we are at the root of document
+ if (parentNode == dom.getRoot()) {
+ if (!format.list_block || !isEq(node, format.list_block)) {
+ each(grep(node.childNodes), function(node) {
+ if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) {
+ if (!rootBlockElm) {
+ rootBlockElm = wrap(node, forcedRootBlock);
+ dom.setAttribs(rootBlockElm, ed.settings.forced_root_block_attrs);
+ } else {
+ rootBlockElm.appendChild(node);
+ }
+ } else {
+ rootBlockElm = 0;
+ }
+ });
+ }
+ }
+ }
+ }
+
+ // Never remove nodes that isn't the specified inline element if a selector is specified too
+ if (format.selector && format.inline && !isEq(format.inline, node)) {
+ return;
+ }
+
+ dom.remove(node, 1);
+ }
+
+ /**
+ * Returns the next/previous non whitespace node.
+ *
+ * @private
+ * @param {Node} node Node to start at.
+ * @param {boolean} next (Optional) Include next or previous node defaults to previous.
+ * @param {boolean} inc (Optional) Include the current node in checking. Defaults to false.
+ * @return {Node} Next or previous node or undefined if it wasn't found.
+ */
+ function getNonWhiteSpaceSibling(node, next, inc) {
+ if (node) {
+ next = next ? 'nextSibling' : 'previousSibling';
+
+ for (node = inc ? node : node[next]; node; node = node[next]) {
+ if (node.nodeType == 1 || !isWhiteSpaceNode(node)) {
+ return node;
+ }
+ }
+ }
+ }
+
+ /**
+ * Merges the next/previous sibling element if they match.
+ *
+ * @private
+ * @param {Node} prev Previous node to compare/merge.
+ * @param {Node} next Next node to compare/merge.
+ * @return {Node} Next node if we didn't merge and prev node if we did.
+ */
+ function mergeSiblings(prev, next) {
+ var sibling, tmpSibling, elementUtils = new ElementUtils(dom);
+
+ function findElementSibling(node, sibling_name) {
+ for (sibling = node; sibling; sibling = sibling[sibling_name]) {
+ if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) {
+ return node;
+ }
+
+ if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) {
+ return sibling;
+ }
+ }
+
+ return node;
+ }
+
+ // Check if next/prev exists and that they are elements
+ if (prev && next) {
+ // If previous sibling is empty then jump over it
+ prev = findElementSibling(prev, 'previousSibling');
+ next = findElementSibling(next, 'nextSibling');
+
+ // Compare next and previous nodes
+ if (elementUtils.compare(prev, next)) {
+ // Append nodes between
+ for (sibling = prev.nextSibling; sibling && sibling != next;) {
+ tmpSibling = sibling;
+ sibling = sibling.nextSibling;
+ prev.appendChild(tmpSibling);
+ }
+
+ // Remove next node
+ dom.remove(next);
+
+ // Move children into prev node
+ each(grep(next.childNodes), function(node) {
+ prev.appendChild(node);
+ });
+
+ return prev;
+ }
+ }
+
+ return next;
+ }
+
+ function getContainer(rng, start) {
+ var container, offset, lastIdx;
+
+ container = rng[start ? 'startContainer' : 'endContainer'];
+ offset = rng[start ? 'startOffset' : 'endOffset'];
+
+ if (container.nodeType == 1) {
+ lastIdx = container.childNodes.length - 1;
+
+ if (!start && offset) {
+ offset--;
+ }
+
+ container = container.childNodes[offset > lastIdx ? lastIdx : offset];
+ }
+
+ // If start text node is excluded then walk to the next node
+ if (container.nodeType === 3 && start && offset >= container.nodeValue.length) {
+ container = new TreeWalker(container, ed.getBody()).next() || container;
+ }
+
+ // If end text node is excluded then walk to the previous node
+ if (container.nodeType === 3 && !start && offset === 0) {
+ container = new TreeWalker(container, ed.getBody()).prev() || container;
+ }
+
+ return container;
+ }
+
+ function performCaretAction(type, name, vars, similar) {
+ var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug;
+
+ // Creates a caret container bogus element
+ function createCaretContainer(fill) {
+ var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''});
+
+ if (fill) {
+ caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR));
+ }
+
+ return caretContainer;
+ }
+
+ function isCaretContainerEmpty(node, nodes) {
+ while (node) {
+ if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) {
+ return false;
+ }
+
+ // Collect nodes
+ if (nodes && node.nodeType === 1) {
+ nodes.push(node);
+ }
+
+ node = node.firstChild;
+ }
+
+ return true;
+ }
+
+ // Returns any parent caret container element
+ function getParentCaretContainer(node) {
+ while (node) {
+ if (node.id === caretContainerId) {
+ return node;
+ }
+
+ node = node.parentNode;
+ }
+ }
+
+ // Finds the first text node in the specified node
+ function findFirstTextNode(node) {
+ var walker;
+
+ if (node) {
+ walker = new TreeWalker(node, node);
+
+ for (node = walker.current(); node; node = walker.next()) {
+ if (node.nodeType === 3) {
+ return node;
+ }
+ }
+ }
+ }
+
+ // Removes the caret container for the specified node or all on the current document
+ function removeCaretContainer(node, move_caret) {
+ var child, rng;
+
+ if (!node) {
+ node = getParentCaretContainer(selection.getStart());
+
+ if (!node) {
+ while ((node = dom.get(caretContainerId))) {
+ removeCaretContainer(node, false);
+ }
+ }
+ } else {
+ rng = selection.getRng(true);
+
+ if (isCaretContainerEmpty(node)) {
+ if (move_caret !== false) {
+ rng.setStartBefore(node);
+ rng.setEndBefore(node);
+ }
+
+ dom.remove(node);
+ } else {
+ child = findFirstTextNode(node);
+
+ if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) {
+ child.deleteData(0, 1);
+
+ // Fix for bug #6976
+ if (rng.startContainer == child && rng.startOffset > 0) {
+ rng.setStart(child, rng.startOffset - 1);
+ }
+
+ if (rng.endContainer == child && rng.endOffset > 0) {
+ rng.setEnd(child, rng.endOffset - 1);
+ }
+ }
+
+ dom.remove(node, 1);
+ }
+
+ selection.setRng(rng);
+ }
+ }
+
+ // Applies formatting to the caret position
+ function applyCaretFormat() {
+ var rng, caretContainer, textNode, offset, bookmark, container, text;
+
+ rng = selection.getRng(true);
+ offset = rng.startOffset;
+ container = rng.startContainer;
+ text = container.nodeValue;
+
+ caretContainer = getParentCaretContainer(selection.getStart());
+ if (caretContainer) {
+ textNode = findFirstTextNode(caretContainer);
+ }
+
+ // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character
+ if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) {
+ // Get bookmark of caret position
+ bookmark = selection.getBookmark();
+
+ // Collapse bookmark range (WebKit)
+ rng.collapse(true);
+
+ // Expand the range to the closest word and split it at those points
+ rng = expandRng(rng, get(name));
+ rng = rangeUtils.split(rng);
+
+ // Apply the format to the range
+ apply(name, vars, rng);
+
+ // Move selection back to caret position
+ selection.moveToBookmark(bookmark);
+ } else {
+ if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) {
+ caretContainer = createCaretContainer(true);
+ textNode = caretContainer.firstChild;
+
+ rng.insertNode(caretContainer);
+ offset = 1;
+
+ apply(name, vars, caretContainer);
+ } else {
+ apply(name, vars, caretContainer);
+ }
+
+ // Move selection to text node
+ selection.setCursorLocation(textNode, offset);
+ }
+ }
+
+ function removeCaretFormat() {
+ var rng = selection.getRng(true), container, offset, bookmark,
+ hasContentAfter, node, formatNode, parents = [], i, caretContainer;
+
+ container = rng.startContainer;
+ offset = rng.startOffset;
+ node = container;
+
+ if (container.nodeType == 3) {
+ if (offset != container.nodeValue.length) {
+ hasContentAfter = true;
+ }
+
+ node = node.parentNode;
+ }
+
+ while (node) {
+ if (matchNode(node, name, vars, similar)) {
+ formatNode = node;
+ break;
+ }
+
+ if (node.nextSibling) {
+ hasContentAfter = true;
+ }
+
+ parents.push(node);
+ node = node.parentNode;
+ }
+
+ // Node doesn't have the specified format
+ if (!formatNode) {
+ return;
+ }
+
+ // Is there contents after the caret then remove the format on the element
+ if (hasContentAfter) {
+ // Get bookmark of caret position
+ bookmark = selection.getBookmark();
+
+ // Collapse bookmark range (WebKit)
+ rng.collapse(true);
+
+ // Expand the range to the closest word and split it at those points
+ rng = expandRng(rng, get(name), true);
+ rng = rangeUtils.split(rng);
+
+ // Remove the format from the range
+ remove(name, vars, rng);
+
+ // Move selection back to caret position
+ selection.moveToBookmark(bookmark);
+ } else {
+ caretContainer = createCaretContainer();
+
+ node = caretContainer;
+ for (i = parents.length - 1; i >= 0; i--) {
+ node.appendChild(dom.clone(parents[i], false));
+ node = node.firstChild;
+ }
+
+ // Insert invisible character into inner most format element
+ node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR));
+ node = node.firstChild;
+
+ var block = dom.getParent(formatNode, isTextBlock);
+
+ if (block && dom.isEmpty(block)) {
+ // Replace formatNode with caretContainer when removing format from empty block like <p><b>|</b></p>
+ formatNode.parentNode.replaceChild(caretContainer, formatNode);
+ } else {
+ // Insert caret container after the formatted node
+ dom.insertAfter(caretContainer, formatNode);
+ }
+
+ // Move selection to text node
+ selection.setCursorLocation(node, 1);
+
+ // If the formatNode is empty, we can remove it safely.
+ if (dom.isEmpty(formatNode)) {
+ dom.remove(formatNode);
+ }
+ }
+ }
+
+ // Checks if the parent caret container node isn't empty if that is the case it
+ // will remove the bogus state on all children that isn't empty
+ function unmarkBogusCaretParents() {
+ var caretContainer;
+
+ caretContainer = getParentCaretContainer(selection.getStart());
+ if (caretContainer && !dom.isEmpty(caretContainer)) {
+ walk(caretContainer, function(node) {
+ if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) {
+ dom.setAttrib(node, 'data-mce-bogus', null);
+ }
+ }, 'childNodes');
+ }
+ }
+
+ // Only bind the caret events once
+ if (!ed._hasCaretEvents) {
+ // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements
+ markCaretContainersBogus = function() {
+ var nodes = [], i;
+
+ if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) {
+ // Mark children
+ i = nodes.length;
+ while (i--) {
+ dom.setAttrib(nodes[i], 'data-mce-bogus', '1');
+ }
+ }
+ };
+
+ disableCaretContainer = function(e) {
+ var keyCode = e.keyCode;
+
+ removeCaretContainer();
+
+ // Remove caret container if it's empty
+ if (keyCode == 8 && selection.isCollapsed() && selection.getStart().innerHTML == INVISIBLE_CHAR) {
+ removeCaretContainer(getParentCaretContainer(selection.getStart()));
+ }
+
+ // Remove caret container on keydown and it's left/right arrow keys
+ if (keyCode == 37 || keyCode == 39) {
+ removeCaretContainer(getParentCaretContainer(selection.getStart()));
+ }
+
+ unmarkBogusCaretParents();
+ };
+
+ // Remove bogus state if they got filled by contents using editor.selection.setContent
+ ed.on('SetContent', function(e) {
+ if (e.selection) {
+ unmarkBogusCaretParents();
+ }
+ });
+ ed._hasCaretEvents = true;
+ }
+
+ // Do apply or remove caret format
+ if (type == "apply") {
+ applyCaretFormat();
+ } else {
+ removeCaretFormat();
+ }
+ }
+
+ /**
+ * Moves the start to the first suitable text node.
+ */
+ function moveStart(rng) {
+ var container = rng.startContainer,
+ offset = rng.startOffset, isAtEndOfText,
+ walker, node, nodes, tmpNode;
+
+ if (rng.startContainer == rng.endContainer) {
+ if (isInlineBlock(rng.startContainer.childNodes[rng.startOffset])) {
+ return;
+ }
+ }
+
+ // Convert text node into index if possible
+ if (container.nodeType == 3 && offset >= container.nodeValue.length) {
+ // Get the parent container location and walk from there
+ offset = nodeIndex(container);
+ container = container.parentNode;
+ isAtEndOfText = true;
+ }
+
+ // Move startContainer/startOffset in to a suitable node
+ if (container.nodeType == 1) {
+ nodes = container.childNodes;
+ container = nodes[Math.min(offset, nodes.length - 1)];
+ walker = new TreeWalker(container, dom.getParent(container, dom.isBlock));
+
+ // If offset is at end of the parent node walk to the next one
+ if (offset > nodes.length - 1 || isAtEndOfText) {
+ walker.next();
+ }
+
+ for (node = walker.current(); node; node = walker.next()) {
+ if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
+ // IE has a "neat" feature where it moves the start node into the closest element
+ // we can avoid this by inserting an element before it and then remove it after we set the selection
+ tmpNode = dom.create('a', {'data-mce-bogus': 'all'}, INVISIBLE_CHAR);
+ node.parentNode.insertBefore(tmpNode, node);
+
+ // Set selection and remove tmpNode
+ rng.setStart(node, 0);
+ selection.setRng(rng);
+ dom.remove(tmpNode);
+
+ return;
+ }
+ }
+ }
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/undo/Diff.js
+
+/**
+ * Diff.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * JS Implementation of the O(ND) Difference Algorithm by Eugene W. Myers.
+ *
+ * @class tinymce.undo.Diff
+ * @private
+ */
+define("tinymce/undo/Diff", [
+], function () {
+ var KEEP = 0, INSERT = 1, DELETE = 2;
+
+ var diff = function (left, right) {
+ var size = left.length + right.length + 2;
+ var vDown = new Array(size);
+ var vUp = new Array(size);
+
+ var snake = function (start, end, diag) {
+ return {
+ start: start,
+ end: end,
+ diag: diag
+ };
+ };
+
+ var buildScript = function (start1, end1, start2, end2, script) {
+ var middle = getMiddleSnake(start1, end1, start2, end2);
+
+ if (middle === null || middle.start === end1 && middle.diag === end1 - end2 ||
+ middle.end === start1 && middle.diag === start1 - start2) {
+ var i = start1;
+ var j = start2;
+ while (i < end1 || j < end2) {
+ if (i < end1 && j < end2 && left[i] === right[j]) {
+ script.push([KEEP, left[i]]);
+ ++i;
+ ++j;
+ } else {
+ if (end1 - start1 > end2 - start2) {
+ script.push([DELETE, left[i]]);
+ ++i;
+ } else {
+ script.push([INSERT, right[j]]);
+ ++j;
+ }
+ }
+ }
+ } else {
+ buildScript(start1, middle.start, start2, middle.start - middle.diag, script);
+ for (var i2 = middle.start; i2 < middle.end; ++i2) {
+ script.push([KEEP, left[i2]]);
+ }
+ buildScript(middle.end, end1, middle.end - middle.diag, end2, script);
+ }
+ };
+
+ var buildSnake = function (start, diag, end1, end2) {
+ var end = start;
+ while (end - diag < end2 && end < end1 && left[end] === right[end - diag]) {
+ ++end;
+ }
+ return snake(start, end, diag);
+ };
+
+ var getMiddleSnake = function (start1, end1, start2, end2) {
+ // Myers Algorithm
+ // Initialisations
+ var m = end1 - start1;
+ var n = end2 - start2;
+ if (m === 0 || n === 0) {
+ return null;
+ }
+
+ var delta = m - n;
+ var sum = n + m;
+ var offset = (sum % 2 === 0 ? sum : sum + 1) / 2;
+ vDown[1 + offset] = start1;
+ vUp[1 + offset] = end1 + 1;
+
+ for (var d = 0; d <= offset; ++d) {
+ // Down
+ for (var k = -d; k <= d; k += 2) {
+ // First step
+
+ var i = k + offset;
+ if (k === -d || k != d && vDown[i - 1] < vDown[i + 1]) {
+ vDown[i] = vDown[i + 1];
+ } else {
+ vDown[i] = vDown[i - 1] + 1;
+ }
+
+ var x = vDown[i];
+ var y = x - start1 + start2 - k;
+
+ while (x < end1 && y < end2 && left[x] === right[y]) {
+ vDown[i] = ++x;
+ ++y;
+ }
+ // Second step
+ if (delta % 2 != 0 && delta - d <= k && k <= delta + d) {
+ if (vUp[i - delta] <= vDown[i]) {
+ return buildSnake(vUp[i - delta], k + start1 - start2, end1, end2);
+ }
+ }
+ }
+
+ // Up
+ for (k = delta - d; k <= delta + d; k += 2) {
+ // First step
+ i = k + offset - delta;
+ if (k === delta - d || k != delta + d && vUp[i + 1] <= vUp[i - 1]) {
+ vUp[i] = vUp[i + 1] - 1;
+ } else {
+ vUp[i] = vUp[i - 1];
+ }
+
+ x = vUp[i] - 1;
+ y = x - start1 + start2 - k;
+ while (x >= start1 && y >= start2 && left[x] === right[y]) {
+ vUp[i] = x--;
+ y--;
+ }
+ // Second step
+ if (delta % 2 === 0 && -d <= k && k <= d) {
+ if (vUp[i] <= vDown[i + delta]) {
+ return buildSnake(vUp[i], k + start1 - start2, end1, end2);
+ }
+ }
+ }
+ }
+ };
+
+ var script = [];
+ buildScript(0, left.length, 0, right.length, script);
+ return script;
+ };
+
+ return {
+ KEEP: KEEP,
+ DELETE: DELETE,
+ INSERT: INSERT,
+ diff: diff
+ };
+});
+
+// Included from: js/tinymce/classes/undo/Fragments.js
+
+/**
+ * Fragments.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module reads and applies html fragments from/to dom nodes.
+ *
+ * @class tinymce.undo.Fragments
+ * @private
+ */
+define("tinymce/undo/Fragments", [
+ "tinymce/util/Arr",
+ "tinymce/html/Entities",
+ "tinymce/undo/Diff"
+], function (Arr, Entities, Diff) {
+ var getOuterHtml = function (elm) {
+ if (elm.nodeType === 1) {
+ return elm.outerHTML;
+ } else if (elm.nodeType === 3) {
+ return Entities.encodeRaw(elm.data, false);
+ } else if (elm.nodeType === 8) {
+ return '<!--' + elm.data + '-->';
+ }
+
+ return '';
+ };
+
+ var createFragment = function(html) {
+ var frag, node, container;
+
+ container = document.createElement("div");
+ frag = document.createDocumentFragment();
+
+ if (html) {
+ container.innerHTML = html;
+ }
+
+ while ((node = container.firstChild)) {
+ frag.appendChild(node);
+ }
+
+ return frag;
+ };
+
+ var insertAt = function (elm, html, index) {
+ var fragment = createFragment(html);
+ if (elm.hasChildNodes() && index < elm.childNodes.length) {
+ var target = elm.childNodes[index];
+ target.parentNode.insertBefore(fragment, target);
+ } else {
+ elm.appendChild(fragment);
+ }
+ };
+
+ var removeAt = function (elm, index) {
+ if (elm.hasChildNodes() && index < elm.childNodes.length) {
+ var target = elm.childNodes[index];
+ target.parentNode.removeChild(target);
+ }
+ };
+
+ var applyDiff = function (diff, elm) {
+ var index = 0;
+ Arr.each(diff, function (action) {
+ if (action[0] === Diff.KEEP) {
+ index++;
+ } else if (action[0] === Diff.INSERT) {
+ insertAt(elm, action[1], index);
+ index++;
+ } else if (action[0] === Diff.DELETE) {
+ removeAt(elm, index);
+ }
+ });
+ };
+
+ var read = function (elm) {
+ return Arr.map(elm.childNodes, getOuterHtml);
+ };
+
+ var write = function (fragments, elm) {
+ var currentFragments = Arr.map(elm.childNodes, getOuterHtml);
+ applyDiff(Diff.diff(currentFragments, fragments), elm);
+ return elm;
+ };
+
+ return {
+ read: read,
+ write: write
+ };
+});
+
+// Included from: js/tinymce/classes/undo/Levels.js
+
+/**
+ * Levels.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module handles getting/setting undo levels to/from editor instances.
+ *
+ * @class tinymce.undo.Levels
+ * @private
+ */
+define("tinymce/undo/Levels", [
+ "tinymce/util/Arr",
+ "tinymce/undo/Fragments"
+], function (Arr, Fragments) {
+ var hasIframes = function (html) {
+ return html.indexOf('</iframe>') !== -1;
+ };
+
+ var createFragmentedLevel = function (fragments) {
+ return {
+ type: 'fragmented',
+ fragments: fragments,
+ content: '',
+ bookmark: null,
+ beforeBookmark: null
+ };
+ };
+
+ var createCompleteLevel = function (content) {
+ return {
+ type: 'complete',
+ fragments: null,
+ content: content,
+ bookmark: null,
+ beforeBookmark: null
+ };
+ };
+
+ var createFromEditor = function (editor) {
+ var fragments, content;
+
+ fragments = Fragments.read(editor.getBody());
+ content = Arr.map(fragments, function (html) {
+ return editor.serializer.trimContent(html);
+ }).join('');
+
+ return hasIframes(content) ? createFragmentedLevel(fragments) : createCompleteLevel(content);
+ };
+
+ var applyToEditor = function (editor, level, before) {
+ if (level.type === 'fragmented') {
+ Fragments.write(level.fragments, editor.getBody());
+ } else {
+ editor.setContent(level.content, {format: 'raw'});
+ }
+
+ editor.selection.moveToBookmark(before ? level.beforeBookmark : level.bookmark);
+ };
+
+ var getLevelContent = function (level) {
+ return level.type === 'fragmented' ? level.fragments.join('') : level.content;
+ };
+
+ var isEq = function (level1, level2) {
+ return getLevelContent(level1) === getLevelContent(level2);
+ };
+
+ return {
+ createFragmentedLevel: createFragmentedLevel,
+ createCompleteLevel: createCompleteLevel,
+ createFromEditor: createFromEditor,
+ applyToEditor: applyToEditor,
+ isEq: isEq
+ };
+});
+
+// Included from: js/tinymce/classes/UndoManager.js
+
+/**
+ * UndoManager.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class handles the undo/redo history levels for the editor. Since the built-in undo/redo has major drawbacks a custom one was needed.
+ *
+ * @class tinymce.UndoManager
+ */
+define("tinymce/UndoManager", [
+ "tinymce/util/VK",
+ "tinymce/util/Tools",
+ "tinymce/undo/Levels",
+ "tinymce/Env"
+], function(VK, Tools, Levels, Env) {
+ return function(editor) {
+ var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0;
+
+ function setDirty(state) {
+ editor.setDirty(state);
+ }
+
+ function addNonTypingUndoLevel(e) {
+ self.typing = false;
+ self.add({}, e);
+ }
+
+ function endTyping() {
+ if (self.typing) {
+ self.typing = false;
+ self.add();
+ }
+ }
+
+ // Add initial undo level when the editor is initialized
+ editor.on('init', function() {
+ self.add();
+ });
+
+ // Get position before an execCommand is processed
+ editor.on('BeforeExecCommand', function(e) {
+ var cmd = e.command;
+
+ if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') {
+ endTyping();
+ self.beforeChange();
+ }
+ });
+
+ // Add undo level after an execCommand call was made
+ editor.on('ExecCommand', function(e) {
+ var cmd = e.command;
+
+ if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') {
+ addNonTypingUndoLevel(e);
+ }
+ });
+
+ editor.on('ObjectResizeStart Cut', function() {
+ self.beforeChange();
+ });
+
+ editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel);
+ editor.on('DragEnd', addNonTypingUndoLevel);
+
+ editor.on('KeyUp', function(e) {
+ var keyCode = e.keyCode;
+
+ // If key is prevented then don't add undo level
+ // This would happen on keyboard shortcuts for example
+ if (e.isDefaultPrevented()) {
+ return;
+ }
+
+ if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45 || e.ctrlKey) {
+ addNonTypingUndoLevel();
+ editor.nodeChanged();
+ }
+
+ if (keyCode === 46 || keyCode === 8 || (Env.mac && (keyCode === 91 || keyCode === 93))) {
+ editor.nodeChanged();
+ }
+
+ // Fire a TypingUndo event on the first character entered
+ if (isFirstTypedCharacter && self.typing) {
+ // Make it dirty if the content was changed after typing the first character
+ if (!editor.isDirty()) {
+ setDirty(data[0] && !Levels.isEq(Levels.createFromEditor(editor), data[0]));
+
+ // Fire initial change event
+ if (editor.isDirty()) {
+ editor.fire('change', {level: data[0], lastLevel: null});
+ }
+ }
+
+ editor.fire('TypingUndo');
+ isFirstTypedCharacter = false;
+ editor.nodeChanged();
+ }
+ });
+
+ editor.on('KeyDown', function(e) {
+ var keyCode = e.keyCode;
+
+ // If key is prevented then don't add undo level
+ // This would happen on keyboard shortcuts for example
+ if (e.isDefaultPrevented()) {
+ return;
+ }
+
+ // Is character position keys left,right,up,down,home,end,pgdown,pgup,enter
+ if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45) {
+ if (self.typing) {
+ addNonTypingUndoLevel(e);
+ }
+
+ return;
+ }
+
+ // If key isn't Ctrl+Alt/AltGr
+ var modKey = (e.ctrlKey && !e.altKey) || e.metaKey;
+ if ((keyCode < 16 || keyCode > 20) && keyCode !== 224 && keyCode !== 91 && !self.typing && !modKey) {
+ self.beforeChange();
+ self.typing = true;
+ self.add({}, e);
+ isFirstTypedCharacter = true;
+ }
+ });
+
+ editor.on('MouseDown', function(e) {
+ if (self.typing) {
+ addNonTypingUndoLevel(e);
+ }
+ });
+
+ // Add keyboard shortcuts for undo/redo keys
+ editor.addShortcut('meta+z', '', 'Undo');
+ editor.addShortcut('meta+y,meta+shift+z', '', 'Redo');
+
+ editor.on('AddUndo Undo Redo ClearUndos', function(e) {
+ if (!e.isDefaultPrevented()) {
+ editor.nodeChanged();
+ }
+ });
+
+ /*eslint consistent-this:0 */
+ self = {
+ // Explode for debugging reasons
+ data: data,
+
+ /**
+ * State if the user is currently typing or not. This will add a typing operation into one undo
+ * level instead of one new level for each keystroke.
+ *
+ * @field {Boolean} typing
+ */
+ typing: false,
+
+ /**
+ * Stores away a bookmark to be used when performing an undo action so that the selection is before
+ * the change has been made.
+ *
+ * @method beforeChange
+ */
+ beforeChange: function() {
+ if (!locks) {
+ beforeBookmark = editor.selection.getBookmark(2, true);
+ }
+ },
+
+ /**
+ * Adds a new undo level/snapshot to the undo list.
+ *
+ * @method add
+ * @param {Object} level Optional undo level object to add.
+ * @param {DOMEvent} event Optional event responsible for the creation of the undo level.
+ * @return {Object} Undo level that got added or null it a level wasn't needed.
+ */
+ add: function(level, event) {
+ var i, settings = editor.settings, lastLevel, currentLevel;
+
+ currentLevel = Levels.createFromEditor(editor);
+ level = level || {};
+ level = Tools.extend(level, currentLevel);
+
+ if (locks || editor.removed) {
+ return null;
+ }
+
+ lastLevel = data[index];
+ if (editor.fire('BeforeAddUndo', {level: level, lastLevel: lastLevel, originalEvent: event}).isDefaultPrevented()) {
+ return null;
+ }
+
+ // Add undo level if needed
+ if (lastLevel && Levels.isEq(lastLevel, level)) {
+ return null;
+ }
+
+ // Set before bookmark on previous level
+ if (data[index]) {
+ data[index].beforeBookmark = beforeBookmark;
+ }
+
+ // Time to compress
+ if (settings.custom_undo_redo_levels) {
+ if (data.length > settings.custom_undo_redo_levels) {
+ for (i = 0; i < data.length - 1; i++) {
+ data[i] = data[i + 1];
+ }
+
+ data.length--;
+ index = data.length;
+ }
+ }
+
+ // Get a non intrusive normalized bookmark
+ level.bookmark = editor.selection.getBookmark(2, true);
+
+ // Crop array if needed
+ if (index < data.length - 1) {
+ data.length = index + 1;
+ }
+
+ data.push(level);
+ index = data.length - 1;
+
+ var args = {level: level, lastLevel: lastLevel, originalEvent: event};
+
+ editor.fire('AddUndo', args);
+
+ if (index > 0) {
+ setDirty(true);
+ editor.fire('change', args);
+ }
+
+ return level;
+ },
+
+ /**
+ * Undoes the last action.
+ *
+ * @method undo
+ * @return {Object} Undo level or null if no undo was performed.
+ */
+ undo: function() {
+ var level;
+
+ if (self.typing) {
+ self.add();
+ self.typing = false;
+ }
+
+ if (index > 0) {
+ level = data[--index];
+ Levels.applyToEditor(editor, level, true);
+ setDirty(true);
+ editor.fire('undo', {level: level});
+ }
+
+ return level;
+ },
+
+ /**
+ * Redoes the last action.
+ *
+ * @method redo
+ * @return {Object} Redo level or null if no redo was performed.
+ */
+ redo: function() {
+ var level;
+
+ if (index < data.length - 1) {
+ level = data[++index];
+ Levels.applyToEditor(editor, level, false);
+ setDirty(true);
+ editor.fire('redo', {level: level});
+ }
+
+ return level;
+ },
+
+ /**
+ * Removes all undo levels.
+ *
+ * @method clear
+ */
+ clear: function() {
+ data = [];
+ index = 0;
+ self.typing = false;
+ self.data = data;
+ editor.fire('ClearUndos');
+ },
+
+ /**
+ * Returns true/false if the undo manager has any undo levels.
+ *
+ * @method hasUndo
+ * @return {Boolean} true/false if the undo manager has any undo levels.
+ */
+ hasUndo: function() {
+ // Has undo levels or typing and content isn't the same as the initial level
+ return index > 0 || (self.typing && data[0] && !Levels.isEq(Levels.createFromEditor(editor), data[0]));
+ },
+
+ /**
+ * Returns true/false if the undo manager has any redo levels.
+ *
+ * @method hasRedo
+ * @return {Boolean} true/false if the undo manager has any redo levels.
+ */
+ hasRedo: function() {
+ return index < data.length - 1 && !self.typing;
+ },
+
+ /**
+ * Executes the specified mutator function as an undo transaction. The selection
+ * before the modification will be stored to the undo stack and if the DOM changes
+ * it will add a new undo level. Any methods within the translation that adds undo levels will
+ * be ignored. So a translation can include calls to execCommand or editor.insertContent.
+ *
+ * @method transact
+ * @param {function} callback Function that gets executed and has dom manipulation logic in it.
+ * @return {Object} Undo level that got added or null it a level wasn't needed.
+ */
+ transact: function(callback) {
+ endTyping();
+ self.beforeChange();
+
+ try {
+ locks++;
+ callback();
+ } finally {
+ locks--;
+ }
+
+ return self.add();
+ },
+
+ /**
+ * Adds an extra "hidden" undo level by first applying the first mutation and store that to the undo stack
+ * then roll back that change and do the second mutation on top of the stack. This will produce an extra
+ * undo level that the user doesn't see until they undo.
+ *
+ * @method extra
+ * @param {function} callback1 Function that does mutation but gets stored as a "hidden" extra undo level.
+ * @param {function} callback2 Function that does mutation but gets displayed to the user.
+ */
+ extra: function (callback1, callback2) {
+ var lastLevel, bookmark;
+
+ if (self.transact(callback1)) {
+ bookmark = data[index].bookmark;
+ lastLevel = data[index - 1];
+ Levels.applyToEditor(editor, lastLevel, true);
+
+ if (self.transact(callback2)) {
+ data[index - 1].beforeBookmark = bookmark;
+ }
+ }
+ }
+ };
+
+ return self;
+ };
+});
+
+// Included from: js/tinymce/classes/EnterKey.js
+
+/**
+ * EnterKey.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Contains logic for handling the enter key to split/generate block elements.
+ *
+ * @private
+ * @class tinymce.EnterKey
+ */
+define("tinymce/EnterKey", [
+ "tinymce/dom/TreeWalker",
+ "tinymce/dom/RangeUtils",
+ "tinymce/caret/CaretContainer",
+ "tinymce/Env"
+], function(TreeWalker, RangeUtils, CaretContainer, Env) {
+ var isIE = Env.ie && Env.ie < 11;
+
+ return function(editor) {
+ var dom = editor.dom, selection = editor.selection, settings = editor.settings;
+ var undoManager = editor.undoManager, schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(),
+ moveCaretBeforeOnEnterElementsMap = schema.getMoveCaretBeforeOnEnterElements();
+
+ function handleEnterKey(evt) {
+ var rng, tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey,
+ newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer;
+
+ // Returns true if the block can be split into two blocks or not
+ function canSplitBlock(node) {
+ return node &&
+ dom.isBlock(node) &&
+ !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) &&
+ !/^(fixed|absolute)/i.test(node.style.position) &&
+ dom.getContentEditable(node) !== "true";
+ }
+
+ function isTableCell(node) {
+ return node && /^(TD|TH|CAPTION)$/.test(node.nodeName);
+ }
+
+ // Renders empty block on IE
+ function renderBlockOnIE(block) {
+ var oldRng;
+
+ if (dom.isBlock(block)) {
+ oldRng = selection.getRng();
+ block.appendChild(dom.create('span', null, '\u00a0'));
+ selection.select(block);
+ block.lastChild.outerHTML = '';
+ selection.setRng(oldRng);
+ }
+ }
+
+ // Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p>
+ function trimInlineElementsOnLeftSideOfBlock(block) {
+ var node = block, firstChilds = [], i;
+
+ if (!node) {
+ return;
+ }
+
+ // Find inner most first child ex: <p><i><b>*</b></i></p>
+ while ((node = node.firstChild)) {
+ if (dom.isBlock(node)) {
+ return;
+ }
+
+ if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
+ firstChilds.push(node);
+ }
+ }
+
+ i = firstChilds.length;
+ while (i--) {
+ node = firstChilds[i];
+ if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) {
+ dom.remove(node);
+ } else {
+ // Remove <a> </a> see #5381
+ if (node.nodeName == "A" && (node.innerText || node.textContent) === ' ') {
+ dom.remove(node);
+ }
+ }
+ }
+ }
+
+ // Moves the caret to a suitable position within the root for example in the first non
+ // pure whitespace text node or before an image
+ function moveToCaretPosition(root) {
+ var walker, node, rng, lastNode = root, tempElm;
+ function firstNonWhiteSpaceNodeSibling(node) {
+ while (node) {
+ if (node.nodeType == 1 || (node.nodeType == 3 && node.data && /[\r\n\s]/.test(node.data))) {
+ return node;
+ }
+
+ node = node.nextSibling;
+ }
+ }
+
+ if (!root) {
+ return;
+ }
+
+ // Old IE versions doesn't properly render blocks with br elements in them
+ // For example <p><br></p> wont be rendered correctly in a contentEditable area
+ // until you remove the br producing <p></p>
+ if (Env.ie && Env.ie < 9 && parentBlock && parentBlock.firstChild) {
+ if (parentBlock.firstChild == parentBlock.lastChild && parentBlock.firstChild.tagName == 'BR') {
+ dom.remove(parentBlock.firstChild);
+ }
+ }
+
+ if (/^(LI|DT|DD)$/.test(root.nodeName)) {
+ var firstChild = firstNonWhiteSpaceNodeSibling(root.firstChild);
+
+ if (firstChild && /^(UL|OL|DL)$/.test(firstChild.nodeName)) {
+ root.insertBefore(dom.doc.createTextNode('\u00a0'), root.firstChild);
+ }
+ }
+
+ rng = dom.createRng();
+
+ // Normalize whitespace to remove empty text nodes. Fix for: #6904
+ // Gecko will be able to place the caret in empty text nodes but it won't render propery
+ // Older IE versions will sometimes crash so for now ignore all IE versions
+ if (!Env.ie) {
+ root.normalize();
+ }
+
+ if (root.hasChildNodes()) {
+ walker = new TreeWalker(root, root);
+
+ while ((node = walker.current())) {
+ if (node.nodeType == 3) {
+ rng.setStart(node, 0);
+ rng.setEnd(node, 0);
+ break;
+ }
+
+ if (moveCaretBeforeOnEnterElementsMap[node.nodeName.toLowerCase()]) {
+ rng.setStartBefore(node);
+ rng.setEndBefore(node);
+ break;
+ }
+
+ lastNode = node;
+ node = walker.next();
+ }
+
+ if (!node) {
+ rng.setStart(lastNode, 0);
+ rng.setEnd(lastNode, 0);
+ }
+ } else {
+ if (root.nodeName == 'BR') {
+ if (root.nextSibling && dom.isBlock(root.nextSibling)) {
+ // Trick on older IE versions to render the caret before the BR between two lists
+ if (!documentMode || documentMode < 9) {
+ tempElm = dom.create('br');
+ root.parentNode.insertBefore(tempElm, root);
+ }
+
+ rng.setStartBefore(root);
+ rng.setEndBefore(root);
+ } else {
+ rng.setStartAfter(root);
+ rng.setEndAfter(root);
+ }
+ } else {
+ rng.setStart(root, 0);
+ rng.setEnd(root, 0);
+ }
+ }
+
+ selection.setRng(rng);
+
+ // Remove tempElm created for old IE:s
+ dom.remove(tempElm);
+ selection.scrollIntoView(root);
+ }
+
+ function setForcedBlockAttrs(node) {
+ var forcedRootBlockName = settings.forced_root_block;
+
+ if (forcedRootBlockName && forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) {
+ dom.setAttribs(node, settings.forced_root_block_attrs);
+ }
+ }
+
+ function emptyBlock(elm) {
+ // BR is needed in empty blocks on non IE browsers
+ elm.innerHTML = !isIE ? '<br data-mce-bogus="1">' : '';
+ }
+
+ // Creates a new block element by cloning the current one or creating a new one if the name is specified
+ // This function will also copy any text formatting from the parent block and add it to the new one
+ function createNewBlock(name) {
+ var node = container, block, clonedNode, caretNode, textInlineElements = schema.getTextInlineElements();
+
+ if (name || parentBlockName == "TABLE") {
+ block = dom.create(name || newBlockName);
+ setForcedBlockAttrs(block);
+ } else {
+ block = parentBlock.cloneNode(false);
+ }
+
+ caretNode = block;
+
+ // Clone any parent styles
+ if (settings.keep_styles !== false) {
+ do {
+ if (textInlineElements[node.nodeName]) {
+ // Never clone a caret containers
+ if (node.id == '_mce_caret') {
+ continue;
+ }
+
+ clonedNode = node.cloneNode(false);
+ dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique
+
+ if (block.hasChildNodes()) {
+ clonedNode.appendChild(block.firstChild);
+ block.appendChild(clonedNode);
+ } else {
+ caretNode = clonedNode;
+ block.appendChild(clonedNode);
+ }
+ }
+ } while ((node = node.parentNode) && node != editableRoot);
+ }
+
+ // BR is needed in empty blocks on non IE browsers
+ if (!isIE) {
+ caretNode.innerHTML = '<br data-mce-bogus="1">';
+ }
+
+ return block;
+ }
+
+ // Returns true/false if the caret is at the start/end of the parent block element
+ function isCaretAtStartOrEndOfBlock(start) {
+ var walker, node, name;
+
+ // Caret is in the middle of a text node like "a|b"
+ if (container.nodeType == 3 && (start ? offset > 0 : offset < container.nodeValue.length)) {
+ return false;
+ }
+
+ // If after the last element in block node edge case for #5091
+ if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) {
+ return true;
+ }
+
+ // If the caret if before the first element in parentBlock
+ if (start && container.nodeType == 1 && container == parentBlock.firstChild) {
+ return true;
+ }
+
+ // Caret can be before/after a table
+ if (container.nodeName === "TABLE" || (container.previousSibling && container.previousSibling.nodeName == "TABLE")) {
+ return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start);
+ }
+
+ // Walk the DOM and look for text nodes or non empty elements
+ walker = new TreeWalker(container, parentBlock);
+
+ // If caret is in beginning or end of a text block then jump to the next/previous node
+ if (container.nodeType == 3) {
+ if (start && offset === 0) {
+ walker.prev();
+ } else if (!start && offset == container.nodeValue.length) {
+ walker.next();
+ }
+ }
+
+ while ((node = walker.current())) {
+ if (node.nodeType === 1) {
+ // Ignore bogus elements
+ if (!node.getAttribute('data-mce-bogus')) {
+ // Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p>
+ name = node.nodeName.toLowerCase();
+ if (nonEmptyElementsMap[name] && name !== 'br') {
+ return false;
+ }
+ }
+ } else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) {
+ return false;
+ }
+
+ if (start) {
+ walker.prev();
+ } else {
+ walker.next();
+ }
+ }
+
+ return true;
+ }
+
+ // Wraps any text nodes or inline elements in the specified forced root block name
+ function wrapSelfAndSiblingsInDefaultBlock(container, offset) {
+ var newBlock, parentBlock, startNode, node, next, rootBlockName, blockName = newBlockName || 'P';
+
+ // Not in a block element or in a table cell or caption
+ parentBlock = dom.getParent(container, dom.isBlock);
+ if (!parentBlock || !canSplitBlock(parentBlock)) {
+ parentBlock = parentBlock || editableRoot;
+
+ if (parentBlock == editor.getBody() || isTableCell(parentBlock)) {
+ rootBlockName = parentBlock.nodeName.toLowerCase();
+ } else {
+ rootBlockName = parentBlock.parentNode.nodeName.toLowerCase();
+ }
+
+ if (!parentBlock.hasChildNodes()) {
+ newBlock = dom.create(blockName);
+ setForcedBlockAttrs(newBlock);
+ parentBlock.appendChild(newBlock);
+ rng.setStart(newBlock, 0);
+ rng.setEnd(newBlock, 0);
+ return newBlock;
+ }
+
+ // Find parent that is the first child of parentBlock
+ node = container;
+ while (node.parentNode != parentBlock) {
+ node = node.parentNode;
+ }
+
+ // Loop left to find start node start wrapping at
+ while (node && !dom.isBlock(node)) {
+ startNode = node;
+ node = node.previousSibling;
+ }
+
+ if (startNode && schema.isValidChild(rootBlockName, blockName.toLowerCase())) {
+ newBlock = dom.create(blockName);
+ setForcedBlockAttrs(newBlock);
+ startNode.parentNode.insertBefore(newBlock, startNode);
+
+ // Start wrapping until we hit a block
+ node = startNode;
+ while (node && !dom.isBlock(node)) {
+ next = node.nextSibling;
+ newBlock.appendChild(node);
+ node = next;
+ }
+
+ // Restore range to it's past location
+ rng.setStart(container, offset);
+ rng.setEnd(container, offset);
+ }
+ }
+
+ return container;
+ }
+
+ // Inserts a block or br before/after or in the middle of a split list of the LI is empty
+ function handleEmptyListItem() {
+ function isFirstOrLastLi(first) {
+ var node = containerBlock[first ? 'firstChild' : 'lastChild'];
+
+ // Find first/last element since there might be whitespace there
+ while (node) {
+ if (node.nodeType == 1) {
+ break;
+ }
+
+ node = node[first ? 'nextSibling' : 'previousSibling'];
+ }
+
+ return node === parentBlock;
+ }
+
+ function getContainerBlock() {
+ var containerBlockParent = containerBlock.parentNode;
+
+ if (/^(LI|DT|DD)$/.test(containerBlockParent.nodeName)) {
+ return containerBlockParent;
+ }
+
+ return containerBlock;
+ }
+
+ if (containerBlock == editor.getBody()) {
+ return;
+ }
+
+ // Check if we are in an nested list
+ var containerBlockParentName = containerBlock.parentNode.nodeName;
+ if (/^(OL|UL|LI)$/.test(containerBlockParentName)) {
+ newBlockName = 'LI';
+ }
+
+ newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR');
+
+ if (isFirstOrLastLi(true) && isFirstOrLastLi()) {
+ if (containerBlockParentName == 'LI') {
+ // Nested list is inside a LI
+ dom.insertAfter(newBlock, getContainerBlock());
+ } else {
+ // Is first and last list item then replace the OL/UL with a text block
+ dom.replace(newBlock, containerBlock);
+ }
+ } else if (isFirstOrLastLi(true)) {
+ if (containerBlockParentName == 'LI') {
+ // List nested in an LI then move the list to a new sibling LI
+ dom.insertAfter(newBlock, getContainerBlock());
+ newBlock.appendChild(dom.doc.createTextNode(' ')); // Needed for IE so the caret can be placed
+ newBlock.appendChild(containerBlock);
+ } else {
+ // First LI in list then remove LI and add text block before list
+ containerBlock.parentNode.insertBefore(newBlock, containerBlock);
+ }
+ } else if (isFirstOrLastLi()) {
+ // Last LI in list then remove LI and add text block after list
+ dom.insertAfter(newBlock, getContainerBlock());
+ renderBlockOnIE(newBlock);
+ } else {
+ // Middle LI in list the split the list and insert a text block in the middle
+ // Extract after fragment and insert it after the current block
+ containerBlock = getContainerBlock();
+ tmpRng = rng.cloneRange();
+ tmpRng.setStartAfter(parentBlock);
+ tmpRng.setEndAfter(containerBlock);
+ fragment = tmpRng.extractContents();
+
+ if (newBlockName == 'LI' && fragment.firstChild.nodeName == 'LI') {
+ newBlock = fragment.firstChild;
+ dom.insertAfter(fragment, containerBlock);
+ } else {
+ dom.insertAfter(fragment, containerBlock);
+ dom.insertAfter(newBlock, containerBlock);
+ }
+ }
+
+ dom.remove(parentBlock);
+ moveToCaretPosition(newBlock);
+ undoManager.add();
+ }
+
+ // Inserts a BR element if the forced_root_block option is set to false or empty string
+ function insertBr() {
+ editor.execCommand("InsertLineBreak", false, evt);
+ }
+
+ // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element
+ function trimLeadingLineBreaks(node) {
+ do {
+ if (node.nodeType === 3) {
+ node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, '');
+ }
+
+ node = node.firstChild;
+ } while (node);
+ }
+
+ function getEditableRoot(node) {
+ var root = dom.getRoot(), parent, editableRoot;
+
+ // Get all parents until we hit a non editable parent or the root
+ parent = node;
+ while (parent !== root && dom.getContentEditable(parent) !== "false") {
+ if (dom.getContentEditable(parent) === "true") {
+ editableRoot = parent;
+ }
+
+ parent = parent.parentNode;
+ }
+
+ return parent !== root ? editableRoot : root;
+ }
+
+ // Adds a BR at the end of blocks that only contains an IMG or INPUT since
+ // these might be floated and then they won't expand the block
+ function addBrToBlockIfNeeded(block) {
+ var lastChild;
+
+ // IE will render the blocks correctly other browsers needs a BR
+ if (!isIE) {
+ block.normalize(); // Remove empty text nodes that got left behind by the extract
+
+ // Check if the block is empty or contains a floated last child
+ lastChild = block.lastChild;
+ if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) {
+ dom.add(block, 'br');
+ }
+ }
+ }
+
+ function insertNewBlockAfter() {
+ // If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup
+ if (/^(H[1-6]|PRE|FIGURE)$/.test(parentBlockName) && containerBlockName != 'HGROUP') {
+ newBlock = createNewBlock(newBlockName);
+ } else {
+ newBlock = createNewBlock();
+ }
+
+ // Split the current container block element if enter is pressed inside an empty inner block element
+ if (settings.end_container_on_empty_block && canSplitBlock(containerBlock) && dom.isEmpty(parentBlock)) {
+ // Split container block for example a BLOCKQUOTE at the current blockParent location for example a P
+ newBlock = dom.split(containerBlock, parentBlock);
+ } else {
+ dom.insertAfter(newBlock, parentBlock);
+ }
+
+ moveToCaretPosition(newBlock);
+ }
+
+ rng = selection.getRng(true);
+
+ // Event is blocked by some other handler for example the lists plugin
+ if (evt.isDefaultPrevented()) {
+ return;
+ }
+
+ // Delete any selected contents
+ if (!rng.collapsed) {
+ editor.execCommand('Delete');
+ return;
+ }
+
+ // Setup range items and newBlockName
+ new RangeUtils(dom).normalize(rng);
+ container = rng.startContainer;
+ offset = rng.startOffset;
+ newBlockName = (settings.force_p_newlines ? 'p' : '') || settings.forced_root_block;
+ newBlockName = newBlockName ? newBlockName.toUpperCase() : '';
+ documentMode = dom.doc.documentMode;
+ shiftKey = evt.shiftKey;
+
+ // Resolve node index
+ if (container.nodeType == 1 && container.hasChildNodes()) {
+ isAfterLastNodeInContainer = offset > container.childNodes.length - 1;
+
+ container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
+ if (isAfterLastNodeInContainer && container.nodeType == 3) {
+ offset = container.nodeValue.length;
+ } else {
+ offset = 0;
+ }
+ }
+
+ // Get editable root node, normally the body element but sometimes a div or span
+ editableRoot = getEditableRoot(container);
+
+ // If there is no editable root then enter is done inside a contentEditable false element
+ if (!editableRoot) {
+ return;
+ }
+
+ undoManager.beforeChange();
+
+ // If editable root isn't block nor the root of the editor
+ if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) {
+ if (!newBlockName || shiftKey) {
+ insertBr();
+ }
+
+ return;
+ }
+
+ // Wrap the current node and it's sibling in a default block if it's needed.
+ // for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td>
+ // This won't happen if root blocks are disabled or the shiftKey is pressed
+ if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) {
+ container = wrapSelfAndSiblingsInDefaultBlock(container, offset);
+ }
+
+ // Find parent block and setup empty block paddings
+ parentBlock = dom.getParent(container, dom.isBlock);
+ containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null;
+
+ // Setup block names
+ parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
+ containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
+
+ // Enter inside block contained within a LI then split or insert before/after LI
+ if (containerBlockName == 'LI' && !evt.ctrlKey) {
+ parentBlock = containerBlock;
+ parentBlockName = containerBlockName;
+ }
+
+ if (editor.undoManager.typing) {
+ editor.undoManager.typing = false;
+ editor.undoManager.add();
+ }
+
+ // Handle enter in list item
+ if (/^(LI|DT|DD)$/.test(parentBlockName)) {
+ if (!newBlockName && shiftKey) {
+ insertBr();
+ return;
+ }
+
+ // Handle enter inside an empty list item
+ if (dom.isEmpty(parentBlock)) {
+ handleEmptyListItem();
+ return;
+ }
+ }
+
+ // Don't split PRE tags but insert a BR instead easier when writing code samples etc
+ if (parentBlockName == 'PRE' && settings.br_in_pre !== false) {
+ if (!shiftKey) {
+ insertBr();
+ return;
+ }
+ } else {
+ // If no root block is configured then insert a BR by default or if the shiftKey is pressed
+ if ((!newBlockName && !shiftKey && parentBlockName != 'LI') || (newBlockName && shiftKey)) {
+ insertBr();
+ return;
+ }
+ }
+
+ // If parent block is root then never insert new blocks
+ if (newBlockName && parentBlock === editor.getBody()) {
+ return;
+ }
+
+ // Default block name if it's not configured
+ newBlockName = newBlockName || 'P';
+
+ // Insert new block before/after the parent block depending on caret location
+ if (CaretContainer.isCaretContainerBlock(parentBlock)) {
+ newBlock = CaretContainer.showCaretContainerBlock(parentBlock);
+ } else if (isCaretAtStartOrEndOfBlock()) {
+ insertNewBlockAfter();
+ } else if (isCaretAtStartOrEndOfBlock(true)) {
+ // Insert new block before
+ newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock);
+ renderBlockOnIE(newBlock);
+ moveToCaretPosition(parentBlock);
+ } else {
+ // Extract after fragment and insert it after the current block
+ tmpRng = rng.cloneRange();
+ tmpRng.setEndAfter(parentBlock);
+ fragment = tmpRng.extractContents();
+ trimLeadingLineBreaks(fragment);
+ newBlock = fragment.firstChild;
+ dom.insertAfter(fragment, parentBlock);
+ trimInlineElementsOnLeftSideOfBlock(newBlock);
+ addBrToBlockIfNeeded(parentBlock);
+
+ if (dom.isEmpty(parentBlock)) {
+ emptyBlock(parentBlock);
+ }
+
+ newBlock.normalize();
+
+ // New block might become empty if it's <p><b>a |</b></p>
+ if (dom.isEmpty(newBlock)) {
+ dom.remove(newBlock);
+ insertNewBlockAfter();
+ } else {
+ moveToCaretPosition(newBlock);
+ }
+ }
+
+ dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique
+
+ // Allow custom handling of new blocks
+ editor.fire('NewBlock', {newBlock: newBlock});
+
+ undoManager.typing = false;
+ undoManager.add();
+ }
+
+ editor.on('keydown', function(evt) {
+ if (evt.keyCode == 13) {
+ if (handleEnterKey(evt) !== false) {
+ evt.preventDefault();
+ }
+ }
+ });
+ };
+});
+
+// Included from: js/tinymce/classes/ForceBlocks.js
+
+/**
+ * ForceBlocks.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Makes sure that everything gets wrapped in paragraphs.
+ *
+ * @private
+ * @class tinymce.ForceBlocks
+ */
+define("tinymce/ForceBlocks", [], function() {
+ return function(editor) {
+ var settings = editor.settings, dom = editor.dom, selection = editor.selection;
+ var schema = editor.schema, blockElements = schema.getBlockElements();
+
+ function addRootBlocks() {
+ var node = selection.getStart(), rootNode = editor.getBody(), rng;
+ var startContainer, startOffset, endContainer, endOffset, rootBlockNode;
+ var tempNode, offset = -0xFFFFFF, wrapped, restoreSelection;
+ var tmpRng, rootNodeName, forcedRootBlock;
+
+ forcedRootBlock = settings.forced_root_block;
+
+ if (!node || node.nodeType !== 1 || !forcedRootBlock) {
+ return;
+ }
+
+ // Check if node is wrapped in block
+ while (node && node != rootNode) {
+ if (blockElements[node.nodeName]) {
+ return;
+ }
+
+ node = node.parentNode;
+ }
+
+ // Get current selection
+ rng = selection.getRng();
+ if (rng.setStart) {
+ startContainer = rng.startContainer;
+ startOffset = rng.startOffset;
+ endContainer = rng.endContainer;
+ endOffset = rng.endOffset;
+
+ try {
+ restoreSelection = editor.getDoc().activeElement === rootNode;
+ } catch (ex) {
+ // IE throws unspecified error here sometimes
+ }
+ } else {
+ // Force control range into text range
+ if (rng.item) {
+ node = rng.item(0);
+ rng = editor.getDoc().body.createTextRange();
+ rng.moveToElementText(node);
+ }
+
+ restoreSelection = rng.parentElement().ownerDocument === editor.getDoc();
+ tmpRng = rng.duplicate();
+ tmpRng.collapse(true);
+ startOffset = tmpRng.move('character', offset) * -1;
+
+ if (!tmpRng.collapsed) {
+ tmpRng = rng.duplicate();
+ tmpRng.collapse(false);
+ endOffset = (tmpRng.move('character', offset) * -1) - startOffset;
+ }
+ }
+
+ // Wrap non block elements and text nodes
+ node = rootNode.firstChild;
+ rootNodeName = rootNode.nodeName.toLowerCase();
+ while (node) {
+ // TODO: Break this up, too complex
+ if (((node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName]))) &&
+ schema.isValidChild(rootNodeName, forcedRootBlock.toLowerCase())) {
+ // Remove empty text nodes
+ if (node.nodeType === 3 && node.nodeValue.length === 0) {
+ tempNode = node;
+ node = node.nextSibling;
+ dom.remove(tempNode);
+ continue;
+ }
+
+ if (!rootBlockNode) {
+ rootBlockNode = dom.create(forcedRootBlock, editor.settings.forced_root_block_attrs);
+ node.parentNode.insertBefore(rootBlockNode, node);
+ wrapped = true;
+ }
+
+ tempNode = node;
+ node = node.nextSibling;
+ rootBlockNode.appendChild(tempNode);
+ } else {
+ rootBlockNode = null;
+ node = node.nextSibling;
+ }
+ }
+
+ if (wrapped && restoreSelection) {
+ if (rng.setStart) {
+ rng.setStart(startContainer, startOffset);
+ rng.setEnd(endContainer, endOffset);
+ selection.setRng(rng);
+ } else {
+ // Only select if the previous selection was inside the document to prevent auto focus in quirks mode
+ try {
+ rng = editor.getDoc().body.createTextRange();
+ rng.moveToElementText(rootNode);
+ rng.collapse(true);
+ rng.moveStart('character', startOffset);
+
+ if (endOffset > 0) {
+ rng.moveEnd('character', endOffset);
+ }
+
+ rng.select();
+ } catch (ex) {
+ // Ignore
+ }
+ }
+
+ editor.nodeChanged();
+ }
+ }
+
+ // Force root blocks
+ if (settings.forced_root_block) {
+ editor.on('NodeChange', addRootBlocks);
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/caret/CaretUtils.js
+
+/**
+ * CaretUtils.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Utility functions shared by the caret logic.
+ *
+ * @private
+ * @class tinymce.caret.CaretUtils
+ */
+define("tinymce/caret/CaretUtils", [
+ "tinymce/util/Fun",
+ "tinymce/dom/TreeWalker",
+ "tinymce/dom/NodeType",
+ "tinymce/caret/CaretPosition",
+ "tinymce/caret/CaretContainer",
+ "tinymce/caret/CaretCandidate"
+], function(Fun, TreeWalker, NodeType, CaretPosition, CaretContainer, CaretCandidate) {
+ var isContentEditableTrue = NodeType.isContentEditableTrue,
+ isContentEditableFalse = NodeType.isContentEditableFalse,
+ isBlockLike = NodeType.matchStyleValues('display', 'block table table-cell table-caption'),
+ isCaretContainer = CaretContainer.isCaretContainer,
+ isCaretContainerBlock = CaretContainer.isCaretContainerBlock,
+ curry = Fun.curry,
+ isElement = NodeType.isElement,
+ isCaretCandidate = CaretCandidate.isCaretCandidate;
+
+ function isForwards(direction) {
+ return direction > 0;
+ }
+
+ function isBackwards(direction) {
+ return direction < 0;
+ }
+
+ function skipCaretContainers(walk, shallow) {
+ var node;
+
+ while ((node = walk(shallow))) {
+ if (!isCaretContainerBlock(node)) {
+ return node;
+ }
+ }
+
+ return null;
+ }
+
+ function findNode(node, direction, predicateFn, rootNode, shallow) {
+ var walker = new TreeWalker(node, rootNode);
+
+ if (isBackwards(direction)) {
+ if (isContentEditableFalse(node) || isCaretContainerBlock(node)) {
+ node = skipCaretContainers(walker.prev, true);
+ if (predicateFn(node)) {
+ return node;
+ }
+ }
+
+ while ((node = skipCaretContainers(walker.prev, shallow))) {
+ if (predicateFn(node)) {
+ return node;
+ }
+ }
+ }
+
+ if (isForwards(direction)) {
+ if (isContentEditableFalse(node) || isCaretContainerBlock(node)) {
+ node = skipCaretContainers(walker.next, true);
+ if (predicateFn(node)) {
+ return node;
+ }
+ }
+
+ while ((node = skipCaretContainers(walker.next, shallow))) {
+ if (predicateFn(node)) {
+ return node;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ function getEditingHost(node, rootNode) {
+ for (node = node.parentNode; node && node != rootNode; node = node.parentNode) {
+ if (isContentEditableTrue(node)) {
+ return node;
+ }
+ }
+
+ return rootNode;
+ }
+
+ function getParentBlock(node, rootNode) {
+ while (node && node != rootNode) {
+ if (isBlockLike(node)) {
+ return node;
+ }
+
+ node = node.parentNode;
+ }
+
+ return null;
+ }
+
+ function isInSameBlock(caretPosition1, caretPosition2, rootNode) {
+ return getParentBlock(caretPosition1.container(), rootNode) == getParentBlock(caretPosition2.container(), rootNode);
+ }
+
+ function isInSameEditingHost(caretPosition1, caretPosition2, rootNode) {
+ return getEditingHost(caretPosition1.container(), rootNode) == getEditingHost(caretPosition2.container(), rootNode);
+ }
+
+ function getChildNodeAtRelativeOffset(relativeOffset, caretPosition) {
+ var container, offset;
+
+ if (!caretPosition) {
+ return null;
+ }
+
+ container = caretPosition.container();
+ offset = caretPosition.offset();
+
+ if (!isElement(container)) {
+ return null;
+ }
+
+ return container.childNodes[offset + relativeOffset];
+ }
+
+ function beforeAfter(before, node) {
+ var range = node.ownerDocument.createRange();
+
+ if (before) {
+ range.setStartBefore(node);
+ range.setEndBefore(node);
+ } else {
+ range.setStartAfter(node);
+ range.setEndAfter(node);
+ }
+
+ return range;
+ }
+
+ function isNodesInSameBlock(rootNode, node1, node2) {
+ return getParentBlock(node1, rootNode) == getParentBlock(node2, rootNode);
+ }
+
+ function lean(left, rootNode, node) {
+ var sibling, siblingName;
+
+ if (left) {
+ siblingName = 'previousSibling';
+ } else {
+ siblingName = 'nextSibling';
+ }
+
+ while (node && node != rootNode) {
+ sibling = node[siblingName];
+
+ if (isCaretContainer(sibling)) {
+ sibling = sibling[siblingName];
+ }
+
+ if (isContentEditableFalse(sibling)) {
+ if (isNodesInSameBlock(rootNode, sibling, node)) {
+ return sibling;
+ }
+
+ break;
+ }
+
+ if (isCaretCandidate(sibling)) {
+ break;
+ }
+
+ node = node.parentNode;
+ }
+
+ return null;
+ }
+
+ var before = curry(beforeAfter, true);
+ var after = curry(beforeAfter, false);
+
+ function normalizeRange(direction, rootNode, range) {
+ var node, container, offset, location;
+ var leanLeft = curry(lean, true, rootNode);
+ var leanRight = curry(lean, false, rootNode);
+
+ container = range.startContainer;
+ offset = range.startOffset;
+
+ if (CaretContainer.isCaretContainerBlock(container)) {
+ if (!isElement(container)) {
+ container = container.parentNode;
+ }
+
+ location = container.getAttribute('data-mce-caret');
+
+ if (location == 'before') {
+ node = container.nextSibling;
+ if (isContentEditableFalse(node)) {
+ return before(node);
+ }
+ }
+
+ if (location == 'after') {
+ node = container.previousSibling;
+ if (isContentEditableFalse(node)) {
+ return after(node);
+ }
+ }
+ }
+
+ if (!range.collapsed) {
+ return range;
+ }
+
+ if (NodeType.isText(container)) {
+ if (isCaretContainer(container)) {
+ if (direction === 1) {
+ node = leanRight(container);
+ if (node) {
+ return before(node);
+ }
+
+ node = leanLeft(container);
+ if (node) {
+ return after(node);
+ }
+ }
+
+ if (direction === -1) {
+ node = leanLeft(container);
+ if (node) {
+ return after(node);
+ }
+
+ node = leanRight(container);
+ if (node) {
+ return before(node);
+ }
+ }
+
+ return range;
+ }
+
+ if (CaretContainer.endsWithCaretContainer(container) && offset >= container.data.length - 1) {
+ if (direction === 1) {
+ node = leanRight(container);
+ if (node) {
+ return before(node);
+ }
+ }
+
+ return range;
+ }
+
+ if (CaretContainer.startsWithCaretContainer(container) && offset <= 1) {
+ if (direction === -1) {
+ node = leanLeft(container);
+ if (node) {
+ return after(node);
+ }
+ }
+
+ return range;
+ }
+
+ if (offset === container.data.length) {
+ node = leanRight(container);
+ if (node) {
+ return before(node);
+ }
+
+ return range;
+ }
+
+ if (offset === 0) {
+ node = leanLeft(container);
+ if (node) {
+ return after(node);
+ }
+
+ return range;
+ }
+ }
+
+ return range;
+ }
+
+ function isNextToContentEditableFalse(relativeOffset, caretPosition) {
+ return isContentEditableFalse(getChildNodeAtRelativeOffset(relativeOffset, caretPosition));
+ }
+
+ return {
+ isForwards: isForwards,
+ isBackwards: isBackwards,
+ findNode: findNode,
+ getEditingHost: getEditingHost,
+ getParentBlock: getParentBlock,
+ isInSameBlock: isInSameBlock,
+ isInSameEditingHost: isInSameEditingHost,
+ isBeforeContentEditableFalse: curry(isNextToContentEditableFalse, 0),
+ isAfterContentEditableFalse: curry(isNextToContentEditableFalse, -1),
+ normalizeRange: normalizeRange
+ };
+});
+
+// Included from: js/tinymce/classes/caret/CaretWalker.js
+
+/**
+ * CaretWalker.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module contains logic for moving around a virtual caret in logical order within a DOM element.
+ *
+ * It ignores the most obvious invalid caret locations such as within a script element or within a
+ * contentEditable=false element but it will return locations that isn't possible to render visually.
+ *
+ * @private
+ * @class tinymce.caret.CaretWalker
+ * @example
+ * var caretWalker = new CaretWalker(rootElm);
+ *
+ * var prevLogicalCaretPosition = caretWalker.prev(CaretPosition.fromRangeStart(range));
+ * var nextLogicalCaretPosition = caretWalker.next(CaretPosition.fromRangeEnd(range));
+ */
+define("tinymce/caret/CaretWalker", [
+ "tinymce/dom/NodeType",
+ "tinymce/caret/CaretCandidate",
+ "tinymce/caret/CaretPosition",
+ "tinymce/caret/CaretUtils",
+ "tinymce/util/Arr",
+ "tinymce/util/Fun"
+], function(NodeType, CaretCandidate, CaretPosition, CaretUtils, Arr, Fun) {
+ var isContentEditableFalse = NodeType.isContentEditableFalse,
+ isText = NodeType.isText,
+ isElement = NodeType.isElement,
+ isBr = NodeType.isBr,
+ isForwards = CaretUtils.isForwards,
+ isBackwards = CaretUtils.isBackwards,
+ isCaretCandidate = CaretCandidate.isCaretCandidate,
+ isAtomic = CaretCandidate.isAtomic,
+ isEditableCaretCandidate = CaretCandidate.isEditableCaretCandidate;
+
+ function getParents(node, rootNode) {
+ var parents = [];
+
+ while (node && node != rootNode) {
+ parents.push(node);
+ node = node.parentNode;
+ }
+
+ return parents;
+ }
+
+ function nodeAtIndex(container, offset) {
+ if (container.hasChildNodes() && offset < container.childNodes.length) {
+ return container.childNodes[offset];
+ }
+
+ return null;
+ }
+
+ function getCaretCandidatePosition(direction, node) {
+ if (isForwards(direction)) {
+ if (isCaretCandidate(node.previousSibling) && !isText(node.previousSibling)) {
+ return CaretPosition.before(node);
+ }
+
+ if (isText(node)) {
+ return CaretPosition(node, 0);
+ }
+ }
+
+ if (isBackwards(direction)) {
+ if (isCaretCandidate(node.nextSibling) && !isText(node.nextSibling)) {
+ return CaretPosition.after(node);
+ }
+
+ if (isText(node)) {
+ return CaretPosition(node, node.data.length);
+ }
+ }
+
+ if (isBackwards(direction)) {
+ if (isBr(node)) {
+ return CaretPosition.before(node);
+ }
+
+ return CaretPosition.after(node);
+ }
+
+ return CaretPosition.before(node);
+ }
+
+ // Jumps over BR elements <p>|<br></p><p>a</p> -> <p><br></p><p>|a</p>
+ function isBrBeforeBlock(node, rootNode) {
+ var next;
+
+ if (!NodeType.isBr(node)) {
+ return false;
+ }
+
+ next = findCaretPosition(1, CaretPosition.after(node), rootNode);
+ if (!next) {
+ return false;
+ }
+
+ return !CaretUtils.isInSameBlock(CaretPosition.before(node), CaretPosition.before(next), rootNode);
+ }
+
+ function findCaretPosition(direction, startCaretPosition, rootNode) {
+ var container, offset, node, nextNode, innerNode,
+ rootContentEditableFalseElm, caretPosition;
+
+ if (!isElement(rootNode) || !startCaretPosition) {
+ return null;
+ }
+
+ caretPosition = startCaretPosition;
+ container = caretPosition.container();
+ offset = caretPosition.offset();
+
+ if (isText(container)) {
+ if (isBackwards(direction) && offset > 0) {
+ return CaretPosition(container, --offset);
+ }
+
+ if (isForwards(direction) && offset < container.length) {
+ return CaretPosition(container, ++offset);
+ }
+
+ node = container;
+ } else {
+ if (isBackwards(direction) && offset > 0) {
+ nextNode = nodeAtIndex(container, offset - 1);
+ if (isCaretCandidate(nextNode)) {
+ if (!isAtomic(nextNode)) {
+ innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode);
+ if (innerNode) {
+ if (isText(innerNode)) {
+ return CaretPosition(innerNode, innerNode.data.length);
+ }
+
+ return CaretPosition.after(innerNode);
+ }
+ }
+
+ if (isText(nextNode)) {
+ return CaretPosition(nextNode, nextNode.data.length);
+ }
+
+ return CaretPosition.before(nextNode);
+ }
+ }
+
+ if (isForwards(direction) && offset < container.childNodes.length) {
+ nextNode = nodeAtIndex(container, offset);
+ if (isCaretCandidate(nextNode)) {
+ if (isBrBeforeBlock(nextNode, rootNode)) {
+ return findCaretPosition(direction, CaretPosition.after(nextNode), rootNode);
+ }
+
+ if (!isAtomic(nextNode)) {
+ innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode);
+ if (innerNode) {
+ if (isText(innerNode)) {
+ return CaretPosition(innerNode, 0);
+ }
+
+ return CaretPosition.before(innerNode);
+ }
+ }
+
+ if (isText(nextNode)) {
+ return CaretPosition(nextNode, 0);
+ }
+
+ return CaretPosition.after(nextNode);
+ }
+ }
+
+ node = caretPosition.getNode();
+ }
+
+ if ((isForwards(direction) && caretPosition.isAtEnd()) || (isBackwards(direction) && caretPosition.isAtStart())) {
+ node = CaretUtils.findNode(node, direction, Fun.constant(true), rootNode, true);
+ if (isEditableCaretCandidate(node)) {
+ return getCaretCandidatePosition(direction, node);
+ }
+ }
+
+ nextNode = CaretUtils.findNode(node, direction, isEditableCaretCandidate, rootNode);
+
+ rootContentEditableFalseElm = Arr.last(Arr.filter(getParents(container, rootNode), isContentEditableFalse));
+ if (rootContentEditableFalseElm && (!nextNode || !rootContentEditableFalseElm.contains(nextNode))) {
+ if (isForwards(direction)) {
+ caretPosition = CaretPosition.after(rootContentEditableFalseElm);
+ } else {
+ caretPosition = CaretPosition.before(rootContentEditableFalseElm);
+ }
+
+ return caretPosition;
+ }
+
+ if (nextNode) {
+ return getCaretCandidatePosition(direction, nextNode);
+ }
+
+ return null;
+ }
+
+ return function(rootNode) {
+ return {
+ /**
+ * Returns the next logical caret position from the specificed input
+ * caretPoisiton or null if there isn't any more positions left for example
+ * at the end specified root element.
+ *
+ * @method next
+ * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from.
+ * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found.
+ */
+ next: function(caretPosition) {
+ return findCaretPosition(1, caretPosition, rootNode);
+ },
+
+ /**
+ * Returns the previous logical caret position from the specificed input
+ * caretPoisiton or null if there isn't any more positions left for example
+ * at the end specified root element.
+ *
+ * @method prev
+ * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from.
+ * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found.
+ */
+ prev: function(caretPosition) {
+ return findCaretPosition(-1, caretPosition, rootNode);
+ }
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/InsertList.js
+
+/**
+ * InsertList.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Handles inserts of lists into the editor instance.
+ *
+ * @class tinymce.InsertList
+ * @private
+ */
+define("tinymce/InsertList", [
+ "tinymce/util/Tools",
+ "tinymce/caret/CaretWalker",
+ "tinymce/caret/CaretPosition"
+], function(Tools, CaretWalker, CaretPosition) {
+ var isListFragment = function(fragment) {
+ var firstChild = fragment.firstChild;
+ var lastChild = fragment.lastChild;
+
+ // Skip meta since it's likely <meta><ul>..</ul>
+ if (firstChild && firstChild.name === 'meta') {
+ firstChild = firstChild.next;
+ }
+
+ // Skip mce_marker since it's likely <ul>..</ul><span id="mce_marker"></span>
+ if (lastChild && lastChild.attr('id') === 'mce_marker') {
+ lastChild = lastChild.prev;
+ }
+
+ if (!firstChild || firstChild !== lastChild) {
+ return false;
+ }
+
+ return firstChild.name === 'ul' || firstChild.name === 'ol';
+ };
+
+ var cleanupDomFragment = function (domFragment) {
+ var firstChild = domFragment.firstChild;
+ var lastChild = domFragment.lastChild;
+
+ // TODO: remove the meta tag from paste logic
+ if (firstChild && firstChild.nodeName === 'META') {
+ firstChild.parentNode.removeChild(firstChild);
+ }
+
+ if (lastChild && lastChild.id === 'mce_marker') {
+ lastChild.parentNode.removeChild(lastChild);
+ }
+
+ return domFragment;
+ };
+
+ var toDomFragment = function(dom, serializer, fragment) {
+ var html = serializer.serialize(fragment);
+ var domFragment = dom.createFragment(html);
+
+ return cleanupDomFragment(domFragment);
+ };
+
+ var listItems = function(elm) {
+ return Tools.grep(elm.childNodes, function(child) {
+ return child.nodeName === 'LI';
+ });
+ };
+
+ var isEmpty = function (elm) {
+ return !elm.firstChild;
+ };
+
+ var trimListItems = function(elms) {
+ return elms.length > 0 && isEmpty(elms[elms.length - 1]) ? elms.slice(0, -1) : elms;
+ };
+
+ var getParentLi = function(dom, node) {
+ var parentBlock = dom.getParent(node, dom.isBlock);
+ return parentBlock && parentBlock.nodeName === 'LI' ? parentBlock : null;
+ };
+
+ var isParentBlockLi = function(dom, node) {
+ return !!getParentLi(dom, node);
+ };
+
+ var getSplit = function(parentNode, rng) {
+ var beforeRng = rng.cloneRange();
+ var afterRng = rng.cloneRange();
+
+ beforeRng.setStartBefore(parentNode);
+ afterRng.setEndAfter(parentNode);
+
+ return [
+ beforeRng.cloneContents(),
+ afterRng.cloneContents()
+ ];
+ };
+
+ var findFirstIn = function(node, rootNode) {
+ var caretPos = CaretPosition.before(node);
+ var caretWalker = new CaretWalker(rootNode);
+ var newCaretPos = caretWalker.next(caretPos);
+
+ return newCaretPos ? newCaretPos.toRange() : null;
+ };
+
+ var findLastOf = function(node, rootNode) {
+ var caretPos = CaretPosition.after(node);
+ var caretWalker = new CaretWalker(rootNode);
+ var newCaretPos = caretWalker.prev(caretPos);
+
+ return newCaretPos ? newCaretPos.toRange() : null;
+ };
+
+ var insertMiddle = function(target, elms, rootNode, rng) {
+ var parts = getSplit(target, rng);
+ var parentElm = target.parentNode;
+
+ parentElm.insertBefore(parts[0], target);
+ Tools.each(elms, function(li) {
+ parentElm.insertBefore(li, target);
+ });
+ parentElm.insertBefore(parts[1], target);
+ parentElm.removeChild(target);
+
+ return findLastOf(elms[elms.length - 1], rootNode);
+ };
+
+ var insertBefore = function(target, elms, rootNode) {
+ var parentElm = target.parentNode;
+
+ Tools.each(elms, function(elm) {
+ parentElm.insertBefore(elm, target);
+ });
+
+ return findFirstIn(target, rootNode);
+ };
+
+ var insertAfter = function(target, elms, rootNode, dom) {
+ dom.insertAfter(elms.reverse(), target);
+ return findLastOf(elms[0], rootNode);
+ };
+
+ var insertAtCaret = function(serializer, dom, rng, fragment) {
+ var domFragment = toDomFragment(dom, serializer, fragment);
+ var liTarget = getParentLi(dom, rng.startContainer);
+ var liElms = trimListItems(listItems(domFragment.firstChild));
+ var BEGINNING = 1, END = 2;
+ var rootNode = dom.getRoot();
+
+ var isAt = function(location) {
+ var caretPos = CaretPosition.fromRangeStart(rng);
+ var caretWalker = new CaretWalker(dom.getRoot());
+ var newPos = location === BEGINNING ? caretWalker.prev(caretPos) : caretWalker.next(caretPos);
+
+ return newPos ? getParentLi(dom, newPos.getNode()) !== liTarget : true;
+ };
+
+ if (isAt(BEGINNING)) {
+ return insertBefore(liTarget, liElms, rootNode);
+ } else if (isAt(END)) {
+ return insertAfter(liTarget, liElms, rootNode, dom);
+ }
+
+ return insertMiddle(liTarget, liElms, rootNode, rng);
+ };
+
+ return {
+ isListFragment: isListFragment,
+ insertAtCaret: insertAtCaret,
+ isParentBlockLi: isParentBlockLi,
+ trimListItems: trimListItems,
+ listItems: listItems
+ };
+});
+
+// Included from: js/tinymce/classes/InsertContent.js
+
+/**
+ * InsertContent.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Handles inserts of contents into the editor instance.
+ *
+ * @class tinymce.InsertContent
+ * @private
+ */
+define("tinymce/InsertContent", [
+ "tinymce/Env",
+ "tinymce/util/Tools",
+ "tinymce/html/Serializer",
+ "tinymce/caret/CaretWalker",
+ "tinymce/caret/CaretPosition",
+ "tinymce/dom/ElementUtils",
+ "tinymce/dom/NodeType",
+ "tinymce/InsertList"
+], function(Env, Tools, Serializer, CaretWalker, CaretPosition, ElementUtils, NodeType, InsertList) {
+ var isTableCell = NodeType.matchNodeNames('td th');
+
+ var insertHtmlAtCaret = function(editor, value, details) {
+ var parser, serializer, parentNode, rootNode, fragment, args;
+ var marker, rng, node, node2, bookmarkHtml, merge;
+ var textInlineElements = editor.schema.getTextInlineElements();
+ var selection = editor.selection, dom = editor.dom;
+
+ function trimOrPaddLeftRight(html) {
+ var rng, container, offset;
+
+ rng = selection.getRng(true);
+ container = rng.startContainer;
+ offset = rng.startOffset;
+
+ function hasSiblingText(siblingName) {
+ return container[siblingName] && container[siblingName].nodeType == 3;
+ }
+
+ if (container.nodeType == 3) {
+ if (offset > 0) {
+ html = html.replace(/^ /, ' ');
+ } else if (!hasSiblingText('previousSibling')) {
+ html = html.replace(/^ /, ' ');
+ }
+
+ if (offset < container.length) {
+ html = html.replace(/ (<br>|)$/, ' ');
+ } else if (!hasSiblingText('nextSibling')) {
+ html = html.replace(/( | )(<br>|)$/, ' ');
+ }
+ }
+
+ return html;
+ }
+
+ // Removes from a [b] c -> a c -> a c
+ function trimNbspAfterDeleteAndPaddValue() {
+ var rng, container, offset;
+
+ rng = selection.getRng(true);
+ container = rng.startContainer;
+ offset = rng.startOffset;
+
+ if (container.nodeType == 3 && rng.collapsed) {
+ if (container.data[offset] === '\u00a0') {
+ container.deleteData(offset, 1);
+
+ if (!/[\u00a0| ]$/.test(value)) {
+ value += ' ';
+ }
+ } else if (container.data[offset - 1] === '\u00a0') {
+ container.deleteData(offset - 1, 1);
+
+ if (!/[\u00a0| ]$/.test(value)) {
+ value = ' ' + value;
+ }
+ }
+ }
+ }
+
+ function reduceInlineTextElements() {
+ if (merge) {
+ var root = editor.getBody(), elementUtils = new ElementUtils(dom);
+
+ Tools.each(dom.select('*[data-mce-fragment]'), function(node) {
+ for (var testNode = node.parentNode; testNode && testNode != root; testNode = testNode.parentNode) {
+ if (textInlineElements[node.nodeName.toLowerCase()] && elementUtils.compare(testNode, node)) {
+ dom.remove(node, true);
+ }
+ }
+ });
+ }
+ }
+
+ function markFragmentElements(fragment) {
+ var node = fragment;
+
+ while ((node = node.walk())) {
+ if (node.type === 1) {
+ node.attr('data-mce-fragment', '1');
+ }
+ }
+ }
+
+ function umarkFragmentElements(elm) {
+ Tools.each(elm.getElementsByTagName('*'), function(elm) {
+ elm.removeAttribute('data-mce-fragment');
+ });
+ }
+
+ function isPartOfFragment(node) {
+ return !!node.getAttribute('data-mce-fragment');
+ }
+
+ function canHaveChildren(node) {
+ return node && !editor.schema.getShortEndedElements()[node.nodeName];
+ }
+
+ function moveSelectionToMarker(marker) {
+ var parentEditableFalseElm, parentBlock, nextRng;
+
+ function getContentEditableFalseParent(node) {
+ var root = editor.getBody();
+
+ for (; node && node !== root; node = node.parentNode) {
+ if (editor.dom.getContentEditable(node) === 'false') {
+ return node;
+ }
+ }
+
+ return null;
+ }
+
+ if (!marker) {
+ return;
+ }
+
+ selection.scrollIntoView(marker);
+
+ // If marker is in cE=false then move selection to that element instead
+ parentEditableFalseElm = getContentEditableFalseParent(marker);
+ if (parentEditableFalseElm) {
+ dom.remove(marker);
+ selection.select(parentEditableFalseElm);
+ return;
+ }
+
+ // Move selection before marker and remove it
+ rng = dom.createRng();
+
+ // If previous sibling is a text node set the selection to the end of that node
+ node = marker.previousSibling;
+ if (node && node.nodeType == 3) {
+ rng.setStart(node, node.nodeValue.length);
+
+ // TODO: Why can't we normalize on IE
+ if (!Env.ie) {
+ node2 = marker.nextSibling;
+ if (node2 && node2.nodeType == 3) {
+ node.appendData(node2.data);
+ node2.parentNode.removeChild(node2);
+ }
+ }
+ } else {
+ // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node
+ rng.setStartBefore(marker);
+ rng.setEndBefore(marker);
+ }
+
+ function findNextCaretRng(rng) {
+ var caretPos = CaretPosition.fromRangeStart(rng);
+ var caretWalker = new CaretWalker(editor.getBody());
+
+ caretPos = caretWalker.next(caretPos);
+ if (caretPos) {
+ return caretPos.toRange();
+ }
+ }
+
+ // Remove the marker node and set the new range
+ parentBlock = dom.getParent(marker, dom.isBlock);
+ dom.remove(marker);
+
+ if (parentBlock && dom.isEmpty(parentBlock)) {
+ editor.$(parentBlock).empty();
+
+ rng.setStart(parentBlock, 0);
+ rng.setEnd(parentBlock, 0);
+
+ if (!isTableCell(parentBlock) && !isPartOfFragment(parentBlock) && (nextRng = findNextCaretRng(rng))) {
+ rng = nextRng;
+ dom.remove(parentBlock);
+ } else {
+ dom.add(parentBlock, dom.create('br', {'data-mce-bogus': '1'}));
+ }
+ }
+
+ selection.setRng(rng);
+ }
+
+ // Check for whitespace before/after value
+ if (/^ | $/.test(value)) {
+ value = trimOrPaddLeftRight(value);
+ }
+
+ // Setup parser and serializer
+ parser = editor.parser;
+ merge = details.merge;
+
+ serializer = new Serializer({
+ validate: editor.settings.validate
+ }, editor.schema);
+ bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">​</span>';
+
+ // Run beforeSetContent handlers on the HTML to be inserted
+ args = {content: value, format: 'html', selection: true};
+ editor.fire('BeforeSetContent', args);
+ value = args.content;
+
+ // Add caret at end of contents if it's missing
+ if (value.indexOf('{$caret}') == -1) {
+ value += '{$caret}';
+ }
+
+ // Replace the caret marker with a span bookmark element
+ value = value.replace(/\{\$caret\}/, bookmarkHtml);
+
+ // If selection is at <body>|<p></p> then move it into <body><p>|</p>
+ rng = selection.getRng();
+ var caretElement = rng.startContainer || (rng.parentElement ? rng.parentElement() : null);
+ var body = editor.getBody();
+ if (caretElement === body && selection.isCollapsed()) {
+ if (dom.isBlock(body.firstChild) && canHaveChildren(body.firstChild) && dom.isEmpty(body.firstChild)) {
+ rng = dom.createRng();
+ rng.setStart(body.firstChild, 0);
+ rng.setEnd(body.firstChild, 0);
+ selection.setRng(rng);
+ }
+ }
+
+ // Insert node maker where we will insert the new HTML and get it's parent
+ if (!selection.isCollapsed()) {
+ // Fix for #2595 seems that delete removes one extra character on
+ // WebKit for some odd reason if you double click select a word
+ editor.selection.setRng(editor.selection.getRng());
+ editor.getDoc().execCommand('Delete', false, null);
+ trimNbspAfterDeleteAndPaddValue();
+ }
+
+ parentNode = selection.getNode();
+
+ // Parse the fragment within the context of the parent node
+ var parserArgs = {context: parentNode.nodeName.toLowerCase(), data: details.data};
+ fragment = parser.parse(value, parserArgs);
+
+ // Custom handling of lists
+ if (details.paste === true && InsertList.isListFragment(fragment) && InsertList.isParentBlockLi(dom, parentNode)) {
+ rng = InsertList.insertAtCaret(serializer, dom, editor.selection.getRng(true), fragment);
+ editor.selection.setRng(rng);
+ editor.fire('SetContent', args);
+ return;
+ }
+
+ markFragmentElements(fragment);
+
+ // Move the caret to a more suitable location
+ node = fragment.lastChild;
+ if (node.attr('id') == 'mce_marker') {
+ marker = node;
+
+ for (node = node.prev; node; node = node.walk(true)) {
+ if (node.type == 3 || !dom.isBlock(node.name)) {
+ if (editor.schema.isValidChild(node.parent.name, 'span')) {
+ node.parent.insert(marker, node, node.name === 'br');
+ }
+ break;
+ }
+ }
+ }
+
+ editor._selectionOverrides.showBlockCaretContainer(parentNode);
+
+ // If parser says valid we can insert the contents into that parent
+ if (!parserArgs.invalid) {
+ value = serializer.serialize(fragment);
+
+ // Check if parent is empty or only has one BR element then set the innerHTML of that parent
+ node = parentNode.firstChild;
+ node2 = parentNode.lastChild;
+ if (!node || (node === node2 && node.nodeName === 'BR')) {
+ dom.setHTML(parentNode, value);
+ } else {
+ selection.setContent(value);
+ }
+ } else {
+ // If the fragment was invalid within that context then we need
+ // to parse and process the parent it's inserted into
+
+ // Insert bookmark node and get the parent
+ selection.setContent(bookmarkHtml);
+ parentNode = selection.getNode();
+ rootNode = editor.getBody();
+
+ // Opera will return the document node when selection is in root
+ if (parentNode.nodeType == 9) {
+ parentNode = node = rootNode;
+ } else {
+ node = parentNode;
+ }
+
+ // Find the ancestor just before the root element
+ while (node !== rootNode) {
+ parentNode = node;
+ node = node.parentNode;
+ }
+
+ // Get the outer/inner HTML depending on if we are in the root and parser and serialize that
+ value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode);
+ value = serializer.serialize(
+ parser.parse(
+ // Need to replace by using a function since $ in the contents would otherwise be a problem
+ value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function() {
+ return serializer.serialize(fragment);
+ })
+ )
+ );
+
+ // Set the inner/outer HTML depending on if we are in the root or not
+ if (parentNode == rootNode) {
+ dom.setHTML(rootNode, value);
+ } else {
+ dom.setOuterHTML(parentNode, value);
+ }
+ }
+
+ reduceInlineTextElements();
+ moveSelectionToMarker(dom.get('mce_marker'));
+ umarkFragmentElements(editor.getBody());
+ editor.fire('SetContent', args);
+ editor.addVisual();
+ };
+
+ var processValue = function (value) {
+ var details;
+
+ if (typeof value !== 'string') {
+ details = Tools.extend({
+ paste: value.paste,
+ data: {
+ paste: value.paste
+ }
+ }, value);
+
+ return {
+ content: value.content,
+ details: details
+ };
+ }
+
+ return {
+ content: value,
+ details: {}
+ };
+ };
+
+ var insertAtCaret = function (editor, value) {
+ var result = processValue(value);
+ insertHtmlAtCaret(editor, result.content, result.details);
+ };
+
+ return {
+ insertAtCaret: insertAtCaret
+ };
+});
+
+// Included from: js/tinymce/classes/EditorCommands.js
+
+/**
+ * EditorCommands.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class enables you to add custom editor commands and it contains
+ * overrides for native browser commands to address various bugs and issues.
+ *
+ * @class tinymce.EditorCommands
+ */
+define("tinymce/EditorCommands", [
+ "tinymce/Env",
+ "tinymce/util/Tools",
+ "tinymce/dom/RangeUtils",
+ "tinymce/dom/TreeWalker",
+ "tinymce/InsertContent"
+], function(Env, Tools, RangeUtils, TreeWalker, InsertContent) {
+ // Added for compression purposes
+ var each = Tools.each, extend = Tools.extend;
+ var map = Tools.map, inArray = Tools.inArray, explode = Tools.explode;
+ var isOldIE = Env.ie && Env.ie < 11;
+ var TRUE = true, FALSE = false;
+
+ return function(editor) {
+ var dom, selection, formatter,
+ commands = {state: {}, exec: {}, value: {}},
+ settings = editor.settings,
+ bookmark;
+
+ editor.on('PreInit', function() {
+ dom = editor.dom;
+ selection = editor.selection;
+ settings = editor.settings;
+ formatter = editor.formatter;
+ });
+
+ /**
+ * Executes the specified command.
+ *
+ * @method execCommand
+ * @param {String} command Command to execute.
+ * @param {Boolean} ui Optional user interface state.
+ * @param {Object} value Optional value for command.
+ * @param {Object} args Optional extra arguments to the execCommand.
+ * @return {Boolean} true/false if the command was found or not.
+ */
+ function execCommand(command, ui, value, args) {
+ var func, customCommand, state = 0;
+
+ if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(command) && (!args || !args.skip_focus)) {
+ editor.focus();
+ }
+
+ args = editor.fire('BeforeExecCommand', {command: command, ui: ui, value: value});
+ if (args.isDefaultPrevented()) {
+ return false;
+ }
+
+ customCommand = command.toLowerCase();
+ if ((func = commands.exec[customCommand])) {
+ func(customCommand, ui, value);
+ editor.fire('ExecCommand', {command: command, ui: ui, value: value});
+ return true;
+ }
+
+ // Plugin commands
+ each(editor.plugins, function(p) {
+ if (p.execCommand && p.execCommand(command, ui, value)) {
+ editor.fire('ExecCommand', {command: command, ui: ui, value: value});
+ state = true;
+ return false;
+ }
+ });
+
+ if (state) {
+ return state;
+ }
+
+ // Theme commands
+ if (editor.theme && editor.theme.execCommand && editor.theme.execCommand(command, ui, value)) {
+ editor.fire('ExecCommand', {command: command, ui: ui, value: value});
+ return true;
+ }
+
+ // Browser commands
+ try {
+ state = editor.getDoc().execCommand(command, ui, value);
+ } catch (ex) {
+ // Ignore old IE errors
+ }
+
+ if (state) {
+ editor.fire('ExecCommand', {command: command, ui: ui, value: value});
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Queries the current state for a command for example if the current selection is "bold".
+ *
+ * @method queryCommandState
+ * @param {String} command Command to check the state of.
+ * @return {Boolean/Number} true/false if the selected contents is bold or not, -1 if it's not found.
+ */
+ function queryCommandState(command) {
+ var func;
+
+ // Is hidden then return undefined
+ if (editor.quirks.isHidden()) {
+ return;
+ }
+
+ command = command.toLowerCase();
+ if ((func = commands.state[command])) {
+ return func(command);
+ }
+
+ // Browser commands
+ try {
+ return editor.getDoc().queryCommandState(command);
+ } catch (ex) {
+ // Fails sometimes see bug: 1896577
+ }
+
+ return false;
+ }
+
+ /**
+ * Queries the command value for example the current fontsize.
+ *
+ * @method queryCommandValue
+ * @param {String} command Command to check the value of.
+ * @return {Object} Command value of false if it's not found.
+ */
+ function queryCommandValue(command) {
+ var func;
+
+ // Is hidden then return undefined
+ if (editor.quirks.isHidden()) {
+ return;
+ }
+
+ command = command.toLowerCase();
+ if ((func = commands.value[command])) {
+ return func(command);
+ }
+
+ // Browser commands
+ try {
+ return editor.getDoc().queryCommandValue(command);
+ } catch (ex) {
+ // Fails sometimes see bug: 1896577
+ }
+ }
+
+ /**
+ * Adds commands to the command collection.
+ *
+ * @method addCommands
+ * @param {Object} command_list Name/value collection with commands to add, the names can also be comma separated.
+ * @param {String} type Optional type to add, defaults to exec. Can be value or state as well.
+ */
+ function addCommands(command_list, type) {
+ type = type || 'exec';
+
+ each(command_list, function(callback, command) {
+ each(command.toLowerCase().split(','), function(command) {
+ commands[type][command] = callback;
+ });
+ });
+ }
+
+ function addCommand(command, callback, scope) {
+ command = command.toLowerCase();
+ commands.exec[command] = function(command, ui, value, args) {
+ return callback.call(scope || editor, ui, value, args);
+ };
+ }
+
+ /**
+ * Returns true/false if the command is supported or not.
+ *
+ * @method queryCommandSupported
+ * @param {String} command Command that we check support for.
+ * @return {Boolean} true/false if the command is supported or not.
+ */
+ function queryCommandSupported(command) {
+ command = command.toLowerCase();
+
+ if (commands.exec[command]) {
+ return true;
+ }
+
+ // Browser commands
+ try {
+ return editor.getDoc().queryCommandSupported(command);
+ } catch (ex) {
+ // Fails sometimes see bug: 1896577
+ }
+
+ return false;
+ }
+
+ function addQueryStateHandler(command, callback, scope) {
+ command = command.toLowerCase();
+ commands.state[command] = function() {
+ return callback.call(scope || editor);
+ };
+ }
+
+ function addQueryValueHandler(command, callback, scope) {
+ command = command.toLowerCase();
+ commands.value[command] = function() {
+ return callback.call(scope || editor);
+ };
+ }
+
+ function hasCustomCommand(command) {
+ command = command.toLowerCase();
+ return !!commands.exec[command];
+ }
+
+ // Expose public methods
+ extend(this, {
+ execCommand: execCommand,
+ queryCommandState: queryCommandState,
+ queryCommandValue: queryCommandValue,
+ queryCommandSupported: queryCommandSupported,
+ addCommands: addCommands,
+ addCommand: addCommand,
+ addQueryStateHandler: addQueryStateHandler,
+ addQueryValueHandler: addQueryValueHandler,
+ hasCustomCommand: hasCustomCommand
+ });
+
+ // Private methods
+
+ function execNativeCommand(command, ui, value) {
+ if (ui === undefined) {
+ ui = FALSE;
+ }
+
+ if (value === undefined) {
+ value = null;
+ }
+
+ return editor.getDoc().execCommand(command, ui, value);
+ }
+
+ function isFormatMatch(name) {
+ return formatter.match(name);
+ }
+
+ function toggleFormat(name, value) {
+ formatter.toggle(name, value ? {value: value} : undefined);
+ editor.nodeChanged();
+ }
+
+ function storeSelection(type) {
+ bookmark = selection.getBookmark(type);
+ }
+
+ function restoreSelection() {
+ selection.moveToBookmark(bookmark);
+ }
+
+ // Add execCommand overrides
+ addCommands({
+ // Ignore these, added for compatibility
+ 'mceResetDesignMode,mceBeginUndoLevel': function() {},
+
+ // Add undo manager logic
+ 'mceEndUndoLevel,mceAddUndoLevel': function() {
+ editor.undoManager.add();
+ },
+
+ 'Cut,Copy,Paste': function(command) {
+ var doc = editor.getDoc(), failed;
+
+ // Try executing the native command
+ try {
+ execNativeCommand(command);
+ } catch (ex) {
+ // Command failed
+ failed = TRUE;
+ }
+
+ // Chrome reports the paste command as supported however older IE:s will return false for cut/paste
+ if (command === 'paste' && !doc.queryCommandEnabled(command)) {
+ failed = true;
+ }
+
+ // Present alert message about clipboard access not being available
+ if (failed || !doc.queryCommandSupported(command)) {
+ var msg = editor.translate(
+ "Your browser doesn't support direct access to the clipboard. " +
+ "Please use the Ctrl+X/C/V keyboard shortcuts instead."
+ );
+
+ if (Env.mac) {
+ msg = msg.replace(/Ctrl\+/g, '\u2318+');
+ }
+
+ editor.notificationManager.open({text: msg, type: 'error'});
+ }
+ },
+
+ // Override unlink command
+ unlink: function() {
+ if (selection.isCollapsed()) {
+ var elm = editor.dom.getParent(editor.selection.getStart(), 'a');
+ if (elm) {
+ editor.dom.remove(elm, true);
+ }
+
+ return;
+ }
+
+ formatter.remove("link");
+ },
+
+ // Override justify commands to use the text formatter engine
+ 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull,JustifyNone': function(command) {
+ var align = command.substring(7);
+
+ if (align == 'full') {
+ align = 'justify';
+ }
+
+ // Remove all other alignments first
+ each('left,center,right,justify'.split(','), function(name) {
+ if (align != name) {
+ formatter.remove('align' + name);
+ }
+ });
+
+ if (align != 'none') {
+ toggleFormat('align' + align);
+ }
+ },
+
+ // Override list commands to fix WebKit bug
+ 'InsertUnorderedList,InsertOrderedList': function(command) {
+ var listElm, listParent;
+
+ execNativeCommand(command);
+
+ // WebKit produces lists within block elements so we need to split them
+ // we will replace the native list creation logic to custom logic later on
+ // TODO: Remove this when the list creation logic is removed
+ listElm = dom.getParent(selection.getNode(), 'ol,ul');
+ if (listElm) {
+ listParent = listElm.parentNode;
+
+ // If list is within a text block then split that block
+ if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) {
+ storeSelection();
+ dom.split(listParent, listElm);
+ restoreSelection();
+ }
+ }
+ },
+
+ // Override commands to use the text formatter engine
+ 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function(command) {
+ toggleFormat(command);
+ },
+
+ // Override commands to use the text formatter engine
+ 'ForeColor,HiliteColor,FontName': function(command, ui, value) {
+ toggleFormat(command, value);
+ },
+
+ FontSize: function(command, ui, value) {
+ var fontClasses, fontSizes;
+
+ // Convert font size 1-7 to styles
+ if (value >= 1 && value <= 7) {
+ fontSizes = explode(settings.font_size_style_values);
+ fontClasses = explode(settings.font_size_classes);
+
+ if (fontClasses) {
+ value = fontClasses[value - 1] || value;
+ } else {
+ value = fontSizes[value - 1] || value;
+ }
+ }
+
+ toggleFormat(command, value);
+ },
+
+ RemoveFormat: function(command) {
+ formatter.remove(command);
+ },
+
+ mceBlockQuote: function() {
+ toggleFormat('blockquote');
+ },
+
+ FormatBlock: function(command, ui, value) {
+ return toggleFormat(value || 'p');
+ },
+
+ mceCleanup: function() {
+ var bookmark = selection.getBookmark();
+
+ editor.setContent(editor.getContent({cleanup: TRUE}), {cleanup: TRUE});
+
+ selection.moveToBookmark(bookmark);
+ },
+
+ mceRemoveNode: function(command, ui, value) {
+ var node = value || selection.getNode();
+
+ // Make sure that the body node isn't removed
+ if (node != editor.getBody()) {
+ storeSelection();
+ editor.dom.remove(node, TRUE);
+ restoreSelection();
+ }
+ },
+
+ mceSelectNodeDepth: function(command, ui, value) {
+ var counter = 0;
+
+ dom.getParent(selection.getNode(), function(node) {
+ if (node.nodeType == 1 && counter++ == value) {
+ selection.select(node);
+ return FALSE;
+ }
+ }, editor.getBody());
+ },
+
+ mceSelectNode: function(command, ui, value) {
+ selection.select(value);
+ },
+
+ mceInsertContent: function(command, ui, value) {
+ InsertContent.insertAtCaret(editor, value);
+ },
+
+ mceInsertRawHTML: function(command, ui, value) {
+ selection.setContent('tiny_mce_marker');
+ editor.setContent(
+ editor.getContent().replace(/tiny_mce_marker/g, function() {
+ return value;
+ })
+ );
+ },
+
+ mceToggleFormat: function(command, ui, value) {
+ toggleFormat(value);
+ },
+
+ mceSetContent: function(command, ui, value) {
+ editor.setContent(value);
+ },
+
+ 'Indent,Outdent': function(command) {
+ var intentValue, indentUnit, value;
+
+ // Setup indent level
+ intentValue = settings.indentation;
+ indentUnit = /[a-z%]+$/i.exec(intentValue);
+ intentValue = parseInt(intentValue, 10);
+
+ if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) {
+ // If forced_root_blocks is set to false we don't have a block to indent so lets create a div
+ if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) {
+ formatter.apply('div');
+ }
+
+ each(selection.getSelectedBlocks(), function(element) {
+ if (dom.getContentEditable(element) === "false") {
+ return;
+ }
+
+ if (element.nodeName !== "LI") {
+ var indentStyleName = editor.getParam('indent_use_margin', false) ? 'margin' : 'padding';
+ indentStyleName = element.nodeName === 'TABLE' ? 'margin' : indentStyleName;
+ indentStyleName += dom.getStyle(element, 'direction', true) == 'rtl' ? 'Right' : 'Left';
+
+ if (command == 'outdent') {
+ value = Math.max(0, parseInt(element.style[indentStyleName] || 0, 10) - intentValue);
+ dom.setStyle(element, indentStyleName, value ? value + indentUnit : '');
+ } else {
+ value = (parseInt(element.style[indentStyleName] || 0, 10) + intentValue) + indentUnit;
+ dom.setStyle(element, indentStyleName, value);
+ }
+ }
+ });
+ } else {
+ execNativeCommand(command);
+ }
+ },
+
+ mceRepaint: function() {
+ },
+
+ InsertHorizontalRule: function() {
+ editor.execCommand('mceInsertContent', false, '<hr />');
+ },
+
+ mceToggleVisualAid: function() {
+ editor.hasVisual = !editor.hasVisual;
+ editor.addVisual();
+ },
+
+ mceReplaceContent: function(command, ui, value) {
+ editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format: 'text'})));
+ },
+
+ mceInsertLink: function(command, ui, value) {
+ var anchor;
+
+ if (typeof value == 'string') {
+ value = {href: value};
+ }
+
+ anchor = dom.getParent(selection.getNode(), 'a');
+
+ // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here.
+ value.href = value.href.replace(' ', '%20');
+
+ // Remove existing links if there could be child links or that the href isn't specified
+ if (!anchor || !value.href) {
+ formatter.remove('link');
+ }
+
+ // Apply new link to selection
+ if (value.href) {
+ formatter.apply('link', value, anchor);
+ }
+ },
+
+ selectAll: function() {
+ var root = dom.getRoot(), rng;
+
+ if (selection.getRng().setStart) {
+ rng = dom.createRng();
+ rng.setStart(root, 0);
+ rng.setEnd(root, root.childNodes.length);
+ selection.setRng(rng);
+ } else {
+ // IE will render it's own root level block elements and sometimes
+ // even put font elements in them when the user starts typing. So we need to
+ // move the selection to a more suitable element from this:
+ // <body>|<p></p></body> to this: <body><p>|</p></body>
+ rng = selection.getRng();
+ if (!rng.item) {
+ rng.moveToElementText(root);
+ rng.select();
+ }
+ }
+ },
+
+ "delete": function() {
+ execNativeCommand("Delete");
+
+ // Check if body is empty after the delete call if so then set the contents
+ // to an empty string and move the caret to any block produced by that operation
+ // this fixes the issue with root blocks not being properly produced after a delete call on IE
+ var body = editor.getBody();
+
+ if (dom.isEmpty(body)) {
+ editor.setContent('');
+
+ if (body.firstChild && dom.isBlock(body.firstChild)) {
+ editor.selection.setCursorLocation(body.firstChild, 0);
+ } else {
+ editor.selection.setCursorLocation(body, 0);
+ }
+ }
+ },
+
+ mceNewDocument: function() {
+ editor.setContent('');
+ },
+
+ InsertLineBreak: function(command, ui, value) {
+ // We load the current event in from EnterKey.js when appropriate to heed
+ // certain event-specific variations such as ctrl-enter in a list
+ var evt = value;
+ var brElm, extraBr, marker;
+ var rng = selection.getRng(true);
+ new RangeUtils(dom).normalize(rng);
+
+ var offset = rng.startOffset;
+ var container = rng.startContainer;
+
+ // Resolve node index
+ if (container.nodeType == 1 && container.hasChildNodes()) {
+ var isAfterLastNodeInContainer = offset > container.childNodes.length - 1;
+
+ container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
+ if (isAfterLastNodeInContainer && container.nodeType == 3) {
+ offset = container.nodeValue.length;
+ } else {
+ offset = 0;
+ }
+ }
+
+ var parentBlock = dom.getParent(container, dom.isBlock);
+ var parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
+ var containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null;
+ var containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
+
+ // Enter inside block contained within a LI then split or insert before/after LI
+ var isControlKey = evt && evt.ctrlKey;
+ if (containerBlockName == 'LI' && !isControlKey) {
+ parentBlock = containerBlock;
+ parentBlockName = containerBlockName;
+ }
+
+ // Walks the parent block to the right and look for BR elements
+ function hasRightSideContent() {
+ var walker = new TreeWalker(container, parentBlock), node;
+ var nonEmptyElementsMap = editor.schema.getNonEmptyElements();
+
+ while ((node = walker.next())) {
+ if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) {
+ return true;
+ }
+ }
+ }
+
+ if (container && container.nodeType == 3 && offset >= container.nodeValue.length) {
+ // Insert extra BR element at the end block elements
+ if (!isOldIE && !hasRightSideContent()) {
+ brElm = dom.create('br');
+ rng.insertNode(brElm);
+ rng.setStartAfter(brElm);
+ rng.setEndAfter(brElm);
+ extraBr = true;
+ }
+ }
+
+ brElm = dom.create('br');
+ rng.insertNode(brElm);
+
+ // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it
+ var documentMode = dom.doc.documentMode;
+ if (isOldIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) {
+ brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm);
+ }
+
+ // Insert temp marker and scroll to that
+ marker = dom.create('span', {}, ' ');
+ brElm.parentNode.insertBefore(marker, brElm);
+ selection.scrollIntoView(marker);
+ dom.remove(marker);
+
+ if (!extraBr) {
+ rng.setStartAfter(brElm);
+ rng.setEndAfter(brElm);
+ } else {
+ rng.setStartBefore(brElm);
+ rng.setEndBefore(brElm);
+ }
+
+ selection.setRng(rng);
+ editor.undoManager.add();
+
+ return TRUE;
+ }
+ });
+
+ // Add queryCommandState overrides
+ addCommands({
+ // Override justify commands
+ 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function(command) {
+ var name = 'align' + command.substring(7);
+ var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks();
+ var matches = map(nodes, function(node) {
+ return !!formatter.matchNode(node, name);
+ });
+ return inArray(matches, TRUE) !== -1;
+ },
+
+ 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function(command) {
+ return isFormatMatch(command);
+ },
+
+ mceBlockQuote: function() {
+ return isFormatMatch('blockquote');
+ },
+
+ Outdent: function() {
+ var node;
+
+ if (settings.inline_styles) {
+ if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) {
+ return TRUE;
+ }
+
+ if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) {
+ return TRUE;
+ }
+ }
+
+ return (
+ queryCommandState('InsertUnorderedList') ||
+ queryCommandState('InsertOrderedList') ||
+ (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE'))
+ );
+ },
+
+ 'InsertUnorderedList,InsertOrderedList': function(command) {
+ var list = dom.getParent(selection.getNode(), 'ul,ol');
+
+ return list &&
+ (
+ command === 'insertunorderedlist' && list.tagName === 'UL' ||
+ command === 'insertorderedlist' && list.tagName === 'OL'
+ );
+ }
+ }, 'state');
+
+ // Add queryCommandValue overrides
+ addCommands({
+ 'FontSize,FontName': function(command) {
+ var value = 0, parent;
+
+ if ((parent = dom.getParent(selection.getNode(), 'span'))) {
+ if (command == 'fontsize') {
+ value = parent.style.fontSize;
+ } else {
+ value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase();
+ }
+ }
+
+ return value;
+ }
+ }, 'value');
+
+ // Add undo manager logic
+ addCommands({
+ Undo: function() {
+ editor.undoManager.undo();
+ },
+
+ Redo: function() {
+ editor.undoManager.redo();
+ }
+ });
+ };
+});
+
+// Included from: js/tinymce/classes/util/URI.js
+
+/**
+ * URI.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class handles parsing, modification and serialization of URI/URL strings.
+ * @class tinymce.util.URI
+ */
+define("tinymce/util/URI", [
+ "tinymce/util/Tools"
+], function(Tools) {
+ var each = Tools.each, trim = Tools.trim;
+ var queryParts = "source protocol authority userInfo user password host port relative path directory file query anchor".split(' ');
+ var DEFAULT_PORTS = {
+ 'ftp': 21,
+ 'http': 80,
+ 'https': 443,
+ 'mailto': 25
+ };
+
+ /**
+ * Constructs a new URI instance.
+ *
+ * @constructor
+ * @method URI
+ * @param {String} url URI string to parse.
+ * @param {Object} settings Optional settings object.
+ */
+ function URI(url, settings) {
+ var self = this, baseUri, base_url;
+
+ url = trim(url);
+ settings = self.settings = settings || {};
+ baseUri = settings.base_uri;
+
+ // Strange app protocol that isn't http/https or local anchor
+ // For example: mailto,skype,tel etc.
+ if (/^([\w\-]+):([^\/]{2})/i.test(url) || /^\s*#/.test(url)) {
+ self.source = url;
+ return;
+ }
+
+ var isProtocolRelative = url.indexOf('//') === 0;
+
+ // Absolute path with no host, fake host and protocol
+ if (url.indexOf('/') === 0 && !isProtocolRelative) {
+ url = (baseUri ? baseUri.protocol || 'http' : 'http') + '://mce_host' + url;
+ }
+
+ // Relative path http:// or protocol relative //path
+ if (!/^[\w\-]*:?\/\//.test(url)) {
+ base_url = settings.base_uri ? settings.base_uri.path : new URI(location.href).directory;
+ if (settings.base_uri.protocol === "") {
+ url = '//mce_host' + self.toAbsPath(base_url, url);
+ } else {
+ url = /([^#?]*)([#?]?.*)/.exec(url);
+ url = ((baseUri && baseUri.protocol) || 'http') + '://mce_host' + self.toAbsPath(base_url, url[1]) + url[2];
+ }
+ }
+
+ // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri)
+ url = url.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something
+
+ /*jshint maxlen: 255 */
+ /*eslint max-len: 0 */
+ url = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(url);
+
+ each(queryParts, function(v, i) {
+ var part = url[i];
+
+ // Zope 3 workaround, they use @@something
+ if (part) {
+ part = part.replace(/\(mce_at\)/g, '@@');
+ }
+
+ self[v] = part;
+ });
+
+ if (baseUri) {
+ if (!self.protocol) {
+ self.protocol = baseUri.protocol;
+ }
+
+ if (!self.userInfo) {
+ self.userInfo = baseUri.userInfo;
+ }
+
+ if (!self.port && self.host === 'mce_host') {
+ self.port = baseUri.port;
+ }
+
+ if (!self.host || self.host === 'mce_host') {
+ self.host = baseUri.host;
+ }
+
+ self.source = '';
+ }
+
+ if (isProtocolRelative) {
+ self.protocol = '';
+ }
+
+ //t.path = t.path || '/';
+ }
+
+ URI.prototype = {
+ /**
+ * Sets the internal path part of the URI.
+ *
+ * @method setPath
+ * @param {string} path Path string to set.
+ */
+ setPath: function(path) {
+ var self = this;
+
+ path = /^(.*?)\/?(\w+)?$/.exec(path);
+
+ // Update path parts
+ self.path = path[0];
+ self.directory = path[1];
+ self.file = path[2];
+
+ // Rebuild source
+ self.source = '';
+ self.getURI();
+ },
+
+ /**
+ * Converts the specified URI into a relative URI based on the current URI instance location.
+ *
+ * @method toRelative
+ * @param {String} uri URI to convert into a relative path/URI.
+ * @return {String} Relative URI from the point specified in the current URI instance.
+ * @example
+ * // Converts an absolute URL to an relative URL url will be somedir/somefile.htm
+ * var url = new tinymce.util.URI('http://www.site.com/dir/').toRelative('http://www.site.com/dir/somedir/somefile.htm');
+ */
+ toRelative: function(uri) {
+ var self = this, output;
+
+ if (uri === "./") {
+ return uri;
+ }
+
+ uri = new URI(uri, {base_uri: self});
+
+ // Not on same domain/port or protocol
+ if ((uri.host != 'mce_host' && self.host != uri.host && uri.host) || self.port != uri.port ||
+ (self.protocol != uri.protocol && uri.protocol !== "")) {
+ return uri.getURI();
+ }
+
+ var tu = self.getURI(), uu = uri.getURI();
+
+ // Allow usage of the base_uri when relative_urls = true
+ if (tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) {
+ return tu;
+ }
+
+ output = self.toRelPath(self.path, uri.path);
+
+ // Add query
+ if (uri.query) {
+ output += '?' + uri.query;
+ }
+
+ // Add anchor
+ if (uri.anchor) {
+ output += '#' + uri.anchor;
+ }
+
+ return output;
+ },
+
+ /**
+ * Converts the specified URI into a absolute URI based on the current URI instance location.
+ *
+ * @method toAbsolute
+ * @param {String} uri URI to convert into a relative path/URI.
+ * @param {Boolean} noHost No host and protocol prefix.
+ * @return {String} Absolute URI from the point specified in the current URI instance.
+ * @example
+ * // Converts an relative URL to an absolute URL url will be http://www.site.com/dir/somedir/somefile.htm
+ * var url = new tinymce.util.URI('http://www.site.com/dir/').toAbsolute('somedir/somefile.htm');
+ */
+ toAbsolute: function(uri, noHost) {
+ uri = new URI(uri, {base_uri: this});
+
+ return uri.getURI(noHost && this.isSameOrigin(uri));
+ },
+
+ /**
+ * Determine whether the given URI has the same origin as this URI. Based on RFC-6454.
+ * Supports default ports for protocols listed in DEFAULT_PORTS. Unsupported protocols will fail safe: they
+ * won't match, if the port specifications differ.
+ *
+ * @method isSameOrigin
+ * @param {tinymce.util.URI} uri Uri instance to compare.
+ * @returns {Boolean} True if the origins are the same.
+ */
+ isSameOrigin: function(uri) {
+ if (this.host == uri.host && this.protocol == uri.protocol) {
+ if (this.port == uri.port) {
+ return true;
+ }
+
+ var defaultPort = DEFAULT_PORTS[this.protocol];
+ if (defaultPort && ((this.port || defaultPort) == (uri.port || defaultPort))) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Converts a absolute path into a relative path.
+ *
+ * @method toRelPath
+ * @param {String} base Base point to convert the path from.
+ * @param {String} path Absolute path to convert into a relative path.
+ */
+ toRelPath: function(base, path) {
+ var items, breakPoint = 0, out = '', i, l;
+
+ // Split the paths
+ base = base.substring(0, base.lastIndexOf('/'));
+ base = base.split('/');
+ items = path.split('/');
+
+ if (base.length >= items.length) {
+ for (i = 0, l = base.length; i < l; i++) {
+ if (i >= items.length || base[i] != items[i]) {
+ breakPoint = i + 1;
+ break;
+ }
+ }
+ }
+
+ if (base.length < items.length) {
+ for (i = 0, l = items.length; i < l; i++) {
+ if (i >= base.length || base[i] != items[i]) {
+ breakPoint = i + 1;
+ break;
+ }
+ }
+ }
+
+ if (breakPoint === 1) {
+ return path;
+ }
+
+ for (i = 0, l = base.length - (breakPoint - 1); i < l; i++) {
+ out += "../";
+ }
+
+ for (i = breakPoint - 1, l = items.length; i < l; i++) {
+ if (i != breakPoint - 1) {
+ out += "/" + items[i];
+ } else {
+ out += items[i];
+ }
+ }
+
+ return out;
+ },
+
+ /**
+ * Converts a relative path into a absolute path.
+ *
+ * @method toAbsPath
+ * @param {String} base Base point to convert the path from.
+ * @param {String} path Relative path to convert into an absolute path.
+ */
+ toAbsPath: function(base, path) {
+ var i, nb = 0, o = [], tr, outPath;
+
+ // Split paths
+ tr = /\/$/.test(path) ? '/' : '';
+ base = base.split('/');
+ path = path.split('/');
+
+ // Remove empty chunks
+ each(base, function(k) {
+ if (k) {
+ o.push(k);
+ }
+ });
+
+ base = o;
+
+ // Merge relURLParts chunks
+ for (i = path.length - 1, o = []; i >= 0; i--) {
+ // Ignore empty or .
+ if (path[i].length === 0 || path[i] === ".") {
+ continue;
+ }
+
+ // Is parent
+ if (path[i] === '..') {
+ nb++;
+ continue;
+ }
+
+ // Move up
+ if (nb > 0) {
+ nb--;
+ continue;
+ }
+
+ o.push(path[i]);
+ }
+
+ i = base.length - nb;
+
+ // If /a/b/c or /
+ if (i <= 0) {
+ outPath = o.reverse().join('/');
+ } else {
+ outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/');
+ }
+
+ // Add front / if it's needed
+ if (outPath.indexOf('/') !== 0) {
+ outPath = '/' + outPath;
+ }
+
+ // Add traling / if it's needed
+ if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) {
+ outPath += tr;
+ }
+
+ return outPath;
+ },
+
+ /**
+ * Returns the full URI of the internal structure.
+ *
+ * @method getURI
+ * @param {Boolean} noProtoHost Optional no host and protocol part. Defaults to false.
+ */
+ getURI: function(noProtoHost) {
+ var s, self = this;
+
+ // Rebuild source
+ if (!self.source || noProtoHost) {
+ s = '';
+
+ if (!noProtoHost) {
+ if (self.protocol) {
+ s += self.protocol + '://';
+ } else {
+ s += '//';
+ }
+
+ if (self.userInfo) {
+ s += self.userInfo + '@';
+ }
+
+ if (self.host) {
+ s += self.host;
+ }
+
+ if (self.port) {
+ s += ':' + self.port;
+ }
+ }
+
+ if (self.path) {
+ s += self.path;
+ }
+
+ if (self.query) {
+ s += '?' + self.query;
+ }
+
+ if (self.anchor) {
+ s += '#' + self.anchor;
+ }
+
+ self.source = s;
+ }
+
+ return self.source;
+ }
+ };
+
+ URI.parseDataUri = function(uri) {
+ var type, matches;
+
+ uri = decodeURIComponent(uri).split(',');
+
+ matches = /data:([^;]+)/.exec(uri[0]);
+ if (matches) {
+ type = matches[1];
+ }
+
+ return {
+ type: type,
+ data: uri[1]
+ };
+ };
+
+ URI.getDocumentBaseUrl = function(loc) {
+ var baseUrl;
+
+ // Pass applewebdata:// and other non web protocols though
+ if (loc.protocol.indexOf('http') !== 0 && loc.protocol !== 'file:') {
+ baseUrl = loc.href;
+ } else {
+ baseUrl = loc.protocol + '//' + loc.host + loc.pathname;
+ }
+
+ if (/^[^:]+:\/\/\/?[^\/]+\//.test(baseUrl)) {
+ baseUrl = baseUrl.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
+
+ if (!/[\/\\]$/.test(baseUrl)) {
+ baseUrl += '/';
+ }
+ }
+
+ return baseUrl;
+ };
+
+ return URI;
+});
+
+// Included from: js/tinymce/classes/util/Class.js
+
+/**
+ * Class.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This utilitiy class is used for easier inheritance.
+ *
+ * Features:
+ * * Exposed super functions: this._super();
+ * * Mixins
+ * * Dummy functions
+ * * Property functions: var value = object.value(); and object.value(newValue);
+ * * Static functions
+ * * Defaults settings
+ */
+define("tinymce/util/Class", [
+ "tinymce/util/Tools"
+], function(Tools) {
+ var each = Tools.each, extend = Tools.extend;
+
+ var extendClass, initializing;
+
+ function Class() {
+ }
+
+ // Provides classical inheritance, based on code made by John Resig
+ Class.extend = extendClass = function(prop) {
+ var self = this, _super = self.prototype, prototype, name, member;
+
+ // The dummy class constructor
+ function Class() {
+ var i, mixins, mixin, self = this;
+
+ // All construction is actually done in the init method
+ if (!initializing) {
+ // Run class constuctor
+ if (self.init) {
+ self.init.apply(self, arguments);
+ }
+
+ // Run mixin constructors
+ mixins = self.Mixins;
+ if (mixins) {
+ i = mixins.length;
+ while (i--) {
+ mixin = mixins[i];
+ if (mixin.init) {
+ mixin.init.apply(self, arguments);
+ }
+ }
+ }
+ }
+ }
+
+ // Dummy function, needs to be extended in order to provide functionality
+ function dummy() {
+ return this;
+ }
+
+ // Creates a overloaded method for the class
+ // this enables you to use this._super(); to call the super function
+ function createMethod(name, fn) {
+ return function() {
+ var self = this, tmp = self._super, ret;
+
+ self._super = _super[name];
+ ret = fn.apply(self, arguments);
+ self._super = tmp;
+
+ return ret;
+ };
+ }
+
+ // Instantiate a base class (but only create the instance,
+ // don't run the init constructor)
+ initializing = true;
+
+ /*eslint new-cap:0 */
+ prototype = new self();
+ initializing = false;
+
+ // Add mixins
+ if (prop.Mixins) {
+ each(prop.Mixins, function(mixin) {
+ for (var name in mixin) {
+ if (name !== "init") {
+ prop[name] = mixin[name];
+ }
+ }
+ });
+
+ if (_super.Mixins) {
+ prop.Mixins = _super.Mixins.concat(prop.Mixins);
+ }
+ }
+
+ // Generate dummy methods
+ if (prop.Methods) {
+ each(prop.Methods.split(','), function(name) {
+ prop[name] = dummy;
+ });
+ }
+
+ // Generate property methods
+ if (prop.Properties) {
+ each(prop.Properties.split(','), function(name) {
+ var fieldName = '_' + name;
+
+ prop[name] = function(value) {
+ var self = this, undef;
+
+ // Set value
+ if (value !== undef) {
+ self[fieldName] = value;
+
+ return self;
+ }
+
+ // Get value
+ return self[fieldName];
+ };
+ });
+ }
+
+ // Static functions
+ if (prop.Statics) {
+ each(prop.Statics, function(func, name) {
+ Class[name] = func;
+ });
+ }
+
+ // Default settings
+ if (prop.Defaults && _super.Defaults) {
+ prop.Defaults = extend({}, _super.Defaults, prop.Defaults);
+ }
+
+ // Copy the properties over onto the new prototype
+ for (name in prop) {
+ member = prop[name];
+
+ if (typeof member == "function" && _super[name]) {
+ prototype[name] = createMethod(name, member);
+ } else {
+ prototype[name] = member;
+ }
+ }
+
+ // Populate our constructed prototype object
+ Class.prototype = prototype;
+
+ // Enforce the constructor to be what we expect
+ Class.constructor = Class;
+
+ // And make this class extendible
+ Class.extend = extendClass;
+
+ return Class;
+ };
+
+ return Class;
+});
+
+// Included from: js/tinymce/classes/util/EventDispatcher.js
+
+/**
+ * EventDispatcher.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class lets you add/remove and fire events by name on the specified scope. This makes
+ * it easy to add event listener logic to any class.
+ *
+ * @class tinymce.util.EventDispatcher
+ * @example
+ * var eventDispatcher = new EventDispatcher();
+ *
+ * eventDispatcher.on('click', function() {console.log('data');});
+ * eventDispatcher.fire('click', {data: 123});
+ */
+define("tinymce/util/EventDispatcher", [
+ "tinymce/util/Tools"
+], function(Tools) {
+ var nativeEvents = Tools.makeMap(
+ "focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange " +
+ "mouseout mouseenter mouseleave wheel keydown keypress keyup input contextmenu dragstart dragend dragover " +
+ "draggesture dragdrop drop drag submit " +
+ "compositionstart compositionend compositionupdate touchstart touchmove touchend",
+ ' '
+ );
+
+ function Dispatcher(settings) {
+ var self = this, scope, bindings = {}, toggleEvent;
+
+ function returnFalse() {
+ return false;
+ }
+
+ function returnTrue() {
+ return true;
+ }
+
+ settings = settings || {};
+ scope = settings.scope || self;
+ toggleEvent = settings.toggleEvent || returnFalse;
+
+ /**
+ * Fires the specified event by name.
+ *
+ * @method fire
+ * @param {String} name Name of the event to fire.
+ * @param {Object?} args Event arguments.
+ * @return {Object} Event args instance passed in.
+ * @example
+ * instance.fire('event', {...});
+ */
+ function fire(name, args) {
+ var handlers, i, l, callback;
+
+ name = name.toLowerCase();
+ args = args || {};
+ args.type = name;
+
+ // Setup target is there isn't one
+ if (!args.target) {
+ args.target = scope;
+ }
+
+ // Add event delegation methods if they are missing
+ if (!args.preventDefault) {
+ // Add preventDefault method
+ args.preventDefault = function() {
+ args.isDefaultPrevented = returnTrue;
+ };
+
+ // Add stopPropagation
+ args.stopPropagation = function() {
+ args.isPropagationStopped = returnTrue;
+ };
+
+ // Add stopImmediatePropagation
+ args.stopImmediatePropagation = function() {
+ args.isImmediatePropagationStopped = returnTrue;
+ };
+
+ // Add event delegation states
+ args.isDefaultPrevented = returnFalse;
+ args.isPropagationStopped = returnFalse;
+ args.isImmediatePropagationStopped = returnFalse;
+ }
+
+ if (settings.beforeFire) {
+ settings.beforeFire(args);
+ }
+
+ handlers = bindings[name];
+ if (handlers) {
+ for (i = 0, l = handlers.length; i < l; i++) {
+ callback = handlers[i];
+
+ // Unbind handlers marked with "once"
+ if (callback.once) {
+ off(name, callback.func);
+ }
+
+ // Stop immediate propagation if needed
+ if (args.isImmediatePropagationStopped()) {
+ args.stopPropagation();
+ return args;
+ }
+
+ // If callback returns false then prevent default and stop all propagation
+ if (callback.func.call(scope, args) === false) {
+ args.preventDefault();
+ return args;
+ }
+ }
+ }
+
+ return args;
+ }
+
+ /**
+ * Binds an event listener to a specific event by name.
+ *
+ * @method on
+ * @param {String} name Event name or space separated list of events to bind.
+ * @param {callback} callback Callback to be executed when the event occurs.
+ * @param {Boolean} first Optional flag if the event should be prepended. Use this with care.
+ * @return {Object} Current class instance.
+ * @example
+ * instance.on('event', function(e) {
+ * // Callback logic
+ * });
+ */
+ function on(name, callback, prepend, extra) {
+ var handlers, names, i;
+
+ if (callback === false) {
+ callback = returnFalse;
+ }
+
+ if (callback) {
+ callback = {
+ func: callback
+ };
+
+ if (extra) {
+ Tools.extend(callback, extra);
+ }
+
+ names = name.toLowerCase().split(' ');
+ i = names.length;
+ while (i--) {
+ name = names[i];
+ handlers = bindings[name];
+ if (!handlers) {
+ handlers = bindings[name] = [];
+ toggleEvent(name, true);
+ }
+
+ if (prepend) {
+ handlers.unshift(callback);
+ } else {
+ handlers.push(callback);
+ }
+ }
+ }
+
+ return self;
+ }
+
+ /**
+ * Unbinds an event listener to a specific event by name.
+ *
+ * @method off
+ * @param {String?} name Name of the event to unbind.
+ * @param {callback?} callback Callback to unbind.
+ * @return {Object} Current class instance.
+ * @example
+ * // Unbind specific callback
+ * instance.off('event', handler);
+ *
+ * // Unbind all listeners by name
+ * instance.off('event');
+ *
+ * // Unbind all events
+ * instance.off();
+ */
+ function off(name, callback) {
+ var i, handlers, bindingName, names, hi;
+
+ if (name) {
+ names = name.toLowerCase().split(' ');
+ i = names.length;
+ while (i--) {
+ name = names[i];
+ handlers = bindings[name];
+
+ // Unbind all handlers
+ if (!name) {
+ for (bindingName in bindings) {
+ toggleEvent(bindingName, false);
+ delete bindings[bindingName];
+ }
+
+ return self;
+ }
+
+ if (handlers) {
+ // Unbind all by name
+ if (!callback) {
+ handlers.length = 0;
+ } else {
+ // Unbind specific ones
+ hi = handlers.length;
+ while (hi--) {
+ if (handlers[hi].func === callback) {
+ handlers = handlers.slice(0, hi).concat(handlers.slice(hi + 1));
+ bindings[name] = handlers;
+ }
+ }
+ }
+
+ if (!handlers.length) {
+ toggleEvent(name, false);
+ delete bindings[name];
+ }
+ }
+ }
+ } else {
+ for (name in bindings) {
+ toggleEvent(name, false);
+ }
+
+ bindings = {};
+ }
+
+ return self;
+ }
+
+ /**
+ * Binds an event listener to a specific event by name
+ * and automatically unbind the event once the callback fires.
+ *
+ * @method once
+ * @param {String} name Event name or space separated list of events to bind.
+ * @param {callback} callback Callback to be executed when the event occurs.
+ * @param {Boolean} first Optional flag if the event should be prepended. Use this with care.
+ * @return {Object} Current class instance.
+ * @example
+ * instance.once('event', function(e) {
+ * // Callback logic
+ * });
+ */
+ function once(name, callback, prepend) {
+ return on(name, callback, prepend, {once: true});
+ }
+
+ /**
+ * Returns true/false if the dispatcher has a event of the specified name.
+ *
+ * @method has
+ * @param {String} name Name of the event to check for.
+ * @return {Boolean} true/false if the event exists or not.
+ */
+ function has(name) {
+ name = name.toLowerCase();
+ return !(!bindings[name] || bindings[name].length === 0);
+ }
+
+ // Expose
+ self.fire = fire;
+ self.on = on;
+ self.off = off;
+ self.once = once;
+ self.has = has;
+ }
+
+ /**
+ * Returns true/false if the specified event name is a native browser event or not.
+ *
+ * @method isNative
+ * @param {String} name Name to check if it's native.
+ * @return {Boolean} true/false if the event is native or not.
+ * @static
+ */
+ Dispatcher.isNative = function(name) {
+ return !!nativeEvents[name.toLowerCase()];
+ };
+
+ return Dispatcher;
+});
+
+// Included from: js/tinymce/classes/data/Binding.js
+
+/**
+ * Binding.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class gets dynamically extended to provide a binding between two models. This makes it possible to
+ * sync the state of two properties in two models by a layer of abstraction.
+ *
+ * @private
+ * @class tinymce.data.Binding
+ */
+define("tinymce/data/Binding", [], function() {
+ /**
+ * Constructs a new bidning.
+ *
+ * @constructor
+ * @method Binding
+ * @param {Object} settings Settings to the binding.
+ */
+ function Binding(settings) {
+ this.create = settings.create;
+ }
+
+ /**
+ * Creates a binding for a property on a model.
+ *
+ * @method create
+ * @param {tinymce.data.ObservableObject} model Model to create binding to.
+ * @param {String} name Name of property to bind.
+ * @return {tinymce.data.Binding} Binding instance.
+ */
+ Binding.create = function(model, name) {
+ return new Binding({
+ create: function(otherModel, otherName) {
+ var bindings;
+
+ function fromSelfToOther(e) {
+ otherModel.set(otherName, e.value);
+ }
+
+ function fromOtherToSelf(e) {
+ model.set(name, e.value);
+ }
+
+ otherModel.on('change:' + otherName, fromOtherToSelf);
+ model.on('change:' + name, fromSelfToOther);
+
+ // Keep track of the bindings
+ bindings = otherModel._bindings;
+
+ if (!bindings) {
+ bindings = otherModel._bindings = [];
+
+ otherModel.on('destroy', function() {
+ var i = bindings.length;
+
+ while (i--) {
+ bindings[i]();
+ }
+ });
+ }
+
+ bindings.push(function() {
+ model.off('change:' + name, fromSelfToOther);
+ });
+
+ return model.get(name);
+ }
+ });
+ };
+
+ return Binding;
+});
+
+// Included from: js/tinymce/classes/util/Observable.js
+
+/**
+ * Observable.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This mixin will add event binding logic to classes.
+ *
+ * @mixin tinymce.util.Observable
+ */
+define("tinymce/util/Observable", [
+ "tinymce/util/EventDispatcher"
+], function(EventDispatcher) {
+ function getEventDispatcher(obj) {
+ if (!obj._eventDispatcher) {
+ obj._eventDispatcher = new EventDispatcher({
+ scope: obj,
+ toggleEvent: function(name, state) {
+ if (EventDispatcher.isNative(name) && obj.toggleNativeEvent) {
+ obj.toggleNativeEvent(name, state);
+ }
+ }
+ });
+ }
+
+ return obj._eventDispatcher;
+ }
+
+ return {
+ /**
+ * Fires the specified event by name. Consult the
+ * <a href="/advanced/events">event reference</a> for more details on each event.
+ *
+ * @method fire
+ * @param {String} name Name of the event to fire.
+ * @param {Object?} args Event arguments.
+ * @param {Boolean?} bubble True/false if the event is to be bubbled.
+ * @return {Object} Event args instance passed in.
+ * @example
+ * instance.fire('event', {...});
+ */
+ fire: function(name, args, bubble) {
+ var self = this;
+
+ // Prevent all events except the remove event after the instance has been removed
+ if (self.removed && name !== "remove") {
+ return args;
+ }
+
+ args = getEventDispatcher(self).fire(name, args, bubble);
+
+ // Bubble event up to parents
+ if (bubble !== false && self.parent) {
+ var parent = self.parent();
+ while (parent && !args.isPropagationStopped()) {
+ parent.fire(name, args, false);
+ parent = parent.parent();
+ }
+ }
+
+ return args;
+ },
+
+ /**
+ * Binds an event listener to a specific event by name. Consult the
+ * <a href="/advanced/events">event reference</a> for more details on each event.
+ *
+ * @method on
+ * @param {String} name Event name or space separated list of events to bind.
+ * @param {callback} callback Callback to be executed when the event occurs.
+ * @param {Boolean} first Optional flag if the event should be prepended. Use this with care.
+ * @return {Object} Current class instance.
+ * @example
+ * instance.on('event', function(e) {
+ * // Callback logic
+ * });
+ */
+ on: function(name, callback, prepend) {
+ return getEventDispatcher(this).on(name, callback, prepend);
+ },
+
+ /**
+ * Unbinds an event listener to a specific event by name. Consult the
+ * <a href="/advanced/events">event reference</a> for more details on each event.
+ *
+ * @method off
+ * @param {String?} name Name of the event to unbind.
+ * @param {callback?} callback Callback to unbind.
+ * @return {Object} Current class instance.
+ * @example
+ * // Unbind specific callback
+ * instance.off('event', handler);
+ *
+ * // Unbind all listeners by name
+ * instance.off('event');
+ *
+ * // Unbind all events
+ * instance.off();
+ */
+ off: function(name, callback) {
+ return getEventDispatcher(this).off(name, callback);
+ },
+
+ /**
+ * Bind the event callback and once it fires the callback is removed. Consult the
+ * <a href="/advanced/events">event reference</a> for more details on each event.
+ *
+ * @method once
+ * @param {String} name Name of the event to bind.
+ * @param {callback} callback Callback to bind only once.
+ * @return {Object} Current class instance.
+ */
+ once: function(name, callback) {
+ return getEventDispatcher(this).once(name, callback);
+ },
+
+ /**
+ * Returns true/false if the object has a event of the specified name.
+ *
+ * @method hasEventListeners
+ * @param {String} name Name of the event to check for.
+ * @return {Boolean} true/false if the event exists or not.
+ */
+ hasEventListeners: function(name) {
+ return getEventDispatcher(this).has(name);
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/data/ObservableObject.js
+
+/**
+ * ObservableObject.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class is a object that is observable when properties changes a change event gets emitted.
+ *
+ * @private
+ * @class tinymce.data.ObservableObject
+ */
+define("tinymce/data/ObservableObject", [
+ "tinymce/data/Binding",
+ "tinymce/util/Observable",
+ "tinymce/util/Class",
+ "tinymce/util/Tools"
+], function(Binding, Observable, Class, Tools) {
+ function isNode(node) {
+ return node.nodeType > 0;
+ }
+
+ // Todo: Maybe this should be shallow compare since it might be huge object references
+ function isEqual(a, b) {
+ var k, checked;
+
+ // Strict equals
+ if (a === b) {
+ return true;
+ }
+
+ // Compare null
+ if (a === null || b === null) {
+ return a === b;
+ }
+
+ // Compare number, boolean, string, undefined
+ if (typeof a !== "object" || typeof b !== "object") {
+ return a === b;
+ }
+
+ // Compare arrays
+ if (Tools.isArray(b)) {
+ if (a.length !== b.length) {
+ return false;
+ }
+
+ k = a.length;
+ while (k--) {
+ if (!isEqual(a[k], b[k])) {
+ return false;
+ }
+ }
+ }
+
+ // Shallow compare nodes
+ if (isNode(a) || isNode(b)) {
+ return a === b;
+ }
+
+ // Compare objects
+ checked = {};
+ for (k in b) {
+ if (!isEqual(a[k], b[k])) {
+ return false;
+ }
+
+ checked[k] = true;
+ }
+
+ for (k in a) {
+ if (!checked[k] && !isEqual(a[k], b[k])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return Class.extend({
+ Mixins: [Observable],
+
+ /**
+ * Constructs a new observable object instance.
+ *
+ * @constructor
+ * @param {Object} data Initial data for the object.
+ */
+ init: function(data) {
+ var name, value;
+
+ data = data || {};
+
+ for (name in data) {
+ value = data[name];
+
+ if (value instanceof Binding) {
+ data[name] = value.create(this, name);
+ }
+ }
+
+ this.data = data;
+ },
+
+ /**
+ * Sets a property on the value this will call
+ * observers if the value is a change from the current value.
+ *
+ * @method set
+ * @param {String/object} name Name of the property to set or a object of items to set.
+ * @param {Object} value Value to set for the property.
+ * @return {tinymce.data.ObservableObject} Observable object instance.
+ */
+ set: function(name, value) {
+ var key, args, oldValue = this.data[name];
+
+ if (value instanceof Binding) {
+ value = value.create(this, name);
+ }
+
+ if (typeof name === "object") {
+ for (key in name) {
+ this.set(key, name[key]);
+ }
+
+ return this;
+ }
+
+ if (!isEqual(oldValue, value)) {
+ this.data[name] = value;
+
+ args = {
+ target: this,
+ name: name,
+ value: value,
+ oldValue: oldValue
+ };
+
+ this.fire('change:' + name, args);
+ this.fire('change', args);
+ }
+
+ return this;
+ },
+
+ /**
+ * Gets a property by name.
+ *
+ * @method get
+ * @param {String} name Name of the property to get.
+ * @return {Object} Object value of propery.
+ */
+ get: function(name) {
+ return this.data[name];
+ },
+
+ /**
+ * Returns true/false if the specified property exists.
+ *
+ * @method has
+ * @param {String} name Name of the property to check for.
+ * @return {Boolean} true/false if the item exists.
+ */
+ has: function(name) {
+ return name in this.data;
+ },
+
+ /**
+ * Returns a dynamic property binding for the specified property name. This makes
+ * it possible to sync the state of two properties in two ObservableObject instances.
+ *
+ * @method bind
+ * @param {String} name Name of the property to sync with the property it's inserted to.
+ * @return {tinymce.data.Binding} Data binding instance.
+ */
+ bind: function(name) {
+ return Binding.create(this, name);
+ },
+
+ /**
+ * Destroys the observable object and fires the "destroy"
+ * event and clean up any internal resources.
+ *
+ * @method destroy
+ */
+ destroy: function() {
+ this.fire('destroy');
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Selector.js
+
+/**
+ * Selector.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*eslint no-nested-ternary:0 */
+
+/**
+ * Selector engine, enables you to select controls by using CSS like expressions.
+ * We currently only support basic CSS expressions to reduce the size of the core
+ * and the ones we support should be enough for most cases.
+ *
+ * @example
+ * Supported expressions:
+ * element
+ * element#name
+ * element.class
+ * element[attr]
+ * element[attr*=value]
+ * element[attr~=value]
+ * element[attr!=value]
+ * element[attr^=value]
+ * element[attr$=value]
+ * element:<state>
+ * element:not(<expression>)
+ * element:first
+ * element:last
+ * element:odd
+ * element:even
+ * element element
+ * element > element
+ *
+ * @class tinymce.ui.Selector
+ */
+define("tinymce/ui/Selector", [
+ "tinymce/util/Class"
+], function(Class) {
+ "use strict";
+
+ /**
+ * Produces an array with a unique set of objects. It will not compare the values
+ * but the references of the objects.
+ *
+ * @private
+ * @method unqiue
+ * @param {Array} array Array to make into an array with unique items.
+ * @return {Array} Array with unique items.
+ */
+ function unique(array) {
+ var uniqueItems = [], i = array.length, item;
+
+ while (i--) {
+ item = array[i];
+
+ if (!item.__checked) {
+ uniqueItems.push(item);
+ item.__checked = 1;
+ }
+ }
+
+ i = uniqueItems.length;
+ while (i--) {
+ delete uniqueItems[i].__checked;
+ }
+
+ return uniqueItems;
+ }
+
+ var expression = /^([\w\\*]+)?(?:#([\w\-\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i;
+
+ /*jshint maxlen:255 */
+ /*eslint max-len:0 */
+ var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
+ whiteSpace = /^\s*|\s*$/g,
+ Collection;
+
+ var Selector = Class.extend({
+ /**
+ * Constructs a new Selector instance.
+ *
+ * @constructor
+ * @method init
+ * @param {String} selector CSS like selector expression.
+ */
+ init: function(selector) {
+ var match = this.match;
+
+ function compileNameFilter(name) {
+ if (name) {
+ name = name.toLowerCase();
+
+ return function(item) {
+ return name === '*' || item.type === name;
+ };
+ }
+ }
+
+ function compileIdFilter(id) {
+ if (id) {
+ return function(item) {
+ return item._name === id;
+ };
+ }
+ }
+
+ function compileClassesFilter(classes) {
+ if (classes) {
+ classes = classes.split('.');
+
+ return function(item) {
+ var i = classes.length;
+
+ while (i--) {
+ if (!item.classes.contains(classes[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ };
+ }
+ }
+
+ function compileAttrFilter(name, cmp, check) {
+ if (name) {
+ return function(item) {
+ var value = item[name] ? item[name]() : '';
+
+ return !cmp ? !!check :
+ cmp === "=" ? value === check :
+ cmp === "*=" ? value.indexOf(check) >= 0 :
+ cmp === "~=" ? (" " + value + " ").indexOf(" " + check + " ") >= 0 :
+ cmp === "!=" ? value != check :
+ cmp === "^=" ? value.indexOf(check) === 0 :
+ cmp === "$=" ? value.substr(value.length - check.length) === check :
+ false;
+ };
+ }
+ }
+
+ function compilePsuedoFilter(name) {
+ var notSelectors;
+
+ if (name) {
+ name = /(?:not\((.+)\))|(.+)/i.exec(name);
+
+ if (!name[1]) {
+ name = name[2];
+
+ return function(item, index, length) {
+ return name === 'first' ? index === 0 :
+ name === 'last' ? index === length - 1 :
+ name === 'even' ? index % 2 === 0 :
+ name === 'odd' ? index % 2 === 1 :
+ item[name] ? item[name]() :
+ false;
+ };
+ }
+
+ // Compile not expression
+ notSelectors = parseChunks(name[1], []);
+
+ return function(item) {
+ return !match(item, notSelectors);
+ };
+ }
+ }
+
+ function compile(selector, filters, direct) {
+ var parts;
+
+ function add(filter) {
+ if (filter) {
+ filters.push(filter);
+ }
+ }
+
+ // Parse expression into parts
+ parts = expression.exec(selector.replace(whiteSpace, ''));
+
+ add(compileNameFilter(parts[1]));
+ add(compileIdFilter(parts[2]));
+ add(compileClassesFilter(parts[3]));
+ add(compileAttrFilter(parts[4], parts[5], parts[6]));
+ add(compilePsuedoFilter(parts[7]));
+
+ // Mark the filter with pseudo for performance
+ filters.pseudo = !!parts[7];
+ filters.direct = direct;
+
+ return filters;
+ }
+
+ // Parser logic based on Sizzle by John Resig
+ function parseChunks(selector, selectors) {
+ var parts = [], extra, matches, i;
+
+ do {
+ chunker.exec("");
+ matches = chunker.exec(selector);
+
+ if (matches) {
+ selector = matches[3];
+ parts.push(matches[1]);
+
+ if (matches[2]) {
+ extra = matches[3];
+ break;
+ }
+ }
+ } while (matches);
+
+ if (extra) {
+ parseChunks(extra, selectors);
+ }
+
+ selector = [];
+ for (i = 0; i < parts.length; i++) {
+ if (parts[i] != '>') {
+ selector.push(compile(parts[i], [], parts[i - 1] === '>'));
+ }
+ }
+
+ selectors.push(selector);
+
+ return selectors;
+ }
+
+ this._selectors = parseChunks(selector, []);
+ },
+
+ /**
+ * Returns true/false if the selector matches the specified control.
+ *
+ * @method match
+ * @param {tinymce.ui.Control} control Control to match against the selector.
+ * @param {Array} selectors Optional array of selectors, mostly used internally.
+ * @return {Boolean} true/false state if the control matches or not.
+ */
+ match: function(control, selectors) {
+ var i, l, si, sl, selector, fi, fl, filters, index, length, siblings, count, item;
+
+ selectors = selectors || this._selectors;
+ for (i = 0, l = selectors.length; i < l; i++) {
+ selector = selectors[i];
+ sl = selector.length;
+ item = control;
+ count = 0;
+
+ for (si = sl - 1; si >= 0; si--) {
+ filters = selector[si];
+
+ while (item) {
+ // Find the index and length since a pseudo filter like :first needs it
+ if (filters.pseudo) {
+ siblings = item.parent().items();
+ index = length = siblings.length;
+ while (index--) {
+ if (siblings[index] === item) {
+ break;
+ }
+ }
+ }
+
+ for (fi = 0, fl = filters.length; fi < fl; fi++) {
+ if (!filters[fi](item, index, length)) {
+ fi = fl + 1;
+ break;
+ }
+ }
+
+ if (fi === fl) {
+ count++;
+ break;
+ } else {
+ // If it didn't match the right most expression then
+ // break since it's no point looking at the parents
+ if (si === sl - 1) {
+ break;
+ }
+ }
+
+ item = item.parent();
+ }
+ }
+
+ // If we found all selectors then return true otherwise continue looking
+ if (count === sl) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Returns a tinymce.ui.Collection with matches of the specified selector inside the specified container.
+ *
+ * @method find
+ * @param {tinymce.ui.Control} container Container to look for items in.
+ * @return {tinymce.ui.Collection} Collection with matched elements.
+ */
+ find: function(container) {
+ var matches = [], i, l, selectors = this._selectors;
+
+ function collect(items, selector, index) {
+ var i, l, fi, fl, item, filters = selector[index];
+
+ for (i = 0, l = items.length; i < l; i++) {
+ item = items[i];
+
+ // Run each filter against the item
+ for (fi = 0, fl = filters.length; fi < fl; fi++) {
+ if (!filters[fi](item, i, l)) {
+ fi = fl + 1;
+ break;
+ }
+ }
+
+ // All filters matched the item
+ if (fi === fl) {
+ // Matched item is on the last expression like: panel toolbar [button]
+ if (index == selector.length - 1) {
+ matches.push(item);
+ } else {
+ // Collect next expression type
+ if (item.items) {
+ collect(item.items(), selector, index + 1);
+ }
+ }
+ } else if (filters.direct) {
+ return;
+ }
+
+ // Collect child items
+ if (item.items) {
+ collect(item.items(), selector, index);
+ }
+ }
+ }
+
+ if (container.items) {
+ for (i = 0, l = selectors.length; i < l; i++) {
+ collect(container.items(), selectors[i], 0);
+ }
+
+ // Unique the matches if needed
+ if (l > 1) {
+ matches = unique(matches);
+ }
+ }
+
+ // Fix for circular reference
+ if (!Collection) {
+ // TODO: Fix me!
+ Collection = Selector.Collection;
+ }
+
+ return new Collection(matches);
+ }
+ });
+
+ return Selector;
+});
+
+// Included from: js/tinymce/classes/ui/Collection.js
+
+/**
+ * Collection.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Control collection, this class contains control instances and it enables you to
+ * perform actions on all the contained items. This is very similar to how jQuery works.
+ *
+ * @example
+ * someCollection.show().disabled(true);
+ *
+ * @class tinymce.ui.Collection
+ */
+define("tinymce/ui/Collection", [
+ "tinymce/util/Tools",
+ "tinymce/ui/Selector",
+ "tinymce/util/Class"
+], function(Tools, Selector, Class) {
+ "use strict";
+
+ var Collection, proto, push = Array.prototype.push, slice = Array.prototype.slice;
+
+ proto = {
+ /**
+ * Current number of contained control instances.
+ *
+ * @field length
+ * @type Number
+ */
+ length: 0,
+
+ /**
+ * Constructor for the collection.
+ *
+ * @constructor
+ * @method init
+ * @param {Array} items Optional array with items to add.
+ */
+ init: function(items) {
+ if (items) {
+ this.add(items);
+ }
+ },
+
+ /**
+ * Adds new items to the control collection.
+ *
+ * @method add
+ * @param {Array} items Array if items to add to collection.
+ * @return {tinymce.ui.Collection} Current collection instance.
+ */
+ add: function(items) {
+ var self = this;
+
+ // Force single item into array
+ if (!Tools.isArray(items)) {
+ if (items instanceof Collection) {
+ self.add(items.toArray());
+ } else {
+ push.call(self, items);
+ }
+ } else {
+ push.apply(self, items);
+ }
+
+ return self;
+ },
+
+ /**
+ * Sets the contents of the collection. This will remove any existing items
+ * and replace them with the ones specified in the input array.
+ *
+ * @method set
+ * @param {Array} items Array with items to set into the Collection.
+ * @return {tinymce.ui.Collection} Collection instance.
+ */
+ set: function(items) {
+ var self = this, len = self.length, i;
+
+ self.length = 0;
+ self.add(items);
+
+ // Remove old entries
+ for (i = self.length; i < len; i++) {
+ delete self[i];
+ }
+
+ return self;
+ },
+
+ /**
+ * Filters the collection item based on the specified selector expression or selector function.
+ *
+ * @method filter
+ * @param {String} selector Selector expression to filter items by.
+ * @return {tinymce.ui.Collection} Collection containing the filtered items.
+ */
+ filter: function(selector) {
+ var self = this, i, l, matches = [], item, match;
+
+ // Compile string into selector expression
+ if (typeof selector === "string") {
+ selector = new Selector(selector);
+
+ match = function(item) {
+ return selector.match(item);
+ };
+ } else {
+ // Use selector as matching function
+ match = selector;
+ }
+
+ for (i = 0, l = self.length; i < l; i++) {
+ item = self[i];
+
+ if (match(item)) {
+ matches.push(item);
+ }
+ }
+
+ return new Collection(matches);
+ },
+
+ /**
+ * Slices the items within the collection.
+ *
+ * @method slice
+ * @param {Number} index Index to slice at.
+ * @param {Number} len Optional length to slice.
+ * @return {tinymce.ui.Collection} Current collection.
+ */
+ slice: function() {
+ return new Collection(slice.apply(this, arguments));
+ },
+
+ /**
+ * Makes the current collection equal to the specified index.
+ *
+ * @method eq
+ * @param {Number} index Index of the item to set the collection to.
+ * @return {tinymce.ui.Collection} Current collection.
+ */
+ eq: function(index) {
+ return index === -1 ? this.slice(index) : this.slice(index, +index + 1);
+ },
+
+ /**
+ * Executes the specified callback on each item in collection.
+ *
+ * @method each
+ * @param {function} callback Callback to execute for each item in collection.
+ * @return {tinymce.ui.Collection} Current collection instance.
+ */
+ each: function(callback) {
+ Tools.each(this, callback);
+
+ return this;
+ },
+
+ /**
+ * Returns an JavaScript array object of the contents inside the collection.
+ *
+ * @method toArray
+ * @return {Array} Array with all items from collection.
+ */
+ toArray: function() {
+ return Tools.toArray(this);
+ },
+
+ /**
+ * Finds the index of the specified control or return -1 if it isn't in the collection.
+ *
+ * @method indexOf
+ * @param {Control} ctrl Control instance to look for.
+ * @return {Number} Index of the specified control or -1.
+ */
+ indexOf: function(ctrl) {
+ var self = this, i = self.length;
+
+ while (i--) {
+ if (self[i] === ctrl) {
+ break;
+ }
+ }
+
+ return i;
+ },
+
+ /**
+ * Returns a new collection of the contents in reverse order.
+ *
+ * @method reverse
+ * @return {tinymce.ui.Collection} Collection instance with reversed items.
+ */
+ reverse: function() {
+ return new Collection(Tools.toArray(this).reverse());
+ },
+
+ /**
+ * Returns true/false if the class exists or not.
+ *
+ * @method hasClass
+ * @param {String} cls Class to check for.
+ * @return {Boolean} true/false state if the class exists or not.
+ */
+ hasClass: function(cls) {
+ return this[0] ? this[0].classes.contains(cls) : false;
+ },
+
+ /**
+ * Sets/gets the specific property on the items in the collection. The same as executing control.<property>(<value>);
+ *
+ * @method prop
+ * @param {String} name Property name to get/set.
+ * @param {Object} value Optional object value to set.
+ * @return {tinymce.ui.Collection} Current collection instance or value of the first item on a get operation.
+ */
+ prop: function(name, value) {
+ var self = this, undef, item;
+
+ if (value !== undef) {
+ self.each(function(item) {
+ if (item[name]) {
+ item[name](value);
+ }
+ });
+
+ return self;
+ }
+
+ item = self[0];
+
+ if (item && item[name]) {
+ return item[name]();
+ }
+ },
+
+ /**
+ * Executes the specific function name with optional arguments an all items in collection if it exists.
+ *
+ * @example collection.exec("myMethod", arg1, arg2, arg3);
+ * @method exec
+ * @param {String} name Name of the function to execute.
+ * @param {Object} ... Multiple arguments to pass to each function.
+ * @return {tinymce.ui.Collection} Current collection.
+ */
+ exec: function(name) {
+ var self = this, args = Tools.toArray(arguments).slice(1);
+
+ self.each(function(item) {
+ if (item[name]) {
+ item[name].apply(item, args);
+ }
+ });
+
+ return self;
+ },
+
+ /**
+ * Remove all items from collection and DOM.
+ *
+ * @method remove
+ * @return {tinymce.ui.Collection} Current collection.
+ */
+ remove: function() {
+ var i = this.length;
+
+ while (i--) {
+ this[i].remove();
+ }
+
+ return this;
+ },
+
+ /**
+ * Adds a class to all items in the collection.
+ *
+ * @method addClass
+ * @param {String} cls Class to add to each item.
+ * @return {tinymce.ui.Collection} Current collection instance.
+ */
+ addClass: function(cls) {
+ return this.each(function(item) {
+ item.classes.add(cls);
+ });
+ },
+
+ /**
+ * Removes the specified class from all items in collection.
+ *
+ * @method removeClass
+ * @param {String} cls Class to remove from each item.
+ * @return {tinymce.ui.Collection} Current collection instance.
+ */
+ removeClass: function(cls) {
+ return this.each(function(item) {
+ item.classes.remove(cls);
+ });
+ }
+
+ /**
+ * Fires the specified event by name and arguments on the control. This will execute all
+ * bound event handlers.
+ *
+ * @method fire
+ * @param {String} name Name of the event to fire.
+ * @param {Object} args Optional arguments to pass to the event.
+ * @return {tinymce.ui.Collection} Current collection instance.
+ */
+ // fire: function(event, args) {}, -- Generated by code below
+
+ /**
+ * Binds a callback to the specified event. This event can both be
+ * native browser events like "click" or custom ones like PostRender.
+ *
+ * The callback function will have two parameters the first one being the control that received the event
+ * the second one will be the event object either the browsers native event object or a custom JS object.
+ *
+ * @method on
+ * @param {String} name Name of the event to bind. For example "click".
+ * @param {String/function} callback Callback function to execute ones the event occurs.
+ * @return {tinymce.ui.Collection} Current collection instance.
+ */
+ // on: function(name, callback) {}, -- Generated by code below
+
+ /**
+ * Unbinds the specified event and optionally a specific callback. If you omit the name
+ * parameter all event handlers will be removed. If you omit the callback all event handles
+ * by the specified name will be removed.
+ *
+ * @method off
+ * @param {String} name Optional name for the event to unbind.
+ * @param {function} callback Optional callback function to unbind.
+ * @return {tinymce.ui.Collection} Current collection instance.
+ */
+ // off: function(name, callback) {}, -- Generated by code below
+
+ /**
+ * Shows the items in the current collection.
+ *
+ * @method show
+ * @return {tinymce.ui.Collection} Current collection instance.
+ */
+ // show: function() {}, -- Generated by code below
+
+ /**
+ * Hides the items in the current collection.
+ *
+ * @method hide
+ * @return {tinymce.ui.Collection} Current collection instance.
+ */
+ // hide: function() {}, -- Generated by code below
+
+ /**
+ * Sets/gets the text contents of the items in the current collection.
+ *
+ * @method text
+ * @return {tinymce.ui.Collection} Current collection instance or text value of the first item on a get operation.
+ */
+ // text: function(value) {}, -- Generated by code below
+
+ /**
+ * Sets/gets the name contents of the items in the current collection.
+ *
+ * @method name
+ * @return {tinymce.ui.Collection} Current collection instance or name value of the first item on a get operation.
+ */
+ // name: function(value) {}, -- Generated by code below
+
+ /**
+ * Sets/gets the disabled state on the items in the current collection.
+ *
+ * @method disabled
+ * @return {tinymce.ui.Collection} Current collection instance or disabled state of the first item on a get operation.
+ */
+ // disabled: function(state) {}, -- Generated by code below
+
+ /**
+ * Sets/gets the active state on the items in the current collection.
+ *
+ * @method active
+ * @return {tinymce.ui.Collection} Current collection instance or active state of the first item on a get operation.
+ */
+ // active: function(state) {}, -- Generated by code below
+
+ /**
+ * Sets/gets the selected state on the items in the current collection.
+ *
+ * @method selected
+ * @return {tinymce.ui.Collection} Current collection instance or selected state of the first item on a get operation.
+ */
+ // selected: function(state) {}, -- Generated by code below
+
+ /**
+ * Sets/gets the selected state on the items in the current collection.
+ *
+ * @method visible
+ * @return {tinymce.ui.Collection} Current collection instance or visible state of the first item on a get operation.
+ */
+ // visible: function(state) {}, -- Generated by code below
+ };
+
+ // Extend tinymce.ui.Collection prototype with some generated control specific methods
+ Tools.each('fire on off show hide append prepend before after reflow'.split(' '), function(name) {
+ proto[name] = function() {
+ var args = Tools.toArray(arguments);
+
+ this.each(function(ctrl) {
+ if (name in ctrl) {
+ ctrl[name].apply(ctrl, args);
+ }
+ });
+
+ return this;
+ };
+ });
+
+ // Extend tinymce.ui.Collection prototype with some property methods
+ Tools.each('text name disabled active selected checked visible parent value data'.split(' '), function(name) {
+ proto[name] = function(value) {
+ return this.prop(name, value);
+ };
+ });
+
+ // Create class based on the new prototype
+ Collection = Class.extend(proto);
+
+ // Stick Collection into Selector to prevent circual references
+ Selector.Collection = Collection;
+
+ return Collection;
+});
+
+// Included from: js/tinymce/classes/ui/DomUtils.js
+
+/**
+ * DomUtils.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Private UI DomUtils proxy.
+ *
+ * @private
+ * @class tinymce.ui.DomUtils
+ */
+define("tinymce/ui/DomUtils", [
+ "tinymce/Env",
+ "tinymce/util/Tools",
+ "tinymce/dom/DOMUtils"
+], function(Env, Tools, DOMUtils) {
+ "use strict";
+
+ var count = 0;
+
+ var funcs = {
+ id: function() {
+ return 'mceu_' + (count++);
+ },
+
+ create: function(name, attrs, children) {
+ var elm = document.createElement(name);
+
+ DOMUtils.DOM.setAttribs(elm, attrs);
+
+ if (typeof children === 'string') {
+ elm.innerHTML = children;
+ } else {
+ Tools.each(children, function(child) {
+ if (child.nodeType) {
+ elm.appendChild(child);
+ }
+ });
+ }
+
+ return elm;
+ },
+
+ createFragment: function(html) {
+ return DOMUtils.DOM.createFragment(html);
+ },
+
+ getWindowSize: function() {
+ return DOMUtils.DOM.getViewPort();
+ },
+
+ getSize: function(elm) {
+ var width, height;
+
+ if (elm.getBoundingClientRect) {
+ var rect = elm.getBoundingClientRect();
+
+ width = Math.max(rect.width || (rect.right - rect.left), elm.offsetWidth);
+ height = Math.max(rect.height || (rect.bottom - rect.bottom), elm.offsetHeight);
+ } else {
+ width = elm.offsetWidth;
+ height = elm.offsetHeight;
+ }
+
+ return {width: width, height: height};
+ },
+
+ getPos: function(elm, root) {
+ return DOMUtils.DOM.getPos(elm, root || funcs.getContainer());
+ },
+
+ getContainer: function () {
+ return Env.container ? Env.container : document.body;
+ },
+
+ getViewPort: function(win) {
+ return DOMUtils.DOM.getViewPort(win);
+ },
+
+ get: function(id) {
+ return document.getElementById(id);
+ },
+
+ addClass: function(elm, cls) {
+ return DOMUtils.DOM.addClass(elm, cls);
+ },
+
+ removeClass: function(elm, cls) {
+ return DOMUtils.DOM.removeClass(elm, cls);
+ },
+
+ hasClass: function(elm, cls) {
+ return DOMUtils.DOM.hasClass(elm, cls);
+ },
+
+ toggleClass: function(elm, cls, state) {
+ return DOMUtils.DOM.toggleClass(elm, cls, state);
+ },
+
+ css: function(elm, name, value) {
+ return DOMUtils.DOM.setStyle(elm, name, value);
+ },
+
+ getRuntimeStyle: function(elm, name) {
+ return DOMUtils.DOM.getStyle(elm, name, true);
+ },
+
+ on: function(target, name, callback, scope) {
+ return DOMUtils.DOM.bind(target, name, callback, scope);
+ },
+
+ off: function(target, name, callback) {
+ return DOMUtils.DOM.unbind(target, name, callback);
+ },
+
+ fire: function(target, name, args) {
+ return DOMUtils.DOM.fire(target, name, args);
+ },
+
+ innerHtml: function(elm, html) {
+ // Workaround for <div> in <p> bug on IE 8 #6178
+ DOMUtils.DOM.setHTML(elm, html);
+ }
+ };
+
+ return funcs;
+});
+
+// Included from: js/tinymce/classes/ui/BoxUtils.js
+
+/**
+ * BoxUtils.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Utility class for box parsing and measuring.
+ *
+ * @private
+ * @class tinymce.ui.BoxUtils
+ */
+define("tinymce/ui/BoxUtils", [
+], function() {
+ "use strict";
+
+ return {
+ /**
+ * Parses the specified box value. A box value contains 1-4 properties in clockwise order.
+ *
+ * @method parseBox
+ * @param {String/Number} value Box value "0 1 2 3" or "0" etc.
+ * @return {Object} Object with top/right/bottom/left properties.
+ * @private
+ */
+ parseBox: function(value) {
+ var len, radix = 10;
+
+ if (!value) {
+ return;
+ }
+
+ if (typeof value === "number") {
+ value = value || 0;
+
+ return {
+ top: value,
+ left: value,
+ bottom: value,
+ right: value
+ };
+ }
+
+ value = value.split(' ');
+ len = value.length;
+
+ if (len === 1) {
+ value[1] = value[2] = value[3] = value[0];
+ } else if (len === 2) {
+ value[2] = value[0];
+ value[3] = value[1];
+ } else if (len === 3) {
+ value[3] = value[1];
+ }
+
+ return {
+ top: parseInt(value[0], radix) || 0,
+ right: parseInt(value[1], radix) || 0,
+ bottom: parseInt(value[2], radix) || 0,
+ left: parseInt(value[3], radix) || 0
+ };
+ },
+
+ measureBox: function(elm, prefix) {
+ function getStyle(name) {
+ var defaultView = document.defaultView;
+
+ if (defaultView) {
+ // Remove camelcase
+ name = name.replace(/[A-Z]/g, function(a) {
+ return '-' + a;
+ });
+
+ return defaultView.getComputedStyle(elm, null).getPropertyValue(name);
+ }
+
+ return elm.currentStyle[name];
+ }
+
+ function getSide(name) {
+ var val = parseFloat(getStyle(name), 10);
+
+ return isNaN(val) ? 0 : val;
+ }
+
+ return {
+ top: getSide(prefix + "TopWidth"),
+ right: getSide(prefix + "RightWidth"),
+ bottom: getSide(prefix + "BottomWidth"),
+ left: getSide(prefix + "LeftWidth")
+ };
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/ui/ClassList.js
+
+/**
+ * ClassList.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Handles adding and removal of classes.
+ *
+ * @private
+ * @class tinymce.ui.ClassList
+ */
+define("tinymce/ui/ClassList", [
+ "tinymce/util/Tools"
+], function(Tools) {
+ "use strict";
+
+ function noop() {
+ }
+
+ /**
+ * Constructs a new class list the specified onchange
+ * callback will be executed when the class list gets modifed.
+ *
+ * @constructor ClassList
+ * @param {function} onchange Onchange callback to be executed.
+ */
+ function ClassList(onchange) {
+ this.cls = [];
+ this.cls._map = {};
+ this.onchange = onchange || noop;
+ this.prefix = '';
+ }
+
+ Tools.extend(ClassList.prototype, {
+ /**
+ * Adds a new class to the class list.
+ *
+ * @method add
+ * @param {String} cls Class to be added.
+ * @return {tinymce.ui.ClassList} Current class list instance.
+ */
+ add: function(cls) {
+ if (cls && !this.contains(cls)) {
+ this.cls._map[cls] = true;
+ this.cls.push(cls);
+ this._change();
+ }
+
+ return this;
+ },
+
+ /**
+ * Removes the specified class from the class list.
+ *
+ * @method remove
+ * @param {String} cls Class to be removed.
+ * @return {tinymce.ui.ClassList} Current class list instance.
+ */
+ remove: function(cls) {
+ if (this.contains(cls)) {
+ for (var i = 0; i < this.cls.length; i++) {
+ if (this.cls[i] === cls) {
+ break;
+ }
+ }
+
+ this.cls.splice(i, 1);
+ delete this.cls._map[cls];
+ this._change();
+ }
+
+ return this;
+ },
+
+ /**
+ * Toggles a class in the class list.
+ *
+ * @method toggle
+ * @param {String} cls Class to be added/removed.
+ * @param {Boolean} state Optional state if it should be added/removed.
+ * @return {tinymce.ui.ClassList} Current class list instance.
+ */
+ toggle: function(cls, state) {
+ var curState = this.contains(cls);
+
+ if (curState !== state) {
+ if (curState) {
+ this.remove(cls);
+ } else {
+ this.add(cls);
+ }
+
+ this._change();
+ }
+
+ return this;
+ },
+
+ /**
+ * Returns true if the class list has the specified class.
+ *
+ * @method contains
+ * @param {String} cls Class to look for.
+ * @return {Boolean} true/false if the class exists or not.
+ */
+ contains: function(cls) {
+ return !!this.cls._map[cls];
+ },
+
+ /**
+ * Returns a space separated list of classes.
+ *
+ * @method toString
+ * @return {String} Space separated list of classes.
+ */
+
+ _change: function() {
+ delete this.clsValue;
+ this.onchange.call(this);
+ }
+ });
+
+ // IE 8 compatibility
+ ClassList.prototype.toString = function() {
+ var value;
+
+ if (this.clsValue) {
+ return this.clsValue;
+ }
+
+ value = '';
+ for (var i = 0; i < this.cls.length; i++) {
+ if (i > 0) {
+ value += ' ';
+ }
+
+ value += this.prefix + this.cls[i];
+ }
+
+ return value;
+ };
+
+ return ClassList;
+});
+
+// Included from: js/tinymce/classes/ui/ReflowQueue.js
+
+/**
+ * ReflowQueue.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class will automatically reflow controls on the next animation frame within a few milliseconds on older browsers.
+ * If the user manually reflows then the automatic reflow will be cancelled. This class is used internally when various control states
+ * changes that triggers a reflow.
+ *
+ * @class tinymce.ui.ReflowQueue
+ * @static
+ */
+define("tinymce/ui/ReflowQueue", [
+ "tinymce/util/Delay"
+], function(Delay) {
+ var dirtyCtrls = {}, animationFrameRequested;
+
+ return {
+ /**
+ * Adds a control to the next automatic reflow call. This is the control that had a state
+ * change for example if the control was hidden/shown.
+ *
+ * @method add
+ * @param {tinymce.ui.Control} ctrl Control to add to queue.
+ */
+ add: function(ctrl) {
+ var parent = ctrl.parent();
+
+ if (parent) {
+ if (!parent._layout || parent._layout.isNative()) {
+ return;
+ }
+
+ if (!dirtyCtrls[parent._id]) {
+ dirtyCtrls[parent._id] = parent;
+ }
+
+ if (!animationFrameRequested) {
+ animationFrameRequested = true;
+
+ Delay.requestAnimationFrame(function() {
+ var id, ctrl;
+
+ animationFrameRequested = false;
+
+ for (id in dirtyCtrls) {
+ ctrl = dirtyCtrls[id];
+
+ if (ctrl.state.get('rendered')) {
+ ctrl.reflow();
+ }
+ }
+
+ dirtyCtrls = {};
+ }, document.body);
+ }
+ }
+ },
+
+ /**
+ * Removes the specified control from the automatic reflow. This will happen when for example the user
+ * manually triggers a reflow.
+ *
+ * @method remove
+ * @param {tinymce.ui.Control} ctrl Control to remove from queue.
+ */
+ remove: function(ctrl) {
+ if (dirtyCtrls[ctrl._id]) {
+ delete dirtyCtrls[ctrl._id];
+ }
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/ui/Control.js
+
+/**
+ * Control.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*eslint consistent-this:0 */
+
+/**
+ * This is the base class for all controls and containers. All UI control instances inherit
+ * from this one as it has the base logic needed by all of them.
+ *
+ * @class tinymce.ui.Control
+ */
+define("tinymce/ui/Control", [
+ "tinymce/util/Class",
+ "tinymce/util/Tools",
+ "tinymce/util/EventDispatcher",
+ "tinymce/data/ObservableObject",
+ "tinymce/ui/Collection",
+ "tinymce/ui/DomUtils",
+ "tinymce/dom/DomQuery",
+ "tinymce/ui/BoxUtils",
+ "tinymce/ui/ClassList",
+ "tinymce/ui/ReflowQueue"
+], function(Class, Tools, EventDispatcher, ObservableObject, Collection, DomUtils, $, BoxUtils, ClassList, ReflowQueue) {
+ "use strict";
+
+ var hasMouseWheelEventSupport = "onmousewheel" in document;
+ var hasWheelEventSupport = false;
+ var classPrefix = "mce-";
+ var Control, idCounter = 0;
+
+ var proto = {
+ Statics: {
+ classPrefix: classPrefix
+ },
+
+ isRtl: function() {
+ return Control.rtl;
+ },
+
+ /**
+ * Class/id prefix to use for all controls.
+ *
+ * @final
+ * @field {String} classPrefix
+ */
+ classPrefix: classPrefix,
+
+ /**
+ * Constructs a new control instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {String} style Style CSS properties to add.
+ * @setting {String} border Border box values example: 1 1 1 1
+ * @setting {String} padding Padding box values example: 1 1 1 1
+ * @setting {String} margin Margin box values example: 1 1 1 1
+ * @setting {Number} minWidth Minimal width for the control.
+ * @setting {Number} minHeight Minimal height for the control.
+ * @setting {String} classes Space separated list of classes to add.
+ * @setting {String} role WAI-ARIA role to use for control.
+ * @setting {Boolean} hidden Is the control hidden by default.
+ * @setting {Boolean} disabled Is the control disabled by default.
+ * @setting {String} name Name of the control instance.
+ */
+ init: function(settings) {
+ var self = this, classes, defaultClasses;
+
+ function applyClasses(classes) {
+ var i;
+
+ classes = classes.split(' ');
+ for (i = 0; i < classes.length; i++) {
+ self.classes.add(classes[i]);
+ }
+ }
+
+ self.settings = settings = Tools.extend({}, self.Defaults, settings);
+
+ // Initial states
+ self._id = settings.id || ('mceu_' + (idCounter++));
+ self._aria = {role: settings.role};
+ self._elmCache = {};
+ self.$ = $;
+
+ self.state = new ObservableObject({
+ visible: true,
+ active: false,
+ disabled: false,
+ value: ''
+ });
+
+ self.data = new ObservableObject(settings.data);
+
+ self.classes = new ClassList(function() {
+ if (self.state.get('rendered')) {
+ self.getEl().className = this.toString();
+ }
+ });
+ self.classes.prefix = self.classPrefix;
+
+ // Setup classes
+ classes = settings.classes;
+ if (classes) {
+ if (self.Defaults) {
+ defaultClasses = self.Defaults.classes;
+
+ if (defaultClasses && classes != defaultClasses) {
+ applyClasses(defaultClasses);
+ }
+ }
+
+ applyClasses(classes);
+ }
+
+ Tools.each('title text name visible disabled active value'.split(' '), function(name) {
+ if (name in settings) {
+ self[name](settings[name]);
+ }
+ });
+
+ self.on('click', function() {
+ if (self.disabled()) {
+ return false;
+ }
+ });
+
+ /**
+ * Name/value object with settings for the current control.
+ *
+ * @field {Object} settings
+ */
+ self.settings = settings;
+
+ self.borderBox = BoxUtils.parseBox(settings.border);
+ self.paddingBox = BoxUtils.parseBox(settings.padding);
+ self.marginBox = BoxUtils.parseBox(settings.margin);
+
+ if (settings.hidden) {
+ self.hide();
+ }
+ },
+
+ // Will generate getter/setter methods for these properties
+ Properties: 'parent,name',
+
+ /**
+ * Returns the root element to render controls into.
+ *
+ * @method getContainerElm
+ * @return {Element} HTML DOM element to render into.
+ */
+ getContainerElm: function() {
+ return DomUtils.getContainer();
+ },
+
+ /**
+ * Returns a control instance for the current DOM element.
+ *
+ * @method getParentCtrl
+ * @param {Element} elm HTML dom element to get parent control from.
+ * @return {tinymce.ui.Control} Control instance or undefined.
+ */
+ getParentCtrl: function(elm) {
+ var ctrl, lookup = this.getRoot().controlIdLookup;
+
+ while (elm && lookup) {
+ ctrl = lookup[elm.id];
+ if (ctrl) {
+ break;
+ }
+
+ elm = elm.parentNode;
+ }
+
+ return ctrl;
+ },
+
+ /**
+ * Initializes the current controls layout rect.
+ * This will be executed by the layout managers to determine the
+ * default minWidth/minHeight etc.
+ *
+ * @method initLayoutRect
+ * @return {Object} Layout rect instance.
+ */
+ initLayoutRect: function() {
+ var self = this, settings = self.settings, borderBox, layoutRect;
+ var elm = self.getEl(), width, height, minWidth, minHeight, autoResize;
+ var startMinWidth, startMinHeight, initialSize;
+
+ // Measure the current element
+ borderBox = self.borderBox = self.borderBox || BoxUtils.measureBox(elm, 'border');
+ self.paddingBox = self.paddingBox || BoxUtils.measureBox(elm, 'padding');
+ self.marginBox = self.marginBox || BoxUtils.measureBox(elm, 'margin');
+ initialSize = DomUtils.getSize(elm);
+
+ // Setup minWidth/minHeight and width/height
+ startMinWidth = settings.minWidth;
+ startMinHeight = settings.minHeight;
+ minWidth = startMinWidth || initialSize.width;
+ minHeight = startMinHeight || initialSize.height;
+ width = settings.width;
+ height = settings.height;
+ autoResize = settings.autoResize;
+ autoResize = typeof autoResize != "undefined" ? autoResize : !width && !height;
+
+ width = width || minWidth;
+ height = height || minHeight;
+
+ var deltaW = borderBox.left + borderBox.right;
+ var deltaH = borderBox.top + borderBox.bottom;
+
+ var maxW = settings.maxWidth || 0xFFFF;
+ var maxH = settings.maxHeight || 0xFFFF;
+
+ // Setup initial layout rect
+ self._layoutRect = layoutRect = {
+ x: settings.x || 0,
+ y: settings.y || 0,
+ w: width,
+ h: height,
+ deltaW: deltaW,
+ deltaH: deltaH,
+ contentW: width - deltaW,
+ contentH: height - deltaH,
+ innerW: width - deltaW,
+ innerH: height - deltaH,
+ startMinWidth: startMinWidth || 0,
+ startMinHeight: startMinHeight || 0,
+ minW: Math.min(minWidth, maxW),
+ minH: Math.min(minHeight, maxH),
+ maxW: maxW,
+ maxH: maxH,
+ autoResize: autoResize,
+ scrollW: 0
+ };
+
+ self._lastLayoutRect = {};
+
+ return layoutRect;
+ },
+
+ /**
+ * Getter/setter for the current layout rect.
+ *
+ * @method layoutRect
+ * @param {Object} [newRect] Optional new layout rect.
+ * @return {tinymce.ui.Control/Object} Current control or rect object.
+ */
+ layoutRect: function(newRect) {
+ var self = this, curRect = self._layoutRect, lastLayoutRect, size, deltaWidth, deltaHeight, undef, repaintControls;
+
+ // Initialize default layout rect
+ if (!curRect) {
+ curRect = self.initLayoutRect();
+ }
+
+ // Set new rect values
+ if (newRect) {
+ // Calc deltas between inner and outer sizes
+ deltaWidth = curRect.deltaW;
+ deltaHeight = curRect.deltaH;
+
+ // Set x position
+ if (newRect.x !== undef) {
+ curRect.x = newRect.x;
+ }
+
+ // Set y position
+ if (newRect.y !== undef) {
+ curRect.y = newRect.y;
+ }
+
+ // Set minW
+ if (newRect.minW !== undef) {
+ curRect.minW = newRect.minW;
+ }
+
+ // Set minH
+ if (newRect.minH !== undef) {
+ curRect.minH = newRect.minH;
+ }
+
+ // Set new width and calculate inner width
+ size = newRect.w;
+ if (size !== undef) {
+ size = size < curRect.minW ? curRect.minW : size;
+ size = size > curRect.maxW ? curRect.maxW : size;
+ curRect.w = size;
+ curRect.innerW = size - deltaWidth;
+ }
+
+ // Set new height and calculate inner height
+ size = newRect.h;
+ if (size !== undef) {
+ size = size < curRect.minH ? curRect.minH : size;
+ size = size > curRect.maxH ? curRect.maxH : size;
+ curRect.h = size;
+ curRect.innerH = size - deltaHeight;
+ }
+
+ // Set new inner width and calculate width
+ size = newRect.innerW;
+ if (size !== undef) {
+ size = size < curRect.minW - deltaWidth ? curRect.minW - deltaWidth : size;
+ size = size > curRect.maxW - deltaWidth ? curRect.maxW - deltaWidth : size;
+ curRect.innerW = size;
+ curRect.w = size + deltaWidth;
+ }
+
+ // Set new height and calculate inner height
+ size = newRect.innerH;
+ if (size !== undef) {
+ size = size < curRect.minH - deltaHeight ? curRect.minH - deltaHeight : size;
+ size = size > curRect.maxH - deltaHeight ? curRect.maxH - deltaHeight : size;
+ curRect.innerH = size;
+ curRect.h = size + deltaHeight;
+ }
+
+ // Set new contentW
+ if (newRect.contentW !== undef) {
+ curRect.contentW = newRect.contentW;
+ }
+
+ // Set new contentH
+ if (newRect.contentH !== undef) {
+ curRect.contentH = newRect.contentH;
+ }
+
+ // Compare last layout rect with the current one to see if we need to repaint or not
+ lastLayoutRect = self._lastLayoutRect;
+ if (lastLayoutRect.x !== curRect.x || lastLayoutRect.y !== curRect.y ||
+ lastLayoutRect.w !== curRect.w || lastLayoutRect.h !== curRect.h) {
+ repaintControls = Control.repaintControls;
+
+ if (repaintControls) {
+ if (repaintControls.map && !repaintControls.map[self._id]) {
+ repaintControls.push(self);
+ repaintControls.map[self._id] = true;
+ }
+ }
+
+ lastLayoutRect.x = curRect.x;
+ lastLayoutRect.y = curRect.y;
+ lastLayoutRect.w = curRect.w;
+ lastLayoutRect.h = curRect.h;
+ }
+
+ return self;
+ }
+
+ return curRect;
+ },
+
+ /**
+ * Repaints the control after a layout operation.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ var self = this, style, bodyStyle, bodyElm, rect, borderBox;
+ var borderW, borderH, lastRepaintRect, round, value;
+
+ // Use Math.round on all values on IE < 9
+ round = !document.createRange ? Math.round : function(value) {
+ return value;
+ };
+
+ style = self.getEl().style;
+ rect = self._layoutRect;
+ lastRepaintRect = self._lastRepaintRect || {};
+
+ borderBox = self.borderBox;
+ borderW = borderBox.left + borderBox.right;
+ borderH = borderBox.top + borderBox.bottom;
+
+ if (rect.x !== lastRepaintRect.x) {
+ style.left = round(rect.x) + 'px';
+ lastRepaintRect.x = rect.x;
+ }
+
+ if (rect.y !== lastRepaintRect.y) {
+ style.top = round(rect.y) + 'px';
+ lastRepaintRect.y = rect.y;
+ }
+
+ if (rect.w !== lastRepaintRect.w) {
+ value = round(rect.w - borderW);
+ style.width = (value >= 0 ? value : 0) + 'px';
+ lastRepaintRect.w = rect.w;
+ }
+
+ if (rect.h !== lastRepaintRect.h) {
+ value = round(rect.h - borderH);
+ style.height = (value >= 0 ? value : 0) + 'px';
+ lastRepaintRect.h = rect.h;
+ }
+
+ // Update body if needed
+ if (self._hasBody && rect.innerW !== lastRepaintRect.innerW) {
+ value = round(rect.innerW);
+
+ bodyElm = self.getEl('body');
+ if (bodyElm) {
+ bodyStyle = bodyElm.style;
+ bodyStyle.width = (value >= 0 ? value : 0) + 'px';
+ }
+
+ lastRepaintRect.innerW = rect.innerW;
+ }
+
+ if (self._hasBody && rect.innerH !== lastRepaintRect.innerH) {
+ value = round(rect.innerH);
+
+ bodyElm = bodyElm || self.getEl('body');
+ if (bodyElm) {
+ bodyStyle = bodyStyle || bodyElm.style;
+ bodyStyle.height = (value >= 0 ? value : 0) + 'px';
+ }
+
+ lastRepaintRect.innerH = rect.innerH;
+ }
+
+ self._lastRepaintRect = lastRepaintRect;
+ self.fire('repaint', {}, false);
+ },
+
+ /**
+ * Updates the controls layout rect by re-measuing it.
+ */
+ updateLayoutRect: function() {
+ var self = this;
+
+ self.parent()._lastRect = null;
+
+ DomUtils.css(self.getEl(), {width: '', height: ''});
+
+ self._layoutRect = self._lastRepaintRect = self._lastLayoutRect = null;
+ self.initLayoutRect();
+ },
+
+ /**
+ * Binds a callback to the specified event. This event can both be
+ * native browser events like "click" or custom ones like PostRender.
+ *
+ * The callback function will be passed a DOM event like object that enables yout do stop propagation.
+ *
+ * @method on
+ * @param {String} name Name of the event to bind. For example "click".
+ * @param {String/function} callback Callback function to execute ones the event occurs.
+ * @return {tinymce.ui.Control} Current control object.
+ */
+ on: function(name, callback) {
+ var self = this;
+
+ function resolveCallbackName(name) {
+ var callback, scope;
+
+ if (typeof name != 'string') {
+ return name;
+ }
+
+ return function(e) {
+ if (!callback) {
+ self.parentsAndSelf().each(function(ctrl) {
+ var callbacks = ctrl.settings.callbacks;
+
+ if (callbacks && (callback = callbacks[name])) {
+ scope = ctrl;
+ return false;
+ }
+ });
+ }
+
+ if (!callback) {
+ e.action = name;
+ this.fire('execute', e);
+ return;
+ }
+
+ return callback.call(scope, e);
+ };
+ }
+
+ getEventDispatcher(self).on(name, resolveCallbackName(callback));
+
+ return self;
+ },
+
+ /**
+ * Unbinds the specified event and optionally a specific callback. If you omit the name
+ * parameter all event handlers will be removed. If you omit the callback all event handles
+ * by the specified name will be removed.
+ *
+ * @method off
+ * @param {String} [name] Name for the event to unbind.
+ * @param {function} [callback] Callback function to unbind.
+ * @return {tinymce.ui.Control} Current control object.
+ */
+ off: function(name, callback) {
+ getEventDispatcher(this).off(name, callback);
+ return this;
+ },
+
+ /**
+ * Fires the specified event by name and arguments on the control. This will execute all
+ * bound event handlers.
+ *
+ * @method fire
+ * @param {String} name Name of the event to fire.
+ * @param {Object} [args] Arguments to pass to the event.
+ * @param {Boolean} [bubble] Value to control bubbling. Defaults to true.
+ * @return {Object} Current arguments object.
+ */
+ fire: function(name, args, bubble) {
+ var self = this;
+
+ args = args || {};
+
+ if (!args.control) {
+ args.control = self;
+ }
+
+ args = getEventDispatcher(self).fire(name, args);
+
+ // Bubble event up to parents
+ if (bubble !== false && self.parent) {
+ var parent = self.parent();
+ while (parent && !args.isPropagationStopped()) {
+ parent.fire(name, args, false);
+ parent = parent.parent();
+ }
+ }
+
+ return args;
+ },
+
+ /**
+ * Returns true/false if the specified event has any listeners.
+ *
+ * @method hasEventListeners
+ * @param {String} name Name of the event to check for.
+ * @return {Boolean} True/false state if the event has listeners.
+ */
+ hasEventListeners: function(name) {
+ return getEventDispatcher(this).has(name);
+ },
+
+ /**
+ * Returns a control collection with all parent controls.
+ *
+ * @method parents
+ * @param {String} selector Optional selector expression to find parents.
+ * @return {tinymce.ui.Collection} Collection with all parent controls.
+ */
+ parents: function(selector) {
+ var self = this, ctrl, parents = new Collection();
+
+ // Add each parent to collection
+ for (ctrl = self.parent(); ctrl; ctrl = ctrl.parent()) {
+ parents.add(ctrl);
+ }
+
+ // Filter away everything that doesn't match the selector
+ if (selector) {
+ parents = parents.filter(selector);
+ }
+
+ return parents;
+ },
+
+ /**
+ * Returns the current control and it's parents.
+ *
+ * @method parentsAndSelf
+ * @param {String} selector Optional selector expression to find parents.
+ * @return {tinymce.ui.Collection} Collection with all parent controls.
+ */
+ parentsAndSelf: function(selector) {
+ return new Collection(this).add(this.parents(selector));
+ },
+
+ /**
+ * Returns the control next to the current control.
+ *
+ * @method next
+ * @return {tinymce.ui.Control} Next control instance.
+ */
+ next: function() {
+ var parentControls = this.parent().items();
+
+ return parentControls[parentControls.indexOf(this) + 1];
+ },
+
+ /**
+ * Returns the control previous to the current control.
+ *
+ * @method prev
+ * @return {tinymce.ui.Control} Previous control instance.
+ */
+ prev: function() {
+ var parentControls = this.parent().items();
+
+ return parentControls[parentControls.indexOf(this) - 1];
+ },
+
+ /**
+ * Sets the inner HTML of the control element.
+ *
+ * @method innerHtml
+ * @param {String} html Html string to set as inner html.
+ * @return {tinymce.ui.Control} Current control object.
+ */
+ innerHtml: function(html) {
+ this.$el.html(html);
+ return this;
+ },
+
+ /**
+ * Returns the control DOM element or sub element.
+ *
+ * @method getEl
+ * @param {String} [suffix] Suffix to get element by.
+ * @return {Element} HTML DOM element for the current control or it's children.
+ */
+ getEl: function(suffix) {
+ var id = suffix ? this._id + '-' + suffix : this._id;
+
+ if (!this._elmCache[id]) {
+ this._elmCache[id] = $('#' + id)[0];
+ }
+
+ return this._elmCache[id];
+ },
+
+ /**
+ * Sets the visible state to true.
+ *
+ * @method show
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ show: function() {
+ return this.visible(true);
+ },
+
+ /**
+ * Sets the visible state to false.
+ *
+ * @method hide
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ hide: function() {
+ return this.visible(false);
+ },
+
+ /**
+ * Focuses the current control.
+ *
+ * @method focus
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ focus: function() {
+ try {
+ this.getEl().focus();
+ } catch (ex) {
+ // Ignore IE error
+ }
+
+ return this;
+ },
+
+ /**
+ * Blurs the current control.
+ *
+ * @method blur
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ blur: function() {
+ this.getEl().blur();
+
+ return this;
+ },
+
+ /**
+ * Sets the specified aria property.
+ *
+ * @method aria
+ * @param {String} name Name of the aria property to set.
+ * @param {String} value Value of the aria property.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ aria: function(name, value) {
+ var self = this, elm = self.getEl(self.ariaTarget);
+
+ if (typeof value === "undefined") {
+ return self._aria[name];
+ }
+
+ self._aria[name] = value;
+
+ if (self.state.get('rendered')) {
+ elm.setAttribute(name == 'role' ? name : 'aria-' + name, value);
+ }
+
+ return self;
+ },
+
+ /**
+ * Encodes the specified string with HTML entities. It will also
+ * translate the string to different languages.
+ *
+ * @method encode
+ * @param {String/Object/Array} text Text to entity encode.
+ * @param {Boolean} [translate=true] False if the contents shouldn't be translated.
+ * @return {String} Encoded and possible traslated string.
+ */
+ encode: function(text, translate) {
+ if (translate !== false) {
+ text = this.translate(text);
+ }
+
+ return (text || '').replace(/[&<>"]/g, function(match) {
+ return '&#' + match.charCodeAt(0) + ';';
+ });
+ },
+
+ /**
+ * Returns the translated string.
+ *
+ * @method translate
+ * @param {String} text Text to translate.
+ * @return {String} Translated string or the same as the input.
+ */
+ translate: function(text) {
+ return Control.translate ? Control.translate(text) : text;
+ },
+
+ /**
+ * Adds items before the current control.
+ *
+ * @method before
+ * @param {Array/tinymce.ui.Collection} items Array of items to prepend before this control.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ before: function(items) {
+ var self = this, parent = self.parent();
+
+ if (parent) {
+ parent.insert(items, parent.items().indexOf(self), true);
+ }
+
+ return self;
+ },
+
+ /**
+ * Adds items after the current control.
+ *
+ * @method after
+ * @param {Array/tinymce.ui.Collection} items Array of items to append after this control.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ after: function(items) {
+ var self = this, parent = self.parent();
+
+ if (parent) {
+ parent.insert(items, parent.items().indexOf(self));
+ }
+
+ return self;
+ },
+
+ /**
+ * Removes the current control from DOM and from UI collections.
+ *
+ * @method remove
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ remove: function() {
+ var self = this, elm = self.getEl(), parent = self.parent(), newItems, i;
+
+ if (self.items) {
+ var controls = self.items().toArray();
+ i = controls.length;
+ while (i--) {
+ controls[i].remove();
+ }
+ }
+
+ if (parent && parent.items) {
+ newItems = [];
+
+ parent.items().each(function(item) {
+ if (item !== self) {
+ newItems.push(item);
+ }
+ });
+
+ parent.items().set(newItems);
+ parent._lastRect = null;
+ }
+
+ if (self._eventsRoot && self._eventsRoot == self) {
+ $(elm).off();
+ }
+
+ var lookup = self.getRoot().controlIdLookup;
+ if (lookup) {
+ delete lookup[self._id];
+ }
+
+ if (elm && elm.parentNode) {
+ elm.parentNode.removeChild(elm);
+ }
+
+ self.state.set('rendered', false);
+ self.state.destroy();
+
+ self.fire('remove');
+
+ return self;
+ },
+
+ /**
+ * Renders the control before the specified element.
+ *
+ * @method renderBefore
+ * @param {Element} elm Element to render before.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ renderBefore: function(elm) {
+ $(elm).before(this.renderHtml());
+ this.postRender();
+ return this;
+ },
+
+ /**
+ * Renders the control to the specified element.
+ *
+ * @method renderBefore
+ * @param {Element} elm Element to render to.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ renderTo: function(elm) {
+ $(elm || this.getContainerElm()).append(this.renderHtml());
+ this.postRender();
+ return this;
+ },
+
+ preRender: function() {
+ },
+
+ render: function() {
+ },
+
+ renderHtml: function() {
+ return '<div id="' + this._id + '" class="' + this.classes + '"></div>';
+ },
+
+ /**
+ * Post render method. Called after the control has been rendered to the target.
+ *
+ * @method postRender
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ postRender: function() {
+ var self = this, settings = self.settings, elm, box, parent, name, parentEventsRoot;
+
+ self.$el = $(self.getEl());
+ self.state.set('rendered', true);
+
+ // Bind on<event> settings
+ for (name in settings) {
+ if (name.indexOf("on") === 0) {
+ self.on(name.substr(2), settings[name]);
+ }
+ }
+
+ if (self._eventsRoot) {
+ for (parent = self.parent(); !parentEventsRoot && parent; parent = parent.parent()) {
+ parentEventsRoot = parent._eventsRoot;
+ }
+
+ if (parentEventsRoot) {
+ for (name in parentEventsRoot._nativeEvents) {
+ self._nativeEvents[name] = true;
+ }
+ }
+ }
+
+ bindPendingEvents(self);
+
+ if (settings.style) {
+ elm = self.getEl();
+ if (elm) {
+ elm.setAttribute('style', settings.style);
+ elm.style.cssText = settings.style;
+ }
+ }
+
+ if (self.settings.border) {
+ box = self.borderBox;
+ self.$el.css({
+ 'border-top-width': box.top,
+ 'border-right-width': box.right,
+ 'border-bottom-width': box.bottom,
+ 'border-left-width': box.left
+ });
+ }
+
+ // Add instance to lookup
+ var root = self.getRoot();
+ if (!root.controlIdLookup) {
+ root.controlIdLookup = {};
+ }
+
+ root.controlIdLookup[self._id] = self;
+
+ for (var key in self._aria) {
+ self.aria(key, self._aria[key]);
+ }
+
+ if (self.state.get('visible') === false) {
+ self.getEl().style.display = 'none';
+ }
+
+ self.bindStates();
+
+ self.state.on('change:visible', function(e) {
+ var state = e.value, parentCtrl;
+
+ if (self.state.get('rendered')) {
+ self.getEl().style.display = state === false ? 'none' : '';
+
+ // Need to force a reflow here on IE 8
+ self.getEl().getBoundingClientRect();
+ }
+
+ // Parent container needs to reflow
+ parentCtrl = self.parent();
+ if (parentCtrl) {
+ parentCtrl._lastRect = null;
+ }
+
+ self.fire(state ? 'show' : 'hide');
+
+ ReflowQueue.add(self);
+ });
+
+ self.fire('postrender', {}, false);
+ },
+
+ bindStates: function() {
+ },
+
+ /**
+ * Scrolls the current control into view.
+ *
+ * @method scrollIntoView
+ * @param {String} align Alignment in view top|center|bottom.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ scrollIntoView: function(align) {
+ function getOffset(elm, rootElm) {
+ var x, y, parent = elm;
+
+ x = y = 0;
+ while (parent && parent != rootElm && parent.nodeType) {
+ x += parent.offsetLeft || 0;
+ y += parent.offsetTop || 0;
+ parent = parent.offsetParent;
+ }
+
+ return {x: x, y: y};
+ }
+
+ var elm = this.getEl(), parentElm = elm.parentNode;
+ var x, y, width, height, parentWidth, parentHeight;
+ var pos = getOffset(elm, parentElm);
+
+ x = pos.x;
+ y = pos.y;
+ width = elm.offsetWidth;
+ height = elm.offsetHeight;
+ parentWidth = parentElm.clientWidth;
+ parentHeight = parentElm.clientHeight;
+
+ if (align == "end") {
+ x -= parentWidth - width;
+ y -= parentHeight - height;
+ } else if (align == "center") {
+ x -= (parentWidth / 2) - (width / 2);
+ y -= (parentHeight / 2) - (height / 2);
+ }
+
+ parentElm.scrollLeft = x;
+ parentElm.scrollTop = y;
+
+ return this;
+ },
+
+ getRoot: function() {
+ var ctrl = this, rootControl, parents = [];
+
+ while (ctrl) {
+ if (ctrl.rootControl) {
+ rootControl = ctrl.rootControl;
+ break;
+ }
+
+ parents.push(ctrl);
+ rootControl = ctrl;
+ ctrl = ctrl.parent();
+ }
+
+ if (!rootControl) {
+ rootControl = this;
+ }
+
+ var i = parents.length;
+ while (i--) {
+ parents[i].rootControl = rootControl;
+ }
+
+ return rootControl;
+ },
+
+ /**
+ * Reflows the current control and it's parents.
+ * This should be used after you for example append children to the current control so
+ * that the layout managers know that they need to reposition everything.
+ *
+ * @example
+ * container.append({type: 'button', text: 'My button'}).reflow();
+ *
+ * @method reflow
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ reflow: function() {
+ ReflowQueue.remove(this);
+
+ var parent = this.parent();
+ if (parent._layout && !parent._layout.isNative()) {
+ parent.reflow();
+ }
+
+ return this;
+ }
+
+ /**
+ * Sets/gets the parent container for the control.
+ *
+ * @method parent
+ * @param {tinymce.ui.Container} parent Optional parent to set.
+ * @return {tinymce.ui.Control} Parent control or the current control on a set action.
+ */
+ // parent: function(parent) {} -- Generated
+
+ /**
+ * Sets/gets the text for the control.
+ *
+ * @method text
+ * @param {String} value Value to set to control.
+ * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get.
+ */
+ // text: function(value) {} -- Generated
+
+ /**
+ * Sets/gets the disabled state on the control.
+ *
+ * @method disabled
+ * @param {Boolean} state Value to set to control.
+ * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get.
+ */
+ // disabled: function(state) {} -- Generated
+
+ /**
+ * Sets/gets the active for the control.
+ *
+ * @method active
+ * @param {Boolean} state Value to set to control.
+ * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get.
+ */
+ // active: function(state) {} -- Generated
+
+ /**
+ * Sets/gets the name for the control.
+ *
+ * @method name
+ * @param {String} value Value to set to control.
+ * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get.
+ */
+ // name: function(value) {} -- Generated
+
+ /**
+ * Sets/gets the title for the control.
+ *
+ * @method title
+ * @param {String} value Value to set to control.
+ * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get.
+ */
+ // title: function(value) {} -- Generated
+
+ /**
+ * Sets/gets the visible for the control.
+ *
+ * @method visible
+ * @param {Boolean} state Value to set to control.
+ * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get.
+ */
+ // visible: function(value) {} -- Generated
+ };
+
+ /**
+ * Setup state properties.
+ */
+ Tools.each('text title visible disabled active value'.split(' '), function(name) {
+ proto[name] = function(value) {
+ if (arguments.length === 0) {
+ return this.state.get(name);
+ }
+
+ if (typeof value != "undefined") {
+ this.state.set(name, value);
+ }
+
+ return this;
+ };
+ });
+
+ Control = Class.extend(proto);
+
+ function getEventDispatcher(obj) {
+ if (!obj._eventDispatcher) {
+ obj._eventDispatcher = new EventDispatcher({
+ scope: obj,
+ toggleEvent: function(name, state) {
+ if (state && EventDispatcher.isNative(name)) {
+ if (!obj._nativeEvents) {
+ obj._nativeEvents = {};
+ }
+
+ obj._nativeEvents[name] = true;
+
+ if (obj.state.get('rendered')) {
+ bindPendingEvents(obj);
+ }
+ }
+ }
+ });
+ }
+
+ return obj._eventDispatcher;
+ }
+
+ function bindPendingEvents(eventCtrl) {
+ var i, l, parents, eventRootCtrl, nativeEvents, name;
+
+ function delegate(e) {
+ var control = eventCtrl.getParentCtrl(e.target);
+
+ if (control) {
+ control.fire(e.type, e);
+ }
+ }
+
+ function mouseLeaveHandler() {
+ var ctrl = eventRootCtrl._lastHoverCtrl;
+
+ if (ctrl) {
+ ctrl.fire("mouseleave", {target: ctrl.getEl()});
+
+ ctrl.parents().each(function(ctrl) {
+ ctrl.fire("mouseleave", {target: ctrl.getEl()});
+ });
+
+ eventRootCtrl._lastHoverCtrl = null;
+ }
+ }
+
+ function mouseEnterHandler(e) {
+ var ctrl = eventCtrl.getParentCtrl(e.target), lastCtrl = eventRootCtrl._lastHoverCtrl, idx = 0, i, parents, lastParents;
+
+ // Over on a new control
+ if (ctrl !== lastCtrl) {
+ eventRootCtrl._lastHoverCtrl = ctrl;
+
+ parents = ctrl.parents().toArray().reverse();
+ parents.push(ctrl);
+
+ if (lastCtrl) {
+ lastParents = lastCtrl.parents().toArray().reverse();
+ lastParents.push(lastCtrl);
+
+ for (idx = 0; idx < lastParents.length; idx++) {
+ if (parents[idx] !== lastParents[idx]) {
+ break;
+ }
+ }
+
+ for (i = lastParents.length - 1; i >= idx; i--) {
+ lastCtrl = lastParents[i];
+ lastCtrl.fire("mouseleave", {
+ target: lastCtrl.getEl()
+ });
+ }
+ }
+
+ for (i = idx; i < parents.length; i++) {
+ ctrl = parents[i];
+ ctrl.fire("mouseenter", {
+ target: ctrl.getEl()
+ });
+ }
+ }
+ }
+
+ function fixWheelEvent(e) {
+ e.preventDefault();
+
+ if (e.type == "mousewheel") {
+ e.deltaY = -1 / 40 * e.wheelDelta;
+
+ if (e.wheelDeltaX) {
+ e.deltaX = -1 / 40 * e.wheelDeltaX;
+ }
+ } else {
+ e.deltaX = 0;
+ e.deltaY = e.detail;
+ }
+
+ e = eventCtrl.fire("wheel", e);
+ }
+
+ nativeEvents = eventCtrl._nativeEvents;
+ if (nativeEvents) {
+ // Find event root element if it exists
+ parents = eventCtrl.parents().toArray();
+ parents.unshift(eventCtrl);
+ for (i = 0, l = parents.length; !eventRootCtrl && i < l; i++) {
+ eventRootCtrl = parents[i]._eventsRoot;
+ }
+
+ // Event root wasn't found the use the root control
+ if (!eventRootCtrl) {
+ eventRootCtrl = parents[parents.length - 1] || eventCtrl;
+ }
+
+ // Set the eventsRoot property on children that didn't have it
+ eventCtrl._eventsRoot = eventRootCtrl;
+ for (l = i, i = 0; i < l; i++) {
+ parents[i]._eventsRoot = eventRootCtrl;
+ }
+
+ var eventRootDelegates = eventRootCtrl._delegates;
+ if (!eventRootDelegates) {
+ eventRootDelegates = eventRootCtrl._delegates = {};
+ }
+
+ // Bind native event delegates
+ for (name in nativeEvents) {
+ if (!nativeEvents) {
+ return false;
+ }
+
+ if (name === "wheel" && !hasWheelEventSupport) {
+ if (hasMouseWheelEventSupport) {
+ $(eventCtrl.getEl()).on("mousewheel", fixWheelEvent);
+ } else {
+ $(eventCtrl.getEl()).on("DOMMouseScroll", fixWheelEvent);
+ }
+
+ continue;
+ }
+
+ // Special treatment for mousenter/mouseleave since these doesn't bubble
+ if (name === "mouseenter" || name === "mouseleave") {
+ // Fake mousenter/mouseleave
+ if (!eventRootCtrl._hasMouseEnter) {
+ $(eventRootCtrl.getEl()).on("mouseleave", mouseLeaveHandler).on("mouseover", mouseEnterHandler);
+ eventRootCtrl._hasMouseEnter = 1;
+ }
+ } else if (!eventRootDelegates[name]) {
+ $(eventRootCtrl.getEl()).on(name, delegate);
+ eventRootDelegates[name] = true;
+ }
+
+ // Remove the event once it's bound
+ nativeEvents[name] = false;
+ }
+ }
+ }
+
+ return Control;
+});
+
+// Included from: js/tinymce/classes/ui/Factory.js
+
+/**
+ * Factory.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*global tinymce:true */
+
+/**
+ * This class is a factory for control instances. This enables you
+ * to create instances of controls without having to require the UI controls directly.
+ *
+ * It also allow you to override or add new control types.
+ *
+ * @class tinymce.ui.Factory
+ */
+define("tinymce/ui/Factory", [], function() {
+ "use strict";
+
+ var types = {}, namespaceInit;
+
+ return {
+ /**
+ * Adds a new control instance type to the factory.
+ *
+ * @method add
+ * @param {String} type Type name for example "button".
+ * @param {function} typeClass Class type function.
+ */
+ add: function(type, typeClass) {
+ types[type.toLowerCase()] = typeClass;
+ },
+
+ /**
+ * Returns true/false if the specified type exists or not.
+ *
+ * @method has
+ * @param {String} type Type to look for.
+ * @return {Boolean} true/false if the control by name exists.
+ */
+ has: function(type) {
+ return !!types[type.toLowerCase()];
+ },
+
+ /**
+ * Creates a new control instance based on the settings provided. The instance created will be
+ * based on the specified type property it can also create whole structures of components out of
+ * the specified JSON object.
+ *
+ * @example
+ * tinymce.ui.Factory.create({
+ * type: 'button',
+ * text: 'Hello world!'
+ * });
+ *
+ * @method create
+ * @param {Object/String} settings Name/Value object with items used to create the type.
+ * @return {tinymce.ui.Control} Control instance based on the specified type.
+ */
+ create: function(type, settings) {
+ var ControlType, name, namespace;
+
+ // Build type lookup
+ if (!namespaceInit) {
+ namespace = tinymce.ui;
+
+ for (name in namespace) {
+ types[name.toLowerCase()] = namespace[name];
+ }
+
+ namespaceInit = true;
+ }
+
+ // If string is specified then use it as the type
+ if (typeof type == 'string') {
+ settings = settings || {};
+ settings.type = type;
+ } else {
+ settings = type;
+ type = settings.type;
+ }
+
+ // Find control type
+ type = type.toLowerCase();
+ ControlType = types[type];
+
+ // #if debug
+
+ if (!ControlType) {
+ throw new Error("Could not find control by type: " + type);
+ }
+
+ // #endif
+
+ ControlType = new ControlType(settings);
+ ControlType.type = type; // Set the type on the instance, this will be used by the Selector engine
+
+ return ControlType;
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/ui/KeyboardNavigation.js
+
+/**
+ * KeyboardNavigation.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class handles keyboard navigation of controls and elements.
+ *
+ * @class tinymce.ui.KeyboardNavigation
+ */
+define("tinymce/ui/KeyboardNavigation", [
+], function() {
+ "use strict";
+
+ /**
+ * This class handles all keyboard navigation for WAI-ARIA support. Each root container
+ * gets an instance of this class.
+ *
+ * @constructor
+ */
+ return function(settings) {
+ var root = settings.root, focusedElement, focusedControl;
+
+ function isElement(node) {
+ return node && node.nodeType === 1;
+ }
+
+ try {
+ focusedElement = document.activeElement;
+ } catch (ex) {
+ // IE sometimes fails to return a proper element
+ focusedElement = document.body;
+ }
+
+ focusedControl = root.getParentCtrl(focusedElement);
+
+ /**
+ * Returns the currently focused elements wai aria role of the currently
+ * focused element or specified element.
+ *
+ * @private
+ * @param {Element} elm Optional element to get role from.
+ * @return {String} Role of specified element.
+ */
+ function getRole(elm) {
+ elm = elm || focusedElement;
+
+ if (isElement(elm)) {
+ return elm.getAttribute('role');
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the wai role of the parent element of the currently
+ * focused element or specified element.
+ *
+ * @private
+ * @param {Element} elm Optional element to get parent role from.
+ * @return {String} Role of the first parent that has a role.
+ */
+ function getParentRole(elm) {
+ var role, parent = elm || focusedElement;
+
+ while ((parent = parent.parentNode)) {
+ if ((role = getRole(parent))) {
+ return role;
+ }
+ }
+ }
+
+ /**
+ * Returns a wai aria property by name for example aria-selected.
+ *
+ * @private
+ * @param {String} name Name of the aria property to get for example "disabled".
+ * @return {String} Aria property value.
+ */
+ function getAriaProp(name) {
+ var elm = focusedElement;
+
+ if (isElement(elm)) {
+ return elm.getAttribute('aria-' + name);
+ }
+ }
+
+ /**
+ * Is the element a text input element or not.
+ *
+ * @private
+ * @param {Element} elm Element to check if it's an text input element or not.
+ * @return {Boolean} True/false if the element is a text element or not.
+ */
+ function isTextInputElement(elm) {
+ var tagName = elm.tagName.toUpperCase();
+
+ // Notice: since type can be "email" etc we don't check the type
+ // So all input elements gets treated as text input elements
+ return tagName == "INPUT" || tagName == "TEXTAREA" || tagName == "SELECT";
+ }
+
+ /**
+ * Returns true/false if the specified element can be focused or not.
+ *
+ * @private
+ * @param {Element} elm DOM element to check if it can be focused or not.
+ * @return {Boolean} True/false if the element can have focus.
+ */
+ function canFocus(elm) {
+ if (isTextInputElement(elm) && !elm.hidden) {
+ return true;
+ }
+
+ if (/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell|slider)$/.test(getRole(elm))) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns an array of focusable visible elements within the specified container element.
+ *
+ * @private
+ * @param {Element} elm DOM element to find focusable elements within.
+ * @return {Array} Array of focusable elements.
+ */
+ function getFocusElements(elm) {
+ var elements = [];
+
+ function collect(elm) {
+ if (elm.nodeType != 1 || elm.style.display == 'none' || elm.disabled) {
+ return;
+ }
+
+ if (canFocus(elm)) {
+ elements.push(elm);
+ }
+
+ for (var i = 0; i < elm.childNodes.length; i++) {
+ collect(elm.childNodes[i]);
+ }
+ }
+
+ collect(elm || root.getEl());
+
+ return elements;
+ }
+
+ /**
+ * Returns the navigation root control for the specified control. The navigation root
+ * is the control that the keyboard navigation gets scoped to for example a menubar or toolbar group.
+ * It will look for parents of the specified target control or the currently focused control if this option is omitted.
+ *
+ * @private
+ * @param {tinymce.ui.Control} targetControl Optional target control to find root of.
+ * @return {tinymce.ui.Control} Navigation root control.
+ */
+ function getNavigationRoot(targetControl) {
+ var navigationRoot, controls;
+
+ targetControl = targetControl || focusedControl;
+ controls = targetControl.parents().toArray();
+ controls.unshift(targetControl);
+
+ for (var i = 0; i < controls.length; i++) {
+ navigationRoot = controls[i];
+
+ if (navigationRoot.settings.ariaRoot) {
+ break;
+ }
+ }
+
+ return navigationRoot;
+ }
+
+ /**
+ * Focuses the first item in the specified targetControl element or the last aria index if the
+ * navigation root has the ariaRemember option enabled.
+ *
+ * @private
+ * @param {tinymce.ui.Control} targetControl Target control to focus the first item in.
+ */
+ function focusFirst(targetControl) {
+ var navigationRoot = getNavigationRoot(targetControl);
+ var focusElements = getFocusElements(navigationRoot.getEl());
+
+ if (navigationRoot.settings.ariaRemember && "lastAriaIndex" in navigationRoot) {
+ moveFocusToIndex(navigationRoot.lastAriaIndex, focusElements);
+ } else {
+ moveFocusToIndex(0, focusElements);
+ }
+ }
+
+ /**
+ * Moves the focus to the specified index within the elements list.
+ * This will scope the index to the size of the element list if it changed.
+ *
+ * @private
+ * @param {Number} idx Specified index to move to.
+ * @param {Array} elements Array with dom elements to move focus within.
+ * @return {Number} Input index or a changed index if it was out of range.
+ */
+ function moveFocusToIndex(idx, elements) {
+ if (idx < 0) {
+ idx = elements.length - 1;
+ } else if (idx >= elements.length) {
+ idx = 0;
+ }
+
+ if (elements[idx]) {
+ elements[idx].focus();
+ }
+
+ return idx;
+ }
+
+ /**
+ * Moves the focus forwards or backwards.
+ *
+ * @private
+ * @param {Number} dir Direction to move in positive means forward, negative means backwards.
+ * @param {Array} elements Optional array of elements to move within defaults to the current navigation roots elements.
+ */
+ function moveFocus(dir, elements) {
+ var idx = -1, navigationRoot = getNavigationRoot();
+
+ elements = elements || getFocusElements(navigationRoot.getEl());
+
+ for (var i = 0; i < elements.length; i++) {
+ if (elements[i] === focusedElement) {
+ idx = i;
+ }
+ }
+
+ idx += dir;
+ navigationRoot.lastAriaIndex = moveFocusToIndex(idx, elements);
+ }
+
+ /**
+ * Moves the focus to the left this is called by the left key.
+ *
+ * @private
+ */
+ function left() {
+ var parentRole = getParentRole();
+
+ if (parentRole == "tablist") {
+ moveFocus(-1, getFocusElements(focusedElement.parentNode));
+ } else if (focusedControl.parent().submenu) {
+ cancel();
+ } else {
+ moveFocus(-1);
+ }
+ }
+
+ /**
+ * Moves the focus to the right this is called by the right key.
+ *
+ * @private
+ */
+ function right() {
+ var role = getRole(), parentRole = getParentRole();
+
+ if (parentRole == "tablist") {
+ moveFocus(1, getFocusElements(focusedElement.parentNode));
+ } else if (role == "menuitem" && parentRole == "menu" && getAriaProp('haspopup')) {
+ enter();
+ } else {
+ moveFocus(1);
+ }
+ }
+
+ /**
+ * Moves the focus to the up this is called by the up key.
+ *
+ * @private
+ */
+ function up() {
+ moveFocus(-1);
+ }
+
+ /**
+ * Moves the focus to the up this is called by the down key.
+ *
+ * @private
+ */
+ function down() {
+ var role = getRole(), parentRole = getParentRole();
+
+ if (role == "menuitem" && parentRole == "menubar") {
+ enter();
+ } else if (role == "button" && getAriaProp('haspopup')) {
+ enter({key: 'down'});
+ } else {
+ moveFocus(1);
+ }
+ }
+
+ /**
+ * Moves the focus to the next item or previous item depending on shift key.
+ *
+ * @private
+ * @param {DOMEvent} e DOM event object.
+ */
+ function tab(e) {
+ var parentRole = getParentRole();
+
+ if (parentRole == "tablist") {
+ var elm = getFocusElements(focusedControl.getEl('body'))[0];
+
+ if (elm) {
+ elm.focus();
+ }
+ } else {
+ moveFocus(e.shiftKey ? -1 : 1);
+ }
+ }
+
+ /**
+ * Calls the cancel event on the currently focused control. This is normally done using the Esc key.
+ *
+ * @private
+ */
+ function cancel() {
+ focusedControl.fire('cancel');
+ }
+
+ /**
+ * Calls the click event on the currently focused control. This is normally done using the Enter/Space keys.
+ *
+ * @private
+ * @param {Object} aria Optional aria data to pass along with the enter event.
+ */
+ function enter(aria) {
+ aria = aria || {};
+ focusedControl.fire('click', {target: focusedElement, aria: aria});
+ }
+
+ root.on('keydown', function(e) {
+ function handleNonTabOrEscEvent(e, handler) {
+ // Ignore non tab keys for text elements
+ if (isTextInputElement(focusedElement)) {
+ return;
+ }
+
+ if (getRole(focusedElement) === 'slider') {
+ return;
+ }
+
+ if (handler(e) !== false) {
+ e.preventDefault();
+ }
+ }
+
+ if (e.isDefaultPrevented()) {
+ return;
+ }
+
+ switch (e.keyCode) {
+ case 37: // DOM_VK_LEFT
+ handleNonTabOrEscEvent(e, left);
+ break;
+
+ case 39: // DOM_VK_RIGHT
+ handleNonTabOrEscEvent(e, right);
+ break;
+
+ case 38: // DOM_VK_UP
+ handleNonTabOrEscEvent(e, up);
+ break;
+
+ case 40: // DOM_VK_DOWN
+ handleNonTabOrEscEvent(e, down);
+ break;
+
+ case 27: // DOM_VK_ESCAPE
+ cancel();
+ break;
+
+ case 14: // DOM_VK_ENTER
+ case 13: // DOM_VK_RETURN
+ case 32: // DOM_VK_SPACE
+ handleNonTabOrEscEvent(e, enter);
+ break;
+
+ case 9: // DOM_VK_TAB
+ if (tab(e) !== false) {
+ e.preventDefault();
+ }
+ break;
+ }
+ });
+
+ root.on('focusin', function(e) {
+ focusedElement = e.target;
+ focusedControl = e.control;
+ });
+
+ return {
+ focusFirst: focusFirst
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/ui/Container.js
+
+/**
+ * Container.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Container control. This is extended by all controls that can have
+ * children such as panels etc. You can also use this class directly as an
+ * generic container instance. The container doesn't have any specific role or style.
+ *
+ * @-x-less Container.less
+ * @class tinymce.ui.Container
+ * @extends tinymce.ui.Control
+ */
+define("tinymce/ui/Container", [
+ "tinymce/ui/Control",
+ "tinymce/ui/Collection",
+ "tinymce/ui/Selector",
+ "tinymce/ui/Factory",
+ "tinymce/ui/KeyboardNavigation",
+ "tinymce/util/Tools",
+ "tinymce/dom/DomQuery",
+ "tinymce/ui/ClassList",
+ "tinymce/ui/ReflowQueue"
+], function(Control, Collection, Selector, Factory, KeyboardNavigation, Tools, $, ClassList, ReflowQueue) {
+ "use strict";
+
+ var selectorCache = {};
+
+ return Control.extend({
+ /**
+ * Constructs a new control instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Array} items Items to add to container in JSON format or control instances.
+ * @setting {String} layout Layout manager by name to use.
+ * @setting {Object} defaults Default settings to apply to all items.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+ settings = self.settings;
+
+ if (settings.fixed) {
+ self.state.set('fixed', true);
+ }
+
+ self._items = new Collection();
+
+ if (self.isRtl()) {
+ self.classes.add('rtl');
+ }
+
+ self.bodyClasses = new ClassList(function() {
+ if (self.state.get('rendered')) {
+ self.getEl('body').className = this.toString();
+ }
+ });
+ self.bodyClasses.prefix = self.classPrefix;
+
+ self.classes.add('container');
+ self.bodyClasses.add('container-body');
+
+ if (settings.containerCls) {
+ self.classes.add(settings.containerCls);
+ }
+
+ self._layout = Factory.create((settings.layout || '') + 'layout');
+
+ if (self.settings.items) {
+ self.add(self.settings.items);
+ } else {
+ self.add(self.render());
+ }
+
+ // TODO: Fix this!
+ self._hasBody = true;
+ },
+
+ /**
+ * Returns a collection of child items that the container currently have.
+ *
+ * @method items
+ * @return {tinymce.ui.Collection} Control collection direct child controls.
+ */
+ items: function() {
+ return this._items;
+ },
+
+ /**
+ * Find child controls by selector.
+ *
+ * @method find
+ * @param {String} selector Selector CSS pattern to find children by.
+ * @return {tinymce.ui.Collection} Control collection with child controls.
+ */
+ find: function(selector) {
+ selector = selectorCache[selector] = selectorCache[selector] || new Selector(selector);
+
+ return selector.find(this);
+ },
+
+ /**
+ * Adds one or many items to the current container. This will create instances of
+ * the object representations if needed.
+ *
+ * @method add
+ * @param {Array/Object/tinymce.ui.Control} items Array or item that will be added to the container.
+ * @return {tinymce.ui.Collection} Current collection control.
+ */
+ add: function(items) {
+ var self = this;
+
+ self.items().add(self.create(items)).parent(self);
+
+ return self;
+ },
+
+ /**
+ * Focuses the current container instance. This will look
+ * for the first control in the container and focus that.
+ *
+ * @method focus
+ * @param {Boolean} keyboard Optional true/false if the focus was a keyboard focus or not.
+ * @return {tinymce.ui.Collection} Current instance.
+ */
+ focus: function(keyboard) {
+ var self = this, focusCtrl, keyboardNav, items;
+
+ if (keyboard) {
+ keyboardNav = self.keyboardNav || self.parents().eq(-1)[0].keyboardNav;
+
+ if (keyboardNav) {
+ keyboardNav.focusFirst(self);
+ return;
+ }
+ }
+
+ items = self.find('*');
+
+ // TODO: Figure out a better way to auto focus alert dialog buttons
+ if (self.statusbar) {
+ items.add(self.statusbar.items());
+ }
+
+ items.each(function(ctrl) {
+ if (ctrl.settings.autofocus) {
+ focusCtrl = null;
+ return false;
+ }
+
+ if (ctrl.canFocus) {
+ focusCtrl = focusCtrl || ctrl;
+ }
+ });
+
+ if (focusCtrl) {
+ focusCtrl.focus();
+ }
+
+ return self;
+ },
+
+ /**
+ * Replaces the specified child control with a new control.
+ *
+ * @method replace
+ * @param {tinymce.ui.Control} oldItem Old item to be replaced.
+ * @param {tinymce.ui.Control} newItem New item to be inserted.
+ */
+ replace: function(oldItem, newItem) {
+ var ctrlElm, items = this.items(), i = items.length;
+
+ // Replace the item in collection
+ while (i--) {
+ if (items[i] === oldItem) {
+ items[i] = newItem;
+ break;
+ }
+ }
+
+ if (i >= 0) {
+ // Remove new item from DOM
+ ctrlElm = newItem.getEl();
+ if (ctrlElm) {
+ ctrlElm.parentNode.removeChild(ctrlElm);
+ }
+
+ // Remove old item from DOM
+ ctrlElm = oldItem.getEl();
+ if (ctrlElm) {
+ ctrlElm.parentNode.removeChild(ctrlElm);
+ }
+ }
+
+ // Adopt the item
+ newItem.parent(this);
+ },
+
+ /**
+ * Creates the specified items. If any of the items is plain JSON style objects
+ * it will convert these into real tinymce.ui.Control instances.
+ *
+ * @method create
+ * @param {Array} items Array of items to convert into control instances.
+ * @return {Array} Array with control instances.
+ */
+ create: function(items) {
+ var self = this, settings, ctrlItems = [];
+
+ // Non array structure, then force it into an array
+ if (!Tools.isArray(items)) {
+ items = [items];
+ }
+
+ // Add default type to each child control
+ Tools.each(items, function(item) {
+ if (item) {
+ // Construct item if needed
+ if (!(item instanceof Control)) {
+ // Name only then convert it to an object
+ if (typeof item == "string") {
+ item = {type: item};
+ }
+
+ // Create control instance based on input settings and default settings
+ settings = Tools.extend({}, self.settings.defaults, item);
+ item.type = settings.type = settings.type || item.type || self.settings.defaultType ||
+ (settings.defaults ? settings.defaults.type : null);
+ item = Factory.create(settings);
+ }
+
+ ctrlItems.push(item);
+ }
+ });
+
+ return ctrlItems;
+ },
+
+ /**
+ * Renders new control instances.
+ *
+ * @private
+ */
+ renderNew: function() {
+ var self = this;
+
+ // Render any new items
+ self.items().each(function(ctrl, index) {
+ var containerElm;
+
+ ctrl.parent(self);
+
+ if (!ctrl.state.get('rendered')) {
+ containerElm = self.getEl('body');
+
+ // Insert or append the item
+ if (containerElm.hasChildNodes() && index <= containerElm.childNodes.length - 1) {
+ $(containerElm.childNodes[index]).before(ctrl.renderHtml());
+ } else {
+ $(containerElm).append(ctrl.renderHtml());
+ }
+
+ ctrl.postRender();
+ ReflowQueue.add(ctrl);
+ }
+ });
+
+ self._layout.applyClasses(self.items().filter(':visible'));
+ self._lastRect = null;
+
+ return self;
+ },
+
+ /**
+ * Appends new instances to the current container.
+ *
+ * @method append
+ * @param {Array/tinymce.ui.Collection} items Array if controls to append.
+ * @return {tinymce.ui.Container} Current container instance.
+ */
+ append: function(items) {
+ return this.add(items).renderNew();
+ },
+
+ /**
+ * Prepends new instances to the current container.
+ *
+ * @method prepend
+ * @param {Array/tinymce.ui.Collection} items Array if controls to prepend.
+ * @return {tinymce.ui.Container} Current container instance.
+ */
+ prepend: function(items) {
+ var self = this;
+
+ self.items().set(self.create(items).concat(self.items().toArray()));
+
+ return self.renderNew();
+ },
+
+ /**
+ * Inserts an control at a specific index.
+ *
+ * @method insert
+ * @param {Array/tinymce.ui.Collection} items Array if controls to insert.
+ * @param {Number} index Index to insert controls at.
+ * @param {Boolean} [before=false] Inserts controls before the index.
+ */
+ insert: function(items, index, before) {
+ var self = this, curItems, beforeItems, afterItems;
+
+ items = self.create(items);
+ curItems = self.items();
+
+ if (!before && index < curItems.length - 1) {
+ index += 1;
+ }
+
+ if (index >= 0 && index < curItems.length) {
+ beforeItems = curItems.slice(0, index).toArray();
+ afterItems = curItems.slice(index).toArray();
+ curItems.set(beforeItems.concat(items, afterItems));
+ }
+
+ return self.renderNew();
+ },
+
+ /**
+ * Populates the form fields from the specified JSON data object.
+ *
+ * Control items in the form that matches the data will have it's value set.
+ *
+ * @method fromJSON
+ * @param {Object} data JSON data object to set control values by.
+ * @return {tinymce.ui.Container} Current form instance.
+ */
+ fromJSON: function(data) {
+ var self = this;
+
+ for (var name in data) {
+ self.find('#' + name).value(data[name]);
+ }
+
+ return self;
+ },
+
+ /**
+ * Serializes the form into a JSON object by getting all items
+ * that has a name and a value.
+ *
+ * @method toJSON
+ * @return {Object} JSON object with form data.
+ */
+ toJSON: function() {
+ var self = this, data = {};
+
+ self.find('*').each(function(ctrl) {
+ var name = ctrl.name(), value = ctrl.value();
+
+ if (name && typeof value != "undefined") {
+ data[name] = value;
+ }
+ });
+
+ return data;
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, layout = self._layout, role = this.settings.role;
+
+ self.preRender();
+ layout.preRender(self);
+
+ return (
+ '<div id="' + self._id + '" class="' + self.classes + '"' + (role ? ' role="' + this.settings.role + '"' : '') + '>' +
+ '<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
+ (self.settings.html || '') + layout.renderHtml(self) +
+ '</div>' +
+ '</div>'
+ );
+ },
+
+ /**
+ * Post render method. Called after the control has been rendered to the target.
+ *
+ * @method postRender
+ * @return {tinymce.ui.Container} Current combobox instance.
+ */
+ postRender: function() {
+ var self = this, box;
+
+ self.items().exec('postRender');
+ self._super();
+
+ self._layout.postRender(self);
+ self.state.set('rendered', true);
+
+ if (self.settings.style) {
+ self.$el.css(self.settings.style);
+ }
+
+ if (self.settings.border) {
+ box = self.borderBox;
+ self.$el.css({
+ 'border-top-width': box.top,
+ 'border-right-width': box.right,
+ 'border-bottom-width': box.bottom,
+ 'border-left-width': box.left
+ });
+ }
+
+ if (!self.parent()) {
+ self.keyboardNav = new KeyboardNavigation({
+ root: self
+ });
+ }
+
+ return self;
+ },
+
+ /**
+ * Initializes the current controls layout rect.
+ * This will be executed by the layout managers to determine the
+ * default minWidth/minHeight etc.
+ *
+ * @method initLayoutRect
+ * @return {Object} Layout rect instance.
+ */
+ initLayoutRect: function() {
+ var self = this, layoutRect = self._super();
+
+ // Recalc container size by asking layout manager
+ self._layout.recalc(self);
+
+ return layoutRect;
+ },
+
+ /**
+ * Recalculates the positions of the controls in the current container.
+ * This is invoked by the reflow method and shouldn't be called directly.
+ *
+ * @method recalc
+ */
+ recalc: function() {
+ var self = this, rect = self._layoutRect, lastRect = self._lastRect;
+
+ if (!lastRect || lastRect.w != rect.w || lastRect.h != rect.h) {
+ self._layout.recalc(self);
+ rect = self.layoutRect();
+ self._lastRect = {x: rect.x, y: rect.y, w: rect.w, h: rect.h};
+ return true;
+ }
+ },
+
+ /**
+ * Reflows the current container and it's children and possible parents.
+ * This should be used after you for example append children to the current control so
+ * that the layout managers know that they need to reposition everything.
+ *
+ * @example
+ * container.append({type: 'button', text: 'My button'}).reflow();
+ *
+ * @method reflow
+ * @return {tinymce.ui.Container} Current container instance.
+ */
+ reflow: function() {
+ var i;
+
+ ReflowQueue.remove(this);
+
+ if (this.visible()) {
+ Control.repaintControls = [];
+ Control.repaintControls.map = {};
+
+ this.recalc();
+ i = Control.repaintControls.length;
+
+ while (i--) {
+ Control.repaintControls[i].repaint();
+ }
+
+ // TODO: Fix me!
+ if (this.settings.layout !== "flow" && this.settings.layout !== "stack") {
+ this.repaint();
+ }
+
+ Control.repaintControls = [];
+ }
+
+ return this;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/DragHelper.js
+
+/**
+ * DragHelper.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Drag/drop helper class.
+ *
+ * @example
+ * var dragHelper = new tinymce.ui.DragHelper('mydiv', {
+ * start: function(evt) {
+ * },
+ *
+ * drag: function(evt) {
+ * },
+ *
+ * end: function(evt) {
+ * }
+ * });
+ *
+ * @class tinymce.ui.DragHelper
+ */
+define("tinymce/ui/DragHelper", [
+ "tinymce/dom/DomQuery"
+], function($) {
+ "use strict";
+
+ function getDocumentSize(doc) {
+ var documentElement, body, scrollWidth, clientWidth;
+ var offsetWidth, scrollHeight, clientHeight, offsetHeight, max = Math.max;
+
+ documentElement = doc.documentElement;
+ body = doc.body;
+
+ scrollWidth = max(documentElement.scrollWidth, body.scrollWidth);
+ clientWidth = max(documentElement.clientWidth, body.clientWidth);
+ offsetWidth = max(documentElement.offsetWidth, body.offsetWidth);
+
+ scrollHeight = max(documentElement.scrollHeight, body.scrollHeight);
+ clientHeight = max(documentElement.clientHeight, body.clientHeight);
+ offsetHeight = max(documentElement.offsetHeight, body.offsetHeight);
+
+ return {
+ width: scrollWidth < offsetWidth ? clientWidth : scrollWidth,
+ height: scrollHeight < offsetHeight ? clientHeight : scrollHeight
+ };
+ }
+
+ function updateWithTouchData(e) {
+ var keys, i;
+
+ if (e.changedTouches) {
+ keys = "screenX screenY pageX pageY clientX clientY".split(' ');
+ for (i = 0; i < keys.length; i++) {
+ e[keys[i]] = e.changedTouches[0][keys[i]];
+ }
+ }
+ }
+
+ return function(id, settings) {
+ var $eventOverlay, doc = settings.document || document, downButton, start, stop, drag, startX, startY;
+
+ settings = settings || {};
+
+ function getHandleElm() {
+ return doc.getElementById(settings.handle || id);
+ }
+
+ start = function(e) {
+ var docSize = getDocumentSize(doc), handleElm, cursor;
+
+ updateWithTouchData(e);
+
+ e.preventDefault();
+ downButton = e.button;
+ handleElm = getHandleElm();
+ startX = e.screenX;
+ startY = e.screenY;
+
+ // Grab cursor from handle so we can place it on overlay
+ if (window.getComputedStyle) {
+ cursor = window.getComputedStyle(handleElm, null).getPropertyValue("cursor");
+ } else {
+ cursor = handleElm.runtimeStyle.cursor;
+ }
+
+ $eventOverlay = $('<div></div>').css({
+ position: "absolute",
+ top: 0, left: 0,
+ width: docSize.width,
+ height: docSize.height,
+ zIndex: 0x7FFFFFFF,
+ opacity: 0.0001,
+ cursor: cursor
+ }).appendTo(doc.body);
+
+ $(doc).on('mousemove touchmove', drag).on('mouseup touchend', stop);
+
+ settings.start(e);
+ };
+
+ drag = function(e) {
+ updateWithTouchData(e);
+
+ if (e.button !== downButton) {
+ return stop(e);
+ }
+
+ e.deltaX = e.screenX - startX;
+ e.deltaY = e.screenY - startY;
+
+ e.preventDefault();
+ settings.drag(e);
+ };
+
+ stop = function(e) {
+ updateWithTouchData(e);
+
+ $(doc).off('mousemove touchmove', drag).off('mouseup touchend', stop);
+
+ $eventOverlay.remove();
+
+ if (settings.stop) {
+ settings.stop(e);
+ }
+ };
+
+ /**
+ * Destroys the drag/drop helper instance.
+ *
+ * @method destroy
+ */
+ this.destroy = function() {
+ $(getHandleElm()).off();
+ };
+
+ $(getHandleElm()).on('mousedown touchstart', start);
+ };
+});
+
+// Included from: js/tinymce/classes/ui/Scrollable.js
+
+/**
+ * Scrollable.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This mixin makes controls scrollable using custom scrollbars.
+ *
+ * @-x-less Scrollable.less
+ * @mixin tinymce.ui.Scrollable
+ */
+define("tinymce/ui/Scrollable", [
+ "tinymce/dom/DomQuery",
+ "tinymce/ui/DragHelper"
+], function($, DragHelper) {
+ "use strict";
+
+ return {
+ init: function() {
+ var self = this;
+ self.on('repaint', self.renderScroll);
+ },
+
+ renderScroll: function() {
+ var self = this, margin = 2;
+
+ function repaintScroll() {
+ var hasScrollH, hasScrollV, bodyElm;
+
+ function repaintAxis(axisName, posName, sizeName, contentSizeName, hasScroll, ax) {
+ var containerElm, scrollBarElm, scrollThumbElm;
+ var containerSize, scrollSize, ratio, rect;
+ var posNameLower, sizeNameLower;
+
+ scrollBarElm = self.getEl('scroll' + axisName);
+ if (scrollBarElm) {
+ posNameLower = posName.toLowerCase();
+ sizeNameLower = sizeName.toLowerCase();
+
+ $(self.getEl('absend')).css(posNameLower, self.layoutRect()[contentSizeName] - 1);
+
+ if (!hasScroll) {
+ $(scrollBarElm).css('display', 'none');
+ return;
+ }
+
+ $(scrollBarElm).css('display', 'block');
+ containerElm = self.getEl('body');
+ scrollThumbElm = self.getEl('scroll' + axisName + "t");
+ containerSize = containerElm["client" + sizeName] - (margin * 2);
+ containerSize -= hasScrollH && hasScrollV ? scrollBarElm["client" + ax] : 0;
+ scrollSize = containerElm["scroll" + sizeName];
+ ratio = containerSize / scrollSize;
+
+ rect = {};
+ rect[posNameLower] = containerElm["offset" + posName] + margin;
+ rect[sizeNameLower] = containerSize;
+ $(scrollBarElm).css(rect);
+
+ rect = {};
+ rect[posNameLower] = containerElm["scroll" + posName] * ratio;
+ rect[sizeNameLower] = containerSize * ratio;
+ $(scrollThumbElm).css(rect);
+ }
+ }
+
+ bodyElm = self.getEl('body');
+ hasScrollH = bodyElm.scrollWidth > bodyElm.clientWidth;
+ hasScrollV = bodyElm.scrollHeight > bodyElm.clientHeight;
+
+ repaintAxis("h", "Left", "Width", "contentW", hasScrollH, "Height");
+ repaintAxis("v", "Top", "Height", "contentH", hasScrollV, "Width");
+ }
+
+ function addScroll() {
+ function addScrollAxis(axisName, posName, sizeName, deltaPosName, ax) {
+ var scrollStart, axisId = self._id + '-scroll' + axisName, prefix = self.classPrefix;
+
+ $(self.getEl()).append(
+ '<div id="' + axisId + '" class="' + prefix + 'scrollbar ' + prefix + 'scrollbar-' + axisName + '">' +
+ '<div id="' + axisId + 't" class="' + prefix + 'scrollbar-thumb"></div>' +
+ '</div>'
+ );
+
+ self.draghelper = new DragHelper(axisId + 't', {
+ start: function() {
+ scrollStart = self.getEl('body')["scroll" + posName];
+ $('#' + axisId).addClass(prefix + 'active');
+ },
+
+ drag: function(e) {
+ var ratio, hasScrollH, hasScrollV, containerSize, layoutRect = self.layoutRect();
+
+ hasScrollH = layoutRect.contentW > layoutRect.innerW;
+ hasScrollV = layoutRect.contentH > layoutRect.innerH;
+ containerSize = self.getEl('body')["client" + sizeName] - (margin * 2);
+ containerSize -= hasScrollH && hasScrollV ? self.getEl('scroll' + axisName)["client" + ax] : 0;
+
+ ratio = containerSize / self.getEl('body')["scroll" + sizeName];
+ self.getEl('body')["scroll" + posName] = scrollStart + (e["delta" + deltaPosName] / ratio);
+ },
+
+ stop: function() {
+ $('#' + axisId).removeClass(prefix + 'active');
+ }
+ });
+ }
+
+ self.classes.add('scroll');
+
+ addScrollAxis("v", "Top", "Height", "Y", "Width");
+ addScrollAxis("h", "Left", "Width", "X", "Height");
+ }
+
+ if (self.settings.autoScroll) {
+ if (!self._hasScroll) {
+ self._hasScroll = true;
+ addScroll();
+
+ self.on('wheel', function(e) {
+ var bodyEl = self.getEl('body');
+
+ bodyEl.scrollLeft += (e.deltaX || 0) * 10;
+ bodyEl.scrollTop += e.deltaY * 10;
+
+ repaintScroll();
+ });
+
+ $(self.getEl('body')).on("scroll", repaintScroll);
+ }
+
+ repaintScroll();
+ }
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/ui/Panel.js
+
+/**
+ * Panel.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new panel.
+ *
+ * @-x-less Panel.less
+ * @class tinymce.ui.Panel
+ * @extends tinymce.ui.Container
+ * @mixes tinymce.ui.Scrollable
+ */
+define("tinymce/ui/Panel", [
+ "tinymce/ui/Container",
+ "tinymce/ui/Scrollable"
+], function(Container, Scrollable) {
+ "use strict";
+
+ return Container.extend({
+ Defaults: {
+ layout: 'fit',
+ containerCls: 'panel'
+ },
+
+ Mixins: [Scrollable],
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, layout = self._layout, innerHtml = self.settings.html;
+
+ self.preRender();
+ layout.preRender(self);
+
+ if (typeof innerHtml == "undefined") {
+ innerHtml = (
+ '<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
+ layout.renderHtml(self) +
+ '</div>'
+ );
+ } else {
+ if (typeof innerHtml == 'function') {
+ innerHtml = innerHtml.call(self);
+ }
+
+ self._hasBody = false;
+ }
+
+ return (
+ '<div id="' + self._id + '" class="' + self.classes + '" hidefocus="1" tabindex="-1" role="group">' +
+ (self._preBodyHtml || '') +
+ innerHtml +
+ '</div>'
+ );
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Movable.js
+
+/**
+ * Movable.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Movable mixin. Makes controls movable absolute and relative to other elements.
+ *
+ * @mixin tinymce.ui.Movable
+ */
+define("tinymce/ui/Movable", [
+ "tinymce/ui/DomUtils"
+], function(DomUtils) {
+ "use strict";
+
+ function calculateRelativePosition(ctrl, targetElm, rel) {
+ var ctrlElm, pos, x, y, selfW, selfH, targetW, targetH, viewport, size;
+
+ viewport = DomUtils.getViewPort();
+
+ // Get pos of target
+ pos = DomUtils.getPos(targetElm);
+ x = pos.x;
+ y = pos.y;
+
+ if (ctrl.state.get('fixed') && DomUtils.getRuntimeStyle(document.body, 'position') == 'static') {
+ x -= viewport.x;
+ y -= viewport.y;
+ }
+
+ // Get size of self
+ ctrlElm = ctrl.getEl();
+ size = DomUtils.getSize(ctrlElm);
+ selfW = size.width;
+ selfH = size.height;
+
+ // Get size of target
+ size = DomUtils.getSize(targetElm);
+ targetW = size.width;
+ targetH = size.height;
+
+ // Parse align string
+ rel = (rel || '').split('');
+
+ // Target corners
+ if (rel[0] === 'b') {
+ y += targetH;
+ }
+
+ if (rel[1] === 'r') {
+ x += targetW;
+ }
+
+ if (rel[0] === 'c') {
+ y += Math.round(targetH / 2);
+ }
+
+ if (rel[1] === 'c') {
+ x += Math.round(targetW / 2);
+ }
+
+ // Self corners
+ if (rel[3] === 'b') {
+ y -= selfH;
+ }
+
+ if (rel[4] === 'r') {
+ x -= selfW;
+ }
+
+ if (rel[3] === 'c') {
+ y -= Math.round(selfH / 2);
+ }
+
+ if (rel[4] === 'c') {
+ x -= Math.round(selfW / 2);
+ }
+
+ return {
+ x: x,
+ y: y,
+ w: selfW,
+ h: selfH
+ };
+ }
+
+ return {
+ /**
+ * Tests various positions to get the most suitable one.
+ *
+ * @method testMoveRel
+ * @param {DOMElement} elm Element to position against.
+ * @param {Array} rels Array with relative positions.
+ * @return {String} Best suitable relative position.
+ */
+ testMoveRel: function(elm, rels) {
+ var viewPortRect = DomUtils.getViewPort();
+
+ for (var i = 0; i < rels.length; i++) {
+ var pos = calculateRelativePosition(this, elm, rels[i]);
+
+ if (this.state.get('fixed')) {
+ if (pos.x > 0 && pos.x + pos.w < viewPortRect.w && pos.y > 0 && pos.y + pos.h < viewPortRect.h) {
+ return rels[i];
+ }
+ } else {
+ if (pos.x > viewPortRect.x && pos.x + pos.w < viewPortRect.w + viewPortRect.x &&
+ pos.y > viewPortRect.y && pos.y + pos.h < viewPortRect.h + viewPortRect.y) {
+ return rels[i];
+ }
+ }
+ }
+
+ return rels[0];
+ },
+
+ /**
+ * Move relative to the specified element.
+ *
+ * @method moveRel
+ * @param {Element} elm Element to move relative to.
+ * @param {String} rel Relative mode. For example: br-tl.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ moveRel: function(elm, rel) {
+ if (typeof rel != 'string') {
+ rel = this.testMoveRel(elm, rel);
+ }
+
+ var pos = calculateRelativePosition(this, elm, rel);
+ return this.moveTo(pos.x, pos.y);
+ },
+
+ /**
+ * Move by a relative x, y values.
+ *
+ * @method moveBy
+ * @param {Number} dx Relative x position.
+ * @param {Number} dy Relative y position.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ moveBy: function(dx, dy) {
+ var self = this, rect = self.layoutRect();
+
+ self.moveTo(rect.x + dx, rect.y + dy);
+
+ return self;
+ },
+
+ /**
+ * Move to absolute position.
+ *
+ * @method moveTo
+ * @param {Number} x Absolute x position.
+ * @param {Number} y Absolute y position.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ moveTo: function(x, y) {
+ var self = this;
+
+ // TODO: Move this to some global class
+ function constrain(value, max, size) {
+ if (value < 0) {
+ return 0;
+ }
+
+ if (value + size > max) {
+ value = max - size;
+ return value < 0 ? 0 : value;
+ }
+
+ return value;
+ }
+
+ if (self.settings.constrainToViewport) {
+ var viewPortRect = DomUtils.getViewPort(window);
+ var layoutRect = self.layoutRect();
+
+ x = constrain(x, viewPortRect.w + viewPortRect.x, layoutRect.w);
+ y = constrain(y, viewPortRect.h + viewPortRect.y, layoutRect.h);
+ }
+
+ if (self.state.get('rendered')) {
+ self.layoutRect({x: x, y: y}).repaint();
+ } else {
+ self.settings.x = x;
+ self.settings.y = y;
+ }
+
+ self.fire('move', {x: x, y: y});
+
+ return self;
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/ui/Resizable.js
+
+/**
+ * Resizable.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Resizable mixin. Enables controls to be resized.
+ *
+ * @mixin tinymce.ui.Resizable
+ */
+define("tinymce/ui/Resizable", [
+ "tinymce/ui/DomUtils"
+], function(DomUtils) {
+ "use strict";
+
+ return {
+ /**
+ * Resizes the control to contents.
+ *
+ * @method resizeToContent
+ */
+ resizeToContent: function() {
+ this._layoutRect.autoResize = true;
+ this._lastRect = null;
+ this.reflow();
+ },
+
+ /**
+ * Resizes the control to a specific width/height.
+ *
+ * @method resizeTo
+ * @param {Number} w Control width.
+ * @param {Number} h Control height.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ resizeTo: function(w, h) {
+ // TODO: Fix hack
+ if (w <= 1 || h <= 1) {
+ var rect = DomUtils.getWindowSize();
+
+ w = w <= 1 ? w * rect.w : w;
+ h = h <= 1 ? h * rect.h : h;
+ }
+
+ this._layoutRect.autoResize = false;
+ return this.layoutRect({minW: w, minH: h, w: w, h: h}).reflow();
+ },
+
+ /**
+ * Resizes the control to a specific relative width/height.
+ *
+ * @method resizeBy
+ * @param {Number} dw Relative control width.
+ * @param {Number} dh Relative control height.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ resizeBy: function(dw, dh) {
+ var self = this, rect = self.layoutRect();
+
+ return self.resizeTo(rect.w + dw, rect.h + dh);
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/ui/FloatPanel.js
+
+/**
+ * FloatPanel.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class creates a floating panel.
+ *
+ * @-x-less FloatPanel.less
+ * @class tinymce.ui.FloatPanel
+ * @extends tinymce.ui.Panel
+ * @mixes tinymce.ui.Movable
+ * @mixes tinymce.ui.Resizable
+ */
+define("tinymce/ui/FloatPanel", [
+ "tinymce/ui/Panel",
+ "tinymce/ui/Movable",
+ "tinymce/ui/Resizable",
+ "tinymce/ui/DomUtils",
+ "tinymce/dom/DomQuery",
+ "tinymce/util/Delay"
+], function(Panel, Movable, Resizable, DomUtils, $, Delay) {
+ "use strict";
+
+ var documentClickHandler, documentScrollHandler, windowResizeHandler, visiblePanels = [];
+ var zOrder = [], hasModal;
+
+ function isChildOf(ctrl, parent) {
+ while (ctrl) {
+ if (ctrl == parent) {
+ return true;
+ }
+
+ ctrl = ctrl.parent();
+ }
+ }
+
+ function skipOrHidePanels(e) {
+ // Hide any float panel when a click/focus out is out side that float panel and the
+ // float panels direct parent for example a click on a menu button
+ var i = visiblePanels.length;
+
+ while (i--) {
+ var panel = visiblePanels[i], clickCtrl = panel.getParentCtrl(e.target);
+
+ if (panel.settings.autohide) {
+ if (clickCtrl) {
+ if (isChildOf(clickCtrl, panel) || panel.parent() === clickCtrl) {
+ continue;
+ }
+ }
+
+ e = panel.fire('autohide', {target: e.target});
+ if (!e.isDefaultPrevented()) {
+ panel.hide();
+ }
+ }
+ }
+ }
+
+ function bindDocumentClickHandler() {
+
+ if (!documentClickHandler) {
+ documentClickHandler = function(e) {
+ // Gecko fires click event and in the wrong order on Mac so lets normalize
+ if (e.button == 2) {
+ return;
+ }
+
+ skipOrHidePanels(e);
+ };
+
+ $(document).on('click touchstart', documentClickHandler);
+ }
+ }
+
+ function bindDocumentScrollHandler() {
+ if (!documentScrollHandler) {
+ documentScrollHandler = function() {
+ var i;
+
+ i = visiblePanels.length;
+ while (i--) {
+ repositionPanel(visiblePanels[i]);
+ }
+ };
+
+ $(window).on('scroll', documentScrollHandler);
+ }
+ }
+
+ function bindWindowResizeHandler() {
+ if (!windowResizeHandler) {
+ var docElm = document.documentElement, clientWidth = docElm.clientWidth, clientHeight = docElm.clientHeight;
+
+ windowResizeHandler = function() {
+ // Workaround for #7065 IE 7 fires resize events event though the window wasn't resized
+ if (!document.all || clientWidth != docElm.clientWidth || clientHeight != docElm.clientHeight) {
+ clientWidth = docElm.clientWidth;
+ clientHeight = docElm.clientHeight;
+ FloatPanel.hideAll();
+ }
+ };
+
+ $(window).on('resize', windowResizeHandler);
+ }
+ }
+
+ /**
+ * Repositions the panel to the top of page if the panel is outside of the visual viewport. It will
+ * also reposition all child panels of the current panel.
+ */
+ function repositionPanel(panel) {
+ var scrollY = DomUtils.getViewPort().y;
+
+ function toggleFixedChildPanels(fixed, deltaY) {
+ var parent;
+
+ for (var i = 0; i < visiblePanels.length; i++) {
+ if (visiblePanels[i] != panel) {
+ parent = visiblePanels[i].parent();
+
+ while (parent && (parent = parent.parent())) {
+ if (parent == panel) {
+ visiblePanels[i].fixed(fixed).moveBy(0, deltaY).repaint();
+ }
+ }
+ }
+ }
+ }
+
+ if (panel.settings.autofix) {
+ if (!panel.state.get('fixed')) {
+ panel._autoFixY = panel.layoutRect().y;
+
+ if (panel._autoFixY < scrollY) {
+ panel.fixed(true).layoutRect({y: 0}).repaint();
+ toggleFixedChildPanels(true, scrollY - panel._autoFixY);
+ }
+ } else {
+ if (panel._autoFixY > scrollY) {
+ panel.fixed(false).layoutRect({y: panel._autoFixY}).repaint();
+ toggleFixedChildPanels(false, panel._autoFixY - scrollY);
+ }
+ }
+ }
+ }
+
+ function addRemove(add, ctrl) {
+ var i, zIndex = FloatPanel.zIndex || 0xFFFF, topModal;
+
+ if (add) {
+ zOrder.push(ctrl);
+ } else {
+ i = zOrder.length;
+
+ while (i--) {
+ if (zOrder[i] === ctrl) {
+ zOrder.splice(i, 1);
+ }
+ }
+ }
+
+ if (zOrder.length) {
+ for (i = 0; i < zOrder.length; i++) {
+ if (zOrder[i].modal) {
+ zIndex++;
+ topModal = zOrder[i];
+ }
+
+ zOrder[i].getEl().style.zIndex = zIndex;
+ zOrder[i].zIndex = zIndex;
+ zIndex++;
+ }
+ }
+
+ var modalBlockEl = $('#' + ctrl.classPrefix + 'modal-block', ctrl.getContainerElm())[0];
+
+ if (topModal) {
+ $(modalBlockEl).css('z-index', topModal.zIndex - 1);
+ } else if (modalBlockEl) {
+ modalBlockEl.parentNode.removeChild(modalBlockEl);
+ hasModal = false;
+ }
+
+ FloatPanel.currentZIndex = zIndex;
+ }
+
+ var FloatPanel = Panel.extend({
+ Mixins: [Movable, Resizable],
+
+ /**
+ * Constructs a new control instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Boolean} autohide Automatically hide the panel.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+ self._eventsRoot = self;
+
+ self.classes.add('floatpanel');
+
+ // Hide floatpanes on click out side the root button
+ if (settings.autohide) {
+ bindDocumentClickHandler();
+ bindWindowResizeHandler();
+ visiblePanels.push(self);
+ }
+
+ if (settings.autofix) {
+ bindDocumentScrollHandler();
+
+ self.on('move', function() {
+ repositionPanel(this);
+ });
+ }
+
+ self.on('postrender show', function(e) {
+ if (e.control == self) {
+ var $modalBlockEl, prefix = self.classPrefix;
+
+ if (self.modal && !hasModal) {
+ $modalBlockEl = $('#' + prefix + 'modal-block', self.getContainerElm());
+ if (!$modalBlockEl[0]) {
+ $modalBlockEl = $(
+ '<div id="' + prefix + 'modal-block" class="' + prefix + 'reset ' + prefix + 'fade"></div>'
+ ).appendTo(self.getContainerElm());
+ }
+
+ Delay.setTimeout(function() {
+ $modalBlockEl.addClass(prefix + 'in');
+ $(self.getEl()).addClass(prefix + 'in');
+ });
+
+ hasModal = true;
+ }
+
+ addRemove(true, self);
+ }
+ });
+
+ self.on('show', function() {
+ self.parents().each(function(ctrl) {
+ if (ctrl.state.get('fixed')) {
+ self.fixed(true);
+ return false;
+ }
+ });
+ });
+
+ if (settings.popover) {
+ self._preBodyHtml = '<div class="' + self.classPrefix + 'arrow"></div>';
+ self.classes.add('popover').add('bottom').add(self.isRtl() ? 'end' : 'start');
+ }
+
+ self.aria('label', settings.ariaLabel);
+ self.aria('labelledby', self._id);
+ self.aria('describedby', self.describedBy || self._id + '-none');
+ },
+
+ fixed: function(state) {
+ var self = this;
+
+ if (self.state.get('fixed') != state) {
+ if (self.state.get('rendered')) {
+ var viewport = DomUtils.getViewPort();
+
+ if (state) {
+ self.layoutRect().y -= viewport.y;
+ } else {
+ self.layoutRect().y += viewport.y;
+ }
+ }
+
+ self.classes.toggle('fixed', state);
+ self.state.set('fixed', state);
+ }
+
+ return self;
+ },
+
+ /**
+ * Shows the current float panel.
+ *
+ * @method show
+ * @return {tinymce.ui.FloatPanel} Current floatpanel instance.
+ */
+ show: function() {
+ var self = this, i, state = self._super();
+
+ i = visiblePanels.length;
+ while (i--) {
+ if (visiblePanels[i] === self) {
+ break;
+ }
+ }
+
+ if (i === -1) {
+ visiblePanels.push(self);
+ }
+
+ return state;
+ },
+
+ /**
+ * Hides the current float panel.
+ *
+ * @method hide
+ * @return {tinymce.ui.FloatPanel} Current floatpanel instance.
+ */
+ hide: function() {
+ removeVisiblePanel(this);
+ addRemove(false, this);
+
+ return this._super();
+ },
+
+ /**
+ * Hide all visible float panels with he autohide setting enabled. This is for
+ * manually hiding floating menus or panels.
+ *
+ * @method hideAll
+ */
+ hideAll: function() {
+ FloatPanel.hideAll();
+ },
+
+ /**
+ * Closes the float panel. This will remove the float panel from page and fire the close event.
+ *
+ * @method close
+ */
+ close: function() {
+ var self = this;
+
+ if (!self.fire('close').isDefaultPrevented()) {
+ self.remove();
+ addRemove(false, self);
+ }
+
+ return self;
+ },
+
+ /**
+ * Removes the float panel from page.
+ *
+ * @method remove
+ */
+ remove: function() {
+ removeVisiblePanel(this);
+ this._super();
+ },
+
+ postRender: function() {
+ var self = this;
+
+ if (self.settings.bodyRole) {
+ this.getEl('body').setAttribute('role', self.settings.bodyRole);
+ }
+
+ return self._super();
+ }
+ });
+
+ /**
+ * Hide all visible float panels with he autohide setting enabled. This is for
+ * manually hiding floating menus or panels.
+ *
+ * @static
+ * @method hideAll
+ */
+ FloatPanel.hideAll = function() {
+ var i = visiblePanels.length;
+
+ while (i--) {
+ var panel = visiblePanels[i];
+
+ if (panel && panel.settings.autohide) {
+ panel.hide();
+ visiblePanels.splice(i, 1);
+ }
+ }
+ };
+
+ function removeVisiblePanel(panel) {
+ var i;
+
+ i = visiblePanels.length;
+ while (i--) {
+ if (visiblePanels[i] === panel) {
+ visiblePanels.splice(i, 1);
+ }
+ }
+
+ i = zOrder.length;
+ while (i--) {
+ if (zOrder[i] === panel) {
+ zOrder.splice(i, 1);
+ }
+ }
+ }
+
+ return FloatPanel;
+});
+
+// Included from: js/tinymce/classes/ui/Window.js
+
+/**
+ * Window.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new window.
+ *
+ * @-x-less Window.less
+ * @class tinymce.ui.Window
+ * @extends tinymce.ui.FloatPanel
+ */
+define("tinymce/ui/Window", [
+ "tinymce/ui/FloatPanel",
+ "tinymce/ui/Panel",
+ "tinymce/ui/DomUtils",
+ "tinymce/dom/DomQuery",
+ "tinymce/ui/DragHelper",
+ "tinymce/ui/BoxUtils",
+ "tinymce/Env",
+ "tinymce/util/Delay"
+], function(FloatPanel, Panel, DomUtils, $, DragHelper, BoxUtils, Env, Delay) {
+ "use strict";
+
+ var windows = [], oldMetaValue = '';
+
+ function toggleFullScreenState(state) {
+ var noScaleMetaValue = 'width=device-width,initial-scale=1.0,user-scalable=0,minimum-scale=1.0,maximum-scale=1.0',
+ viewport = $("meta[name=viewport]")[0],
+ contentValue;
+
+ if (Env.overrideViewPort === false) {
+ return;
+ }
+
+ if (!viewport) {
+ viewport = document.createElement('meta');
+ viewport.setAttribute('name', 'viewport');
+ document.getElementsByTagName('head')[0].appendChild(viewport);
+ }
+
+ contentValue = viewport.getAttribute('content');
+ if (contentValue && typeof oldMetaValue != 'undefined') {
+ oldMetaValue = contentValue;
+ }
+
+ viewport.setAttribute('content', state ? noScaleMetaValue : oldMetaValue);
+ }
+
+ function toggleBodyFullScreenClasses(classPrefix, state) {
+ if (checkFullscreenWindows() && state === false) {
+ $([document.documentElement, document.body]).removeClass(classPrefix + 'fullscreen');
+ }
+ }
+
+ function checkFullscreenWindows() {
+ for (var i = 0; i < windows.length; i++) {
+ if (windows[i]._fullscreen) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function handleWindowResize() {
+ if (!Env.desktop) {
+ var lastSize = {
+ w: window.innerWidth,
+ h: window.innerHeight
+ };
+
+ Delay.setInterval(function() {
+ var w = window.innerWidth,
+ h = window.innerHeight;
+
+ if (lastSize.w != w || lastSize.h != h) {
+ lastSize = {
+ w: w,
+ h: h
+ };
+
+ $(window).trigger('resize');
+ }
+ }, 100);
+ }
+
+ function reposition() {
+ var i, rect = DomUtils.getWindowSize(), layoutRect;
+
+ for (i = 0; i < windows.length; i++) {
+ layoutRect = windows[i].layoutRect();
+
+ windows[i].moveTo(
+ windows[i].settings.x || Math.max(0, rect.w / 2 - layoutRect.w / 2),
+ windows[i].settings.y || Math.max(0, rect.h / 2 - layoutRect.h / 2)
+ );
+ }
+ }
+
+ $(window).on('resize', reposition);
+ }
+
+ var Window = FloatPanel.extend({
+ modal: true,
+
+ Defaults: {
+ border: 1,
+ layout: 'flex',
+ containerCls: 'panel',
+ role: 'dialog',
+ callbacks: {
+ submit: function() {
+ this.fire('submit', {data: this.toJSON()});
+ },
+
+ close: function() {
+ this.close();
+ }
+ }
+ },
+
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+
+ if (self.isRtl()) {
+ self.classes.add('rtl');
+ }
+
+ self.classes.add('window');
+ self.bodyClasses.add('window-body');
+ self.state.set('fixed', true);
+
+ // Create statusbar
+ if (settings.buttons) {
+ self.statusbar = new Panel({
+ layout: 'flex',
+ border: '1 0 0 0',
+ spacing: 3,
+ padding: 10,
+ align: 'center',
+ pack: self.isRtl() ? 'start' : 'end',
+ defaults: {
+ type: 'button'
+ },
+ items: settings.buttons
+ });
+
+ self.statusbar.classes.add('foot');
+ self.statusbar.parent(self);
+ }
+
+ self.on('click', function(e) {
+ var closeClass = self.classPrefix + 'close';
+
+ if (DomUtils.hasClass(e.target, closeClass) || DomUtils.hasClass(e.target.parentNode, closeClass)) {
+ self.close();
+ }
+ });
+
+ self.on('cancel', function() {
+ self.close();
+ });
+
+ self.aria('describedby', self.describedBy || self._id + '-none');
+ self.aria('label', settings.title);
+ self._fullscreen = false;
+ },
+
+ /**
+ * Recalculates the positions of the controls in the current container.
+ * This is invoked by the reflow method and shouldn't be called directly.
+ *
+ * @method recalc
+ */
+ recalc: function() {
+ var self = this, statusbar = self.statusbar, layoutRect, width, x, needsRecalc;
+
+ if (self._fullscreen) {
+ self.layoutRect(DomUtils.getWindowSize());
+ self.layoutRect().contentH = self.layoutRect().innerH;
+ }
+
+ self._super();
+
+ layoutRect = self.layoutRect();
+
+ // Resize window based on title width
+ if (self.settings.title && !self._fullscreen) {
+ width = layoutRect.headerW;
+ if (width > layoutRect.w) {
+ x = layoutRect.x - Math.max(0, width / 2);
+ self.layoutRect({w: width, x: x});
+ needsRecalc = true;
+ }
+ }
+
+ // Resize window based on statusbar width
+ if (statusbar) {
+ statusbar.layoutRect({w: self.layoutRect().innerW}).recalc();
+
+ width = statusbar.layoutRect().minW + layoutRect.deltaW;
+ if (width > layoutRect.w) {
+ x = layoutRect.x - Math.max(0, width - layoutRect.w);
+ self.layoutRect({w: width, x: x});
+ needsRecalc = true;
+ }
+ }
+
+ // Recalc body and disable auto resize
+ if (needsRecalc) {
+ self.recalc();
+ }
+ },
+
+ /**
+ * Initializes the current controls layout rect.
+ * This will be executed by the layout managers to determine the
+ * default minWidth/minHeight etc.
+ *
+ * @method initLayoutRect
+ * @return {Object} Layout rect instance.
+ */
+ initLayoutRect: function() {
+ var self = this, layoutRect = self._super(), deltaH = 0, headEl;
+
+ // Reserve vertical space for title
+ if (self.settings.title && !self._fullscreen) {
+ headEl = self.getEl('head');
+
+ var size = DomUtils.getSize(headEl);
+
+ layoutRect.headerW = size.width;
+ layoutRect.headerH = size.height;
+
+ deltaH += layoutRect.headerH;
+ }
+
+ // Reserve vertical space for statusbar
+ if (self.statusbar) {
+ deltaH += self.statusbar.layoutRect().h;
+ }
+
+ layoutRect.deltaH += deltaH;
+ layoutRect.minH += deltaH;
+ //layoutRect.innerH -= deltaH;
+ layoutRect.h += deltaH;
+
+ var rect = DomUtils.getWindowSize();
+
+ layoutRect.x = self.settings.x || Math.max(0, rect.w / 2 - layoutRect.w / 2);
+ layoutRect.y = self.settings.y || Math.max(0, rect.h / 2 - layoutRect.h / 2);
+
+ return layoutRect;
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, layout = self._layout, id = self._id, prefix = self.classPrefix;
+ var settings = self.settings, headerHtml = '', footerHtml = '', html = settings.html;
+
+ self.preRender();
+ layout.preRender(self);
+
+ if (settings.title) {
+ headerHtml = (
+ '<div id="' + id + '-head" class="' + prefix + 'window-head">' +
+ '<div id="' + id + '-title" class="' + prefix + 'title">' + self.encode(settings.title) + '</div>' +
+ '<div id="' + id + '-dragh" class="' + prefix + 'dragh"></div>' +
+ '<button type="button" class="' + prefix + 'close" aria-hidden="true">' +
+ '<i class="mce-ico mce-i-remove"></i>' +
+ '</button>' +
+ '</div>'
+ );
+ }
+
+ if (settings.url) {
+ html = '<iframe src="' + settings.url + '" tabindex="-1"></iframe>';
+ }
+
+ if (typeof html == "undefined") {
+ html = layout.renderHtml(self);
+ }
+
+ if (self.statusbar) {
+ footerHtml = self.statusbar.renderHtml();
+ }
+
+ return (
+ '<div id="' + id + '" class="' + self.classes + '" hidefocus="1">' +
+ '<div class="' + self.classPrefix + 'reset" role="application">' +
+ headerHtml +
+ '<div id="' + id + '-body" class="' + self.bodyClasses + '">' +
+ html +
+ '</div>' +
+ footerHtml +
+ '</div>' +
+ '</div>'
+ );
+ },
+
+ /**
+ * Switches the window fullscreen mode.
+ *
+ * @method fullscreen
+ * @param {Boolean} state True/false state.
+ * @return {tinymce.ui.Window} Current window instance.
+ */
+ fullscreen: function(state) {
+ var self = this, documentElement = document.documentElement, slowRendering, prefix = self.classPrefix, layoutRect;
+
+ if (state != self._fullscreen) {
+ $(window).on('resize', function() {
+ var time;
+
+ if (self._fullscreen) {
+ // Time the layout time if it's to slow use a timeout to not hog the CPU
+ if (!slowRendering) {
+ time = new Date().getTime();
+
+ var rect = DomUtils.getWindowSize();
+ self.moveTo(0, 0).resizeTo(rect.w, rect.h);
+
+ if ((new Date().getTime()) - time > 50) {
+ slowRendering = true;
+ }
+ } else {
+ if (!self._timer) {
+ self._timer = Delay.setTimeout(function() {
+ var rect = DomUtils.getWindowSize();
+ self.moveTo(0, 0).resizeTo(rect.w, rect.h);
+
+ self._timer = 0;
+ }, 50);
+ }
+ }
+ }
+ });
+
+ layoutRect = self.layoutRect();
+ self._fullscreen = state;
+
+ if (!state) {
+ self.borderBox = BoxUtils.parseBox(self.settings.border);
+ self.getEl('head').style.display = '';
+ layoutRect.deltaH += layoutRect.headerH;
+ $([documentElement, document.body]).removeClass(prefix + 'fullscreen');
+ self.classes.remove('fullscreen');
+ self.moveTo(self._initial.x, self._initial.y).resizeTo(self._initial.w, self._initial.h);
+ } else {
+ self._initial = {x: layoutRect.x, y: layoutRect.y, w: layoutRect.w, h: layoutRect.h};
+
+ self.borderBox = BoxUtils.parseBox('0');
+ self.getEl('head').style.display = 'none';
+ layoutRect.deltaH -= layoutRect.headerH + 2;
+ $([documentElement, document.body]).addClass(prefix + 'fullscreen');
+ self.classes.add('fullscreen');
+
+ var rect = DomUtils.getWindowSize();
+ self.moveTo(0, 0).resizeTo(rect.w, rect.h);
+ }
+ }
+
+ return self.reflow();
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this, startPos;
+
+ setTimeout(function() {
+ self.classes.add('in');
+ self.fire('open');
+ }, 0);
+
+ self._super();
+
+ if (self.statusbar) {
+ self.statusbar.postRender();
+ }
+
+ self.focus();
+
+ this.dragHelper = new DragHelper(self._id + '-dragh', {
+ start: function() {
+ startPos = {
+ x: self.layoutRect().x,
+ y: self.layoutRect().y
+ };
+ },
+
+ drag: function(e) {
+ self.moveTo(startPos.x + e.deltaX, startPos.y + e.deltaY);
+ }
+ });
+
+ self.on('submit', function(e) {
+ if (!e.isDefaultPrevented()) {
+ self.close();
+ }
+ });
+
+ windows.push(self);
+ toggleFullScreenState(true);
+ },
+
+ /**
+ * Fires a submit event with the serialized form.
+ *
+ * @method submit
+ * @return {Object} Event arguments object.
+ */
+ submit: function() {
+ return this.fire('submit', {data: this.toJSON()});
+ },
+
+ /**
+ * Removes the current control from DOM and from UI collections.
+ *
+ * @method remove
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ remove: function() {
+ var self = this, i;
+
+ self.dragHelper.destroy();
+ self._super();
+
+ if (self.statusbar) {
+ this.statusbar.remove();
+ }
+
+ toggleBodyFullScreenClasses(self.classPrefix, false);
+
+ i = windows.length;
+ while (i--) {
+ if (windows[i] === self) {
+ windows.splice(i, 1);
+ }
+ }
+
+ toggleFullScreenState(windows.length > 0);
+ },
+
+ /**
+ * Returns the contentWindow object of the iframe if it exists.
+ *
+ * @method getContentWindow
+ * @return {Window} window object or null.
+ */
+ getContentWindow: function() {
+ var ifr = this.getEl().getElementsByTagName('iframe')[0];
+ return ifr ? ifr.contentWindow : null;
+ }
+ });
+
+ handleWindowResize();
+
+ return Window;
+});
+
+// Included from: js/tinymce/classes/ui/MessageBox.js
+
+/**
+ * MessageBox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class is used to create MessageBoxes like alerts/confirms etc.
+ *
+ * @class tinymce.ui.MessageBox
+ * @extends tinymce.ui.FloatPanel
+ */
+define("tinymce/ui/MessageBox", [
+ "tinymce/ui/Window"
+], function(Window) {
+ "use strict";
+
+ var MessageBox = Window.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ settings = {
+ border: 1,
+ padding: 20,
+ layout: 'flex',
+ pack: "center",
+ align: "center",
+ containerCls: 'panel',
+ autoScroll: true,
+ buttons: {type: "button", text: "Ok", action: "ok"},
+ items: {
+ type: "label",
+ multiline: true,
+ maxWidth: 500,
+ maxHeight: 200
+ }
+ };
+
+ this._super(settings);
+ },
+
+ Statics: {
+ /**
+ * Ok buttons constant.
+ *
+ * @static
+ * @final
+ * @field {Number} OK
+ */
+ OK: 1,
+
+ /**
+ * Ok/cancel buttons constant.
+ *
+ * @static
+ * @final
+ * @field {Number} OK_CANCEL
+ */
+ OK_CANCEL: 2,
+
+ /**
+ * yes/no buttons constant.
+ *
+ * @static
+ * @final
+ * @field {Number} YES_NO
+ */
+ YES_NO: 3,
+
+ /**
+ * yes/no/cancel buttons constant.
+ *
+ * @static
+ * @final
+ * @field {Number} YES_NO_CANCEL
+ */
+ YES_NO_CANCEL: 4,
+
+ /**
+ * Constructs a new message box and renders it to the body element.
+ *
+ * @static
+ * @method msgBox
+ * @param {Object} settings Name/value object with settings.
+ */
+ msgBox: function(settings) {
+ var buttons, callback = settings.callback || function() {};
+
+ function createButton(text, status, primary) {
+ return {
+ type: "button",
+ text: text,
+ subtype: primary ? 'primary' : '',
+ onClick: function(e) {
+ e.control.parents()[1].close();
+ callback(status);
+ }
+ };
+ }
+
+ switch (settings.buttons) {
+ case MessageBox.OK_CANCEL:
+ buttons = [
+ createButton('Ok', true, true),
+ createButton('Cancel', false)
+ ];
+ break;
+
+ case MessageBox.YES_NO:
+ case MessageBox.YES_NO_CANCEL:
+ buttons = [
+ createButton('Yes', 1, true),
+ createButton('No', 0)
+ ];
+
+ if (settings.buttons == MessageBox.YES_NO_CANCEL) {
+ buttons.push(createButton('Cancel', -1));
+ }
+ break;
+
+ default:
+ buttons = [
+ createButton('Ok', true, true)
+ ];
+ break;
+ }
+
+ return new Window({
+ padding: 20,
+ x: settings.x,
+ y: settings.y,
+ minWidth: 300,
+ minHeight: 100,
+ layout: "flex",
+ pack: "center",
+ align: "center",
+ buttons: buttons,
+ title: settings.title,
+ role: 'alertdialog',
+ items: {
+ type: "label",
+ multiline: true,
+ maxWidth: 500,
+ maxHeight: 200,
+ text: settings.text
+ },
+ onPostRender: function() {
+ this.aria('describedby', this.items()[0]._id);
+ },
+ onClose: settings.onClose,
+ onCancel: function() {
+ callback(false);
+ }
+ }).renderTo(document.body).reflow();
+ },
+
+ /**
+ * Creates a new alert dialog.
+ *
+ * @method alert
+ * @param {Object} settings Settings for the alert dialog.
+ * @param {function} [callback] Callback to execute when the user makes a choice.
+ */
+ alert: function(settings, callback) {
+ if (typeof settings == "string") {
+ settings = {text: settings};
+ }
+
+ settings.callback = callback;
+ return MessageBox.msgBox(settings);
+ },
+
+ /**
+ * Creates a new confirm dialog.
+ *
+ * @method confirm
+ * @param {Object} settings Settings for the confirm dialog.
+ * @param {function} [callback] Callback to execute when the user makes a choice.
+ */
+ confirm: function(settings, callback) {
+ if (typeof settings == "string") {
+ settings = {text: settings};
+ }
+
+ settings.callback = callback;
+ settings.buttons = MessageBox.OK_CANCEL;
+
+ return MessageBox.msgBox(settings);
+ }
+ }
+ });
+
+ return MessageBox;
+});
+
+// Included from: js/tinymce/classes/WindowManager.js
+
+/**
+ * WindowManager.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class handles the creation of native windows and dialogs. This class can be extended to provide for example inline dialogs.
+ *
+ * @class tinymce.WindowManager
+ * @example
+ * // Opens a new dialog with the file.htm file and the size 320x240
+ * // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog.
+ * tinymce.activeEditor.windowManager.open({
+ * url: 'file.htm',
+ * width: 320,
+ * height: 240
+ * }, {
+ * custom_param: 1
+ * });
+ *
+ * // Displays an alert box using the active editors window manager instance
+ * tinymce.activeEditor.windowManager.alert('Hello world!');
+ *
+ * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm
+ * tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) {
+ * if (s)
+ * tinymce.activeEditor.windowManager.alert("Ok");
+ * else
+ * tinymce.activeEditor.windowManager.alert("Cancel");
+ * });
+ */
+define("tinymce/WindowManager", [
+ "tinymce/ui/Window",
+ "tinymce/ui/MessageBox"
+], function(Window, MessageBox) {
+ return function(editor) {
+ var self = this, windows = [];
+
+ function getTopMostWindow() {
+ if (windows.length) {
+ return windows[windows.length - 1];
+ }
+ }
+
+ function fireOpenEvent(win) {
+ editor.fire('OpenWindow', {
+ win: win
+ });
+ }
+
+ function fireCloseEvent(win) {
+ editor.fire('CloseWindow', {
+ win: win
+ });
+ }
+
+ self.windows = windows;
+
+ editor.on('remove', function() {
+ var i = windows.length;
+
+ while (i--) {
+ windows[i].close();
+ }
+ });
+
+ /**
+ * Opens a new window.
+ *
+ * @method open
+ * @param {Object} args Optional name/value settings collection contains things like width/height/url etc.
+ * @param {Object} params Options like title, file, width, height etc.
+ * @option {String} title Window title.
+ * @option {String} file URL of the file to open in the window.
+ * @option {Number} width Width in pixels.
+ * @option {Number} height Height in pixels.
+ * @option {Boolean} autoScroll Specifies whether the popup window can have scrollbars if required (i.e. content
+ * larger than the popup size specified).
+ */
+ self.open = function(args, params) {
+ var win;
+
+ editor.editorManager.setActive(editor);
+
+ args.title = args.title || ' ';
+
+ // Handle URL
+ args.url = args.url || args.file; // Legacy
+ if (args.url) {
+ args.width = parseInt(args.width || 320, 10);
+ args.height = parseInt(args.height || 240, 10);
+ }
+
+ // Handle body
+ if (args.body) {
+ args.items = {
+ defaults: args.defaults,
+ type: args.bodyType || 'form',
+ items: args.body,
+ data: args.data,
+ callbacks: args.commands
+ };
+ }
+
+ if (!args.url && !args.buttons) {
+ args.buttons = [
+ {text: 'Ok', subtype: 'primary', onclick: function() {
+ win.find('form')[0].submit();
+ }},
+
+ {text: 'Cancel', onclick: function() {
+ win.close();
+ }}
+ ];
+ }
+
+ win = new Window(args);
+ windows.push(win);
+
+ win.on('close', function() {
+ var i = windows.length;
+
+ while (i--) {
+ if (windows[i] === win) {
+ windows.splice(i, 1);
+ }
+ }
+
+ if (!windows.length) {
+ editor.focus();
+ }
+
+ fireCloseEvent(win);
+ });
+
+ // Handle data
+ if (args.data) {
+ win.on('postRender', function() {
+ this.find('*').each(function(ctrl) {
+ var name = ctrl.name();
+
+ if (name in args.data) {
+ ctrl.value(args.data[name]);
+ }
+ });
+ });
+ }
+
+ // store args and parameters
+ win.features = args || {};
+ win.params = params || {};
+
+ // Takes a snapshot in the FocusManager of the selection before focus is lost to dialog
+ if (windows.length === 1) {
+ editor.nodeChanged();
+ }
+
+ win = win.renderTo().reflow();
+
+ fireOpenEvent(win);
+
+ return win;
+ };
+
+ /**
+ * Creates a alert dialog. Please don't use the blocking behavior of this
+ * native version use the callback method instead then it can be extended.
+ *
+ * @method alert
+ * @param {String} message Text to display in the new alert dialog.
+ * @param {function} callback Callback function to be executed after the user has selected ok.
+ * @param {Object} scope Optional scope to execute the callback in.
+ * @example
+ * // Displays an alert box using the active editors window manager instance
+ * tinymce.activeEditor.windowManager.alert('Hello world!');
+ */
+ self.alert = function(message, callback, scope) {
+ var win;
+
+ win = MessageBox.alert(message, function() {
+ if (callback) {
+ callback.call(scope || this);
+ } else {
+ editor.focus();
+ }
+ });
+
+ win.on('close', function() {
+ fireCloseEvent(win);
+ });
+
+ fireOpenEvent(win);
+ };
+
+ /**
+ * Creates a confirm dialog. Please don't use the blocking behavior of this
+ * native version use the callback method instead then it can be extended.
+ *
+ * @method confirm
+ * @param {String} message Text to display in the new confirm dialog.
+ * @param {function} callback Callback function to be executed after the user has selected ok or cancel.
+ * @param {Object} scope Optional scope to execute the callback in.
+ * @example
+ * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm
+ * tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) {
+ * if (s)
+ * tinymce.activeEditor.windowManager.alert("Ok");
+ * else
+ * tinymce.activeEditor.windowManager.alert("Cancel");
+ * });
+ */
+ self.confirm = function(message, callback, scope) {
+ var win;
+
+ win = MessageBox.confirm(message, function(state) {
+ callback.call(scope || this, state);
+ });
+
+ win.on('close', function() {
+ fireCloseEvent(win);
+ });
+
+ fireOpenEvent(win);
+ };
+
+ /**
+ * Closes the top most window.
+ *
+ * @method close
+ */
+ self.close = function() {
+ if (getTopMostWindow()) {
+ getTopMostWindow().close();
+ }
+ };
+
+ /**
+ * Returns the params of the last window open call. This can be used in iframe based
+ * dialog to get params passed from the tinymce plugin.
+ *
+ * @example
+ * var dialogArguments = top.tinymce.activeEditor.windowManager.getParams();
+ *
+ * @method getParams
+ * @return {Object} Name/value object with parameters passed from windowManager.open call.
+ */
+ self.getParams = function() {
+ return getTopMostWindow() ? getTopMostWindow().params : null;
+ };
+
+ /**
+ * Sets the params of the last opened window.
+ *
+ * @method setParams
+ * @param {Object} params Params object to set for the last opened window.
+ */
+ self.setParams = function(params) {
+ if (getTopMostWindow()) {
+ getTopMostWindow().params = params;
+ }
+ };
+
+ /**
+ * Returns the currently opened window objects.
+ *
+ * @method getWindows
+ * @return {Array} Array of the currently opened windows.
+ */
+ self.getWindows = function() {
+ return windows;
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/ui/Tooltip.js
+
+/**
+ * Tooltip.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a tooltip instance.
+ *
+ * @-x-less ToolTip.less
+ * @class tinymce.ui.ToolTip
+ * @extends tinymce.ui.Control
+ * @mixes tinymce.ui.Movable
+ */
+define("tinymce/ui/Tooltip", [
+ "tinymce/ui/Control",
+ "tinymce/ui/Movable"
+], function(Control, Movable) {
+ return Control.extend({
+ Mixins: [Movable],
+
+ Defaults: {
+ classes: 'widget tooltip tooltip-n'
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, prefix = self.classPrefix;
+
+ return (
+ '<div id="' + self._id + '" class="' + self.classes + '" role="presentation">' +
+ '<div class="' + prefix + 'tooltip-arrow"></div>' +
+ '<div class="' + prefix + 'tooltip-inner">' + self.encode(self.state.get('text')) + '</div>' +
+ '</div>'
+ );
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:text', function(e) {
+ self.getEl().lastChild.innerHTML = self.encode(e.value);
+ });
+
+ return self._super();
+ },
+
+ /**
+ * Repaints the control after a layout operation.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ var self = this, style, rect;
+
+ style = self.getEl().style;
+ rect = self._layoutRect;
+
+ style.left = rect.x + 'px';
+ style.top = rect.y + 'px';
+ style.zIndex = 0xFFFF + 0xFFFF;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Widget.js
+
+/**
+ * Widget.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Widget base class a widget is a control that has a tooltip and some basic states.
+ *
+ * @class tinymce.ui.Widget
+ * @extends tinymce.ui.Control
+ */
+define("tinymce/ui/Widget", [
+ "tinymce/ui/Control",
+ "tinymce/ui/Tooltip"
+], function(Control, Tooltip) {
+ "use strict";
+
+ var tooltip;
+
+ var Widget = Control.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {String} tooltip Tooltip text to display when hovering.
+ * @setting {Boolean} autofocus True if the control should be focused when rendered.
+ * @setting {String} text Text to display inside widget.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+ settings = self.settings;
+ self.canFocus = true;
+
+ if (settings.tooltip && Widget.tooltips !== false) {
+ self.on('mouseenter', function(e) {
+ var tooltip = self.tooltip().moveTo(-0xFFFF);
+
+ if (e.control == self) {
+ var rel = tooltip.text(settings.tooltip).show().testMoveRel(self.getEl(), ['bc-tc', 'bc-tl', 'bc-tr']);
+
+ tooltip.classes.toggle('tooltip-n', rel == 'bc-tc');
+ tooltip.classes.toggle('tooltip-nw', rel == 'bc-tl');
+ tooltip.classes.toggle('tooltip-ne', rel == 'bc-tr');
+
+ tooltip.moveRel(self.getEl(), rel);
+ } else {
+ tooltip.hide();
+ }
+ });
+
+ self.on('mouseleave mousedown click', function() {
+ self.tooltip().hide();
+ });
+ }
+
+ self.aria('label', settings.ariaLabel || settings.tooltip);
+ },
+
+ /**
+ * Returns the current tooltip instance.
+ *
+ * @method tooltip
+ * @return {tinymce.ui.Tooltip} Tooltip instance.
+ */
+ tooltip: function() {
+ if (!tooltip) {
+ tooltip = new Tooltip({type: 'tooltip'});
+ tooltip.renderTo();
+ }
+
+ return tooltip;
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this, settings = self.settings;
+
+ self._super();
+
+ if (!self.parent() && (settings.width || settings.height)) {
+ self.initLayoutRect();
+ self.repaint();
+ }
+
+ if (settings.autofocus) {
+ self.focus();
+ }
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ function disable(state) {
+ self.aria('disabled', state);
+ self.classes.toggle('disabled', state);
+ }
+
+ function active(state) {
+ self.aria('pressed', state);
+ self.classes.toggle('active', state);
+ }
+
+ self.state.on('change:disabled', function(e) {
+ disable(e.value);
+ });
+
+ self.state.on('change:active', function(e) {
+ active(e.value);
+ });
+
+ if (self.state.get('disabled')) {
+ disable(true);
+ }
+
+ if (self.state.get('active')) {
+ active(true);
+ }
+
+ return self._super();
+ },
+
+ /**
+ * Removes the current control from DOM and from UI collections.
+ *
+ * @method remove
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ remove: function() {
+ this._super();
+
+ if (tooltip) {
+ tooltip.remove();
+ tooltip = null;
+ }
+ }
+ });
+
+ return Widget;
+});
+
+// Included from: js/tinymce/classes/ui/Progress.js
+
+/**
+ * Progress.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Progress control.
+ *
+ * @-x-less Progress.less
+ * @class tinymce.ui.Progress
+ * @extends tinymce.ui.Control
+ */
+define("tinymce/ui/Progress", [
+ "tinymce/ui/Widget"
+], function(Widget) {
+ "use strict";
+
+ return Widget.extend({
+ Defaults: {
+ value: 0
+ },
+
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+ self.classes.add('progress');
+
+ if (!self.settings.filter) {
+ self.settings.filter = function(value) {
+ return Math.round(value);
+ };
+ }
+ },
+
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = this.classPrefix;
+
+ return (
+ '<div id="' + id + '" class="' + self.classes + '">' +
+ '<div class="' + prefix + 'bar-container">' +
+ '<div class="' + prefix + 'bar"></div>' +
+ '</div>' +
+ '<div class="' + prefix + 'text">0%</div>' +
+ '</div>'
+ );
+ },
+
+ postRender: function() {
+ var self = this;
+
+ self._super();
+ self.value(self.settings.value);
+
+ return self;
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ function setValue(value) {
+ value = self.settings.filter(value);
+ self.getEl().lastChild.innerHTML = value + '%';
+ self.getEl().firstChild.firstChild.style.width = value + '%';
+ }
+
+ self.state.on('change:value', function(e) {
+ setValue(e.value);
+ });
+
+ setValue(self.state.get('value'));
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Notification.js
+
+/**
+ * Notification.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a notification instance.
+ *
+ * @-x-less Notification.less
+ * @class tinymce.ui.Notification
+ * @extends tinymce.ui.Container
+ * @mixes tinymce.ui.Movable
+ */
+define("tinymce/ui/Notification", [
+ "tinymce/ui/Control",
+ "tinymce/ui/Movable",
+ "tinymce/ui/Progress",
+ "tinymce/util/Delay"
+], function(Control, Movable, Progress, Delay) {
+ return Control.extend({
+ Mixins: [Movable],
+
+ Defaults: {
+ classes: 'widget notification'
+ },
+
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+
+ if (settings.text) {
+ self.text(settings.text);
+ }
+
+ if (settings.icon) {
+ self.icon = settings.icon;
+ }
+
+ if (settings.color) {
+ self.color = settings.color;
+ }
+
+ if (settings.type) {
+ self.classes.add('notification-' + settings.type);
+ }
+
+ if (settings.timeout && (settings.timeout < 0 || settings.timeout > 0) && !settings.closeButton) {
+ self.closeButton = false;
+ } else {
+ self.classes.add('has-close');
+ self.closeButton = true;
+ }
+
+ if (settings.progressBar) {
+ self.progressBar = new Progress();
+ }
+
+ self.on('click', function(e) {
+ if (e.target.className.indexOf(self.classPrefix + 'close') != -1) {
+ self.close();
+ }
+ });
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, prefix = self.classPrefix, icon = '', closeButton = '', progressBar = '', notificationStyle = '';
+
+ if (self.icon) {
+ icon = '<i class="' + prefix + 'ico' + ' ' + prefix + 'i-' + self.icon + '"></i>';
+ }
+
+ if (self.color) {
+ notificationStyle = ' style="background-color: ' + self.color + '"';
+ }
+
+ if (self.closeButton) {
+ closeButton = '<button type="button" class="' + prefix + 'close" aria-hidden="true">\u00d7</button>';
+ }
+
+ if (self.progressBar) {
+ progressBar = self.progressBar.renderHtml();
+ }
+
+ return (
+ '<div id="' + self._id + '" class="' + self.classes + '"' + notificationStyle + ' role="presentation">' +
+ icon +
+ '<div class="' + prefix + 'notification-inner">' + self.state.get('text') + '</div>' +
+ progressBar +
+ closeButton +
+ '</div>'
+ );
+ },
+
+ postRender: function() {
+ var self = this;
+
+ Delay.setTimeout(function() {
+ self.$el.addClass(self.classPrefix + 'in');
+ });
+
+ return self._super();
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:text', function(e) {
+ self.getEl().childNodes[1].innerHTML = e.value;
+ });
+ if (self.progressBar) {
+ self.progressBar.bindStates();
+ }
+ return self._super();
+ },
+
+ close: function() {
+ var self = this;
+
+ if (!self.fire('close').isDefaultPrevented()) {
+ self.remove();
+ }
+
+ return self;
+ },
+
+ /**
+ * Repaints the control after a layout operation.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ var self = this, style, rect;
+
+ style = self.getEl().style;
+ rect = self._layoutRect;
+
+ style.left = rect.x + 'px';
+ style.top = rect.y + 'px';
+
+ // Hardcoded arbitrary z-value because we want the
+ // notifications under the other windows
+ style.zIndex = 0xFFFF - 1;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/NotificationManager.js
+
+/**
+ * NotificationManager.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class handles the creation of TinyMCE's notifications.
+ *
+ * @class tinymce.notificationManager
+ * @example
+ * // Opens a new notification of type "error" with text "An error occurred."
+ * tinymce.activeEditor.notificationManager.open({
+ * text: 'An error occurred.',
+ * type: 'error'
+ * });
+ */
+define("tinymce/NotificationManager", [
+ "tinymce/ui/Notification",
+ "tinymce/util/Delay",
+ "tinymce/util/Tools"
+], function(Notification, Delay, Tools) {
+ return function(editor) {
+ var self = this, notifications = [];
+
+ function getLastNotification() {
+ if (notifications.length) {
+ return notifications[notifications.length - 1];
+ }
+ }
+
+ self.notifications = notifications;
+
+ function resizeWindowEvent() {
+ Delay.requestAnimationFrame(function() {
+ prePositionNotifications();
+ positionNotifications();
+ });
+ }
+
+ // Since the viewport will change based on the present notifications, we need to move them all to the
+ // top left of the viewport to give an accurate size measurement so we can position them later.
+ function prePositionNotifications() {
+ for (var i = 0; i < notifications.length; i++) {
+ notifications[i].moveTo(0, 0);
+ }
+ }
+
+ function positionNotifications() {
+ if (notifications.length > 0) {
+ var firstItem = notifications.slice(0, 1)[0];
+ var container = editor.inline ? editor.getElement() : editor.getContentAreaContainer();
+ firstItem.moveRel(container, 'tc-tc');
+ if (notifications.length > 1) {
+ for (var i = 1; i < notifications.length; i++) {
+ notifications[i].moveRel(notifications[i - 1].getEl(), 'bc-tc');
+ }
+ }
+ }
+ }
+
+ editor.on('remove', function() {
+ var i = notifications.length;
+
+ while (i--) {
+ notifications[i].close();
+ }
+ });
+
+ editor.on('ResizeEditor', positionNotifications);
+ editor.on('ResizeWindow', resizeWindowEvent);
+
+ /**
+ * Opens a new notification.
+ *
+ * @method open
+ * @param {Object} args Optional name/value settings collection contains things like timeout/color/message etc.
+ */
+ self.open = function(args) {
+ // Never open notification if editor has been removed.
+ if (editor.removed) {
+ return;
+ }
+
+ var notif;
+
+ editor.editorManager.setActive(editor);
+
+ var duplicate = findDuplicateMessage(notifications, args);
+
+ if (duplicate === null) {
+ notif = new Notification(args);
+ notifications.push(notif);
+
+ //If we have a timeout value
+ if (args.timeout > 0) {
+ notif.timer = setTimeout(function() {
+ notif.close();
+ }, args.timeout);
+ }
+
+ notif.on('close', function() {
+ var i = notifications.length;
+
+ if (notif.timer) {
+ editor.getWin().clearTimeout(notif.timer);
+ }
+
+ while (i--) {
+ if (notifications[i] === notif) {
+ notifications.splice(i, 1);
+ }
+ }
+
+ positionNotifications();
+ });
+
+ notif.renderTo();
+
+ positionNotifications();
+ } else {
+ notif = duplicate;
+ }
+
+ return notif;
+ };
+
+ /**
+ * Closes the top most notification.
+ *
+ * @method close
+ */
+ self.close = function() {
+ if (getLastNotification()) {
+ getLastNotification().close();
+ }
+ };
+
+ /**
+ * Returns the currently opened notification objects.
+ *
+ * @method getNotifications
+ * @return {Array} Array of the currently opened notifications.
+ */
+ self.getNotifications = function() {
+ return notifications;
+ };
+
+ editor.on('SkinLoaded', function() {
+ var serviceMessage = editor.settings.service_message;
+
+ if (serviceMessage) {
+ editor.notificationManager.open({
+ text: serviceMessage,
+ type: 'warning',
+ timeout: 0,
+ icon: ''
+ });
+ }
+ });
+
+ /**
+ * Finds any existing notification with the same properties as the new one.
+ * Returns either the found notification or null.
+ *
+ * @param {Notification[]} notificationArray - Array of current notifications
+ * @param {type: string, } newNotification - New notification object
+ * @returns {?Notification}
+ */
+ function findDuplicateMessage(notificationArray, newNotification) {
+ if (!isPlainTextNotification(newNotification)) {
+ return null;
+ }
+
+ var filteredNotifications = Tools.grep(notificationArray, function (notification) {
+ return isSameNotification(newNotification, notification);
+ });
+
+ return filteredNotifications.length === 0 ? null : filteredNotifications[0];
+ }
+
+ /**
+ * Checks if the passed in args object has the same
+ * type and text properties as the sent in notification.
+ *
+ * @param {type: string, text: string} a - New notification args object
+ * @param {Notification} b - Old notification
+ * @returns {boolean}
+ */
+ function isSameNotification(a, b) {
+ return a.type === b.settings.type && a.text === b.settings.text;
+ }
+
+ /**
+ * Checks that the notification does not have a progressBar
+ * or timeour property.
+ *
+ * @param {Notification} notification - Notification to check
+ * @returns {boolean}
+ */
+ function isPlainTextNotification(notification) {
+ return !notification.progressBar && !notification.timeout;
+ }
+
+ //self.positionNotifications = positionNotifications;
+ };
+});
+
+// Included from: js/tinymce/classes/dom/NodePath.js
+
+/**
+ * NodePath.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Handles paths of nodes within an element.
+ *
+ * @private
+ * @class tinymce.dom.NodePath
+ */
+define("tinymce/dom/NodePath", [
+ "tinymce/dom/DOMUtils"
+], function(DOMUtils) {
+ function create(rootNode, targetNode, normalized) {
+ var path = [];
+
+ for (; targetNode && targetNode != rootNode; targetNode = targetNode.parentNode) {
+ path.push(DOMUtils.nodeIndex(targetNode, normalized));
+ }
+
+ return path;
+ }
+
+ function resolve(rootNode, path) {
+ var i, node, children;
+
+ for (node = rootNode, i = path.length - 1; i >= 0; i--) {
+ children = node.childNodes;
+
+ if (path[i] > children.length - 1) {
+ return null;
+ }
+
+ node = children[path[i]];
+ }
+
+ return node;
+ }
+
+ return {
+ create: create,
+ resolve: resolve
+ };
+});
+
+// Included from: js/tinymce/classes/util/Quirks.js
+
+/**
+ * Quirks.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ *
+ * @ignore-file
+ */
+
+/**
+ * This file includes fixes for various browser quirks it's made to make it easy to add/remove browser specific fixes.
+ *
+ * @private
+ * @class tinymce.util.Quirks
+ */
+define("tinymce/util/Quirks", [
+ "tinymce/util/VK",
+ "tinymce/dom/RangeUtils",
+ "tinymce/dom/TreeWalker",
+ "tinymce/dom/NodePath",
+ "tinymce/html/Node",
+ "tinymce/html/Entities",
+ "tinymce/Env",
+ "tinymce/util/Tools",
+ "tinymce/util/Delay",
+ "tinymce/caret/CaretContainer",
+ "tinymce/caret/CaretPosition",
+ "tinymce/caret/CaretWalker"
+], function(VK, RangeUtils, TreeWalker, NodePath, Node, Entities, Env, Tools, Delay, CaretContainer, CaretPosition, CaretWalker) {
+ return function(editor) {
+ var each = Tools.each, $ = editor.$;
+ var BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection,
+ settings = editor.settings, parser = editor.parser, serializer = editor.serializer;
+ var isGecko = Env.gecko, isIE = Env.ie, isWebKit = Env.webkit;
+ var mceInternalUrlPrefix = 'data:text/mce-internal,';
+ var mceInternalDataType = isIE ? 'Text' : 'URL';
+
+ /**
+ * Executes a command with a specific state this can be to enable/disable browser editing features.
+ */
+ function setEditorCommandState(cmd, state) {
+ try {
+ editor.getDoc().execCommand(cmd, false, state);
+ } catch (ex) {
+ // Ignore
+ }
+ }
+
+ /**
+ * Returns current IE document mode.
+ */
+ function getDocumentMode() {
+ var documentMode = editor.getDoc().documentMode;
+
+ return documentMode ? documentMode : 6;
+ }
+
+ /**
+ * Returns true/false if the event is prevented or not.
+ *
+ * @private
+ * @param {Event} e Event object.
+ * @return {Boolean} true/false if the event is prevented or not.
+ */
+ function isDefaultPrevented(e) {
+ return e.isDefaultPrevented();
+ }
+
+ /**
+ * Sets Text/URL data on the event's dataTransfer object to a special data:text/mce-internal url.
+ * This is to workaround the inability to set custom contentType on IE and Safari.
+ * The editor's selected content is encoded into this url so drag and drop between editors will work.
+ *
+ * @private
+ * @param {DragEvent} e Event object
+ */
+ function setMceInternalContent(e) {
+ var selectionHtml, internalContent;
+
+ if (e.dataTransfer) {
+ if (editor.selection.isCollapsed() && e.target.tagName == 'IMG') {
+ selection.select(e.target);
+ }
+
+ selectionHtml = editor.selection.getContent();
+
+ // Safari/IE doesn't support custom dataTransfer items so we can only use URL and Text
+ if (selectionHtml.length > 0) {
+ internalContent = mceInternalUrlPrefix + escape(editor.id) + ',' + escape(selectionHtml);
+ e.dataTransfer.setData(mceInternalDataType, internalContent);
+ }
+ }
+ }
+
+ /**
+ * Gets content of special data:text/mce-internal url on the event's dataTransfer object.
+ * This is to workaround the inability to set custom contentType on IE and Safari.
+ * The editor's selected content is encoded into this url so drag and drop between editors will work.
+ *
+ * @private
+ * @param {DragEvent} e Event object
+ * @returns {String} mce-internal content
+ */
+ function getMceInternalContent(e) {
+ var internalContent;
+
+ if (e.dataTransfer) {
+ internalContent = e.dataTransfer.getData(mceInternalDataType);
+
+ if (internalContent && internalContent.indexOf(mceInternalUrlPrefix) >= 0) {
+ internalContent = internalContent.substr(mceInternalUrlPrefix.length).split(',');
+
+ return {
+ id: unescape(internalContent[0]),
+ html: unescape(internalContent[1])
+ };
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Inserts contents using the paste clipboard command if it's available if it isn't it will fallback
+ * to the core command.
+ *
+ * @private
+ * @param {String} content Content to insert at selection.
+ */
+ function insertClipboardContents(content) {
+ if (editor.queryCommandSupported('mceInsertClipboardContent')) {
+ editor.execCommand('mceInsertClipboardContent', false, {content: content});
+ } else {
+ editor.execCommand('mceInsertContent', false, content);
+ }
+ }
+
+ /**
+ * Fixes a WebKit bug when deleting contents using backspace or delete key.
+ * WebKit will produce a span element if you delete across two block elements.
+ *
+ * Example:
+ * <h1>a</h1><p>|b</p>
+ *
+ * Will produce this on backspace:
+ * <h1>a<span style="<all runtime styles>">b</span></p>
+ *
+ * This fixes the backspace to produce:
+ * <h1>a|b</p>
+ *
+ * See bug: https://bugs.webkit.org/show_bug.cgi?id=45784
+ *
+ * This fixes the following delete scenarios:
+ * 1. Delete by pressing backspace key.
+ * 2. Delete by pressing delete key.
+ * 3. Delete by pressing backspace key with ctrl/cmd (Word delete).
+ * 4. Delete by pressing delete key with ctrl/cmd (Word delete).
+ * 5. Delete by drag/dropping contents inside the editor.
+ * 6. Delete by using Cut Ctrl+X/Cmd+X.
+ * 7. Delete by selecting contents and writing a character.
+ *
+ * This code is a ugly hack since writing full custom delete logic for just this bug
+ * fix seemed like a huge task. I hope we can remove this before the year 2030.
+ */
+ function cleanupStylesWhenDeleting() {
+ var doc = editor.getDoc(), dom = editor.dom, selection = editor.selection;
+ var MutationObserver = window.MutationObserver, olderWebKit, dragStartRng;
+
+ // Add mini polyfill for older WebKits
+ // TODO: Remove this when old Safari versions gets updated
+ if (!MutationObserver) {
+ olderWebKit = true;
+
+ MutationObserver = function() {
+ var records = [], target;
+
+ function nodeInsert(e) {
+ var target = e.relatedNode || e.target;
+ records.push({target: target, addedNodes: [target]});
+ }
+
+ function attrModified(e) {
+ var target = e.relatedNode || e.target;
+ records.push({target: target, attributeName: e.attrName});
+ }
+
+ this.observe = function(node) {
+ target = node;
+ target.addEventListener('DOMSubtreeModified', nodeInsert, false);
+ target.addEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false);
+ target.addEventListener('DOMNodeInserted', nodeInsert, false);
+ target.addEventListener('DOMAttrModified', attrModified, false);
+ };
+
+ this.disconnect = function() {
+ target.removeEventListener('DOMSubtreeModified', nodeInsert, false);
+ target.removeEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false);
+ target.removeEventListener('DOMNodeInserted', nodeInsert, false);
+ target.removeEventListener('DOMAttrModified', attrModified, false);
+ };
+
+ this.takeRecords = function() {
+ return records;
+ };
+ };
+ }
+
+ function isTrailingBr(node) {
+ var blockElements = dom.schema.getBlockElements(), rootNode = editor.getBody();
+
+ if (node.nodeName != 'BR') {
+ return false;
+ }
+
+ for (; node != rootNode && !blockElements[node.nodeName]; node = node.parentNode) {
+ if (node.nextSibling) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ function isSiblingsIgnoreWhiteSpace(node1, node2) {
+ var node;
+
+ for (node = node1.nextSibling; node && node != node2; node = node.nextSibling) {
+ if (node.nodeType == 3 && $.trim(node.data).length === 0) {
+ continue;
+ }
+
+ if (node !== node2) {
+ return false;
+ }
+ }
+
+ return node === node2;
+ }
+
+ function findCaretNode(node, forward, startNode) {
+ var walker, current, nonEmptyElements;
+
+ // Protect against the possibility we are asked to find a caret node relative
+ // to a node that is no longer in the DOM tree. In this case attempting to
+ // select on any match leads to a scenario where selection is completely removed
+ // from the editor. This scenario is met in real world at a minimum on
+ // WebKit browsers when selecting all and Cmd-X cutting to delete content.
+ if (!dom.isChildOf(node, editor.getBody())) {
+ return;
+ }
+
+ nonEmptyElements = dom.schema.getNonEmptyElements();
+
+ walker = new TreeWalker(startNode || node, node);
+
+ while ((current = walker[forward ? 'next' : 'prev']())) {
+ if (nonEmptyElements[current.nodeName] && !isTrailingBr(current)) {
+ return current;
+ }
+
+ if (current.nodeType == 3 && current.data.length > 0) {
+ return current;
+ }
+ }
+ }
+
+ function deleteRangeBetweenTextBlocks(rng) {
+ var startBlock, endBlock, caretNodeBefore, caretNodeAfter, textBlockElements;
+
+ if (rng.collapsed) {
+ return;
+ }
+
+ startBlock = dom.getParent(RangeUtils.getNode(rng.startContainer, rng.startOffset), dom.isBlock);
+ endBlock = dom.getParent(RangeUtils.getNode(rng.endContainer, rng.endOffset), dom.isBlock);
+ textBlockElements = editor.schema.getTextBlockElements();
+
+ if (startBlock == endBlock) {
+ return;
+ }
+
+ if (!textBlockElements[startBlock.nodeName] || !textBlockElements[endBlock.nodeName]) {
+ return;
+ }
+
+ if (dom.getContentEditable(startBlock) === "false" || dom.getContentEditable(endBlock) === "false") {
+ return;
+ }
+
+ rng.deleteContents();
+
+ caretNodeBefore = findCaretNode(startBlock, false);
+ caretNodeAfter = findCaretNode(endBlock, true);
+
+ if (!dom.isEmpty(endBlock)) {
+ $(startBlock).append(endBlock.childNodes);
+ }
+
+ $(endBlock).remove();
+
+ if (caretNodeBefore) {
+ if (caretNodeBefore.nodeType == 1) {
+ if (caretNodeBefore.nodeName == "BR") {
+ rng.setStartBefore(caretNodeBefore);
+ rng.setEndBefore(caretNodeBefore);
+ } else {
+ rng.setStartAfter(caretNodeBefore);
+ rng.setEndAfter(caretNodeBefore);
+ }
+ } else {
+ rng.setStart(caretNodeBefore, caretNodeBefore.data.length);
+ rng.setEnd(caretNodeBefore, caretNodeBefore.data.length);
+ }
+ } else if (caretNodeAfter) {
+ if (caretNodeAfter.nodeType == 1) {
+ rng.setStartBefore(caretNodeAfter);
+ rng.setEndBefore(caretNodeAfter);
+ } else {
+ rng.setStart(caretNodeAfter, 0);
+ rng.setEnd(caretNodeAfter, 0);
+ }
+ }
+
+ selection.setRng(rng);
+
+ return true;
+ }
+
+ function expandBetweenBlocks(rng, isForward) {
+ var caretNode, targetCaretNode, textBlock, targetTextBlock, container, offset;
+
+ if (!rng.collapsed) {
+ return rng;
+ }
+
+ container = rng.startContainer;
+ offset = rng.startOffset;
+
+ if (container.nodeType == 3) {
+ if (isForward) {
+ if (offset < container.data.length) {
+ return rng;
+ }
+ } else {
+ if (offset > 0) {
+ return rng;
+ }
+ }
+ }
+
+ caretNode = RangeUtils.getNode(container, offset);
+ textBlock = dom.getParent(caretNode, dom.isBlock);
+ targetCaretNode = findCaretNode(editor.getBody(), isForward, caretNode);
+ targetTextBlock = dom.getParent(targetCaretNode, dom.isBlock);
+ var isAfter = container.nodeType === 1 && offset > container.childNodes.length - 1;
+
+ if (!caretNode || !targetCaretNode) {
+ return rng;
+ }
+
+ if (targetTextBlock && textBlock != targetTextBlock) {
+ if (!isForward) {
+ if (!isSiblingsIgnoreWhiteSpace(targetTextBlock, textBlock)) {
+ return rng;
+ }
+
+ if (targetCaretNode.nodeType == 1) {
+ if (targetCaretNode.nodeName == "BR") {
+ rng.setStartBefore(targetCaretNode);
+ } else {
+ rng.setStartAfter(targetCaretNode);
+ }
+ } else {
+ rng.setStart(targetCaretNode, targetCaretNode.data.length);
+ }
+
+ if (caretNode.nodeType == 1) {
+ if (isAfter) {
+ rng.setEndAfter(caretNode);
+ } else {
+ rng.setEndBefore(caretNode);
+ }
+ } else {
+ rng.setEndBefore(caretNode);
+ }
+ } else {
+ if (!isSiblingsIgnoreWhiteSpace(textBlock, targetTextBlock)) {
+ return rng;
+ }
+
+ if (caretNode.nodeType == 1) {
+ if (caretNode.nodeName == "BR") {
+ rng.setStartBefore(caretNode);
+ } else {
+ rng.setStartAfter(caretNode);
+ }
+ } else {
+ rng.setStart(caretNode, caretNode.data.length);
+ }
+
+ if (targetCaretNode.nodeType == 1) {
+ rng.setEnd(targetCaretNode, 0);
+ } else {
+ rng.setEndBefore(targetCaretNode);
+ }
+ }
+ }
+
+ return rng;
+ }
+
+ function handleTextBlockMergeDelete(isForward) {
+ var rng = selection.getRng();
+
+ rng = expandBetweenBlocks(rng, isForward);
+
+ if (deleteRangeBetweenTextBlocks(rng)) {
+ return true;
+ }
+ }
+
+ /**
+ * This retains the formatting if the last character is to be deleted.
+ *
+ * Backspace on this: <p><b><i>a|</i></b></p> would become <p>|</p> in WebKit.
+ * With this patch: <p><b><i>|<br></i></b></p>
+ */
+ function handleLastBlockCharacterDelete(isForward, rng) {
+ var path, blockElm, newBlockElm, clonedBlockElm, sibling,
+ container, offset, br, currentFormatNodes;
+
+ function cloneTextBlockWithFormats(blockElm, node) {
+ currentFormatNodes = $(node).parents().filter(function(idx, node) {
+ return !!editor.schema.getTextInlineElements()[node.nodeName];
+ });
+
+ newBlockElm = blockElm.cloneNode(false);
+
+ currentFormatNodes = Tools.map(currentFormatNodes, function(formatNode) {
+ formatNode = formatNode.cloneNode(false);
+
+ if (newBlockElm.hasChildNodes()) {
+ formatNode.appendChild(newBlockElm.firstChild);
+ newBlockElm.appendChild(formatNode);
+ } else {
+ newBlockElm.appendChild(formatNode);
+ }
+
+ newBlockElm.appendChild(formatNode);
+
+ return formatNode;
+ });
+
+ if (currentFormatNodes.length) {
+ br = dom.create('br');
+ currentFormatNodes[0].appendChild(br);
+ dom.replace(newBlockElm, blockElm);
+
+ rng.setStartBefore(br);
+ rng.setEndBefore(br);
+ editor.selection.setRng(rng);
+
+ return br;
+ }
+
+ return null;
+ }
+
+ function isTextBlock(node) {
+ return node && editor.schema.getTextBlockElements()[node.tagName];
+ }
+
+ if (!rng.collapsed) {
+ return;
+ }
+
+ container = rng.startContainer;
+ offset = rng.startOffset;
+ blockElm = dom.getParent(container, dom.isBlock);
+ if (!isTextBlock(blockElm)) {
+ return;
+ }
+
+ if (container.nodeType == 1) {
+ container = container.childNodes[offset];
+ if (container && container.tagName != 'BR') {
+ return;
+ }
+
+ if (isForward) {
+ sibling = blockElm.nextSibling;
+ } else {
+ sibling = blockElm.previousSibling;
+ }
+
+ if (dom.isEmpty(blockElm) && isTextBlock(sibling) && dom.isEmpty(sibling)) {
+ if (cloneTextBlockWithFormats(blockElm, container)) {
+ dom.remove(sibling);
+ return true;
+ }
+ }
+ } else if (container.nodeType == 3) {
+ path = NodePath.create(blockElm, container);
+ clonedBlockElm = blockElm.cloneNode(true);
+ container = NodePath.resolve(clonedBlockElm, path);
+
+ if (isForward) {
+ if (offset >= container.data.length) {
+ return;
+ }
+
+ container.deleteData(offset, 1);
+ } else {
+ if (offset <= 0) {
+ return;
+ }
+
+ container.deleteData(offset - 1, 1);
+ }
+
+ if (dom.isEmpty(clonedBlockElm)) {
+ return cloneTextBlockWithFormats(blockElm, container);
+ }
+ }
+ }
+
+ function customDelete(isForward) {
+ var mutationObserver, rng, caretElement;
+
+ if (handleTextBlockMergeDelete(isForward)) {
+ return;
+ }
+
+ Tools.each(editor.getBody().getElementsByTagName('*'), function(elm) {
+ // Mark existing spans
+ if (elm.tagName == 'SPAN') {
+ elm.setAttribute('mce-data-marked', 1);
+ }
+
+ // Make sure all elements has a data-mce-style attribute
+ if (!elm.hasAttribute('data-mce-style') && elm.hasAttribute('style')) {
+ editor.dom.setAttrib(elm, 'style', editor.dom.getAttrib(elm, 'style'));
+ }
+ });
+
+ // Observe added nodes and style attribute changes
+ mutationObserver = new MutationObserver(function() {});
+ mutationObserver.observe(editor.getDoc(), {
+ childList: true,
+ attributes: true,
+ subtree: true,
+ attributeFilter: ['style']
+ });
+
+ editor.getDoc().execCommand(isForward ? 'ForwardDelete' : 'Delete', false, null);
+
+ rng = editor.selection.getRng();
+ caretElement = rng.startContainer.parentNode;
+
+ Tools.each(mutationObserver.takeRecords(), function(record) {
+ if (!dom.isChildOf(record.target, editor.getBody())) {
+ return;
+ }
+
+ // Restore style attribute to previous value
+ if (record.attributeName == "style") {
+ var oldValue = record.target.getAttribute('data-mce-style');
+
+ if (oldValue) {
+ record.target.setAttribute("style", oldValue);
+ } else {
+ record.target.removeAttribute("style");
+ }
+ }
+
+ // Remove all spans that aren't marked and retain selection
+ Tools.each(record.addedNodes, function(node) {
+ if (node.nodeName == "SPAN" && !node.getAttribute('mce-data-marked')) {
+ var offset, container;
+
+ if (node == caretElement) {
+ offset = rng.startOffset;
+ container = node.firstChild;
+ }
+
+ dom.remove(node, true);
+
+ if (container) {
+ rng.setStart(container, offset);
+ rng.setEnd(container, offset);
+ editor.selection.setRng(rng);
+ }
+ }
+ });
+ });
+
+ mutationObserver.disconnect();
+
+ // Remove any left over marks
+ Tools.each(editor.dom.select('span[mce-data-marked]'), function(span) {
+ span.removeAttribute('mce-data-marked');
+ });
+ }
+
+ function transactCustomDelete(isForward) {
+ editor.undoManager.transact(function () {
+ customDelete(isForward);
+ });
+ }
+
+ editor.on('keydown', function(e) {
+ var isForward = e.keyCode == DELETE, isMetaOrCtrl = e.ctrlKey || e.metaKey;
+
+ if (!isDefaultPrevented(e) && (isForward || e.keyCode == BACKSPACE)) {
+ var rng = editor.selection.getRng(), container = rng.startContainer, offset = rng.startOffset;
+
+ // Shift+Delete is cut
+ if (isForward && e.shiftKey) {
+ return;
+ }
+
+ if (handleLastBlockCharacterDelete(isForward, rng)) {
+ e.preventDefault();
+ return;
+ }
+
+ // Ignore non meta delete in the where there is text before/after the caret
+ if (!isMetaOrCtrl && rng.collapsed && container.nodeType == 3) {
+ if (isForward ? offset < container.data.length : offset > 0) {
+ return;
+ }
+ }
+
+ e.preventDefault();
+
+ if (isMetaOrCtrl) {
+ editor.selection.getSel().modify("extend", isForward ? "forward" : "backward", e.metaKey ? "lineboundary" : "word");
+ }
+
+ customDelete(isForward);
+ }
+ });
+
+ // Handle case where text is deleted by typing over
+ editor.on('keypress', function(e) {
+ if (!isDefaultPrevented(e) && !selection.isCollapsed() && e.charCode > 31 && !VK.metaKeyPressed(e)) {
+ var rng, currentFormatNodes, fragmentNode, blockParent, caretNode, charText;
+
+ rng = editor.selection.getRng();
+ charText = String.fromCharCode(e.charCode);
+ e.preventDefault();
+
+ // Keep track of current format nodes
+ currentFormatNodes = $(rng.startContainer).parents().filter(function(idx, node) {
+ return !!editor.schema.getTextInlineElements()[node.nodeName];
+ });
+
+ customDelete(true);
+
+ // Check if the browser removed them
+ currentFormatNodes = currentFormatNodes.filter(function(idx, node) {
+ return !$.contains(editor.getBody(), node);
+ });
+
+ // Then re-add them
+ if (currentFormatNodes.length) {
+ fragmentNode = dom.createFragment();
+
+ currentFormatNodes.each(function(idx, formatNode) {
+ formatNode = formatNode.cloneNode(false);
+
+ if (fragmentNode.hasChildNodes()) {
+ formatNode.appendChild(fragmentNode.firstChild);
+ fragmentNode.appendChild(formatNode);
+ } else {
+ caretNode = formatNode;
+ fragmentNode.appendChild(formatNode);
+ }
+
+ fragmentNode.appendChild(formatNode);
+ });
+
+ caretNode.appendChild(editor.getDoc().createTextNode(charText));
+
+ // Prevent edge case where older WebKit would add an extra BR element
+ blockParent = dom.getParent(rng.startContainer, dom.isBlock);
+ if (dom.isEmpty(blockParent)) {
+ $(blockParent).empty().append(fragmentNode);
+ } else {
+ rng.insertNode(fragmentNode);
+ }
+
+ rng.setStart(caretNode.firstChild, 1);
+ rng.setEnd(caretNode.firstChild, 1);
+ editor.selection.setRng(rng);
+ } else {
+ editor.selection.setContent(charText);
+ }
+ }
+ });
+
+ editor.addCommand('Delete', function() {
+ customDelete();
+ });
+
+ editor.addCommand('ForwardDelete', function() {
+ customDelete(true);
+ });
+
+ // Older WebKits doesn't properly handle the clipboard so we can't add the rest
+ if (olderWebKit) {
+ return;
+ }
+
+ editor.on('dragstart', function(e) {
+ dragStartRng = selection.getRng();
+ setMceInternalContent(e);
+ });
+
+ editor.on('drop', function(e) {
+ if (!isDefaultPrevented(e)) {
+ var internalContent = getMceInternalContent(e);
+
+ if (internalContent) {
+ e.preventDefault();
+
+ // Safari has a weird issue where drag/dropping images sometimes
+ // produces a green plus icon. When this happens the caretRangeFromPoint
+ // will return "null" even though the x, y coordinate is correct.
+ // But if we detach the insert from the drop event we will get a proper range
+ Delay.setEditorTimeout(editor, function() {
+ var pointRng = RangeUtils.getCaretRangeFromPoint(e.x, e.y, doc);
+
+ if (dragStartRng) {
+ selection.setRng(dragStartRng);
+ dragStartRng = null;
+ transactCustomDelete();
+ }
+
+ selection.setRng(pointRng);
+ insertClipboardContents(internalContent.html);
+ });
+ }
+ }
+ });
+
+ editor.on('cut', function(e) {
+ if (!isDefaultPrevented(e) && e.clipboardData && !editor.selection.isCollapsed()) {
+ e.preventDefault();
+ e.clipboardData.clearData();
+ e.clipboardData.setData('text/html', editor.selection.getContent());
+ e.clipboardData.setData('text/plain', editor.selection.getContent({format: 'text'}));
+
+ // Needed delay for https://code.google.com/p/chromium/issues/detail?id=363288#c3
+ // Nested delete/forwardDelete not allowed on execCommand("cut")
+ // This is ugly but not sure how to work around it otherwise
+ Delay.setEditorTimeout(editor, function() {
+ transactCustomDelete(true);
+ });
+ }
+ });
+ }
+
+ /**
+ * Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors.
+ *
+ * For example:
+ * <p><b>|</b></p>
+ *
+ * Or:
+ * <h1>|</h1>
+ *
+ * Or:
+ * [<h1></h1>]
+ */
+ function emptyEditorWhenDeleting() {
+ function serializeRng(rng) {
+ var body = dom.create("body");
+ var contents = rng.cloneContents();
+ body.appendChild(contents);
+ return selection.serializer.serialize(body, {format: 'html'});
+ }
+
+ function allContentsSelected(rng) {
+ if (!rng.setStart) {
+ if (rng.item) {
+ return false;
+ }
+
+ var bodyRng = rng.duplicate();
+ bodyRng.moveToElementText(editor.getBody());
+ return RangeUtils.compareRanges(rng, bodyRng);
+ }
+
+ var selection = serializeRng(rng);
+
+ var allRng = dom.createRng();
+ allRng.selectNode(editor.getBody());
+
+ var allSelection = serializeRng(allRng);
+ return selection === allSelection;
+ }
+
+ editor.on('keydown', function(e) {
+ var keyCode = e.keyCode, isCollapsed, body;
+
+ // Empty the editor if it's needed for example backspace at <p><b>|</b></p>
+ if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) {
+ isCollapsed = editor.selection.isCollapsed();
+ body = editor.getBody();
+
+ // Selection is collapsed but the editor isn't empty
+ if (isCollapsed && !dom.isEmpty(body)) {
+ return;
+ }
+
+ // Selection isn't collapsed but not all the contents is selected
+ if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) {
+ return;
+ }
+
+ // Manually empty the editor
+ e.preventDefault();
+ editor.setContent('');
+
+ if (body.firstChild && dom.isBlock(body.firstChild)) {
+ editor.selection.setCursorLocation(body.firstChild, 0);
+ } else {
+ editor.selection.setCursorLocation(body, 0);
+ }
+
+ editor.nodeChanged();
+ }
+ });
+ }
+
+ /**
+ * WebKit doesn't select all the nodes in the body when you press Ctrl+A.
+ * IE selects more than the contents <body>[<p>a</p>]</body> instead of <body><p>[a]</p]</body> see bug #6438
+ * This selects the whole body so that backspace/delete logic will delete everything
+ */
+ function selectAll() {
+ editor.shortcuts.add('meta+a', null, 'SelectAll');
+ }
+
+ /**
+ * WebKit has a weird issue where it some times fails to properly convert keypresses to input method keystrokes.
+ * The IME on Mac doesn't initialize when it doesn't fire a proper focus event.
+ *
+ * This seems to happen when the user manages to click the documentElement element then the window doesn't get proper focus until
+ * you enter a character into the editor.
+ *
+ * It also happens when the first focus in made to the body.
+ *
+ * See: https://bugs.webkit.org/show_bug.cgi?id=83566
+ */
+ function inputMethodFocus() {
+ if (!editor.settings.content_editable) {
+ // Case 1 IME doesn't initialize if you focus the document
+ // Disabled since it was interferring with the cE=false logic
+ // Also coultn't reproduce the issue on Safari 9
+ /*dom.bind(editor.getDoc(), 'focusin', function() {
+ selection.setRng(selection.getRng());
+ });*/
+
+ // Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event
+ // Needs to be both down/up due to weird rendering bug on Chrome Windows
+ dom.bind(editor.getDoc(), 'mousedown mouseup', function(e) {
+ var rng;
+
+ if (e.target == editor.getDoc().documentElement) {
+ rng = selection.getRng();
+ editor.getBody().focus();
+
+ if (e.type == 'mousedown') {
+ if (CaretContainer.isCaretContainer(rng.startContainer)) {
+ return;
+ }
+
+ // Edge case for mousedown, drag select and mousedown again within selection on Chrome Windows to render caret
+ selection.placeCaretAt(e.clientX, e.clientY);
+ } else {
+ selection.setRng(rng);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Backspacing in FireFox/IE from a paragraph into a horizontal rule results in a floating text node because the
+ * browser just deletes the paragraph - the browser fails to merge the text node with a horizontal rule so it is
+ * left there. TinyMCE sees a floating text node and wraps it in a paragraph on the key up event (ForceBlocks.js
+ * addRootBlocks), meaning the action does nothing. With this code, FireFox/IE matche the behaviour of other
+ * browsers.
+ *
+ * It also fixes a bug on Firefox where it's impossible to delete HR elements.
+ */
+ function removeHrOnBackspace() {
+ editor.on('keydown', function(e) {
+ if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {
+ // Check if there is any HR elements this is faster since getRng on IE 7 & 8 is slow
+ if (!editor.getBody().getElementsByTagName('hr').length) {
+ return;
+ }
+
+ if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) {
+ var node = selection.getNode();
+ var previousSibling = node.previousSibling;
+
+ if (node.nodeName == 'HR') {
+ dom.remove(node);
+ e.preventDefault();
+ return;
+ }
+
+ if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") {
+ dom.remove(previousSibling);
+ e.preventDefault();
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Firefox 3.x has an issue where the body element won't get proper focus if you click out
+ * side it's rectangle.
+ */
+ function focusBody() {
+ // Fix for a focus bug in FF 3.x where the body element
+ // wouldn't get proper focus if the user clicked on the HTML element
+ if (!window.Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4
+ editor.on('mousedown', function(e) {
+ if (!isDefaultPrevented(e) && e.target.nodeName === "HTML") {
+ var body = editor.getBody();
+
+ // Blur the body it's focused but not correctly focused
+ body.blur();
+
+ // Refocus the body after a little while
+ Delay.setEditorTimeout(editor, function() {
+ body.focus();
+ });
+ }
+ });
+ }
+ }
+
+ /**
+ * WebKit has a bug where it isn't possible to select image, hr or anchor elements
+ * by clicking on them so we need to fake that.
+ */
+ function selectControlElements() {
+ editor.on('click', function(e) {
+ var target = e.target;
+
+ // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250
+ // WebKit can't even do simple things like selecting an image
+ // Needs to be the setBaseAndExtend or it will fail to select floated images
+ if (/^(IMG|HR)$/.test(target.nodeName) && dom.getContentEditableParent(target) !== "false") {
+ e.preventDefault();
+ selection.getSel().setBaseAndExtent(target, 0, target, 1);
+ editor.nodeChanged();
+ }
+
+ if (target.nodeName == 'A' && dom.hasClass(target, 'mce-item-anchor')) {
+ e.preventDefault();
+ selection.select(target);
+ }
+ });
+ }
+
+ /**
+ * Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements.
+ *
+ * Fixes do backspace/delete on this:
+ * <p>bla[ck</p><p style="color:red">r]ed</p>
+ *
+ * Would become:
+ * <p>bla|ed</p>
+ *
+ * Instead of:
+ * <p style="color:red">bla|ed</p>
+ */
+ function removeStylesWhenDeletingAcrossBlockElements() {
+ function getAttributeApplyFunction() {
+ var template = dom.getAttribs(selection.getStart().cloneNode(false));
+
+ return function() {
+ var target = selection.getStart();
+
+ if (target !== editor.getBody()) {
+ dom.setAttrib(target, "style", null);
+
+ each(template, function(attr) {
+ target.setAttributeNode(attr.cloneNode(true));
+ });
+ }
+ };
+ }
+
+ function isSelectionAcrossElements() {
+ return !selection.isCollapsed() &&
+ dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock);
+ }
+
+ editor.on('keypress', function(e) {
+ var applyAttributes;
+
+ if (!isDefaultPrevented(e) && (e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) {
+ applyAttributes = getAttributeApplyFunction();
+ editor.getDoc().execCommand('delete', false, null);
+ applyAttributes();
+ e.preventDefault();
+ return false;
+ }
+ });
+
+ dom.bind(editor.getDoc(), 'cut', function(e) {
+ var applyAttributes;
+
+ if (!isDefaultPrevented(e) && isSelectionAcrossElements()) {
+ applyAttributes = getAttributeApplyFunction();
+
+ Delay.setEditorTimeout(editor, function() {
+ applyAttributes();
+ });
+ }
+ });
+ }
+
+ /**
+ * Screen readers on IE needs to have the role application set on the body.
+ */
+ function ensureBodyHasRoleApplication() {
+ document.body.setAttribute("role", "application");
+ }
+
+ /**
+ * Backspacing into a table behaves differently depending upon browser type.
+ * Therefore, disable Backspace when cursor immediately follows a table.
+ */
+ function disableBackspaceIntoATable() {
+ editor.on('keydown', function(e) {
+ if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {
+ if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) {
+ var previousSibling = selection.getNode().previousSibling;
+ if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") {
+ e.preventDefault();
+ return false;
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Old IE versions can't properly render BR elements in PRE tags white in contentEditable mode. So this
+ * logic adds a \n before the BR so that it will get rendered.
+ */
+ function addNewLinesBeforeBrInPre() {
+ // IE8+ rendering mode does the right thing with BR in PRE
+ if (getDocumentMode() > 7) {
+ return;
+ }
+
+ // Enable display: none in area and add a specific class that hides all BR elements in PRE to
+ // avoid the caret from getting stuck at the BR elements while pressing the right arrow key
+ setEditorCommandState('RespectVisibilityInDesign', true);
+ editor.contentStyles.push('.mceHideBrInPre pre br {display: none}');
+ dom.addClass(editor.getBody(), 'mceHideBrInPre');
+
+ // Adds a \n before all BR elements in PRE to get them visual
+ parser.addNodeFilter('pre', function(nodes) {
+ var i = nodes.length, brNodes, j, brElm, sibling;
+
+ while (i--) {
+ brNodes = nodes[i].getAll('br');
+ j = brNodes.length;
+ while (j--) {
+ brElm = brNodes[j];
+
+ // Add \n before BR in PRE elements on older IE:s so the new lines get rendered
+ sibling = brElm.prev;
+ if (sibling && sibling.type === 3 && sibling.value.charAt(sibling.value - 1) != '\n') {
+ sibling.value += '\n';
+ } else {
+ brElm.parent.insert(new Node('#text', 3), brElm, true).value = '\n';
+ }
+ }
+ }
+ });
+
+ // Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible
+ serializer.addNodeFilter('pre', function(nodes) {
+ var i = nodes.length, brNodes, j, brElm, sibling;
+
+ while (i--) {
+ brNodes = nodes[i].getAll('br');
+ j = brNodes.length;
+ while (j--) {
+ brElm = brNodes[j];
+ sibling = brElm.prev;
+ if (sibling && sibling.type == 3) {
+ sibling.value = sibling.value.replace(/\r?\n$/, '');
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Moves style width/height to attribute width/height when the user resizes an image on IE.
+ */
+ function removePreSerializedStylesWhenSelectingControls() {
+ dom.bind(editor.getBody(), 'mouseup', function() {
+ var value, node = selection.getNode();
+
+ // Moved styles to attributes on IMG eements
+ if (node.nodeName == 'IMG') {
+ // Convert style width to width attribute
+ if ((value = dom.getStyle(node, 'width'))) {
+ dom.setAttrib(node, 'width', value.replace(/[^0-9%]+/g, ''));
+ dom.setStyle(node, 'width', '');
+ }
+
+ // Convert style height to height attribute
+ if ((value = dom.getStyle(node, 'height'))) {
+ dom.setAttrib(node, 'height', value.replace(/[^0-9%]+/g, ''));
+ dom.setStyle(node, 'height', '');
+ }
+ }
+ });
+ }
+
+ /**
+ * Removes a blockquote when backspace is pressed at the beginning of it.
+ *
+ * For example:
+ * <blockquote><p>|x</p></blockquote>
+ *
+ * Becomes:
+ * <p>|x</p>
+ */
+ function removeBlockQuoteOnBackSpace() {
+ // Add block quote deletion handler
+ editor.on('keydown', function(e) {
+ var rng, container, offset, root, parent;
+
+ if (isDefaultPrevented(e) || e.keyCode != VK.BACKSPACE) {
+ return;
+ }
+
+ rng = selection.getRng();
+ container = rng.startContainer;
+ offset = rng.startOffset;
+ root = dom.getRoot();
+ parent = container;
+
+ if (!rng.collapsed || offset !== 0) {
+ return;
+ }
+
+ while (parent && parent.parentNode && parent.parentNode.firstChild == parent && parent.parentNode != root) {
+ parent = parent.parentNode;
+ }
+
+ // Is the cursor at the beginning of a blockquote?
+ if (parent.tagName === 'BLOCKQUOTE') {
+ // Remove the blockquote
+ editor.formatter.toggle('blockquote', null, parent);
+
+ // Move the caret to the beginning of container
+ rng = dom.createRng();
+ rng.setStart(container, 0);
+ rng.setEnd(container, 0);
+ selection.setRng(rng);
+ }
+ });
+ }
+
+ /**
+ * Sets various Gecko editing options on mouse down and before a execCommand to disable inline table editing that is broken etc.
+ */
+ function setGeckoEditingOptions() {
+ function setOpts() {
+ refreshContentEditable();
+
+ setEditorCommandState("StyleWithCSS", false);
+ setEditorCommandState("enableInlineTableEditing", false);
+
+ if (!settings.object_resizing) {
+ setEditorCommandState("enableObjectResizing", false);
+ }
+ }
+
+ if (!settings.readonly) {
+ editor.on('BeforeExecCommand MouseDown', setOpts);
+ }
+ }
+
+ /**
+ * Fixes a gecko link bug, when a link is placed at the end of block elements there is
+ * no way to move the caret behind the link. This fix adds a bogus br element after the link.
+ *
+ * For example this:
+ * <p><b><a href="#">x</a></b></p>
+ *
+ * Becomes this:
+ * <p><b><a href="#">x</a></b><br></p>
+ */
+ function addBrAfterLastLinks() {
+ function fixLinks() {
+ each(dom.select('a'), function(node) {
+ var parentNode = node.parentNode, root = dom.getRoot();
+
+ if (parentNode.lastChild === node) {
+ while (parentNode && !dom.isBlock(parentNode)) {
+ if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) {
+ return;
+ }
+
+ parentNode = parentNode.parentNode;
+ }
+
+ dom.add(parentNode, 'br', {'data-mce-bogus': 1});
+ }
+ });
+ }
+
+ editor.on('SetContent ExecCommand', function(e) {
+ if (e.type == "setcontent" || e.command === 'mceInsertLink') {
+ fixLinks();
+ }
+ });
+ }
+
+ /**
+ * WebKit will produce DIV elements here and there by default. But since TinyMCE uses paragraphs by
+ * default we want to change that behavior.
+ */
+ function setDefaultBlockType() {
+ if (settings.forced_root_block) {
+ editor.on('init', function() {
+ setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block);
+ });
+ }
+ }
+
+ /**
+ * Deletes the selected image on IE instead of navigating to previous page.
+ */
+ function deleteControlItemOnBackSpace() {
+ editor.on('keydown', function(e) {
+ var rng;
+
+ if (!isDefaultPrevented(e) && e.keyCode == BACKSPACE) {
+ rng = editor.getDoc().selection.createRange();
+ if (rng && rng.item) {
+ e.preventDefault();
+ editor.undoManager.beforeChange();
+ dom.remove(rng.item(0));
+ editor.undoManager.add();
+ }
+ }
+ });
+ }
+
+ /**
+ * IE10 doesn't properly render block elements with the right height until you add contents to them.
+ * This fixes that by adding a padding-right to all empty text block elements.
+ * See: https://connect.microsoft.com/IE/feedback/details/743881
+ */
+ function renderEmptyBlocksFix() {
+ var emptyBlocksCSS;
+
+ // IE10+
+ if (getDocumentMode() >= 10) {
+ emptyBlocksCSS = '';
+ each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) {
+ emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty';
+ });
+
+ editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}');
+ }
+ }
+
+ /**
+ * Old IE versions can't retain contents within noscript elements so this logic will store the contents
+ * as a attribute and the insert that value as it's raw text when the DOM is serialized.
+ */
+ function keepNoScriptContents() {
+ if (getDocumentMode() < 9) {
+ parser.addNodeFilter('noscript', function(nodes) {
+ var i = nodes.length, node, textNode;
+
+ while (i--) {
+ node = nodes[i];
+ textNode = node.firstChild;
+
+ if (textNode) {
+ node.attr('data-mce-innertext', textNode.value);
+ }
+ }
+ });
+
+ serializer.addNodeFilter('noscript', function(nodes) {
+ var i = nodes.length, node, textNode, value;
+
+ while (i--) {
+ node = nodes[i];
+ textNode = nodes[i].firstChild;
+
+ if (textNode) {
+ textNode.value = Entities.decode(textNode.value);
+ } else {
+ // Old IE can't retain noscript value so an attribute is used to store it
+ value = node.attributes.map['data-mce-innertext'];
+ if (value) {
+ node.attr('data-mce-innertext', null);
+ textNode = new Node('#text', 3);
+ textNode.value = value;
+ textNode.raw = true;
+ node.append(textNode);
+ }
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode.
+ */
+ function fixCaretSelectionOfDocumentElementOnIe() {
+ var doc = dom.doc, body = doc.body, started, startRng, htmlElm;
+
+ // Return range from point or null if it failed
+ function rngFromPoint(x, y) {
+ var rng = body.createTextRange();
+
+ try {
+ rng.moveToPoint(x, y);
+ } catch (ex) {
+ // IE sometimes throws and exception, so lets just ignore it
+ rng = null;
+ }
+
+ return rng;
+ }
+
+ // Fires while the selection is changing
+ function selectionChange(e) {
+ var pointRng;
+
+ // Check if the button is down or not
+ if (e.button) {
+ // Create range from mouse position
+ pointRng = rngFromPoint(e.x, e.y);
+
+ if (pointRng) {
+ // Check if pointRange is before/after selection then change the endPoint
+ if (pointRng.compareEndPoints('StartToStart', startRng) > 0) {
+ pointRng.setEndPoint('StartToStart', startRng);
+ } else {
+ pointRng.setEndPoint('EndToEnd', startRng);
+ }
+
+ pointRng.select();
+ }
+ } else {
+ endSelection();
+ }
+ }
+
+ // Removes listeners
+ function endSelection() {
+ var rng = doc.selection.createRange();
+
+ // If the range is collapsed then use the last start range
+ if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) {
+ startRng.select();
+ }
+
+ dom.unbind(doc, 'mouseup', endSelection);
+ dom.unbind(doc, 'mousemove', selectionChange);
+ startRng = started = 0;
+ }
+
+ // Make HTML element unselectable since we are going to handle selection by hand
+ doc.documentElement.unselectable = true;
+
+ // Detect when user selects outside BODY
+ dom.bind(doc, 'mousedown contextmenu', function(e) {
+ if (e.target.nodeName === 'HTML') {
+ if (started) {
+ endSelection();
+ }
+
+ // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML
+ htmlElm = doc.documentElement;
+ if (htmlElm.scrollHeight > htmlElm.clientHeight) {
+ return;
+ }
+
+ started = 1;
+ // Setup start position
+ startRng = rngFromPoint(e.x, e.y);
+ if (startRng) {
+ // Listen for selection change events
+ dom.bind(doc, 'mouseup', endSelection);
+ dom.bind(doc, 'mousemove', selectionChange);
+
+ dom.getRoot().focus();
+ startRng.select();
+ }
+ }
+ });
+ }
+
+ /**
+ * Fixes selection issues where the caret can be placed between two inline elements like <b>a</b>|<b>b</b>
+ * this fix will lean the caret right into the closest inline element.
+ */
+ function normalizeSelection() {
+ // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> except for Ctrl+A since it selects everything
+ editor.on('keyup focusin mouseup', function(e) {
+ if (e.keyCode != 65 || !VK.metaKeyPressed(e)) {
+ selection.normalize();
+ }
+ }, true);
+ }
+
+ /**
+ * Forces Gecko to render a broken image icon if it fails to load an image.
+ */
+ function showBrokenImageIcon() {
+ editor.contentStyles.push(
+ 'img:-moz-broken {' +
+ '-moz-force-broken-image-icon:1;' +
+ 'min-width:24px;' +
+ 'min-height:24px' +
+ '}'
+ );
+ }
+
+ /**
+ * iOS has a bug where it's impossible to type if the document has a touchstart event
+ * bound and the user touches the document while having the on screen keyboard visible.
+ *
+ * The touch event moves the focus to the parent document while having the caret inside the iframe
+ * this fix moves the focus back into the iframe document.
+ */
+ function restoreFocusOnKeyDown() {
+ if (!editor.inline) {
+ editor.on('keydown', function() {
+ if (document.activeElement == document.body) {
+ editor.getWin().focus();
+ }
+ });
+ }
+ }
+
+ /**
+ * IE 11 has an annoying issue where you can't move focus into the editor
+ * by clicking on the white area HTML element. We used to be able to to fix this with
+ * the fixCaretSelectionOfDocumentElementOnIe fix. But since M$ removed the selection
+ * object it's not possible anymore. So we need to hack in a ungly CSS to force the
+ * body to be at least 150px. If the user clicks the HTML element out side this 150px region
+ * we simply move the focus into the first paragraph. Not ideal since you loose the
+ * positioning of the caret but goot enough for most cases.
+ */
+ function bodyHeight() {
+ if (!editor.inline) {
+ editor.contentStyles.push('body {min-height: 150px}');
+ editor.on('click', function(e) {
+ var rng;
+
+ if (e.target.nodeName == 'HTML') {
+ // Edge seems to only need focus if we set the range
+ // the caret will become invisible and moved out of the iframe!!
+ if (Env.ie > 11) {
+ editor.getBody().focus();
+ return;
+ }
+
+ // Need to store away non collapsed ranges since the focus call will mess that up see #7382
+ rng = editor.selection.getRng();
+ editor.getBody().focus();
+ editor.selection.setRng(rng);
+ editor.selection.normalize();
+ editor.nodeChanged();
+ }
+ });
+ }
+ }
+
+ /**
+ * Firefox on Mac OS will move the browser back to the previous page if you press CMD+Left arrow.
+ * You might then loose all your work so we need to block that behavior and replace it with our own.
+ */
+ function blockCmdArrowNavigation() {
+ if (Env.mac) {
+ editor.on('keydown', function(e) {
+ if (VK.metaKeyPressed(e) && !e.shiftKey && (e.keyCode == 37 || e.keyCode == 39)) {
+ e.preventDefault();
+ editor.selection.getSel().modify('move', e.keyCode == 37 ? 'backward' : 'forward', 'lineboundary');
+ }
+ });
+ }
+ }
+
+ /**
+ * Disables the autolinking in IE 9+ this is then re-enabled by the autolink plugin.
+ */
+ function disableAutoUrlDetect() {
+ setEditorCommandState("AutoUrlDetect", false);
+ }
+
+ /**
+ * iOS 7.1 introduced two new bugs:
+ * 1) It's possible to open links within a contentEditable area by clicking on them.
+ * 2) If you hold down the finger it will display the link/image touch callout menu.
+ */
+ function tapLinksAndImages() {
+ editor.on('click', function(e) {
+ var elm = e.target;
+
+ do {
+ if (elm.tagName === 'A') {
+ e.preventDefault();
+ return;
+ }
+ } while ((elm = elm.parentNode));
+ });
+
+ editor.contentStyles.push('.mce-content-body {-webkit-touch-callout: none}');
+ }
+
+ /**
+ * iOS Safari and possible other browsers have a bug where it won't fire
+ * a click event when a contentEditable is focused. This function fakes click events
+ * by using touchstart/touchend and measuring the time and distance travelled.
+ */
+ /*
+ function touchClickEvent() {
+ editor.on('touchstart', function(e) {
+ var elm, time, startTouch, changedTouches;
+
+ elm = e.target;
+ time = new Date().getTime();
+ changedTouches = e.changedTouches;
+
+ if (!changedTouches || changedTouches.length > 1) {
+ return;
+ }
+
+ startTouch = changedTouches[0];
+
+ editor.once('touchend', function(e) {
+ var endTouch = e.changedTouches[0], args;
+
+ if (new Date().getTime() - time > 500) {
+ return;
+ }
+
+ if (Math.abs(startTouch.clientX - endTouch.clientX) > 5) {
+ return;
+ }
+
+ if (Math.abs(startTouch.clientY - endTouch.clientY) > 5) {
+ return;
+ }
+
+ args = {
+ target: elm
+ };
+
+ each('pageX pageY clientX clientY screenX screenY'.split(' '), function(key) {
+ args[key] = endTouch[key];
+ });
+
+ args = editor.fire('click', args);
+
+ if (!args.isDefaultPrevented()) {
+ // iOS WebKit can't place the caret properly once
+ // you bind touch events so we need to do this manually
+ // TODO: Expand to the closest word? Touble tap still works.
+ editor.selection.placeCaretAt(endTouch.clientX, endTouch.clientY);
+ editor.nodeChanged();
+ }
+ });
+ });
+ }
+ */
+
+ /**
+ * WebKit has a bug where it will allow forms to be submitted if they are inside a contentEditable element.
+ * For example this: <form><button></form>
+ */
+ function blockFormSubmitInsideEditor() {
+ editor.on('init', function() {
+ editor.dom.bind(editor.getBody(), 'submit', function(e) {
+ e.preventDefault();
+ });
+ });
+ }
+
+ /**
+ * Sometimes WebKit/Blink generates BR elements with the Apple-interchange-newline class.
+ *
+ * Scenario:
+ * 1) Create a table 2x2.
+ * 2) Select and copy cells A2-B2.
+ * 3) Paste and it will add BR element to table cell.
+ */
+ function removeAppleInterchangeBrs() {
+ parser.addNodeFilter('br', function(nodes) {
+ var i = nodes.length;
+
+ while (i--) {
+ if (nodes[i].attr('class') == 'Apple-interchange-newline') {
+ nodes[i].remove();
+ }
+ }
+ });
+ }
+
+ /**
+ * IE cannot set custom contentType's on drag events, and also does not properly drag/drop between
+ * editors. This uses a special data:text/mce-internal URL to pass data when drag/drop between editors.
+ */
+ function ieInternalDragAndDrop() {
+ editor.on('dragstart', function(e) {
+ setMceInternalContent(e);
+ });
+
+ editor.on('drop', function(e) {
+ if (!isDefaultPrevented(e)) {
+ var internalContent = getMceInternalContent(e);
+
+ if (internalContent && internalContent.id != editor.id) {
+ e.preventDefault();
+
+ var rng = RangeUtils.getCaretRangeFromPoint(e.x, e.y, editor.getDoc());
+ selection.setRng(rng);
+ insertClipboardContents(internalContent.html);
+ }
+ }
+ });
+ }
+
+ function refreshContentEditable() {
+ // No-op since Mozilla seems to have fixed the caret repaint issues
+ }
+
+ function isHidden() {
+ var sel;
+
+ if (!isGecko) {
+ return 0;
+ }
+
+ // Weird, wheres that cursor selection?
+ sel = editor.selection.getSel();
+ return (!sel || !sel.rangeCount || sel.rangeCount === 0);
+ }
+
+ /**
+ * Properly empties the editor if all contents is selected and deleted this to
+ * prevent empty paragraphs from being produced at beginning/end of contents.
+ */
+ function emptyEditorOnDeleteEverything() {
+ function isEverythingSelected(editor) {
+ var caretWalker = new CaretWalker(editor.getBody());
+ var rng = editor.selection.getRng();
+ var startCaretPos = CaretPosition.fromRangeStart(rng);
+ var endCaretPos = CaretPosition.fromRangeEnd(rng);
+
+ return !editor.selection.isCollapsed() && !caretWalker.prev(startCaretPos) && !caretWalker.next(endCaretPos);
+ }
+
+ // Type over case delete and insert this won't cover typeover with a IME but at least it covers the common case
+ editor.on('keypress', function (e) {
+ if (!isDefaultPrevented(e) && !selection.isCollapsed() && e.charCode > 31 && !VK.metaKeyPressed(e)) {
+ if (isEverythingSelected(editor)) {
+ e.preventDefault();
+ editor.setContent(String.fromCharCode(e.charCode));
+ editor.selection.select(editor.getBody(), true);
+ editor.selection.collapse(false);
+ editor.nodeChanged();
+ }
+ }
+ });
+
+ editor.on('keydown', function (e) {
+ var keyCode = e.keyCode;
+
+ if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) {
+ if (isEverythingSelected(editor)) {
+ e.preventDefault();
+ editor.setContent('');
+ editor.nodeChanged();
+ }
+ }
+ });
+ }
+
+ // All browsers
+ removeBlockQuoteOnBackSpace();
+ emptyEditorWhenDeleting();
+
+ // Windows phone will return a range like [body, 0] on mousedown so
+ // it will always normalize to the wrong location
+ if (!Env.windowsPhone) {
+ normalizeSelection();
+ }
+
+ // WebKit
+ if (isWebKit) {
+ emptyEditorOnDeleteEverything();
+ cleanupStylesWhenDeleting();
+ inputMethodFocus();
+ selectControlElements();
+ setDefaultBlockType();
+ blockFormSubmitInsideEditor();
+ disableBackspaceIntoATable();
+ removeAppleInterchangeBrs();
+
+ //touchClickEvent();
+
+ // iOS
+ if (Env.iOS) {
+ restoreFocusOnKeyDown();
+ bodyHeight();
+ tapLinksAndImages();
+ } else {
+ selectAll();
+ }
+ }
+
+ // IE
+ if (isIE && Env.ie < 11) {
+ removeHrOnBackspace();
+ ensureBodyHasRoleApplication();
+ addNewLinesBeforeBrInPre();
+ removePreSerializedStylesWhenSelectingControls();
+ deleteControlItemOnBackSpace();
+ renderEmptyBlocksFix();
+ keepNoScriptContents();
+ fixCaretSelectionOfDocumentElementOnIe();
+ }
+
+ if (Env.ie >= 11) {
+ bodyHeight();
+ disableBackspaceIntoATable();
+ }
+
+ if (Env.ie) {
+ selectAll();
+ disableAutoUrlDetect();
+ ieInternalDragAndDrop();
+ }
+
+ // Gecko
+ if (isGecko) {
+ emptyEditorOnDeleteEverything();
+ removeHrOnBackspace();
+ focusBody();
+ removeStylesWhenDeletingAcrossBlockElements();
+ setGeckoEditingOptions();
+ addBrAfterLastLinks();
+ showBrokenImageIcon();
+ blockCmdArrowNavigation();
+ disableBackspaceIntoATable();
+ }
+
+ return {
+ refreshContentEditable: refreshContentEditable,
+ isHidden: isHidden
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/EditorObservable.js
+
+/**
+ * EditorObservable.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This mixin contains the event logic for the tinymce.Editor class.
+ *
+ * @mixin tinymce.EditorObservable
+ * @extends tinymce.util.Observable
+ */
+define("tinymce/EditorObservable", [
+ "tinymce/util/Observable",
+ "tinymce/dom/DOMUtils",
+ "tinymce/util/Tools"
+], function(Observable, DOMUtils, Tools) {
+ var DOM = DOMUtils.DOM, customEventRootDelegates;
+
+ /**
+ * Returns the event target so for the specified event. Some events fire
+ * only on document, some fire on documentElement etc. This also handles the
+ * custom event root setting where it returns that element instead of the body.
+ *
+ * @private
+ * @param {tinymce.Editor} editor Editor instance to get event target from.
+ * @param {String} eventName Name of the event for example "click".
+ * @return {Element/Document} HTML Element or document target to bind on.
+ */
+ function getEventTarget(editor, eventName) {
+ if (eventName == 'selectionchange') {
+ return editor.getDoc();
+ }
+
+ // Need to bind mousedown/mouseup etc to document not body in iframe mode
+ // Since the user might click on the HTML element not the BODY
+ if (!editor.inline && /^mouse|touch|click|contextmenu|drop|dragover|dragend/.test(eventName)) {
+ return editor.getDoc().documentElement;
+ }
+
+ // Bind to event root instead of body if it's defined
+ if (editor.settings.event_root) {
+ if (!editor.eventRoot) {
+ editor.eventRoot = DOM.select(editor.settings.event_root)[0];
+ }
+
+ return editor.eventRoot;
+ }
+
+ return editor.getBody();
+ }
+
+ /**
+ * Binds a event delegate for the specified name this delegate will fire
+ * the event to the editor dispatcher.
+ *
+ * @private
+ * @param {tinymce.Editor} editor Editor instance to get event target from.
+ * @param {String} eventName Name of the event for example "click".
+ */
+ function bindEventDelegate(editor, eventName) {
+ var eventRootElm = getEventTarget(editor, eventName), delegate;
+
+ function isListening(editor) {
+ return !editor.hidden && !editor.readonly;
+ }
+
+ if (!editor.delegates) {
+ editor.delegates = {};
+ }
+
+ if (editor.delegates[eventName]) {
+ return;
+ }
+
+ if (editor.settings.event_root) {
+ if (!customEventRootDelegates) {
+ customEventRootDelegates = {};
+ editor.editorManager.on('removeEditor', function() {
+ var name;
+
+ if (!editor.editorManager.activeEditor) {
+ if (customEventRootDelegates) {
+ for (name in customEventRootDelegates) {
+ editor.dom.unbind(getEventTarget(editor, name));
+ }
+
+ customEventRootDelegates = null;
+ }
+ }
+ });
+ }
+
+ if (customEventRootDelegates[eventName]) {
+ return;
+ }
+
+ delegate = function(e) {
+ var target = e.target, editors = editor.editorManager.editors, i = editors.length;
+
+ while (i--) {
+ var body = editors[i].getBody();
+
+ if (body === target || DOM.isChildOf(target, body)) {
+ if (isListening(editors[i])) {
+ editors[i].fire(eventName, e);
+ }
+ }
+ }
+ };
+
+ customEventRootDelegates[eventName] = delegate;
+ DOM.bind(eventRootElm, eventName, delegate);
+ } else {
+ delegate = function(e) {
+ if (isListening(editor)) {
+ editor.fire(eventName, e);
+ }
+ };
+
+ DOM.bind(eventRootElm, eventName, delegate);
+ editor.delegates[eventName] = delegate;
+ }
+ }
+
+ var EditorObservable = {
+ /**
+ * Bind any pending event delegates. This gets executed after the target body/document is created.
+ *
+ * @private
+ */
+ bindPendingEventDelegates: function() {
+ var self = this;
+
+ Tools.each(self._pendingNativeEvents, function(name) {
+ bindEventDelegate(self, name);
+ });
+ },
+
+ /**
+ * Toggles a native event on/off this is called by the EventDispatcher when
+ * the first native event handler is added and when the last native event handler is removed.
+ *
+ * @private
+ */
+ toggleNativeEvent: function(name, state) {
+ var self = this;
+
+ // Never bind focus/blur since the FocusManager fakes those
+ if (name == "focus" || name == "blur") {
+ return;
+ }
+
+ if (state) {
+ if (self.initialized) {
+ bindEventDelegate(self, name);
+ } else {
+ if (!self._pendingNativeEvents) {
+ self._pendingNativeEvents = [name];
+ } else {
+ self._pendingNativeEvents.push(name);
+ }
+ }
+ } else if (self.initialized) {
+ self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]);
+ delete self.delegates[name];
+ }
+ },
+
+ /**
+ * Unbinds all native event handlers that means delegates, custom events bound using the Events API etc.
+ *
+ * @private
+ */
+ unbindAllNativeEvents: function() {
+ var self = this, name;
+
+ if (self.delegates) {
+ for (name in self.delegates) {
+ self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]);
+ }
+
+ delete self.delegates;
+ }
+
+ if (!self.inline) {
+ self.getBody().onload = null;
+ self.dom.unbind(self.getWin());
+ self.dom.unbind(self.getDoc());
+ }
+
+ self.dom.unbind(self.getBody());
+ self.dom.unbind(self.getContainer());
+ }
+ };
+
+ EditorObservable = Tools.extend({}, Observable, EditorObservable);
+
+ return EditorObservable;
+});
+
+// Included from: js/tinymce/classes/Mode.js
+
+/**
+ * Mode.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Mode switcher logic.
+ *
+ * @private
+ * @class tinymce.Mode
+ */
+define("tinymce/Mode", [], function() {
+ function setEditorCommandState(editor, cmd, state) {
+ try {
+ editor.getDoc().execCommand(cmd, false, state);
+ } catch (ex) {
+ // Ignore
+ }
+ }
+
+ function clickBlocker(editor) {
+ var target, handler;
+
+ target = editor.getBody();
+
+ handler = function(e) {
+ if (editor.dom.getParents(e.target, 'a').length > 0) {
+ e.preventDefault();
+ }
+ };
+
+ editor.dom.bind(target, 'click', handler);
+
+ return {
+ unbind: function() {
+ editor.dom.unbind(target, 'click', handler);
+ }
+ };
+ }
+
+ function toggleReadOnly(editor, state) {
+ if (editor._clickBlocker) {
+ editor._clickBlocker.unbind();
+ editor._clickBlocker = null;
+ }
+
+ if (state) {
+ editor._clickBlocker = clickBlocker(editor);
+ editor.selection.controlSelection.hideResizeRect();
+ editor.readonly = true;
+ editor.getBody().contentEditable = false;
+ } else {
+ editor.readonly = false;
+ editor.getBody().contentEditable = true;
+ setEditorCommandState(editor, "StyleWithCSS", false);
+ setEditorCommandState(editor, "enableInlineTableEditing", false);
+ setEditorCommandState(editor, "enableObjectResizing", false);
+ editor.focus();
+ editor.nodeChanged();
+ }
+ }
+
+ function setMode(editor, mode) {
+ var currentMode = editor.readonly ? 'readonly' : 'design';
+
+ if (mode == currentMode) {
+ return;
+ }
+
+ if (editor.initialized) {
+ toggleReadOnly(editor, mode == 'readonly');
+ } else {
+ editor.on('init', function() {
+ toggleReadOnly(editor, mode == 'readonly');
+ });
+ }
+
+ // Event is NOT preventable
+ editor.fire('SwitchMode', {mode: mode});
+ }
+
+ return {
+ setMode: setMode
+ };
+});
+
+// Included from: js/tinymce/classes/Shortcuts.js
+
+/**
+ * Shortcuts.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Contains all logic for handling of keyboard shortcuts.
+ *
+ * @class tinymce.Shortcuts
+ * @example
+ * editor.shortcuts.add('ctrl+a', function() {});
+ * editor.shortcuts.add('meta+a', function() {}); // "meta" maps to Command on Mac and Ctrl on PC
+ * editor.shortcuts.add('ctrl+alt+a', function() {});
+ * editor.shortcuts.add('access+a', function() {}); // "access" maps to ctrl+alt on Mac and shift+alt on PC
+ */
+define("tinymce/Shortcuts", [
+ "tinymce/util/Tools",
+ "tinymce/Env"
+], function(Tools, Env) {
+ var each = Tools.each, explode = Tools.explode;
+
+ var keyCodeLookup = {
+ "f9": 120,
+ "f10": 121,
+ "f11": 122
+ };
+
+ var modifierNames = Tools.makeMap('alt,ctrl,shift,meta,access');
+
+ return function(editor) {
+ var self = this, shortcuts = {}, pendingPatterns = [];
+
+ function parseShortcut(pattern) {
+ var id, key, shortcut = {};
+
+ // Parse modifiers and keys ctrl+alt+b for example
+ each(explode(pattern, '+'), function(value) {
+ if (value in modifierNames) {
+ shortcut[value] = true;
+ } else {
+ // Allow numeric keycodes like ctrl+219 for ctrl+[
+ if (/^[0-9]{2,}$/.test(value)) {
+ shortcut.keyCode = parseInt(value, 10);
+ } else {
+ shortcut.charCode = value.charCodeAt(0);
+ shortcut.keyCode = keyCodeLookup[value] || value.toUpperCase().charCodeAt(0);
+ }
+ }
+ });
+
+ // Generate unique id for modifier combination and set default state for unused modifiers
+ id = [shortcut.keyCode];
+ for (key in modifierNames) {
+ if (shortcut[key]) {
+ id.push(key);
+ } else {
+ shortcut[key] = false;
+ }
+ }
+ shortcut.id = id.join(',');
+
+ // Handle special access modifier differently depending on Mac/Win
+ if (shortcut.access) {
+ shortcut.alt = true;
+
+ if (Env.mac) {
+ shortcut.ctrl = true;
+ } else {
+ shortcut.shift = true;
+ }
+ }
+
+ // Handle special meta modifier differently depending on Mac/Win
+ if (shortcut.meta) {
+ if (Env.mac) {
+ shortcut.meta = true;
+ } else {
+ shortcut.ctrl = true;
+ shortcut.meta = false;
+ }
+ }
+
+ return shortcut;
+ }
+
+ function createShortcut(pattern, desc, cmdFunc, scope) {
+ var shortcuts;
+
+ shortcuts = Tools.map(explode(pattern, '>'), parseShortcut);
+ shortcuts[shortcuts.length - 1] = Tools.extend(shortcuts[shortcuts.length - 1], {
+ func: cmdFunc,
+ scope: scope || editor
+ });
+
+ return Tools.extend(shortcuts[0], {
+ desc: editor.translate(desc),
+ subpatterns: shortcuts.slice(1)
+ });
+ }
+
+ function hasModifier(e) {
+ return e.altKey || e.ctrlKey || e.metaKey;
+ }
+
+ function isFunctionKey(e) {
+ return e.type === "keydown" && e.keyCode >= 112 && e.keyCode <= 123;
+ }
+
+ function matchShortcut(e, shortcut) {
+ if (!shortcut) {
+ return false;
+ }
+
+ if (shortcut.ctrl != e.ctrlKey || shortcut.meta != e.metaKey) {
+ return false;
+ }
+
+ if (shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) {
+ return false;
+ }
+
+ if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) {
+ e.preventDefault();
+ return true;
+ }
+
+ return false;
+ }
+
+ function executeShortcutAction(shortcut) {
+ return shortcut.func ? shortcut.func.call(shortcut.scope) : null;
+ }
+
+ editor.on('keyup keypress keydown', function(e) {
+ if ((hasModifier(e) || isFunctionKey(e)) && !e.isDefaultPrevented()) {
+ each(shortcuts, function(shortcut) {
+ if (matchShortcut(e, shortcut)) {
+ pendingPatterns = shortcut.subpatterns.slice(0);
+
+ if (e.type == "keydown") {
+ executeShortcutAction(shortcut);
+ }
+
+ return true;
+ }
+ });
+
+ if (matchShortcut(e, pendingPatterns[0])) {
+ if (pendingPatterns.length === 1) {
+ if (e.type == "keydown") {
+ executeShortcutAction(pendingPatterns[0]);
+ }
+ }
+
+ pendingPatterns.shift();
+ }
+ }
+ });
+
+ /**
+ * Adds a keyboard shortcut for some command or function.
+ *
+ * @method add
+ * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
+ * @param {String} desc Text description for the command.
+ * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed.
+ * @param {Object} scope Optional scope to execute the function in.
+ * @return {Boolean} true/false state if the shortcut was added or not.
+ */
+ self.add = function(pattern, desc, cmdFunc, scope) {
+ var cmd;
+
+ cmd = cmdFunc;
+
+ if (typeof cmdFunc === 'string') {
+ cmdFunc = function() {
+ editor.execCommand(cmd, false, null);
+ };
+ } else if (Tools.isArray(cmd)) {
+ cmdFunc = function() {
+ editor.execCommand(cmd[0], cmd[1], cmd[2]);
+ };
+ }
+
+ each(explode(Tools.trim(pattern.toLowerCase())), function(pattern) {
+ var shortcut = createShortcut(pattern, desc, cmdFunc, scope);
+ shortcuts[shortcut.id] = shortcut;
+ });
+
+ return true;
+ };
+
+ /**
+ * Remove a keyboard shortcut by pattern.
+ *
+ * @method remove
+ * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
+ * @return {Boolean} true/false state if the shortcut was removed or not.
+ */
+ self.remove = function(pattern) {
+ var shortcut = createShortcut(pattern);
+
+ if (shortcuts[shortcut.id]) {
+ delete shortcuts[shortcut.id];
+ return true;
+ }
+
+ return false;
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/file/Uploader.js
+
+/**
+ * Uploader.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Upload blobs or blob infos to the specified URL or handler.
+ *
+ * @private
+ * @class tinymce.file.Uploader
+ * @example
+ * var uploader = new Uploader({
+ * url: '/upload.php',
+ * basePath: '/base/path',
+ * credentials: true,
+ * handler: function(data, success, failure) {
+ * ...
+ * }
+ * });
+ *
+ * uploader.upload(blobInfos).then(function(result) {
+ * ...
+ * });
+ */
+define("tinymce/file/Uploader", [
+ "tinymce/util/Promise",
+ "tinymce/util/Tools",
+ "tinymce/util/Fun"
+], function(Promise, Tools, Fun) {
+ return function(uploadStatus, settings) {
+ var pendingPromises = {};
+
+ function filename(blobInfo) {
+ var ext, extensions;
+
+ extensions = {
+ 'image/jpeg': 'jpg',
+ 'image/jpg': 'jpg',
+ 'image/gif': 'gif',
+ 'image/png': 'png'
+ };
+
+ ext = extensions[blobInfo.blob().type.toLowerCase()] || 'dat';
+
+ return blobInfo.filename() + '.' + ext;
+ }
+
+ function pathJoin(path1, path2) {
+ if (path1) {
+ return path1.replace(/\/$/, '') + '/' + path2.replace(/^\//, '');
+ }
+
+ return path2;
+ }
+
+ function blobInfoToData(blobInfo) {
+ return {
+ id: blobInfo.id,
+ blob: blobInfo.blob,
+ base64: blobInfo.base64,
+ filename: Fun.constant(filename(blobInfo))
+ };
+ }
+
+ function defaultHandler(blobInfo, success, failure, progress) {
+ var xhr, formData;
+
+ xhr = new XMLHttpRequest();
+ xhr.open('POST', settings.url);
+ xhr.withCredentials = settings.credentials;
+
+ xhr.upload.onprogress = function(e) {
+ progress(e.loaded / e.total * 100);
+ };
+
+ xhr.onerror = function() {
+ failure("Image upload failed due to a XHR Transport error. Code: " + xhr.status);
+ };
+
+ xhr.onload = function() {
+ var json;
+
+ if (xhr.status != 200) {
+ failure("HTTP Error: " + xhr.status);
+ return;
+ }
+
+ json = JSON.parse(xhr.responseText);
+
+ if (!json || typeof json.location != "string") {
+ failure("Invalid JSON: " + xhr.responseText);
+ return;
+ }
+
+ success(pathJoin(settings.basePath, json.location));
+ };
+
+ formData = new FormData();
+ formData.append('file', blobInfo.blob(), blobInfo.filename());
+
+ xhr.send(formData);
+ }
+
+ function noUpload() {
+ return new Promise(function(resolve) {
+ resolve([]);
+ });
+ }
+
+ function handlerSuccess(blobInfo, url) {
+ return {
+ url: url,
+ blobInfo: blobInfo,
+ status: true
+ };
+ }
+
+ function handlerFailure(blobInfo, error) {
+ return {
+ url: '',
+ blobInfo: blobInfo,
+ status: false,
+ error: error
+ };
+ }
+
+ function resolvePending(blobUri, result) {
+ Tools.each(pendingPromises[blobUri], function(resolve) {
+ resolve(result);
+ });
+
+ delete pendingPromises[blobUri];
+ }
+
+ function uploadBlobInfo(blobInfo, handler, openNotification) {
+ uploadStatus.markPending(blobInfo.blobUri());
+
+ return new Promise(function(resolve) {
+ var notification, progress;
+
+ var noop = function() {
+ };
+
+ try {
+ var closeNotification = function() {
+ if (notification) {
+ notification.close();
+ progress = noop; // Once it's closed it's closed
+ }
+ };
+
+ var success = function(url) {
+ closeNotification();
+ uploadStatus.markUploaded(blobInfo.blobUri(), url);
+ resolvePending(blobInfo.blobUri(), handlerSuccess(blobInfo, url));
+ resolve(handlerSuccess(blobInfo, url));
+ };
+
+ var failure = function() {
+ closeNotification();
+ uploadStatus.removeFailed(blobInfo.blobUri());
+ resolvePending(blobInfo.blobUri(), handlerFailure(blobInfo, failure));
+ resolve(handlerFailure(blobInfo, failure));
+ };
+
+ progress = function(percent) {
+ if (percent < 0 || percent > 100) {
+ return;
+ }
+
+ if (!notification) {
+ notification = openNotification();
+ }
+
+ notification.progressBar.value(percent);
+ };
+
+ handler(blobInfoToData(blobInfo), success, failure, progress);
+ } catch (ex) {
+ resolve(handlerFailure(blobInfo, ex.message));
+ }
+ });
+ }
+
+ function isDefaultHandler(handler) {
+ return handler === defaultHandler;
+ }
+
+ function pendingUploadBlobInfo(blobInfo) {
+ var blobUri = blobInfo.blobUri();
+
+ return new Promise(function(resolve) {
+ pendingPromises[blobUri] = pendingPromises[blobUri] || [];
+ pendingPromises[blobUri].push(resolve);
+ });
+ }
+
+ function uploadBlobs(blobInfos, openNotification) {
+ blobInfos = Tools.grep(blobInfos, function(blobInfo) {
+ return !uploadStatus.isUploaded(blobInfo.blobUri());
+ });
+
+ return Promise.all(Tools.map(blobInfos, function(blobInfo) {
+ return uploadStatus.isPending(blobInfo.blobUri()) ?
+ pendingUploadBlobInfo(blobInfo) : uploadBlobInfo(blobInfo, settings.handler, openNotification);
+ }));
+ }
+
+ function upload(blobInfos, openNotification) {
+ return (!settings.url && isDefaultHandler(settings.handler)) ? noUpload() : uploadBlobs(blobInfos, openNotification);
+ }
+
+ settings = Tools.extend({
+ credentials: false,
+ // We are adding a notify argument to this (at the moment, until it doesn't work)
+ handler: defaultHandler
+ }, settings);
+
+ return {
+ upload: upload
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/file/Conversions.js
+
+/**
+ * Conversions.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Converts blob/uris back and forth.
+ *
+ * @private
+ * @class tinymce.file.Conversions
+ */
+define("tinymce/file/Conversions", [
+ "tinymce/util/Promise"
+], function(Promise) {
+ function blobUriToBlob(url) {
+ return new Promise(function(resolve) {
+ var xhr = new XMLHttpRequest();
+
+ xhr.open('GET', url, true);
+ xhr.responseType = 'blob';
+
+ xhr.onload = function() {
+ if (this.status == 200) {
+ resolve(this.response);
+ }
+ };
+
+ xhr.send();
+ });
+ }
+
+ function parseDataUri(uri) {
+ var type, matches;
+
+ uri = decodeURIComponent(uri).split(',');
+
+ matches = /data:([^;]+)/.exec(uri[0]);
+ if (matches) {
+ type = matches[1];
+ }
+
+ return {
+ type: type,
+ data: uri[1]
+ };
+ }
+
+ function dataUriToBlob(uri) {
+ return new Promise(function(resolve) {
+ var str, arr, i;
+
+ uri = parseDataUri(uri);
+
+ // Might throw error if data isn't proper base64
+ try {
+ str = atob(uri.data);
+ } catch (e) {
+ resolve(new Blob([]));
+ return;
+ }
+
+ arr = new Uint8Array(str.length);
+
+ for (i = 0; i < arr.length; i++) {
+ arr[i] = str.charCodeAt(i);
+ }
+
+ resolve(new Blob([arr], {type: uri.type}));
+ });
+ }
+
+ function uriToBlob(url) {
+ if (url.indexOf('blob:') === 0) {
+ return blobUriToBlob(url);
+ }
+
+ if (url.indexOf('data:') === 0) {
+ return dataUriToBlob(url);
+ }
+
+ return null;
+ }
+
+ function blobToDataUri(blob) {
+ return new Promise(function(resolve) {
+ var reader = new FileReader();
+
+ reader.onloadend = function() {
+ resolve(reader.result);
+ };
+
+ reader.readAsDataURL(blob);
+ });
+ }
+
+ return {
+ uriToBlob: uriToBlob,
+ blobToDataUri: blobToDataUri,
+ parseDataUri: parseDataUri
+ };
+});
+
+// Included from: js/tinymce/classes/file/ImageScanner.js
+
+/**
+ * ImageScanner.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Finds images with data uris or blob uris. If data uris are found it will convert them into blob uris.
+ *
+ * @private
+ * @class tinymce.file.ImageScanner
+ */
+define("tinymce/file/ImageScanner", [
+ "tinymce/util/Promise",
+ "tinymce/util/Arr",
+ "tinymce/util/Fun",
+ "tinymce/file/Conversions",
+ "tinymce/Env"
+], function(Promise, Arr, Fun, Conversions, Env) {
+ var count = 0;
+
+ return function(uploadStatus, blobCache) {
+ var cachedPromises = {};
+
+ function findAll(elm, predicate) {
+ var images, promises;
+
+ function imageToBlobInfo(img, resolve) {
+ var base64, blobInfo;
+
+ if (img.src.indexOf('blob:') === 0) {
+ blobInfo = blobCache.getByUri(img.src);
+
+ if (blobInfo) {
+ resolve({
+ image: img,
+ blobInfo: blobInfo
+ });
+ }
+
+ return;
+ }
+
+ base64 = Conversions.parseDataUri(img.src).data;
+ blobInfo = blobCache.findFirst(function(cachedBlobInfo) {
+ return cachedBlobInfo.base64() === base64;
+ });
+
+ if (blobInfo) {
+ resolve({
+ image: img,
+ blobInfo: blobInfo
+ });
+ } else {
+ Conversions.uriToBlob(img.src).then(function(blob) {
+ var blobInfoId = 'blobid' + (count++),
+ blobInfo = blobCache.create(blobInfoId, blob, base64);
+
+ blobCache.add(blobInfo);
+
+ resolve({
+ image: img,
+ blobInfo: blobInfo
+ });
+ });
+ }
+ }
+
+ if (!predicate) {
+ predicate = Fun.constant(true);
+ }
+
+ images = Arr.filter(elm.getElementsByTagName('img'), function(img) {
+ var src = img.src;
+
+ if (!Env.fileApi) {
+ return false;
+ }
+
+ if (img.hasAttribute('data-mce-bogus')) {
+ return false;
+ }
+
+ if (img.hasAttribute('data-mce-placeholder')) {
+ return false;
+ }
+
+ if (!src || src == Env.transparentSrc) {
+ return false;
+ }
+
+ if (src.indexOf('blob:') === 0) {
+ return !uploadStatus.isUploaded(src);
+ }
+
+ if (src.indexOf('data:') === 0) {
+ return predicate(img);
+ }
+
+ return false;
+ });
+
+ promises = Arr.map(images, function(img) {
+ var newPromise;
+
+ if (cachedPromises[img.src]) {
+ // Since the cached promise will return the cached image
+ // We need to wrap it and resolve with the actual image
+ return new Promise(function(resolve) {
+ cachedPromises[img.src].then(function(imageInfo) {
+ resolve({
+ image: img,
+ blobInfo: imageInfo.blobInfo
+ });
+ });
+ });
+ }
+
+ newPromise = new Promise(function(resolve) {
+ imageToBlobInfo(img, resolve);
+ }).then(function(result) {
+ delete cachedPromises[result.image.src];
+ return result;
+ })['catch'](function(error) {
+ delete cachedPromises[img.src];
+ return error;
+ });
+
+ cachedPromises[img.src] = newPromise;
+
+ return newPromise;
+ });
+
+ return Promise.all(promises);
+ }
+
+ return {
+ findAll: findAll
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/file/BlobCache.js
+
+/**
+ * BlobCache.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Hold blob info objects where a blob has extra internal information.
+ *
+ * @private
+ * @class tinymce.file.BlobCache
+ */
+define("tinymce/file/BlobCache", [
+ "tinymce/util/Arr",
+ "tinymce/util/Fun"
+], function(Arr, Fun) {
+ return function() {
+ var cache = [], constant = Fun.constant;
+
+ function create(id, blob, base64, filename) {
+ return {
+ id: constant(id),
+ filename: constant(filename || id),
+ blob: constant(blob),
+ base64: constant(base64),
+ blobUri: constant(URL.createObjectURL(blob))
+ };
+ }
+
+ function add(blobInfo) {
+ if (!get(blobInfo.id())) {
+ cache.push(blobInfo);
+ }
+ }
+
+ function get(id) {
+ return findFirst(function(cachedBlobInfo) {
+ return cachedBlobInfo.id() === id;
+ });
+ }
+
+ function findFirst(predicate) {
+ return Arr.filter(cache, predicate)[0];
+ }
+
+ function getByUri(blobUri) {
+ return findFirst(function(blobInfo) {
+ return blobInfo.blobUri() == blobUri;
+ });
+ }
+
+ function removeByUri(blobUri) {
+ cache = Arr.filter(cache, function(blobInfo) {
+ if (blobInfo.blobUri() === blobUri) {
+ URL.revokeObjectURL(blobInfo.blobUri());
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+ function destroy() {
+ Arr.each(cache, function(cachedBlobInfo) {
+ URL.revokeObjectURL(cachedBlobInfo.blobUri());
+ });
+
+ cache = [];
+ }
+
+ return {
+ create: create,
+ add: add,
+ get: get,
+ getByUri: getByUri,
+ findFirst: findFirst,
+ removeByUri: removeByUri,
+ destroy: destroy
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/file/UploadStatus.js
+
+/**
+ * UploadStatus.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Holds the current status of a blob uri, if it's pending or uploaded and what the result urls was.
+ *
+ * @private
+ * @class tinymce.file.UploadStatus
+ */
+define("tinymce/file/UploadStatus", [
+], function() {
+ return function() {
+ var PENDING = 1, UPLOADED = 2;
+ var blobUriStatuses = {};
+
+ function createStatus(status, resultUri) {
+ return {
+ status: status,
+ resultUri: resultUri
+ };
+ }
+
+ function hasBlobUri(blobUri) {
+ return blobUri in blobUriStatuses;
+ }
+
+ function getResultUri(blobUri) {
+ var result = blobUriStatuses[blobUri];
+
+ return result ? result.resultUri : null;
+ }
+
+ function isPending(blobUri) {
+ return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === PENDING : false;
+ }
+
+ function isUploaded(blobUri) {
+ return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === UPLOADED : false;
+ }
+
+ function markPending(blobUri) {
+ blobUriStatuses[blobUri] = createStatus(PENDING, null);
+ }
+
+ function markUploaded(blobUri, resultUri) {
+ blobUriStatuses[blobUri] = createStatus(UPLOADED, resultUri);
+ }
+
+ function removeFailed(blobUri) {
+ delete blobUriStatuses[blobUri];
+ }
+
+ function destroy() {
+ blobUriStatuses = {};
+ }
+
+ return {
+ hasBlobUri: hasBlobUri,
+ getResultUri: getResultUri,
+ isPending: isPending,
+ isUploaded: isUploaded,
+ markPending: markPending,
+ markUploaded: markUploaded,
+ removeFailed: removeFailed,
+ destroy: destroy
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/EditorUpload.js
+
+/**
+ * EditorUpload.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Handles image uploads, updates undo stack and patches over various internal functions.
+ *
+ * @private
+ * @class tinymce.EditorUpload
+ */
+define("tinymce/EditorUpload", [
+ "tinymce/util/Arr",
+ "tinymce/file/Uploader",
+ "tinymce/file/ImageScanner",
+ "tinymce/file/BlobCache",
+ "tinymce/file/UploadStatus"
+], function(Arr, Uploader, ImageScanner, BlobCache, UploadStatus) {
+ return function(editor) {
+ var blobCache = new BlobCache(), uploader, imageScanner, settings = editor.settings;
+ var uploadStatus = new UploadStatus();
+
+ function aliveGuard(callback) {
+ return function(result) {
+ if (editor.selection) {
+ return callback(result);
+ }
+
+ return [];
+ };
+ }
+
+ function cacheInvalidator() {
+ return '?' + (new Date()).getTime();
+ }
+
+ // Replaces strings without regexps to avoid FF regexp to big issue
+ function replaceString(content, search, replace) {
+ var index = 0;
+
+ do {
+ index = content.indexOf(search, index);
+
+ if (index !== -1) {
+ content = content.substring(0, index) + replace + content.substr(index + search.length);
+ index += replace.length - search.length + 1;
+ }
+ } while (index !== -1);
+
+ return content;
+ }
+
+ function replaceImageUrl(content, targetUrl, replacementUrl) {
+ content = replaceString(content, 'src="' + targetUrl + '"', 'src="' + replacementUrl + '"');
+ content = replaceString(content, 'data-mce-src="' + targetUrl + '"', 'data-mce-src="' + replacementUrl + '"');
+
+ return content;
+ }
+
+ function replaceUrlInUndoStack(targetUrl, replacementUrl) {
+ Arr.each(editor.undoManager.data, function(level) {
+ if (level.type === 'fragmented') {
+ level.fragments = Arr.map(level.fragments, function (fragment) {
+ return replaceImageUrl(fragment, targetUrl, replacementUrl);
+ });
+ } else {
+ level.content = replaceImageUrl(level.content, targetUrl, replacementUrl);
+ }
+ });
+ }
+
+ function openNotification() {
+ return editor.notificationManager.open({
+ text: editor.translate('Image uploading...'),
+ type: 'info',
+ timeout: -1,
+ progressBar: true
+ });
+ }
+
+ function replaceImageUri(image, resultUri) {
+ blobCache.removeByUri(image.src);
+ replaceUrlInUndoStack(image.src, resultUri);
+
+ editor.$(image).attr({
+ src: settings.images_reuse_filename ? resultUri + cacheInvalidator() : resultUri,
+ 'data-mce-src': editor.convertURL(resultUri, 'src')
+ });
+ }
+
+ function uploadImages(callback) {
+ if (!uploader) {
+ uploader = new Uploader(uploadStatus, {
+ url: settings.images_upload_url,
+ basePath: settings.images_upload_base_path,
+ credentials: settings.images_upload_credentials,
+ handler: settings.images_upload_handler
+ });
+ }
+
+ return scanForImages().then(aliveGuard(function(imageInfos) {
+ var blobInfos;
+
+ blobInfos = Arr.map(imageInfos, function(imageInfo) {
+ return imageInfo.blobInfo;
+ });
+
+ return uploader.upload(blobInfos, openNotification).then(aliveGuard(function(result) {
+ result = Arr.map(result, function(uploadInfo, index) {
+ var image = imageInfos[index].image;
+
+ if (uploadInfo.status && editor.settings.images_replace_blob_uris !== false) {
+ replaceImageUri(image, uploadInfo.url);
+ }
+
+ return {
+ element: image,
+ status: uploadInfo.status
+ };
+ });
+
+ if (callback) {
+ callback(result);
+ }
+
+ return result;
+ }));
+ }));
+ }
+
+ function uploadImagesAuto(callback) {
+ if (settings.automatic_uploads !== false) {
+ return uploadImages(callback);
+ }
+ }
+
+ function isValidDataUriImage(imgElm) {
+ return settings.images_dataimg_filter ? settings.images_dataimg_filter(imgElm) : true;
+ }
+
+ function scanForImages() {
+ if (!imageScanner) {
+ imageScanner = new ImageScanner(uploadStatus, blobCache);
+ }
+
+ return imageScanner.findAll(editor.getBody(), isValidDataUriImage).then(aliveGuard(function(result) {
+ Arr.each(result, function(resultItem) {
+ replaceUrlInUndoStack(resultItem.image.src, resultItem.blobInfo.blobUri());
+ resultItem.image.src = resultItem.blobInfo.blobUri();
+ resultItem.image.removeAttribute('data-mce-src');
+ });
+
+ return result;
+ }));
+ }
+
+ function destroy() {
+ blobCache.destroy();
+ uploadStatus.destroy();
+ imageScanner = uploader = null;
+ }
+
+ function replaceBlobUris(content) {
+ return content.replace(/src="(blob:[^"]+)"/g, function(match, blobUri) {
+ var resultUri = uploadStatus.getResultUri(blobUri);
+
+ if (resultUri) {
+ return 'src="' + resultUri + '"';
+ }
+
+ var blobInfo = blobCache.getByUri(blobUri);
+
+ if (!blobInfo) {
+ blobInfo = Arr.reduce(editor.editorManager.editors, function(result, editor) {
+ return result || editor.editorUpload.blobCache.getByUri(blobUri);
+ }, null);
+ }
+
+ if (blobInfo) {
+ return 'src="data:' + blobInfo.blob().type + ';base64,' + blobInfo.base64() + '"';
+ }
+
+ return match;
+ });
+ }
+
+ editor.on('setContent', function() {
+ if (editor.settings.automatic_uploads !== false) {
+ uploadImagesAuto();
+ } else {
+ scanForImages();
+ }
+ });
+
+ editor.on('RawSaveContent', function(e) {
+ e.content = replaceBlobUris(e.content);
+ });
+
+ editor.on('getContent', function(e) {
+ if (e.source_view || e.format == 'raw') {
+ return;
+ }
+
+ e.content = replaceBlobUris(e.content);
+ });
+
+ editor.on('PostRender', function() {
+ editor.parser.addNodeFilter('img', function(images) {
+ Arr.each(images, function(img) {
+ var src = img.attr('src');
+
+ if (blobCache.getByUri(src)) {
+ return;
+ }
+
+ var resultUri = uploadStatus.getResultUri(src);
+ if (resultUri) {
+ img.attr('src', resultUri);
+ }
+ });
+ });
+ });
+
+ return {
+ blobCache: blobCache,
+ uploadImages: uploadImages,
+ uploadImagesAuto: uploadImagesAuto,
+ scanForImages: scanForImages,
+ destroy: destroy
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/caret/FakeCaret.js
+
+/**
+ * FakeCaret.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module contains logic for rendering a fake visual caret.
+ *
+ * @private
+ * @class tinymce.caret.FakeCaret
+ */
+define("tinymce/caret/FakeCaret", [
+ "tinymce/caret/CaretContainer",
+ "tinymce/caret/CaretPosition",
+ "tinymce/dom/NodeType",
+ "tinymce/dom/RangeUtils",
+ "tinymce/dom/DomQuery",
+ "tinymce/geom/ClientRect",
+ "tinymce/util/Delay"
+], function(CaretContainer, CaretPosition, NodeType, RangeUtils, $, ClientRect, Delay) {
+ var isContentEditableFalse = NodeType.isContentEditableFalse;
+
+ return function(rootNode, isBlock) {
+ var cursorInterval, $lastVisualCaret, caretContainerNode;
+
+ function getAbsoluteClientRect(node, before) {
+ var clientRect = ClientRect.collapse(node.getBoundingClientRect(), before),
+ docElm, scrollX, scrollY, margin, rootRect;
+
+ if (rootNode.tagName == 'BODY') {
+ docElm = rootNode.ownerDocument.documentElement;
+ scrollX = rootNode.scrollLeft || docElm.scrollLeft;
+ scrollY = rootNode.scrollTop || docElm.scrollTop;
+ } else {
+ rootRect = rootNode.getBoundingClientRect();
+ scrollX = rootNode.scrollLeft - rootRect.left;
+ scrollY = rootNode.scrollTop - rootRect.top;
+ }
+
+ clientRect.left += scrollX;
+ clientRect.right += scrollX;
+ clientRect.top += scrollY;
+ clientRect.bottom += scrollY;
+ clientRect.width = 1;
+
+ margin = node.offsetWidth - node.clientWidth;
+
+ if (margin > 0) {
+ if (before) {
+ margin *= -1;
+ }
+
+ clientRect.left += margin;
+ clientRect.right += margin;
+ }
+
+ return clientRect;
+ }
+
+ function trimInlineCaretContainers() {
+ var contentEditableFalseNodes, node, sibling, i, data;
+
+ contentEditableFalseNodes = $('*[contentEditable=false]', rootNode);
+ for (i = 0; i < contentEditableFalseNodes.length; i++) {
+ node = contentEditableFalseNodes[i];
+
+ sibling = node.previousSibling;
+ if (CaretContainer.endsWithCaretContainer(sibling)) {
+ data = sibling.data;
+
+ if (data.length == 1) {
+ sibling.parentNode.removeChild(sibling);
+ } else {
+ sibling.deleteData(data.length - 1, 1);
+ }
+ }
+
+ sibling = node.nextSibling;
+ if (CaretContainer.startsWithCaretContainer(sibling)) {
+ data = sibling.data;
+
+ if (data.length == 1) {
+ sibling.parentNode.removeChild(sibling);
+ } else {
+ sibling.deleteData(0, 1);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ function show(before, node) {
+ var clientRect, rng;
+
+ hide();
+
+ if (isBlock(node)) {
+ caretContainerNode = CaretContainer.insertBlock('p', node, before);
+ clientRect = getAbsoluteClientRect(node, before);
+ $(caretContainerNode).css('top', clientRect.top);
+
+ $lastVisualCaret = $('<div class="mce-visual-caret" data-mce-bogus="all"></div>').css(clientRect).appendTo(rootNode);
+
+ if (before) {
+ $lastVisualCaret.addClass('mce-visual-caret-before');
+ }
+
+ startBlink();
+
+ rng = node.ownerDocument.createRange();
+ rng.setStart(caretContainerNode, 0);
+ rng.setEnd(caretContainerNode, 0);
+ } else {
+ caretContainerNode = CaretContainer.insertInline(node, before);
+ rng = node.ownerDocument.createRange();
+
+ if (isContentEditableFalse(caretContainerNode.nextSibling)) {
+ rng.setStart(caretContainerNode, 0);
+ rng.setEnd(caretContainerNode, 0);
+ } else {
+ rng.setStart(caretContainerNode, 1);
+ rng.setEnd(caretContainerNode, 1);
+ }
+
+ return rng;
+ }
+
+ return rng;
+ }
+
+ function hide() {
+ trimInlineCaretContainers();
+
+ if (caretContainerNode) {
+ CaretContainer.remove(caretContainerNode);
+ caretContainerNode = null;
+ }
+
+ if ($lastVisualCaret) {
+ $lastVisualCaret.remove();
+ $lastVisualCaret = null;
+ }
+
+ clearInterval(cursorInterval);
+ }
+
+ function startBlink() {
+ cursorInterval = Delay.setInterval(function() {
+ $('div.mce-visual-caret', rootNode).toggleClass('mce-visual-caret-hidden');
+ }, 500);
+ }
+
+ function destroy() {
+ Delay.clearInterval(cursorInterval);
+ }
+
+ function getCss() {
+ return (
+ '.mce-visual-caret {' +
+ 'position: absolute;' +
+ 'background-color: black;' +
+ 'background-color: currentcolor;' +
+ '}' +
+ '.mce-visual-caret-hidden {' +
+ 'display: none;' +
+ '}' +
+ '*[data-mce-caret] {' +
+ 'position: absolute;' +
+ 'left: -1000px;' +
+ 'right: auto;' +
+ 'top: 0;' +
+ 'margin: 0;' +
+ 'padding: 0;' +
+ '}'
+ );
+ }
+
+ return {
+ show: show,
+ hide: hide,
+ getCss: getCss,
+ destroy: destroy
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/dom/Dimensions.js
+
+/**
+ * Dimensions.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module measures nodes and returns client rects. The client rects has an
+ * extra node property.
+ *
+ * @private
+ * @class tinymce.dom.Dimensions
+ */
+define("tinymce/dom/Dimensions", [
+ "tinymce/util/Arr",
+ "tinymce/dom/NodeType",
+ "tinymce/geom/ClientRect"
+], function(Arr, NodeType, ClientRect) {
+
+ function getClientRects(node) {
+ function toArrayWithNode(clientRects) {
+ return Arr.map(clientRects, function(clientRect) {
+ clientRect = ClientRect.clone(clientRect);
+ clientRect.node = node;
+
+ return clientRect;
+ });
+ }
+
+ if (Arr.isArray(node)) {
+ return Arr.reduce(node, function(result, node) {
+ return result.concat(getClientRects(node));
+ }, []);
+ }
+
+ if (NodeType.isElement(node)) {
+ return toArrayWithNode(node.getClientRects());
+ }
+
+ if (NodeType.isText(node)) {
+ var rng = node.ownerDocument.createRange();
+
+ rng.setStart(node, 0);
+ rng.setEnd(node, node.data.length);
+
+ return toArrayWithNode(rng.getClientRects());
+ }
+ }
+
+ return {
+ /**
+ * Returns the client rects for a specific node.
+ *
+ * @method getClientRects
+ * @param {Array/DOMNode} node Node or array of nodes to get client rects on.
+ * @param {Array} Array of client rects with a extra node property.
+ */
+ getClientRects: getClientRects
+ };
+});
+
+// Included from: js/tinymce/classes/caret/LineWalker.js
+
+/**
+ * LineWalker.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module lets you walk the document line by line
+ * returing nodes and client rects for each line.
+ *
+ * @private
+ * @class tinymce.caret.LineWalker
+ */
+define("tinymce/caret/LineWalker", [
+ "tinymce/util/Fun",
+ "tinymce/util/Arr",
+ "tinymce/dom/Dimensions",
+ "tinymce/caret/CaretCandidate",
+ "tinymce/caret/CaretUtils",
+ "tinymce/caret/CaretWalker",
+ "tinymce/caret/CaretPosition",
+ "tinymce/geom/ClientRect"
+], function(Fun, Arr, Dimensions, CaretCandidate, CaretUtils, CaretWalker, CaretPosition, ClientRect) {
+ var curry = Fun.curry;
+
+ function findUntil(direction, rootNode, predicateFn, node) {
+ while ((node = CaretUtils.findNode(node, direction, CaretCandidate.isEditableCaretCandidate, rootNode))) {
+ if (predicateFn(node)) {
+ return;
+ }
+ }
+ }
+
+ function walkUntil(direction, isAboveFn, isBeflowFn, rootNode, predicateFn, caretPosition) {
+ var line = 0, node, result = [], targetClientRect;
+
+ function add(node) {
+ var i, clientRect, clientRects;
+
+ clientRects = Dimensions.getClientRects(node);
+ if (direction == -1) {
+ clientRects = clientRects.reverse();
+ }
+
+ for (i = 0; i < clientRects.length; i++) {
+ clientRect = clientRects[i];
+ if (isBeflowFn(clientRect, targetClientRect)) {
+ continue;
+ }
+
+ if (result.length > 0 && isAboveFn(clientRect, Arr.last(result))) {
+ line++;
+ }
+
+ clientRect.line = line;
+
+ if (predicateFn(clientRect)) {
+ return true;
+ }
+
+ result.push(clientRect);
+ }
+ }
+
+ targetClientRect = Arr.last(caretPosition.getClientRects());
+ if (!targetClientRect) {
+ return result;
+ }
+
+ node = caretPosition.getNode();
+ add(node);
+ findUntil(direction, rootNode, add, node);
+
+ return result;
+ }
+
+ function aboveLineNumber(lineNumber, clientRect) {
+ return clientRect.line > lineNumber;
+ }
+
+ function isLine(lineNumber, clientRect) {
+ return clientRect.line === lineNumber;
+ }
+
+ var upUntil = curry(walkUntil, -1, ClientRect.isAbove, ClientRect.isBelow);
+ var downUntil = curry(walkUntil, 1, ClientRect.isBelow, ClientRect.isAbove);
+
+ function positionsUntil(direction, rootNode, predicateFn, node) {
+ var caretWalker = new CaretWalker(rootNode), walkFn, isBelowFn, isAboveFn,
+ caretPosition, result = [], line = 0, clientRect, targetClientRect;
+
+ function getClientRect(caretPosition) {
+ if (direction == 1) {
+ return Arr.last(caretPosition.getClientRects());
+ }
+
+ return Arr.last(caretPosition.getClientRects());
+ }
+
+ if (direction == 1) {
+ walkFn = caretWalker.next;
+ isBelowFn = ClientRect.isBelow;
+ isAboveFn = ClientRect.isAbove;
+ caretPosition = CaretPosition.after(node);
+ } else {
+ walkFn = caretWalker.prev;
+ isBelowFn = ClientRect.isAbove;
+ isAboveFn = ClientRect.isBelow;
+ caretPosition = CaretPosition.before(node);
+ }
+
+ targetClientRect = getClientRect(caretPosition);
+
+ do {
+ if (!caretPosition.isVisible()) {
+ continue;
+ }
+
+ clientRect = getClientRect(caretPosition);
+
+ if (isAboveFn(clientRect, targetClientRect)) {
+ continue;
+ }
+
+ if (result.length > 0 && isBelowFn(clientRect, Arr.last(result))) {
+ line++;
+ }
+
+ clientRect = ClientRect.clone(clientRect);
+ clientRect.position = caretPosition;
+ clientRect.line = line;
+
+ if (predicateFn(clientRect)) {
+ return result;
+ }
+
+ result.push(clientRect);
+ } while ((caretPosition = walkFn(caretPosition)));
+
+ return result;
+ }
+
+ return {
+ upUntil: upUntil,
+ downUntil: downUntil,
+
+ /**
+ * Find client rects with line and caret position until the predicate returns true.
+ *
+ * @method positionsUntil
+ * @param {Number} direction Direction forward/backward 1/-1.
+ * @param {DOMNode} rootNode Root node to walk within.
+ * @param {function} predicateFn Gets the client rect as it's input.
+ * @param {DOMNode} node Node to start walking from.
+ * @return {Array} Array of client rects with line and position properties.
+ */
+ positionsUntil: positionsUntil,
+
+ isAboveLine: curry(aboveLineNumber),
+ isLine: curry(isLine)
+ };
+});
+
+// Included from: js/tinymce/classes/caret/LineUtils.js
+
+/**
+ * LineUtils.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Utility functions for working with lines.
+ *
+ * @private
+ * @class tinymce.caret.LineUtils
+ */
+define("tinymce/caret/LineUtils", [
+ "tinymce/util/Fun",
+ "tinymce/util/Arr",
+ "tinymce/dom/NodeType",
+ "tinymce/dom/Dimensions",
+ "tinymce/geom/ClientRect",
+ "tinymce/caret/CaretUtils",
+ "tinymce/caret/CaretCandidate"
+], function(Fun, Arr, NodeType, Dimensions, ClientRect, CaretUtils, CaretCandidate) {
+ var isContentEditableFalse = NodeType.isContentEditableFalse,
+ findNode = CaretUtils.findNode,
+ curry = Fun.curry;
+
+ function distanceToRectLeft(clientRect, clientX) {
+ return Math.abs(clientRect.left - clientX);
+ }
+
+ function distanceToRectRight(clientRect, clientX) {
+ return Math.abs(clientRect.right - clientX);
+ }
+
+ function findClosestClientRect(clientRects, clientX) {
+ function isInside(clientX, clientRect) {
+ return clientX >= clientRect.left && clientX <= clientRect.right;
+ }
+
+ return Arr.reduce(clientRects, function(oldClientRect, clientRect) {
+ var oldDistance, newDistance;
+
+ oldDistance = Math.min(distanceToRectLeft(oldClientRect, clientX), distanceToRectRight(oldClientRect, clientX));
+ newDistance = Math.min(distanceToRectLeft(clientRect, clientX), distanceToRectRight(clientRect, clientX));
+
+ if (isInside(clientX, clientRect)) {
+ return clientRect;
+ }
+
+ if (isInside(clientX, oldClientRect)) {
+ return oldClientRect;
+ }
+
+ // cE=false has higher priority
+ if (newDistance == oldDistance && isContentEditableFalse(clientRect.node)) {
+ return clientRect;
+ }
+
+ if (newDistance < oldDistance) {
+ return clientRect;
+ }
+
+ return oldClientRect;
+ });
+ }
+
+ function walkUntil(direction, rootNode, predicateFn, node) {
+ while ((node = findNode(node, direction, CaretCandidate.isEditableCaretCandidate, rootNode))) {
+ if (predicateFn(node)) {
+ return;
+ }
+ }
+ }
+
+ function findLineNodeRects(rootNode, targetNodeRect) {
+ var clientRects = [];
+
+ function collect(checkPosFn, node) {
+ var lineRects;
+
+ lineRects = Arr.filter(Dimensions.getClientRects(node), function(clientRect) {
+ return !checkPosFn(clientRect, targetNodeRect);
+ });
+
+ clientRects = clientRects.concat(lineRects);
+
+ return lineRects.length === 0;
+ }
+
+ clientRects.push(targetNodeRect);
+ walkUntil(-1, rootNode, curry(collect, ClientRect.isAbove), targetNodeRect.node);
+ walkUntil(1, rootNode, curry(collect, ClientRect.isBelow), targetNodeRect.node);
+
+ return clientRects;
+ }
+
+ function getContentEditableFalseChildren(rootNode) {
+ return Arr.filter(Arr.toArray(rootNode.getElementsByTagName('*')), isContentEditableFalse);
+ }
+
+ function caretInfo(clientRect, clientX) {
+ return {
+ node: clientRect.node,
+ before: distanceToRectLeft(clientRect, clientX) < distanceToRectRight(clientRect, clientX)
+ };
+ }
+
+ function closestCaret(rootNode, clientX, clientY) {
+ var contentEditableFalseNodeRects, closestNodeRect;
+
+ contentEditableFalseNodeRects = Dimensions.getClientRects(getContentEditableFalseChildren(rootNode));
+ contentEditableFalseNodeRects = Arr.filter(contentEditableFalseNodeRects, function(clientRect) {
+ return clientY >= clientRect.top && clientY <= clientRect.bottom;
+ });
+
+ closestNodeRect = findClosestClientRect(contentEditableFalseNodeRects, clientX);
+ if (closestNodeRect) {
+ closestNodeRect = findClosestClientRect(findLineNodeRects(rootNode, closestNodeRect), clientX);
+ if (closestNodeRect && isContentEditableFalse(closestNodeRect.node)) {
+ return caretInfo(closestNodeRect, clientX);
+ }
+ }
+
+ return null;
+ }
+
+ return {
+ findClosestClientRect: findClosestClientRect,
+ findLineNodeRects: findLineNodeRects,
+ closestCaret: closestCaret
+ };
+});
+
+// Included from: js/tinymce/classes/dom/MousePosition.js
+
+/**
+ * MousePosition.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module calculates an absolute coordinate inside the editor body for both local and global mouse events.
+ *
+ * @private
+ * @class tinymce.dom.MousePosition
+ */
+define("tinymce/dom/MousePosition", [
+], function() {
+ var getAbsolutePosition = function (elm) {
+ var doc, docElem, win, clientRect;
+
+ clientRect = elm.getBoundingClientRect();
+ doc = elm.ownerDocument;
+ docElem = doc.documentElement;
+ win = doc.defaultView;
+
+ return {
+ top: clientRect.top + win.pageYOffset - docElem.clientTop,
+ left: clientRect.left + win.pageXOffset - docElem.clientLeft
+ };
+ };
+
+ var getBodyPosition = function (editor) {
+ return editor.inline ? getAbsolutePosition(editor.getBody()) : {left: 0, top: 0};
+ };
+
+ var getScrollPosition = function (editor) {
+ var body = editor.getBody();
+ return editor.inline ? {left: body.scrollLeft, top: body.scrollTop} : {left: 0, top: 0};
+ };
+
+ var getBodyScroll = function (editor) {
+ var body = editor.getBody(), docElm = editor.getDoc().documentElement;
+ var inlineScroll = {left: body.scrollLeft, top: body.scrollTop};
+ var iframeScroll = {left: body.scrollLeft || docElm.scrollLeft, top: body.scrollTop || docElm.scrollTop};
+
+ return editor.inline ? inlineScroll : iframeScroll;
+ };
+
+ var getMousePosition = function (editor, event) {
+ if (event.target.ownerDocument !== editor.getDoc()) {
+ var iframePosition = getAbsolutePosition(editor.getContentAreaContainer());
+ var scrollPosition = getBodyScroll(editor);
+
+ return {
+ left: event.pageX - iframePosition.left + scrollPosition.left,
+ top: event.pageY - iframePosition.top + scrollPosition.top
+ };
+ }
+
+ return {
+ left: event.pageX,
+ top: event.pageY
+ };
+ };
+
+ var calculatePosition = function (bodyPosition, scrollPosition, mousePosition) {
+ return {
+ pageX: (mousePosition.left - bodyPosition.left) + scrollPosition.left,
+ pageY: (mousePosition.top - bodyPosition.top) + scrollPosition.top
+ };
+ };
+
+ var calc = function (editor, event) {
+ return calculatePosition(getBodyPosition(editor), getScrollPosition(editor), getMousePosition(editor, event));
+ };
+
+ return {
+ calc: calc
+ };
+});
+
+// Included from: js/tinymce/classes/DragDropOverrides.js
+
+/**
+ * DragDropOverrides.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module contains logic overriding the drag/drop logic of the editor.
+ *
+ * @private
+ * @class tinymce.DragDropOverrides
+ */
+define("tinymce/DragDropOverrides", [
+ "tinymce/dom/NodeType",
+ "tinymce/util/Arr",
+ "tinymce/util/Fun",
+ "tinymce/util/Delay",
+ "tinymce/dom/DOMUtils",
+ "tinymce/dom/MousePosition"
+], function(
+ NodeType, Arr, Fun, Delay, DOMUtils, MousePosition
+) {
+ var isContentEditableFalse = NodeType.isContentEditableFalse,
+ isContentEditableTrue = NodeType.isContentEditableTrue;
+
+ var isDraggable = function (elm) {
+ return isContentEditableFalse(elm);
+ };
+
+ var isValidDropTarget = function (editor, targetElement, dragElement) {
+ if (targetElement === dragElement || editor.dom.isChildOf(targetElement, dragElement)) {
+ return false;
+ }
+
+ if (isContentEditableFalse(targetElement)) {
+ return false;
+ }
+
+ return true;
+ };
+
+ var cloneElement = function (elm) {
+ var cloneElm = elm.cloneNode(true);
+ cloneElm.removeAttribute('data-mce-selected');
+ return cloneElm;
+ };
+
+ var createGhost = function (editor, elm, width, height) {
+ var clonedElm = elm.cloneNode(true);
+
+ editor.dom.setStyles(clonedElm, {width: width, height: height});
+ editor.dom.setAttrib(clonedElm, 'data-mce-selected', null);
+
+ var ghostElm = editor.dom.create('div', {
+ 'class': 'mce-drag-container',
+ 'data-mce-bogus': 'all',
+ unselectable: 'on',
+ contenteditable: 'false'
+ });
+
+ editor.dom.setStyles(ghostElm, {
+ position: 'absolute',
+ opacity: 0.5,
+ overflow: 'hidden',
+ border: 0,
+ padding: 0,
+ margin: 0,
+ width: width,
+ height: height
+ });
+
+ editor.dom.setStyles(clonedElm, {
+ margin: 0,
+ boxSizing: 'border-box'
+ });
+
+ ghostElm.appendChild(clonedElm);
+
+ return ghostElm;
+ };
+
+ var appendGhostToBody = function (ghostElm, bodyElm) {
+ if (ghostElm.parentNode !== bodyElm) {
+ bodyElm.appendChild(ghostElm);
+ }
+ };
+
+ var moveGhost = function (ghostElm, position, width, height, maxX, maxY) {
+ var overflowX = 0, overflowY = 0;
+
+ ghostElm.style.left = position.pageX + 'px';
+ ghostElm.style.top = position.pageY + 'px';
+
+ if (position.pageX + width > maxX) {
+ overflowX = (position.pageX + width) - maxX;
+ }
+
+ if (position.pageY + height > maxY) {
+ overflowY = (position.pageY + height) - maxY;
+ }
+
+ ghostElm.style.width = (width - overflowX) + 'px';
+ ghostElm.style.height = (height - overflowY) + 'px';
+ };
+
+ var removeElement = function (elm) {
+ if (elm && elm.parentNode) {
+ elm.parentNode.removeChild(elm);
+ }
+ };
+
+ var isLeftMouseButtonPressed = function (e) {
+ return e.button === 0;
+ };
+
+ var hasDraggableElement = function (state) {
+ return state.element;
+ };
+
+ var applyRelPos = function (state, position) {
+ return {
+ pageX: position.pageX - state.relX,
+ pageY: position.pageY + 5
+ };
+ };
+
+ var start = function (state, editor) {
+ return function (e) {
+ if (isLeftMouseButtonPressed(e)) {
+ var ceElm = Arr.find(editor.dom.getParents(e.target), Fun.or(isContentEditableFalse, isContentEditableTrue));
+
+ if (isDraggable(ceElm)) {
+ var elmPos = editor.dom.getPos(ceElm);
+ var bodyElm = editor.getBody();
+ var docElm = editor.getDoc().documentElement;
+
+ state.element = ceElm;
+ state.screenX = e.screenX;
+ state.screenY = e.screenY;
+ state.maxX = (editor.inline ? bodyElm.scrollWidth : docElm.offsetWidth) - 2;
+ state.maxY = (editor.inline ? bodyElm.scrollHeight : docElm.offsetHeight) - 2;
+ state.relX = e.pageX - elmPos.x;
+ state.relY = e.pageY - elmPos.y;
+ state.width = ceElm.offsetWidth;
+ state.height = ceElm.offsetHeight;
+ state.ghost = createGhost(editor, ceElm, state.width, state.height);
+ }
+ }
+ };
+ };
+
+ var move = function (state, editor) {
+ // Reduces laggy drag behavior on Gecko
+ var throttledPlaceCaretAt = Delay.throttle(function (clientX, clientY) {
+ editor._selectionOverrides.hideFakeCaret();
+ editor.selection.placeCaretAt(clientX, clientY);
+ }, 0);
+
+ return function (e) {
+ var movement = Math.max(Math.abs(e.screenX - state.screenX), Math.abs(e.screenY - state.screenY));
+
+ if (hasDraggableElement(state) && !state.dragging && movement > 10) {
+ var args = editor.fire('dragstart', {target: state.element});
+ if (args.isDefaultPrevented()) {
+ return;
+ }
+
+ state.dragging = true;
+ editor.focus();
+ }
+
+ if (state.dragging) {
+ var targetPos = applyRelPos(state, MousePosition.calc(editor, e));
+
+ appendGhostToBody(state.ghost, editor.getBody());
+ moveGhost(state.ghost, targetPos, state.width, state.height, state.maxX, state.maxY);
+
+ throttledPlaceCaretAt(e.clientX, e.clientY);
+ }
+ };
+ };
+
+ // Returns the raw element instead of the fake cE=false element
+ var getRawTarget = function (selection) {
+ var rng = selection.getSel().getRangeAt(0);
+ var startContainer = rng.startContainer;
+ return startContainer.nodeType === 3 ? startContainer.parentNode : startContainer;
+ };
+
+ var drop = function (state, editor) {
+ return function (e) {
+ if (state.dragging) {
+ if (isValidDropTarget(editor, getRawTarget(editor.selection), state.element)) {
+ var targetClone = cloneElement(state.element);
+
+ var args = editor.fire('drop', {
+ targetClone: targetClone,
+ clientX: e.clientX,
+ clientY: e.clientY
+ });
+
+ if (!args.isDefaultPrevented()) {
+ targetClone = args.targetClone;
+
+ editor.undoManager.transact(function() {
+ removeElement(state.element);
+ editor.insertContent(editor.dom.getOuterHTML(targetClone));
+ editor._selectionOverrides.hideFakeCaret();
+ });
+ }
+ }
+ }
+
+ removeDragState(state);
+ };
+ };
+
+ var stop = function (state, editor) {
+ return function () {
+ removeDragState(state);
+ if (state.dragging) {
+ editor.fire('dragend');
+ }
+ };
+ };
+
+ var removeDragState = function (state) {
+ state.dragging = false;
+ state.element = null;
+ removeElement(state.ghost);
+ };
+
+ var bindFakeDragEvents = function (editor) {
+ var state = {}, pageDom, dragStartHandler, dragHandler, dropHandler, dragEndHandler, rootDocument;
+
+ pageDom = DOMUtils.DOM;
+ rootDocument = document;
+ dragStartHandler = start(state, editor);
+ dragHandler = move(state, editor);
+ dropHandler = drop(state, editor);
+ dragEndHandler = stop(state, editor);
+
+ editor.on('mousedown', dragStartHandler);
+ editor.on('mousemove', dragHandler);
+ editor.on('mouseup', dropHandler);
+
+ pageDom.bind(rootDocument, 'mousemove', dragHandler);
+ pageDom.bind(rootDocument, 'mouseup', dragEndHandler);
+
+ editor.on('remove', function () {
+ pageDom.unbind(rootDocument, 'mousemove', dragHandler);
+ pageDom.unbind(rootDocument, 'mouseup', dragEndHandler);
+ });
+ };
+
+ var blockIeDrop = function (editor) {
+ editor.on('drop', function(e) {
+ // FF doesn't pass out clientX/clientY for drop since this is for IE we just use null instead
+ var realTarget = typeof e.clientX !== 'undefined' ? editor.getDoc().elementFromPoint(e.clientX, e.clientY) : null;
+
+ if (isContentEditableFalse(realTarget) || isContentEditableFalse(editor.dom.getContentEditableParent(realTarget))) {
+ e.preventDefault();
+ }
+ });
+ };
+
+ var init = function (editor) {
+ bindFakeDragEvents(editor);
+ blockIeDrop(editor);
+ };
+
+ return {
+ init: init
+ };
+});
+
+// Included from: js/tinymce/classes/SelectionOverrides.js
+
+/**
+ * SelectionOverrides.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module contains logic overriding the selection with keyboard/mouse
+ * around contentEditable=false regions.
+ *
+ * @example
+ * // Disable the default cE=false selection
+ * tinymce.activeEditor.on('ShowCaret BeforeObjectSelected', function(e) {
+ * e.preventDefault();
+ * });
+ *
+ * @private
+ * @class tinymce.SelectionOverrides
+ */
+define("tinymce/SelectionOverrides", [
+ "tinymce/Env",
+ "tinymce/caret/CaretWalker",
+ "tinymce/caret/CaretPosition",
+ "tinymce/caret/CaretContainer",
+ "tinymce/caret/CaretUtils",
+ "tinymce/caret/FakeCaret",
+ "tinymce/caret/LineWalker",
+ "tinymce/caret/LineUtils",
+ "tinymce/dom/NodeType",
+ "tinymce/dom/RangeUtils",
+ "tinymce/geom/ClientRect",
+ "tinymce/util/VK",
+ "tinymce/util/Fun",
+ "tinymce/util/Arr",
+ "tinymce/util/Delay",
+ "tinymce/DragDropOverrides"
+], function(
+ Env, CaretWalker, CaretPosition, CaretContainer, CaretUtils, FakeCaret, LineWalker,
+ LineUtils, NodeType, RangeUtils, ClientRect, VK, Fun, Arr, Delay, DragDropOverrides
+) {
+ var curry = Fun.curry,
+ isContentEditableTrue = NodeType.isContentEditableTrue,
+ isContentEditableFalse = NodeType.isContentEditableFalse,
+ isElement = NodeType.isElement,
+ isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse,
+ isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse,
+ getSelectedNode = RangeUtils.getSelectedNode;
+
+ function getVisualCaretPosition(walkFn, caretPosition) {
+ while ((caretPosition = walkFn(caretPosition))) {
+ if (caretPosition.isVisible()) {
+ return caretPosition;
+ }
+ }
+
+ return caretPosition;
+ }
+
+ function SelectionOverrides(editor) {
+ var rootNode = editor.getBody(), caretWalker = new CaretWalker(rootNode);
+ var getNextVisualCaretPosition = curry(getVisualCaretPosition, caretWalker.next);
+ var getPrevVisualCaretPosition = curry(getVisualCaretPosition, caretWalker.prev),
+ fakeCaret = new FakeCaret(editor.getBody(), isBlock),
+ realSelectionId = 'sel-' + editor.dom.uniqueId(),
+ selectedContentEditableNode, $ = editor.$;
+
+ function isFakeSelectionElement(elm) {
+ return editor.dom.hasClass(elm, 'mce-offscreen-selection');
+ }
+
+ function getRealSelectionElement() {
+ var container = editor.dom.get(realSelectionId);
+ return container ? container.getElementsByTagName('*')[0] : container;
+ }
+
+ function isBlock(node) {
+ return editor.dom.isBlock(node);
+ }
+
+ function setRange(range) {
+ //console.log('setRange', range);
+ if (range) {
+ editor.selection.setRng(range);
+ }
+ }
+
+ function getRange() {
+ return editor.selection.getRng();
+ }
+
+ function scrollIntoView(node, alignToTop) {
+ editor.selection.scrollIntoView(node, alignToTop);
+ }
+
+ function showCaret(direction, node, before) {
+ var e;
+
+ e = editor.fire('ShowCaret', {
+ target: node,
+ direction: direction,
+ before: before
+ });
+
+ if (e.isDefaultPrevented()) {
+ return null;
+ }
+
+ scrollIntoView(node, direction === -1);
+
+ return fakeCaret.show(before, node);
+ }
+
+ function selectNode(node) {
+ var e;
+
+ e = editor.fire('BeforeObjectSelected', {target: node});
+ if (e.isDefaultPrevented()) {
+ return null;
+ }
+
+ return getNodeRange(node);
+ }
+
+ function getNodeRange(node) {
+ var rng = node.ownerDocument.createRange();
+
+ rng.selectNode(node);
+
+ return rng;
+ }
+
+ function isMoveInsideSameBlock(fromCaretPosition, toCaretPosition) {
+ var inSameBlock = CaretUtils.isInSameBlock(fromCaretPosition, toCaretPosition);
+
+ // Handle bogus BR <p>abc|<br></p>
+ if (!inSameBlock && NodeType.isBr(fromCaretPosition.getNode())) {
+ return true;
+ }
+
+ return inSameBlock;
+ }
+
+ function getNormalizedRangeEndPoint(direction, range) {
+ range = CaretUtils.normalizeRange(direction, rootNode, range);
+
+ if (direction == -1) {
+ return CaretPosition.fromRangeStart(range);
+ }
+
+ return CaretPosition.fromRangeEnd(range);
+ }
+
+ function isRangeInCaretContainerBlock(range) {
+ return CaretContainer.isCaretContainerBlock(range.startContainer);
+ }
+
+ function moveToCeFalseHorizontally(direction, getNextPosFn, isBeforeContentEditableFalseFn, range) {
+ var node, caretPosition, peekCaretPosition, rangeIsInContainerBlock;
+
+ if (!range.collapsed) {
+ node = getSelectedNode(range);
+ if (isContentEditableFalse(node)) {
+ return showCaret(direction, node, direction == -1);
+ }
+ }
+
+ rangeIsInContainerBlock = isRangeInCaretContainerBlock(range);
+ caretPosition = getNormalizedRangeEndPoint(direction, range);
+
+ if (isBeforeContentEditableFalseFn(caretPosition)) {
+ return selectNode(caretPosition.getNode(direction == -1));
+ }
+
+ caretPosition = getNextPosFn(caretPosition);
+ if (!caretPosition) {
+ if (rangeIsInContainerBlock) {
+ return range;
+ }
+
+ return null;
+ }
+
+ if (isBeforeContentEditableFalseFn(caretPosition)) {
+ return showCaret(direction, caretPosition.getNode(direction == -1), direction == 1);
+ }
+
+ // Peek ahead for handling of ab|c<span cE=false> -> abc|<span cE=false>
+ peekCaretPosition = getNextPosFn(caretPosition);
+ if (isBeforeContentEditableFalseFn(peekCaretPosition)) {
+ if (isMoveInsideSameBlock(caretPosition, peekCaretPosition)) {
+ return showCaret(direction, peekCaretPosition.getNode(direction == -1), direction == 1);
+ }
+ }
+
+ if (rangeIsInContainerBlock) {
+ return renderRangeCaret(caretPosition.toRange());
+ }
+
+ return null;
+ }
+
+ function moveToCeFalseVertically(direction, walkerFn, range) {
+ var caretPosition, linePositions, nextLinePositions,
+ closestNextLineRect, caretClientRect, clientX,
+ dist1, dist2, contentEditableFalseNode;
+
+ contentEditableFalseNode = getSelectedNode(range);
+ caretPosition = getNormalizedRangeEndPoint(direction, range);
+ linePositions = walkerFn(rootNode, LineWalker.isAboveLine(1), caretPosition);
+ nextLinePositions = Arr.filter(linePositions, LineWalker.isLine(1));
+ caretClientRect = Arr.last(caretPosition.getClientRects());
+
+ if (isBeforeContentEditableFalse(caretPosition)) {
+ contentEditableFalseNode = caretPosition.getNode();
+ }
+
+ if (isAfterContentEditableFalse(caretPosition)) {
+ contentEditableFalseNode = caretPosition.getNode(true);
+ }
+
+ if (!caretClientRect) {
+ return null;
+ }
+
+ clientX = caretClientRect.left;
+
+ closestNextLineRect = LineUtils.findClosestClientRect(nextLinePositions, clientX);
+ if (closestNextLineRect) {
+ if (isContentEditableFalse(closestNextLineRect.node)) {
+ dist1 = Math.abs(clientX - closestNextLineRect.left);
+ dist2 = Math.abs(clientX - closestNextLineRect.right);
+
+ return showCaret(direction, closestNextLineRect.node, dist1 < dist2);
+ }
+ }
+
+ if (contentEditableFalseNode) {
+ var caretPositions = LineWalker.positionsUntil(direction, rootNode, LineWalker.isAboveLine(1), contentEditableFalseNode);
+
+ closestNextLineRect = LineUtils.findClosestClientRect(Arr.filter(caretPositions, LineWalker.isLine(1)), clientX);
+ if (closestNextLineRect) {
+ return renderRangeCaret(closestNextLineRect.position.toRange());
+ }
+
+ closestNextLineRect = Arr.last(Arr.filter(caretPositions, LineWalker.isLine(0)));
+ if (closestNextLineRect) {
+ return renderRangeCaret(closestNextLineRect.position.toRange());
+ }
+ }
+ }
+
+ function exitPreBlock(direction, range) {
+ var pre, caretPos, newBlock;
+
+ function createTextBlock() {
+ var textBlock = editor.dom.create(editor.settings.forced_root_block);
+
+ if (!Env.ie || Env.ie >= 11) {
+ textBlock.innerHTML = '<br data-mce-bogus="1">';
+ }
+
+ return textBlock;
+ }
+
+ if (range.collapsed && editor.settings.forced_root_block) {
+ pre = editor.dom.getParent(range.startContainer, 'PRE');
+ if (!pre) {
+ return;
+ }
+
+ if (direction == 1) {
+ caretPos = getNextVisualCaretPosition(CaretPosition.fromRangeStart(range));
+ } else {
+ caretPos = getPrevVisualCaretPosition(CaretPosition.fromRangeStart(range));
+ }
+
+ if (!caretPos) {
+ newBlock = createTextBlock();
+
+ if (direction == 1) {
+ editor.$(pre).after(newBlock);
+ } else {
+ editor.$(pre).before(newBlock);
+ }
+
+ editor.selection.select(newBlock, true);
+ editor.selection.collapse();
+ }
+ }
+ }
+
+ function moveH(direction, getNextPosFn, isBeforeContentEditableFalseFn, range) {
+ var newRange;
+
+ newRange = moveToCeFalseHorizontally(direction, getNextPosFn, isBeforeContentEditableFalseFn, range);
+ if (newRange) {
+ return newRange;
+ }
+
+ newRange = exitPreBlock(direction, range);
+ if (newRange) {
+ return newRange;
+ }
+
+ return null;
+ }
+
+ function moveV(direction, walkerFn, range) {
+ var newRange;
+
+ newRange = moveToCeFalseVertically(direction, walkerFn, range);
+ if (newRange) {
+ return newRange;
+ }
+
+ newRange = exitPreBlock(direction, range);
+ if (newRange) {
+ return newRange;
+ }
+
+ return null;
+ }
+
+ function getBlockCaretContainer() {
+ return $('*[data-mce-caret]')[0];
+ }
+
+ function showBlockCaretContainer(blockCaretContainer) {
+ if (blockCaretContainer.hasAttribute('data-mce-caret')) {
+ CaretContainer.showCaretContainerBlock(blockCaretContainer);
+ setRange(getRange()); // Removes control rect on IE
+ scrollIntoView(blockCaretContainer[0]);
+ }
+ }
+
+ function renderCaretAtRange(range) {
+ var caretPosition, ceRoot;
+
+ range = CaretUtils.normalizeRange(1, rootNode, range);
+ caretPosition = CaretPosition.fromRangeStart(range);
+
+ if (isContentEditableFalse(caretPosition.getNode())) {
+ return showCaret(1, caretPosition.getNode(), !caretPosition.isAtEnd());
+ }
+
+ if (isContentEditableFalse(caretPosition.getNode(true))) {
+ return showCaret(1, caretPosition.getNode(true), false);
+ }
+
+ // TODO: Should render caret before/after depending on where you click on the page forces after now
+ ceRoot = editor.dom.getParent(caretPosition.getNode(), Fun.or(isContentEditableFalse, isContentEditableTrue));
+ if (isContentEditableFalse(ceRoot)) {
+ return showCaret(1, ceRoot, false);
+ }
+
+ return null;
+ }
+
+ function renderRangeCaret(range) {
+ var caretRange;
+
+ if (!range || !range.collapsed) {
+ return range;
+ }
+
+ caretRange = renderCaretAtRange(range);
+ if (caretRange) {
+ return caretRange;
+ }
+
+ return range;
+ }
+
+ function deleteContentEditableNode(node) {
+ var nextCaretPosition, prevCaretPosition, prevCeFalseElm, nextElement;
+
+ if (!isContentEditableFalse(node)) {
+ return null;
+ }
+
+ if (isContentEditableFalse(node.previousSibling)) {
+ prevCeFalseElm = node.previousSibling;
+ }
+
+ prevCaretPosition = getPrevVisualCaretPosition(CaretPosition.before(node));
+ if (!prevCaretPosition) {
+ nextCaretPosition = getNextVisualCaretPosition(CaretPosition.after(node));
+ }
+
+ if (nextCaretPosition && isElement(nextCaretPosition.getNode())) {
+ nextElement = nextCaretPosition.getNode();
+ }
+
+ CaretContainer.remove(node.previousSibling);
+ CaretContainer.remove(node.nextSibling);
+ editor.dom.remove(node);
+
+ if (editor.dom.isEmpty(editor.getBody())) {
+ editor.setContent('');
+ editor.focus();
+ return;
+ }
+
+ if (prevCeFalseElm) {
+ return CaretPosition.after(prevCeFalseElm).toRange();
+ }
+
+ if (nextElement) {
+ return CaretPosition.before(nextElement).toRange();
+ }
+
+ if (prevCaretPosition) {
+ return prevCaretPosition.toRange();
+ }
+
+ if (nextCaretPosition) {
+ return nextCaretPosition.toRange();
+ }
+
+ return null;
+ }
+
+ function isTextBlock(node) {
+ var textBlocks = editor.schema.getTextBlockElements();
+ return node.nodeName in textBlocks;
+ }
+
+ function isEmpty(elm) {
+ return editor.dom.isEmpty(elm);
+ }
+
+ function mergeTextBlocks(direction, fromCaretPosition, toCaretPosition) {
+ var dom = editor.dom, fromBlock, toBlock, node, ceTarget;
+
+ fromBlock = dom.getParent(fromCaretPosition.getNode(), dom.isBlock);
+ toBlock = dom.getParent(toCaretPosition.getNode(), dom.isBlock);
+
+ if (direction === -1) {
+ ceTarget = toCaretPosition.getNode(true);
+ if (isAfterContentEditableFalse(toCaretPosition) && isBlock(ceTarget)) {
+ if (isTextBlock(fromBlock)) {
+ if (isEmpty(fromBlock)) {
+ dom.remove(fromBlock);
+ }
+
+ return CaretPosition.after(ceTarget).toRange();
+ }
+
+ return deleteContentEditableNode(toCaretPosition.getNode(true));
+ }
+ } else {
+ ceTarget = fromCaretPosition.getNode();
+ if (isBeforeContentEditableFalse(fromCaretPosition) && isBlock(ceTarget)) {
+ if (isTextBlock(toBlock)) {
+ if (isEmpty(toBlock)) {
+ dom.remove(toBlock);
+ }
+
+ return CaretPosition.before(ceTarget).toRange();
+ }
+
+ return deleteContentEditableNode(fromCaretPosition.getNode());
+ }
+ }
+
+ // Verify that both blocks are text blocks
+ if (fromBlock === toBlock || !isTextBlock(fromBlock) || !isTextBlock(toBlock)) {
+ return null;
+ }
+
+ while ((node = fromBlock.firstChild)) {
+ toBlock.appendChild(node);
+ }
+
+ editor.dom.remove(fromBlock);
+
+ return toCaretPosition.toRange();
+ }
+
+ function backspaceDelete(direction, beforeFn, afterFn, range) {
+ var node, caretPosition, peekCaretPosition, newCaretPosition;
+
+ if (!range.collapsed) {
+ node = getSelectedNode(range);
+ if (isContentEditableFalse(node)) {
+ return renderRangeCaret(deleteContentEditableNode(node));
+ }
+ }
+
+ caretPosition = getNormalizedRangeEndPoint(direction, range);
+
+ if (afterFn(caretPosition) && CaretContainer.isCaretContainerBlock(range.startContainer)) {
+ newCaretPosition = direction == -1 ? caretWalker.prev(caretPosition) : caretWalker.next(caretPosition);
+ return newCaretPosition ? renderRangeCaret(newCaretPosition.toRange()) : range;
+ }
+
+ if (beforeFn(caretPosition)) {
+ return renderRangeCaret(deleteContentEditableNode(caretPosition.getNode(direction == -1)));
+ }
+
+ peekCaretPosition = direction == -1 ? caretWalker.prev(caretPosition) : caretWalker.next(caretPosition);
+ if (beforeFn(peekCaretPosition)) {
+ if (direction === -1) {
+ return mergeTextBlocks(direction, caretPosition, peekCaretPosition);
+ }
+
+ return mergeTextBlocks(direction, peekCaretPosition, caretPosition);
+ }
+ }
+
+ function registerEvents() {
+ var right = curry(moveH, 1, getNextVisualCaretPosition, isBeforeContentEditableFalse);
+ var left = curry(moveH, -1, getPrevVisualCaretPosition, isAfterContentEditableFalse);
+ var deleteForward = curry(backspaceDelete, 1, isBeforeContentEditableFalse, isAfterContentEditableFalse);
+ var backspace = curry(backspaceDelete, -1, isAfterContentEditableFalse, isBeforeContentEditableFalse);
+ var up = curry(moveV, -1, LineWalker.upUntil);
+ var down = curry(moveV, 1, LineWalker.downUntil);
+
+ function override(evt, moveFn) {
+ var range = moveFn(getRange());
+
+ if (range && !evt.isDefaultPrevented()) {
+ evt.preventDefault();
+ setRange(range);
+ }
+ }
+
+ function getContentEditableRoot(node) {
+ var root = editor.getBody();
+
+ while (node && node != root) {
+ if (isContentEditableTrue(node) || isContentEditableFalse(node)) {
+ return node;
+ }
+
+ node = node.parentNode;
+ }
+
+ return null;
+ }
+
+ function isXYWithinRange(clientX, clientY, range) {
+ if (range.collapsed) {
+ return false;
+ }
+
+ return Arr.reduce(range.getClientRects(), function(state, rect) {
+ return state || ClientRect.containsXY(rect, clientX, clientY);
+ }, false);
+ }
+
+ // Some browsers (Chrome) lets you place the caret after a cE=false
+ // Make sure we render the caret container in this case
+ editor.on('mouseup', function() {
+ var range = getRange();
+
+ if (range.collapsed) {
+ setRange(renderCaretAtRange(range));
+ }
+ });
+
+ editor.on('click', function(e) {
+ var contentEditableRoot;
+
+ contentEditableRoot = getContentEditableRoot(e.target);
+ if (contentEditableRoot) {
+ // Prevent clicks on links in a cE=false element
+ if (isContentEditableFalse(contentEditableRoot)) {
+ e.preventDefault();
+ editor.focus();
+ }
+
+ // Removes fake selection if a cE=true is clicked within a cE=false like the toc title
+ if (isContentEditableTrue(contentEditableRoot)) {
+ if (editor.dom.isChildOf(contentEditableRoot, editor.selection.getNode())) {
+ removeContentEditableSelection();
+ }
+ }
+ }
+ });
+
+ editor.on('blur NewBlock', function () {
+ removeContentEditableSelection();
+ hideFakeCaret();
+ });
+
+ function handleTouchSelect(editor) {
+ var moved = false;
+
+ editor.on('touchstart', function () {
+ moved = false;
+ });
+
+ editor.on('touchmove', function () {
+ moved = true;
+ });
+
+ editor.on('touchend', function (e) {
+ var contentEditableRoot = getContentEditableRoot(e.target);
+
+ if (isContentEditableFalse(contentEditableRoot)) {
+ if (!moved) {
+ e.preventDefault();
+ setContentEditableSelection(selectNode(contentEditableRoot));
+ }
+ }
+ });
+ }
+
+ var hasNormalCaretPosition = function (elm) {
+ var caretWalker = new CaretWalker(elm);
+
+ if (!elm.firstChild) {
+ return false;
+ }
+
+ var startPos = CaretPosition.before(elm.firstChild);
+ var newPos = caretWalker.next(startPos);
+
+ return newPos && !isBeforeContentEditableFalse(newPos) && !isAfterContentEditableFalse(newPos);
+ };
+
+ var isInSameBlock = function (node1, node2) {
+ var block1 = editor.dom.getParent(node1, editor.dom.isBlock);
+ var block2 = editor.dom.getParent(node2, editor.dom.isBlock);
+ return block1 === block2;
+ };
+
+ var isContentKey = function (e) {
+ if (e.keyCode >= 112 && e.keyCode <= 123) {
+ return false;
+ }
+
+ return true;
+ };
+
+ // Checks if the target node is in a block and if that block has a caret position better than the
+ // suggested caretNode this is to prevent the caret from being sucked in towards a cE=false block if
+ // they are adjacent on the vertical axis
+ var hasBetterMouseTarget = function (targetNode, caretNode) {
+ var targetBlock = editor.dom.getParent(targetNode, editor.dom.isBlock);
+ var caretBlock = editor.dom.getParent(caretNode, editor.dom.isBlock);
+
+ return targetBlock && !isInSameBlock(targetBlock, caretBlock) && hasNormalCaretPosition(targetBlock);
+ };
+
+ handleTouchSelect(editor);
+
+ editor.on('mousedown', function(e) {
+ var contentEditableRoot;
+
+ contentEditableRoot = getContentEditableRoot(e.target);
+ if (contentEditableRoot) {
+ if (isContentEditableFalse(contentEditableRoot)) {
+ e.preventDefault();
+ setContentEditableSelection(selectNode(contentEditableRoot));
+ } else {
+ if (!isXYWithinRange(e.clientX, e.clientY, editor.selection.getRng())) {
+ editor.selection.placeCaretAt(e.clientX, e.clientY);
+ }
+ }
+ } else {
+ // Remove needs to be called here since the mousedown might alter the selection without calling selection.setRng
+ // and therefore not fire the AfterSetSelectionRange event.
+ removeContentEditableSelection();
+ hideFakeCaret();
+
+ var caretInfo = LineUtils.closestCaret(rootNode, e.clientX, e.clientY);
+ if (caretInfo) {
+ if (!hasBetterMouseTarget(e.target, caretInfo.node)) {
+ e.preventDefault();
+ editor.getBody().focus();
+ setRange(showCaret(1, caretInfo.node, caretInfo.before));
+ }
+ }
+ }
+ });
+
+ editor.on('keydown', function(e) {
+ if (VK.modifierPressed(e)) {
+ return;
+ }
+
+ switch (e.keyCode) {
+ case VK.RIGHT:
+ override(e, right);
+ break;
+
+ case VK.DOWN:
+ override(e, down);
+ break;
+
+ case VK.LEFT:
+ override(e, left);
+ break;
+
+ case VK.UP:
+ override(e, up);
+ break;
+
+ case VK.DELETE:
+ override(e, deleteForward);
+ break;
+
+ case VK.BACKSPACE:
+ override(e, backspace);
+ break;
+
+ default:
+ if (isContentEditableFalse(editor.selection.getNode()) && isContentKey(e)) {
+ e.preventDefault();
+ }
+ break;
+ }
+ });
+
+ function paddEmptyContentEditableArea() {
+ var br, ceRoot = getContentEditableRoot(editor.selection.getNode());
+
+ if (isContentEditableTrue(ceRoot) && isBlock(ceRoot) && editor.dom.isEmpty(ceRoot)) {
+ br = editor.dom.create('br', {"data-mce-bogus": "1"});
+ editor.$(ceRoot).empty().append(br);
+ editor.selection.setRng(CaretPosition.before(br).toRange());
+ }
+ }
+
+ function handleBlockContainer(e) {
+ var blockCaretContainer = getBlockCaretContainer();
+
+ if (!blockCaretContainer) {
+ return;
+ }
+
+ if (e.type == 'compositionstart') {
+ e.preventDefault();
+ e.stopPropagation();
+ showBlockCaretContainer(blockCaretContainer);
+ return;
+ }
+
+ if (CaretContainer.hasContent(blockCaretContainer)) {
+ showBlockCaretContainer(blockCaretContainer);
+ }
+ }
+
+ function handleEmptyBackspaceDelete(e) {
+ var prevent;
+
+ switch (e.keyCode) {
+ case VK.DELETE:
+ prevent = paddEmptyContentEditableArea();
+ break;
+
+ case VK.BACKSPACE:
+ prevent = paddEmptyContentEditableArea();
+ break;
+ }
+
+ if (prevent) {
+ e.preventDefault();
+ }
+ }
+
+ // Must be added to "top" since undoManager needs to be executed after
+ editor.on('keyup compositionstart', function(e) {
+ handleBlockContainer(e);
+ handleEmptyBackspaceDelete(e);
+ }, true);
+
+ editor.on('cut', function() {
+ var node = editor.selection.getNode();
+
+ if (isContentEditableFalse(node)) {
+ Delay.setEditorTimeout(editor, function() {
+ setRange(renderRangeCaret(deleteContentEditableNode(node)));
+ });
+ }
+ });
+
+ editor.on('getSelectionRange', function(e) {
+ var rng = e.range;
+
+ if (selectedContentEditableNode) {
+ if (!selectedContentEditableNode.parentNode) {
+ selectedContentEditableNode = null;
+ return;
+ }
+
+ rng = rng.cloneRange();
+ rng.selectNode(selectedContentEditableNode);
+ e.range = rng;
+ }
+ });
+
+ editor.on('setSelectionRange', function(e) {
+ var rng;
+
+ rng = setContentEditableSelection(e.range);
+ if (rng) {
+ e.range = rng;
+ }
+ });
+
+ editor.on('AfterSetSelectionRange', function(e) {
+ var rng = e.range;
+
+ if (!isRangeInCaretContainer(rng)) {
+ hideFakeCaret();
+ }
+
+ if (!isFakeSelectionElement(rng.startContainer.parentNode)) {
+ removeContentEditableSelection();
+ }
+ });
+
+ editor.on('focus', function() {
+ // Make sure we have a proper fake caret on focus
+ Delay.setEditorTimeout(editor, function() {
+ editor.selection.setRng(renderRangeCaret(editor.selection.getRng()));
+ }, 0);
+ });
+
+ editor.on('copy', function (e) {
+ var clipboardData = e.clipboardData;
+
+ // Make sure we get proper html/text for the fake cE=false selection
+ // Doesn't work at all on Edge since it doesn't have proper clipboardData support
+ if (!e.isDefaultPrevented() && e.clipboardData && !Env.ie) {
+ var realSelectionElement = getRealSelectionElement();
+ if (realSelectionElement) {
+ e.preventDefault();
+ clipboardData.clearData();
+ clipboardData.setData('text/html', realSelectionElement.outerHTML);
+ clipboardData.setData('text/plain', realSelectionElement.outerText);
+ }
+ }
+ });
+
+ DragDropOverrides.init(editor);
+ }
+
+ function addCss() {
+ var styles = editor.contentStyles, rootClass = '.mce-content-body';
+
+ styles.push(fakeCaret.getCss());
+ styles.push(
+ rootClass + ' .mce-offscreen-selection {' +
+ 'position: absolute;' +
+ 'left: -9999999999px;' +
+ 'max-width: 1000000px;' +
+ '}' +
+ rootClass + ' *[contentEditable=false] {' +
+ 'cursor: default;' +
+ '}' +
+ rootClass + ' *[contentEditable=true] {' +
+ 'cursor: text;' +
+ '}'
+ );
+ }
+
+ function isRangeInCaretContainer(rng) {
+ return CaretContainer.isCaretContainer(rng.startContainer) || CaretContainer.isCaretContainer(rng.endContainer);
+ }
+
+ function setContentEditableSelection(range) {
+ var node, $ = editor.$, dom = editor.dom, $realSelectionContainer, sel,
+ startContainer, startOffset, endOffset, e, caretPosition, targetClone, origTargetClone;
+
+ if (!range) {
+ return null;
+ }
+
+ if (range.collapsed) {
+ if (!isRangeInCaretContainer(range)) {
+ caretPosition = getNormalizedRangeEndPoint(1, range);
+
+ if (isContentEditableFalse(caretPosition.getNode())) {
+ return showCaret(1, caretPosition.getNode(), !caretPosition.isAtEnd());
+ }
+
+ if (isContentEditableFalse(caretPosition.getNode(true))) {
+ return showCaret(1, caretPosition.getNode(true), false);
+ }
+ }
+
+ return null;
+ }
+
+ startContainer = range.startContainer;
+ startOffset = range.startOffset;
+ endOffset = range.endOffset;
+
+ // Normalizes <span cE=false>[</span>] to [<span cE=false></span>]
+ if (startContainer.nodeType == 3 && startOffset == 0 && isContentEditableFalse(startContainer.parentNode)) {
+ startContainer = startContainer.parentNode;
+ startOffset = dom.nodeIndex(startContainer);
+ startContainer = startContainer.parentNode;
+ }
+
+ if (startContainer.nodeType != 1) {
+ return null;
+ }
+
+ if (endOffset == startOffset + 1) {
+ node = startContainer.childNodes[startOffset];
+ }
+
+ if (!isContentEditableFalse(node)) {
+ return null;
+ }
+
+ targetClone = origTargetClone = node.cloneNode(true);
+ e = editor.fire('ObjectSelected', {target: node, targetClone: targetClone});
+ if (e.isDefaultPrevented()) {
+ return null;
+ }
+
+ targetClone = e.targetClone;
+ $realSelectionContainer = $('#' + realSelectionId);
+ if ($realSelectionContainer.length === 0) {
+ $realSelectionContainer = $(
+ '<div data-mce-bogus="all" class="mce-offscreen-selection"></div>'
+ ).attr('id', realSelectionId);
+
+ $realSelectionContainer.appendTo(editor.getBody());
+ }
+
+ range = editor.dom.createRng();
+
+ // WHY is IE making things so hard! Copy on <i contentEditable="false">x</i> produces: <em>x</em>
+ // This is a ridiculous hack where we place the selection from a block over the inline element
+ // so that just the inline element is copied as is and not converted.
+ if (targetClone === origTargetClone && Env.ie) {
+ $realSelectionContainer.empty().append('<p style="font-size: 0" data-mce-bogus="all">\u00a0</p>').append(targetClone);
+ range.setStartAfter($realSelectionContainer[0].firstChild.firstChild);
+ range.setEndAfter(targetClone);
+ } else {
+ $realSelectionContainer.empty().append('\u00a0').append(targetClone).append('\u00a0');
+ range.setStart($realSelectionContainer[0].firstChild, 1);
+ range.setEnd($realSelectionContainer[0].lastChild, 0);
+ }
+
+ $realSelectionContainer.css({
+ top: dom.getPos(node, editor.getBody()).y
+ });
+
+ $realSelectionContainer[0].focus();
+ sel = editor.selection.getSel();
+ sel.removeAllRanges();
+ sel.addRange(range);
+
+ editor.$('*[data-mce-selected]').removeAttr('data-mce-selected');
+ node.setAttribute('data-mce-selected', 1);
+ selectedContentEditableNode = node;
+
+ return range;
+ }
+
+ function removeContentEditableSelection() {
+ if (selectedContentEditableNode) {
+ selectedContentEditableNode.removeAttribute('data-mce-selected');
+ editor.$('#' + realSelectionId).remove();
+ selectedContentEditableNode = null;
+ }
+ }
+
+ function destroy() {
+ fakeCaret.destroy();
+ selectedContentEditableNode = null;
+ }
+
+ function hideFakeCaret() {
+ fakeCaret.hide();
+ }
+
+ if (Env.ceFalse) {
+ registerEvents();
+ addCss();
+ }
+
+ return {
+ showBlockCaretContainer: showBlockCaretContainer,
+ hideFakeCaret: hideFakeCaret,
+ destroy: destroy
+ };
+ }
+
+ return SelectionOverrides;
+});
+
+// Included from: js/tinymce/classes/util/Uuid.js
+
+/**
+ * Uuid.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Generates unique ids.
+ *
+ * @class tinymce.util.Uuid
+ * @private
+ */
+define("tinymce/util/Uuid", [
+], function() {
+ var count = 0;
+
+ var seed = function () {
+ var rnd = function () {
+ return Math.round(Math.random() * 0xFFFFFFFF).toString(36);
+ };
+
+ var now = new Date().getTime();
+ return 's' + now.toString(36) + rnd() + rnd() + rnd();
+ };
+
+ var uuid = function (prefix) {
+ return prefix + (count++) + seed();
+ };
+
+ return {
+ uuid: uuid
+ };
+});
+
+// Included from: js/tinymce/classes/ui/Sidebar.js
+
+/**
+ * Sidebar.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module handle sidebar instances for the editor.
+ *
+ * @class tinymce.ui.Sidebar
+ * @private
+ */
+define("tinymce/ui/Sidebar", [
+], function(
+) {
+ var add = function (editor, name, settings) {
+ var sidebars = editor.sidebars ? editor.sidebars : [];
+ sidebars.push({name: name, settings: settings});
+ editor.sidebars = sidebars;
+ };
+
+ return {
+ add: add
+ };
+});
+
+// Included from: js/tinymce/classes/Editor.js
+
+/**
+ * Editor.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*jshint scripturl:true */
+
+/**
+ * Include the base event class documentation.
+ *
+ * @include ../../../tools/docs/tinymce.Event.js
+ */
+
+/**
+ * This class contains the core logic for a TinyMCE editor.
+ *
+ * @class tinymce.Editor
+ * @mixes tinymce.util.Observable
+ * @example
+ * // Add a class to all paragraphs in the editor.
+ * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass');
+ *
+ * // Gets the current editors selection as text
+ * tinymce.activeEditor.selection.getContent({format: 'text'});
+ *
+ * // Creates a new editor instance
+ * var ed = new tinymce.Editor('textareaid', {
+ * some_setting: 1
+ * }, tinymce.EditorManager);
+ *
+ * // Select each item the user clicks on
+ * ed.on('click', function(e) {
+ * ed.selection.select(e.target);
+ * });
+ *
+ * ed.render();
+ */
+define("tinymce/Editor", [
+ "tinymce/dom/DOMUtils",
+ "tinymce/dom/DomQuery",
+ "tinymce/AddOnManager",
+ "tinymce/NodeChange",
+ "tinymce/html/Node",
+ "tinymce/dom/Serializer",
+ "tinymce/html/Serializer",
+ "tinymce/dom/Selection",
+ "tinymce/Formatter",
+ "tinymce/UndoManager",
+ "tinymce/EnterKey",
+ "tinymce/ForceBlocks",
+ "tinymce/EditorCommands",
+ "tinymce/util/URI",
+ "tinymce/dom/ScriptLoader",
+ "tinymce/dom/EventUtils",
+ "tinymce/WindowManager",
+ "tinymce/NotificationManager",
+ "tinymce/html/Schema",
+ "tinymce/html/DomParser",
+ "tinymce/util/Quirks",
+ "tinymce/Env",
+ "tinymce/util/Tools",
+ "tinymce/util/Delay",
+ "tinymce/EditorObservable",
+ "tinymce/Mode",
+ "tinymce/Shortcuts",
+ "tinymce/EditorUpload",
+ "tinymce/SelectionOverrides",
+ "tinymce/util/Uuid",
+ "tinymce/ui/Sidebar"
+], function(
+ DOMUtils, DomQuery, AddOnManager, NodeChange, Node, DomSerializer, Serializer,
+ Selection, Formatter, UndoManager, EnterKey, ForceBlocks, EditorCommands,
+ URI, ScriptLoader, EventUtils, WindowManager, NotificationManager,
+ Schema, DomParser, Quirks, Env, Tools, Delay, EditorObservable, Mode, Shortcuts, EditorUpload,
+ SelectionOverrides, Uuid, Sidebar
+) {
+ // Shorten these names
+ var DOM = DOMUtils.DOM, ThemeManager = AddOnManager.ThemeManager, PluginManager = AddOnManager.PluginManager;
+ var extend = Tools.extend, each = Tools.each, explode = Tools.explode;
+ var inArray = Tools.inArray, trim = Tools.trim, resolve = Tools.resolve;
+ var Event = EventUtils.Event;
+ var isGecko = Env.gecko, ie = Env.ie;
+
+ /**
+ * Include documentation for all the events.
+ *
+ * @include ../../../tools/docs/tinymce.Editor.js
+ */
+
+ /**
+ * Constructs a editor instance by id.
+ *
+ * @constructor
+ * @method Editor
+ * @param {String} id Unique id for the editor.
+ * @param {Object} settings Settings for the editor.
+ * @param {tinymce.EditorManager} editorManager EditorManager instance.
+ */
+ function Editor(id, settings, editorManager) {
+ var self = this, documentBaseUrl, baseUri, defaultSettings;
+
+ documentBaseUrl = self.documentBaseUrl = editorManager.documentBaseURL;
+ baseUri = editorManager.baseURI;
+ defaultSettings = editorManager.defaultSettings;
+
+ /**
+ * Name/value collection with editor settings.
+ *
+ * @property settings
+ * @type Object
+ * @example
+ * // Get the value of the theme setting
+ * tinymce.activeEditor.windowManager.alert("You are using the " + tinymce.activeEditor.settings.theme + " theme");
+ */
+ settings = extend({
+ id: id,
+ theme: 'modern',
+ delta_width: 0,
+ delta_height: 0,
+ popup_css: '',
+ plugins: '',
+ document_base_url: documentBaseUrl,
+ add_form_submit_trigger: true,
+ submit_patch: true,
+ add_unload_trigger: true,
+ convert_urls: true,
+ relative_urls: true,
+ remove_script_host: true,
+ object_resizing: true,
+ doctype: '<!DOCTYPE html>',
+ visual: true,
+ font_size_style_values: 'xx-small,x-small,small,medium,large,x-large,xx-large',
+
+ // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size
+ font_size_legacy_values: 'xx-small,small,medium,large,x-large,xx-large,300%',
+ forced_root_block: 'p',
+ hidden_input: true,
+ padd_empty_editor: true,
+ render_ui: true,
+ indentation: '30px',
+ inline_styles: true,
+ convert_fonts_to_spans: true,
+ indent: 'simple',
+ indent_before: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' +
+ 'tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist',
+ indent_after: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' +
+ 'tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist',
+ validate: true,
+ entity_encoding: 'named',
+ url_converter: self.convertURL,
+ url_converter_scope: self,
+ ie7_compat: true
+ }, defaultSettings, settings);
+
+ // Merge external_plugins
+ if (defaultSettings && defaultSettings.external_plugins && settings.external_plugins) {
+ settings.external_plugins = extend({}, defaultSettings.external_plugins, settings.external_plugins);
+ }
+
+ self.settings = settings;
+ AddOnManager.language = settings.language || 'en';
+ AddOnManager.languageLoad = settings.language_load;
+ AddOnManager.baseURL = editorManager.baseURL;
+
+ /**
+ * Editor instance id, normally the same as the div/textarea that was replaced.
+ *
+ * @property id
+ * @type String
+ */
+ self.id = settings.id = id;
+
+ /**
+ * State to force the editor to return false on a isDirty call.
+ *
+ * @property isNotDirty
+ * @type Boolean
+ * @deprecated Use editor.setDirty instead.
+ */
+ self.setDirty(false);
+
+ /**
+ * Name/Value object containing plugin instances.
+ *
+ * @property plugins
+ * @type Object
+ * @example
+ * // Execute a method inside a plugin directly
+ * tinymce.activeEditor.plugins.someplugin.someMethod();
+ */
+ self.plugins = {};
+
+ /**
+ * URI object to document configured for the TinyMCE instance.
+ *
+ * @property documentBaseURI
+ * @type tinymce.util.URI
+ * @example
+ * // Get relative URL from the location of document_base_url
+ * tinymce.activeEditor.documentBaseURI.toRelative('/somedir/somefile.htm');
+ *
+ * // Get absolute URL from the location of document_base_url
+ * tinymce.activeEditor.documentBaseURI.toAbsolute('somefile.htm');
+ */
+ self.documentBaseURI = new URI(settings.document_base_url || documentBaseUrl, {
+ base_uri: baseUri
+ });
+
+ /**
+ * URI object to current document that holds the TinyMCE editor instance.
+ *
+ * @property baseURI
+ * @type tinymce.util.URI
+ * @example
+ * // Get relative URL from the location of the API
+ * tinymce.activeEditor.baseURI.toRelative('/somedir/somefile.htm');
+ *
+ * // Get absolute URL from the location of the API
+ * tinymce.activeEditor.baseURI.toAbsolute('somefile.htm');
+ */
+ self.baseURI = baseUri;
+
+ /**
+ * Array with CSS files to load into the iframe.
+ *
+ * @property contentCSS
+ * @type Array
+ */
+ self.contentCSS = [];
+
+ /**
+ * Array of CSS styles to add to head of document when the editor loads.
+ *
+ * @property contentStyles
+ * @type Array
+ */
+ self.contentStyles = [];
+
+ // Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic
+ self.shortcuts = new Shortcuts(self);
+ self.loadedCSS = {};
+ self.editorCommands = new EditorCommands(self);
+ self.suffix = editorManager.suffix;
+ self.editorManager = editorManager;
+ self.inline = settings.inline;
+ self.settings.content_editable = self.inline;
+
+ if (settings.cache_suffix) {
+ Env.cacheSuffix = settings.cache_suffix.replace(/^[\?\&]+/, '');
+ }
+
+ if (settings.override_viewport === false) {
+ Env.overrideViewPort = false;
+ }
+
+ // Call setup
+ editorManager.fire('SetupEditor', self);
+ self.execCallback('setup', self);
+
+ /**
+ * Dom query instance with default scope to the editor document and default element is the body of the editor.
+ *
+ * @property $
+ * @type tinymce.dom.DomQuery
+ * @example
+ * tinymce.activeEditor.$('p').css('color', 'red');
+ * tinymce.activeEditor.$().append('<p>new</p>');
+ */
+ self.$ = DomQuery.overrideDefaults(function() {
+ return {
+ context: self.inline ? self.getBody() : self.getDoc(),
+ element: self.getBody()
+ };
+ });
+ }
+
+ Editor.prototype = {
+ /**
+ * Renders the editor/adds it to the page.
+ *
+ * @method render
+ */
+ render: function() {
+ var self = this, settings = self.settings, id = self.id, suffix = self.suffix;
+
+ function readyHandler() {
+ DOM.unbind(window, 'ready', readyHandler);
+ self.render();
+ }
+
+ // Page is not loaded yet, wait for it
+ if (!Event.domLoaded) {
+ DOM.bind(window, 'ready', readyHandler);
+ return;
+ }
+
+ // Element not found, then skip initialization
+ if (!self.getElement()) {
+ return;
+ }
+
+ // No editable support old iOS versions etc
+ if (!Env.contentEditable) {
+ return;
+ }
+
+ // Hide target element early to prevent content flashing
+ if (!settings.inline) {
+ self.orgVisibility = self.getElement().style.visibility;
+ self.getElement().style.visibility = 'hidden';
+ } else {
+ self.inline = true;
+ }
+
+ var form = self.getElement().form || DOM.getParent(id, 'form');
+ if (form) {
+ self.formElement = form;
+
+ // Add hidden input for non input elements inside form elements
+ if (settings.hidden_input && !/TEXTAREA|INPUT/i.test(self.getElement().nodeName)) {
+ DOM.insertAfter(DOM.create('input', {type: 'hidden', name: id}), id);
+ self.hasHiddenInput = true;
+ }
+
+ // Pass submit/reset from form to editor instance
+ self.formEventDelegate = function(e) {
+ self.fire(e.type, e);
+ };
+
+ DOM.bind(form, 'submit reset', self.formEventDelegate);
+
+ // Reset contents in editor when the form is reset
+ self.on('reset', function() {
+ self.setContent(self.startContent, {format: 'raw'});
+ });
+
+ // Check page uses id="submit" or name="submit" for it's submit button
+ if (settings.submit_patch && !form.submit.nodeType && !form.submit.length && !form._mceOldSubmit) {
+ form._mceOldSubmit = form.submit;
+ form.submit = function() {
+ self.editorManager.triggerSave();
+ self.setDirty(false);
+
+ return form._mceOldSubmit(form);
+ };
+ }
+ }
+
+ /**
+ * Window manager reference, use this to open new windows and dialogs.
+ *
+ * @property windowManager
+ * @type tinymce.WindowManager
+ * @example
+ * // Shows an alert message
+ * tinymce.activeEditor.windowManager.alert('Hello world!');
+ *
+ * // Opens a new dialog with the file.htm file and the size 320x240
+ * // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog.
+ * tinymce.activeEditor.windowManager.open({
+ * url: 'file.htm',
+ * width: 320,
+ * height: 240
+ * }, {
+ * custom_param: 1
+ * });
+ */
+ self.windowManager = new WindowManager(self);
+
+ /**
+ * Notification manager reference, use this to open new windows and dialogs.
+ *
+ * @property notificationManager
+ * @type tinymce.NotificationManager
+ * @example
+ * // Shows a notification info message.
+ * tinymce.activeEditor.notificationManager.open({text: 'Hello world!', type: 'info'});
+ */
+ self.notificationManager = new NotificationManager(self);
+
+ if (settings.encoding == 'xml') {
+ self.on('GetContent', function(e) {
+ if (e.save) {
+ e.content = DOM.encode(e.content);
+ }
+ });
+ }
+
+ if (settings.add_form_submit_trigger) {
+ self.on('submit', function() {
+ if (self.initialized) {
+ self.save();
+ }
+ });
+ }
+
+ if (settings.add_unload_trigger) {
+ self._beforeUnload = function() {
+ if (self.initialized && !self.destroyed && !self.isHidden()) {
+ self.save({format: 'raw', no_events: true, set_dirty: false});
+ }
+ };
+
+ self.editorManager.on('BeforeUnload', self._beforeUnload);
+ }
+
+ // Load scripts
+ function loadScripts() {
+ var scriptLoader = ScriptLoader.ScriptLoader;
+
+ if (settings.language && settings.language != 'en' && !settings.language_url) {
+ settings.language_url = self.editorManager.baseURL + '/langs/' + settings.language + '.js';
+ }
+
+ if (settings.language_url) {
+ scriptLoader.add(settings.language_url);
+ }
+
+ if (settings.theme && typeof settings.theme != "function" &&
+ settings.theme.charAt(0) != '-' && !ThemeManager.urls[settings.theme]) {
+ var themeUrl = settings.theme_url;
+
+ if (themeUrl) {
+ themeUrl = self.documentBaseURI.toAbsolute(themeUrl);
+ } else {
+ themeUrl = 'themes/' + settings.theme + '/theme' + suffix + '.js';
+ }
+
+ ThemeManager.load(settings.theme, themeUrl);
+ }
+
+ if (Tools.isArray(settings.plugins)) {
+ settings.plugins = settings.plugins.join(' ');
+ }
+
+ each(settings.external_plugins, function(url, name) {
+ PluginManager.load(name, url);
+ settings.plugins += ' ' + name;
+ });
+
+ each(settings.plugins.split(/[ ,]/), function(plugin) {
+ plugin = trim(plugin);
+
+ if (plugin && !PluginManager.urls[plugin]) {
+ if (plugin.charAt(0) == '-') {
+ plugin = plugin.substr(1, plugin.length);
+
+ var dependencies = PluginManager.dependencies(plugin);
+
+ each(dependencies, function(dep) {
+ var defaultSettings = {
+ prefix: 'plugins/',
+ resource: dep,
+ suffix: '/plugin' + suffix + '.js'
+ };
+
+ dep = PluginManager.createUrl(defaultSettings, dep);
+ PluginManager.load(dep.resource, dep);
+ });
+ } else {
+ PluginManager.load(plugin, {
+ prefix: 'plugins/',
+ resource: plugin,
+ suffix: '/plugin' + suffix + '.js'
+ });
+ }
+ }
+ });
+
+ scriptLoader.loadQueue(function() {
+ if (!self.removed) {
+ self.init();
+ }
+ });
+ }
+
+ self.editorManager.add(self);
+ loadScripts();
+ },
+
+ /**
+ * Initializes the editor this will be called automatically when
+ * all plugins/themes and language packs are loaded by the rendered method.
+ * This method will setup the iframe and create the theme and plugin instances.
+ *
+ * @method init
+ */
+ init: function() {
+ var self = this, settings = self.settings, elm = self.getElement();
+ var w, h, minHeight, n, o, Theme, url, bodyId, bodyClass, re, i, initializedPlugins = [];
+
+ self.rtl = settings.rtl_ui || self.editorManager.i18n.rtl;
+ self.editorManager.i18n.setCode(settings.language);
+ settings.aria_label = settings.aria_label || DOM.getAttrib(elm, 'aria-label', self.getLang('aria.rich_text_area'));
+
+ self.fire('ScriptsLoaded');
+
+ /**
+ * Reference to the theme instance that was used to generate the UI.
+ *
+ * @property theme
+ * @type tinymce.Theme
+ * @example
+ * // Executes a method on the theme directly
+ * tinymce.activeEditor.theme.someMethod();
+ */
+ if (settings.theme) {
+ if (typeof settings.theme != "function") {
+ settings.theme = settings.theme.replace(/-/, '');
+ Theme = ThemeManager.get(settings.theme);
+ self.theme = new Theme(self, ThemeManager.urls[settings.theme]);
+
+ if (self.theme.init) {
+ self.theme.init(self, ThemeManager.urls[settings.theme] || self.documentBaseUrl.replace(/\/$/, ''), self.$);
+ }
+ } else {
+ self.theme = settings.theme;
+ }
+ }
+
+ function initPlugin(plugin) {
+ var Plugin = PluginManager.get(plugin), pluginUrl, pluginInstance;
+
+ pluginUrl = PluginManager.urls[plugin] || self.documentBaseUrl.replace(/\/$/, '');
+ plugin = trim(plugin);
+ if (Plugin && inArray(initializedPlugins, plugin) === -1) {
+ each(PluginManager.dependencies(plugin), function(dep) {
+ initPlugin(dep);
+ });
+
+ if (self.plugins[plugin]) {
+ return;
+ }
+
+ pluginInstance = new Plugin(self, pluginUrl, self.$);
+
+ self.plugins[plugin] = pluginInstance;
+
+ if (pluginInstance.init) {
+ pluginInstance.init(self, pluginUrl);
+ initializedPlugins.push(plugin);
+ }
+ }
+ }
+
+ // Create all plugins
+ each(settings.plugins.replace(/\-/g, '').split(/[ ,]/), initPlugin);
+
+ // Measure box
+ if (settings.render_ui && self.theme) {
+ self.orgDisplay = elm.style.display;
+
+ if (typeof settings.theme != "function") {
+ w = settings.width || elm.style.width || elm.offsetWidth;
+ h = settings.height || elm.style.height || elm.offsetHeight;
+ minHeight = settings.min_height || 100;
+ re = /^[0-9\.]+(|px)$/i;
+
+ if (re.test('' + w)) {
+ w = Math.max(parseInt(w, 10), 100);
+ }
+
+ if (re.test('' + h)) {
+ h = Math.max(parseInt(h, 10), minHeight);
+ }
+
+ // Render UI
+ o = self.theme.renderUI({
+ targetNode: elm,
+ width: w,
+ height: h,
+ deltaWidth: settings.delta_width,
+ deltaHeight: settings.delta_height
+ });
+
+ // Resize editor
+ if (!settings.content_editable) {
+ h = (o.iframeHeight || h) + (typeof h == 'number' ? (o.deltaHeight || 0) : '');
+ if (h < minHeight) {
+ h = minHeight;
+ }
+ }
+ } else {
+ o = settings.theme(self, elm);
+
+ if (o.editorContainer.nodeType) {
+ o.editorContainer.id = o.editorContainer.id || self.id + "_parent";
+ }
+
+ if (o.iframeContainer.nodeType) {
+ o.iframeContainer.id = o.iframeContainer.id || self.id + "_iframecontainer";
+ }
+
+ // Use specified iframe height or the targets offsetHeight
+ h = o.iframeHeight || elm.offsetHeight;
+ }
+
+ self.editorContainer = o.editorContainer;
+ }
+
+ // Load specified content CSS last
+ if (settings.content_css) {
+ each(explode(settings.content_css), function(u) {
+ self.contentCSS.push(self.documentBaseURI.toAbsolute(u));
+ });
+ }
+
+ // Load specified content CSS last
+ if (settings.content_style) {
+ self.contentStyles.push(settings.content_style);
+ }
+
+ // Content editable mode ends here
+ if (settings.content_editable) {
+ elm = n = o = null; // Fix IE leak
+ return self.initContentBody();
+ }
+
+ self.iframeHTML = settings.doctype + '<html><head>';
+
+ // We only need to override paths if we have to
+ // IE has a bug where it remove site absolute urls to relative ones if this is specified
+ if (settings.document_base_url != self.documentBaseUrl) {
+ self.iframeHTML += '<base href="' + self.documentBaseURI.getURI() + '" />';
+ }
+
+ // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode.
+ if (!Env.caretAfter && settings.ie7_compat) {
+ self.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />';
+ }
+
+ self.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />';
+
+ // Load the CSS by injecting them into the HTML this will reduce "flicker"
+ // However we can't do that on Chrome since # will scroll to the editor for some odd reason see #2427
+ if (!/#$/.test(document.location.href)) {
+ for (i = 0; i < self.contentCSS.length; i++) {
+ var cssUrl = self.contentCSS[i];
+ self.iframeHTML += (
+ '<link type="text/css" ' +
+ 'rel="stylesheet" ' +
+ 'href="' + Tools._addCacheSuffix(cssUrl) + '" />'
+ );
+ self.loadedCSS[cssUrl] = true;
+ }
+ }
+
+ bodyId = settings.body_id || 'tinymce';
+ if (bodyId.indexOf('=') != -1) {
+ bodyId = self.getParam('body_id', '', 'hash');
+ bodyId = bodyId[self.id] || bodyId;
+ }
+
+ bodyClass = settings.body_class || '';
+ if (bodyClass.indexOf('=') != -1) {
+ bodyClass = self.getParam('body_class', '', 'hash');
+ bodyClass = bodyClass[self.id] || '';
+ }
+
+ if (settings.content_security_policy) {
+ self.iframeHTML += '<meta http-equiv="Content-Security-Policy" content="' + settings.content_security_policy + '" />';
+ }
+
+ self.iframeHTML += '</head><body id="' + bodyId +
+ '" class="mce-content-body ' + bodyClass +
+ '" data-id="' + self.id + '"><br></body></html>';
+
+ /*eslint no-script-url:0 */
+ var domainRelaxUrl = 'javascript:(function(){' +
+ 'document.open();document.domain="' + document.domain + '";' +
+ 'var ed = window.parent.tinymce.get("' + self.id + '");document.write(ed.iframeHTML);' +
+ 'document.close();ed.initContentBody(true);})()';
+
+ // Domain relaxing is required since the user has messed around with document.domain
+ if (document.domain != location.hostname) {
+ // Edge seems to be able to handle domain relaxing
+ if (Env.ie && Env.ie < 12) {
+ url = domainRelaxUrl;
+ }
+ }
+
+ // Create iframe
+ // TODO: ACC add the appropriate description on this.
+ var ifr = DOM.create('iframe', {
+ id: self.id + "_ifr",
+ //src: url || 'javascript:""', // Workaround for HTTPS warning in IE6/7
+ frameBorder: '0',
+ allowTransparency: "true",
+ title: self.editorManager.translate(
+ "Rich Text Area. Press ALT-F9 for menu. " +
+ "Press ALT-F10 for toolbar. Press ALT-0 for help"
+ ),
+ style: {
+ width: '100%',
+ height: h,
+ display: 'block' // Important for Gecko to render the iframe correctly
+ }
+ });
+
+ ifr.onload = function() {
+ ifr.onload = null;
+ self.fire("load");
+ };
+
+ DOM.setAttrib(ifr, "src", url || 'javascript:""');
+
+ self.contentAreaContainer = o.iframeContainer;
+ self.iframeElement = ifr;
+
+ n = DOM.add(o.iframeContainer, ifr);
+
+ // Try accessing the document this will fail on IE when document.domain is set to the same as location.hostname
+ // Then we have to force domain relaxing using the domainRelaxUrl approach very ugly!!
+ if (ie) {
+ try {
+ self.getDoc();
+ } catch (e) {
+ n.src = url = domainRelaxUrl;
+ }
+ }
+
+ if (o.editorContainer) {
+ DOM.get(o.editorContainer).style.display = self.orgDisplay;
+ self.hidden = DOM.isHidden(o.editorContainer);
+ }
+
+ self.getElement().style.display = 'none';
+ DOM.setAttrib(self.id, 'aria-hidden', true);
+
+ if (!url) {
+ self.initContentBody();
+ }
+
+ elm = n = o = null; // Cleanup
+ },
+
+ /**
+ * This method get called by the init method once the iframe is loaded.
+ * It will fill the iframe with contents, sets up DOM and selection objects for the iframe.
+ *
+ * @method initContentBody
+ * @private
+ */
+ initContentBody: function(skipWrite) {
+ var self = this, settings = self.settings, targetElm = self.getElement(), doc = self.getDoc(), body, contentCssText;
+
+ // Restore visibility on target element
+ if (!settings.inline) {
+ self.getElement().style.visibility = self.orgVisibility;
+ }
+
+ // Setup iframe body
+ if (!skipWrite && !settings.content_editable) {
+ doc.open();
+ doc.write(self.iframeHTML);
+ doc.close();
+ }
+
+ if (settings.content_editable) {
+ self.on('remove', function() {
+ var bodyEl = this.getBody();
+
+ DOM.removeClass(bodyEl, 'mce-content-body');
+ DOM.removeClass(bodyEl, 'mce-edit-focus');
+ DOM.setAttrib(bodyEl, 'contentEditable', null);
+ });
+
+ DOM.addClass(targetElm, 'mce-content-body');
+ self.contentDocument = doc = settings.content_document || document;
+ self.contentWindow = settings.content_window || window;
+ self.bodyElement = targetElm;
+
+ // Prevent leak in IE
+ settings.content_document = settings.content_window = null;
+
+ // TODO: Fix this
+ settings.root_name = targetElm.nodeName.toLowerCase();
+ }
+
+ // It will not steal focus while setting contentEditable
+ body = self.getBody();
+ body.disabled = true;
+ self.readonly = settings.readonly;
+
+ if (!self.readonly) {
+ if (self.inline && DOM.getStyle(body, 'position', true) == 'static') {
+ body.style.position = 'relative';
+ }
+
+ body.contentEditable = self.getParam('content_editable_state', true);
+ }
+
+ body.disabled = false;
+
+ self.editorUpload = new EditorUpload(self);
+
+ /**
+ * Schema instance, enables you to validate elements and its children.
+ *
+ * @property schema
+ * @type tinymce.html.Schema
+ */
+ self.schema = new Schema(settings);
+
+ /**
+ * DOM instance for the editor.
+ *
+ * @property dom
+ * @type tinymce.dom.DOMUtils
+ * @example
+ * // Adds a class to all paragraphs within the editor
+ * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass');
+ */
+ self.dom = new DOMUtils(doc, {
+ keep_values: true,
+ url_converter: self.convertURL,
+ url_converter_scope: self,
+ hex_colors: settings.force_hex_style_colors,
+ class_filter: settings.class_filter,
+ update_styles: true,
+ root_element: self.inline ? self.getBody() : null,
+ collect: settings.content_editable,
+ schema: self.schema,
+ onSetAttrib: function(e) {
+ self.fire('SetAttrib', e);
+ }
+ });
+
+ /**
+ * HTML parser will be used when contents is inserted into the editor.
+ *
+ * @property parser
+ * @type tinymce.html.DomParser
+ */
+ self.parser = new DomParser(settings, self.schema);
+
+ // Convert src and href into data-mce-src, data-mce-href and data-mce-style
+ self.parser.addAttributeFilter('src,href,style,tabindex', function(nodes, name) {
+ var i = nodes.length, node, dom = self.dom, value, internalName;
+
+ while (i--) {
+ node = nodes[i];
+ value = node.attr(name);
+ internalName = 'data-mce-' + name;
+
+ // Add internal attribute if we need to we don't on a refresh of the document
+ if (!node.attributes.map[internalName]) {
+ // Don't duplicate these since they won't get modified by any browser
+ if (value.indexOf('data:') === 0 || value.indexOf('blob:') === 0) {
+ continue;
+ }
+
+ if (name === "style") {
+ value = dom.serializeStyle(dom.parseStyle(value), node.name);
+
+ if (!value.length) {
+ value = null;
+ }
+
+ node.attr(internalName, value);
+ node.attr(name, value);
+ } else if (name === "tabindex") {
+ node.attr(internalName, value);
+ node.attr(name, null);
+ } else {
+ node.attr(internalName, self.convertURL(value, name, node.name));
+ }
+ }
+ }
+ });
+
+ // Keep scripts from executing
+ self.parser.addNodeFilter('script', function(nodes) {
+ var i = nodes.length, node, type;
+
+ while (i--) {
+ node = nodes[i];
+ type = node.attr('type') || 'no/type';
+ if (type.indexOf('mce-') !== 0) {
+ node.attr('type', 'mce-' + type);
+ }
+ }
+ });
+
+ self.parser.addNodeFilter('#cdata', function(nodes) {
+ var i = nodes.length, node;
+
+ while (i--) {
+ node = nodes[i];
+ node.type = 8;
+ node.name = '#comment';
+ node.value = '[CDATA[' + node.value + ']]';
+ }
+ });
+
+ self.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes) {
+ var i = nodes.length, node, nonEmptyElements = self.schema.getNonEmptyElements();
+
+ while (i--) {
+ node = nodes[i];
+
+ if (node.isEmpty(nonEmptyElements)) {
+ node.append(new Node('br', 1)).shortEnded = true;
+ }
+ }
+ });
+
+ /**
+ * DOM serializer for the editor. Will be used when contents is extracted from the editor.
+ *
+ * @property serializer
+ * @type tinymce.dom.Serializer
+ * @example
+ * // Serializes the first paragraph in the editor into a string
+ * tinymce.activeEditor.serializer.serialize(tinymce.activeEditor.dom.select('p')[0]);
+ */
+ self.serializer = new DomSerializer(settings, self);
+
+ /**
+ * Selection instance for the editor.
+ *
+ * @property selection
+ * @type tinymce.dom.Selection
+ * @example
+ * // Sets some contents to the current selection in the editor
+ * tinymce.activeEditor.selection.setContent('Some contents');
+ *
+ * // Gets the current selection
+ * alert(tinymce.activeEditor.selection.getContent());
+ *
+ * // Selects the first paragraph found
+ * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]);
+ */
+ self.selection = new Selection(self.dom, self.getWin(), self.serializer, self);
+
+ /**
+ * Formatter instance.
+ *
+ * @property formatter
+ * @type tinymce.Formatter
+ */
+ self.formatter = new Formatter(self);
+
+ /**
+ * Undo manager instance, responsible for handling undo levels.
+ *
+ * @property undoManager
+ * @type tinymce.UndoManager
+ * @example
+ * // Undoes the last modification to the editor
+ * tinymce.activeEditor.undoManager.undo();
+ */
+ self.undoManager = new UndoManager(self);
+
+ self.forceBlocks = new ForceBlocks(self);
+ self.enterKey = new EnterKey(self);
+ self._nodeChangeDispatcher = new NodeChange(self);
+ self._selectionOverrides = new SelectionOverrides(self);
+
+ self.fire('PreInit');
+
+ if (!settings.browser_spellcheck && !settings.gecko_spellcheck) {
+ doc.body.spellcheck = false; // Gecko
+ DOM.setAttrib(body, "spellcheck", "false");
+ }
+
+ self.quirks = new Quirks(self);
+ self.fire('PostRender');
+
+ if (settings.directionality) {
+ body.dir = settings.directionality;
+ }
+
+ if (settings.nowrap) {
+ body.style.whiteSpace = "nowrap";
+ }
+
+ if (settings.protect) {
+ self.on('BeforeSetContent', function(e) {
+ each(settings.protect, function(pattern) {
+ e.content = e.content.replace(pattern, function(str) {
+ return '<!--mce:protected ' + escape(str) + '-->';
+ });
+ });
+ });
+ }
+
+ self.on('SetContent', function() {
+ self.addVisual(self.getBody());
+ });
+
+ // Remove empty contents
+ if (settings.padd_empty_editor) {
+ self.on('PostProcess', function(e) {
+ e.content = e.content.replace(/^(<p[^>]*>( | |\s|\u00a0|)<\/p>[\r\n]*|<br \/>[\r\n]*)$/, '');
+ });
+ }
+
+ self.load({initial: true, format: 'html'});
+ self.startContent = self.getContent({format: 'raw'});
+
+ /**
+ * Is set to true after the editor instance has been initialized
+ *
+ * @property initialized
+ * @type Boolean
+ * @example
+ * function isEditorInitialized(editor) {
+ * return editor && editor.initialized;
+ * }
+ */
+ self.initialized = true;
+ self.bindPendingEventDelegates();
+
+ self.fire('init');
+ self.focus(true);
+ self.nodeChanged({initial: true});
+ self.execCallback('init_instance_callback', self);
+
+ self.on('compositionstart compositionend', function(e) {
+ self.composing = e.type === 'compositionstart';
+ });
+
+ // Add editor specific CSS styles
+ if (self.contentStyles.length > 0) {
+ contentCssText = '';
+
+ each(self.contentStyles, function(style) {
+ contentCssText += style + "\r\n";
+ });
+
+ self.dom.addStyle(contentCssText);
+ }
+
+ // Load specified content CSS last
+ each(self.contentCSS, function(cssUrl) {
+ if (!self.loadedCSS[cssUrl]) {
+ self.dom.loadCSS(cssUrl);
+ self.loadedCSS[cssUrl] = true;
+ }
+ });
+
+ // Handle auto focus
+ if (settings.auto_focus) {
+ Delay.setEditorTimeout(self, function() {
+ var editor;
+
+ if (settings.auto_focus === true) {
+ editor = self;
+ } else {
+ editor = self.editorManager.get(settings.auto_focus);
+ }
+
+ if (!editor.destroyed) {
+ editor.focus();
+ }
+ }, 100);
+ }
+
+ // Clean up references for IE
+ targetElm = doc = body = null;
+ },
+
+ /**
+ * Focuses/activates the editor. This will set this editor as the activeEditor in the tinymce collection
+ * it will also place DOM focus inside the editor.
+ *
+ * @method focus
+ * @param {Boolean} skipFocus Skip DOM focus. Just set is as the active editor.
+ */
+ focus: function(skipFocus) {
+ var self = this, selection = self.selection, contentEditable = self.settings.content_editable, rng;
+ var controlElm, doc = self.getDoc(), body = self.getBody(), contentEditableHost;
+
+ function getContentEditableHost(node) {
+ return self.dom.getParent(node, function(node) {
+ return self.dom.getContentEditable(node) === "true";
+ });
+ }
+
+ if (!skipFocus) {
+ // Get selected control element
+ rng = selection.getRng();
+ if (rng.item) {
+ controlElm = rng.item(0);
+ }
+
+ self.quirks.refreshContentEditable();
+
+ // Move focus to contentEditable=true child if needed
+ contentEditableHost = getContentEditableHost(selection.getNode());
+ if (self.$.contains(body, contentEditableHost)) {
+ contentEditableHost.focus();
+ selection.normalize();
+ self.editorManager.setActive(self);
+ return;
+ }
+
+ // Focus the window iframe
+ if (!contentEditable) {
+ // WebKit needs this call to fire focusin event properly see #5948
+ // But Opera pre Blink engine will produce an empty selection so skip Opera
+ if (!Env.opera) {
+ self.getBody().focus();
+ }
+
+ self.getWin().focus();
+ }
+
+ // Focus the body as well since it's contentEditable
+ if (isGecko || contentEditable) {
+ // Check for setActive since it doesn't scroll to the element
+ if (body.setActive) {
+ // IE 11 sometimes throws "Invalid function" then fallback to focus
+ try {
+ body.setActive();
+ } catch (ex) {
+ body.focus();
+ }
+ } else {
+ body.focus();
+ }
+
+ if (contentEditable) {
+ selection.normalize();
+ }
+ }
+
+ // Restore selected control element
+ // This is needed when for example an image is selected within a
+ // layer a call to focus will then remove the control selection
+ if (controlElm && controlElm.ownerDocument == doc) {
+ rng = doc.body.createControlRange();
+ rng.addElement(controlElm);
+ rng.select();
+ }
+ }
+
+ self.editorManager.setActive(self);
+ },
+
+ /**
+ * Executes a legacy callback. This method is useful to call old 2.x option callbacks.
+ * There new event model is a better way to add callback so this method might be removed in the future.
+ *
+ * @method execCallback
+ * @param {String} name Name of the callback to execute.
+ * @return {Object} Return value passed from callback function.
+ */
+ execCallback: function(name) {
+ var self = this, callback = self.settings[name], scope;
+
+ if (!callback) {
+ return;
+ }
+
+ // Look through lookup
+ if (self.callbackLookup && (scope = self.callbackLookup[name])) {
+ callback = scope.func;
+ scope = scope.scope;
+ }
+
+ if (typeof callback === 'string') {
+ scope = callback.replace(/\.\w+$/, '');
+ scope = scope ? resolve(scope) : 0;
+ callback = resolve(callback);
+ self.callbackLookup = self.callbackLookup || {};
+ self.callbackLookup[name] = {func: callback, scope: scope};
+ }
+
+ return callback.apply(scope || self, Array.prototype.slice.call(arguments, 1));
+ },
+
+ /**
+ * Translates the specified string by replacing variables with language pack items it will also check if there is
+ * a key matching the input.
+ *
+ * @method translate
+ * @param {String} text String to translate by the language pack data.
+ * @return {String} Translated string.
+ */
+ translate: function(text) {
+ var lang = this.settings.language || 'en', i18n = this.editorManager.i18n;
+
+ if (!text) {
+ return '';
+ }
+
+ text = i18n.data[lang + '.' + text] || text.replace(/\{\#([^\}]+)\}/g, function(a, b) {
+ return i18n.data[lang + '.' + b] || '{#' + b + '}';
+ });
+
+ return this.editorManager.translate(text);
+ },
+
+ /**
+ * Returns a language pack item by name/key.
+ *
+ * @method getLang
+ * @param {String} name Name/key to get from the language pack.
+ * @param {String} defaultVal Optional default value to retrieve.
+ */
+ getLang: function(name, defaultVal) {
+ return (
+ this.editorManager.i18n.data[(this.settings.language || 'en') + '.' + name] ||
+ (defaultVal !== undefined ? defaultVal : '{#' + name + '}')
+ );
+ },
+
+ /**
+ * Returns a configuration parameter by name.
+ *
+ * @method getParam
+ * @param {String} name Configruation parameter to retrieve.
+ * @param {String} defaultVal Optional default value to return.
+ * @param {String} type Optional type parameter.
+ * @return {String} Configuration parameter value or default value.
+ * @example
+ * // Returns a specific config value from the currently active editor
+ * var someval = tinymce.activeEditor.getParam('myvalue');
+ *
+ * // Returns a specific config value from a specific editor instance by id
+ * var someval2 = tinymce.get('my_editor').getParam('myvalue');
+ */
+ getParam: function(name, defaultVal, type) {
+ var value = name in this.settings ? this.settings[name] : defaultVal, output;
+
+ if (type === 'hash') {
+ output = {};
+
+ if (typeof value === 'string') {
+ each(value.indexOf('=') > 0 ? value.split(/[;,](?![^=;,]*(?:[;,]|$))/) : value.split(','), function(value) {
+ value = value.split('=');
+
+ if (value.length > 1) {
+ output[trim(value[0])] = trim(value[1]);
+ } else {
+ output[trim(value[0])] = trim(value);
+ }
+ });
+ } else {
+ output = value;
+ }
+
+ return output;
+ }
+
+ return value;
+ },
+
+ /**
+ * Dispatches out a onNodeChange event to all observers. This method should be called when you
+ * need to update the UI states or element path etc.
+ *
+ * @method nodeChanged
+ * @param {Object} args Optional args to pass to NodeChange event handlers.
+ */
+ nodeChanged: function(args) {
+ this._nodeChangeDispatcher.nodeChanged(args);
+ },
+
+ /**
+ * Adds a button that later gets created by the theme in the editors toolbars.
+ *
+ * @method addButton
+ * @param {String} name Button name to add.
+ * @param {Object} settings Settings object with title, cmd etc.
+ * @example
+ * // Adds a custom button to the editor that inserts contents when clicked
+ * tinymce.init({
+ * ...
+ *
+ * toolbar: 'example'
+ *
+ * setup: function(ed) {
+ * ed.addButton('example', {
+ * title: 'My title',
+ * image: '../js/tinymce/plugins/example/img/example.gif',
+ * onclick: function() {
+ * ed.insertContent('Hello world!!');
+ * }
+ * });
+ * }
+ * });
+ */
+ addButton: function(name, settings) {
+ var self = this;
+
+ if (settings.cmd) {
+ settings.onclick = function() {
+ self.execCommand(settings.cmd);
+ };
+ }
+
+ if (!settings.text && !settings.icon) {
+ settings.icon = name;
+ }
+
+ self.buttons = self.buttons || {};
+ settings.tooltip = settings.tooltip || settings.title;
+ self.buttons[name] = settings;
+ },
+
+ /**
+ * Adds a sidebar for the editor instance.
+ *
+ * @method addSidebar
+ * @param {String} name Sidebar name to add.
+ * @param {Object} settings Settings object with icon, onshow etc.
+ * @example
+ * // Adds a custom sidebar that when clicked logs the panel element
+ * tinymce.init({
+ * ...
+ * setup: function(ed) {
+ * ed.addSidebar('example', {
+ * tooltip: 'My sidebar',
+ * icon: 'my-side-bar',
+ * onshow: function(api) {
+ * console.log(api.element());
+ * }
+ * });
+ * }
+ * });
+ */
+ addSidebar: function (name, settings) {
+ return Sidebar.add(this, name, settings);
+ },
+
+ /**
+ * Adds a menu item to be used in the menus of the theme. There might be multiple instances
+ * of this menu item for example it might be used in the main menus of the theme but also in
+ * the context menu so make sure that it's self contained and supports multiple instances.
+ *
+ * @method addMenuItem
+ * @param {String} name Menu item name to add.
+ * @param {Object} settings Settings object with title, cmd etc.
+ * @example
+ * // Adds a custom menu item to the editor that inserts contents when clicked
+ * // The context option allows you to add the menu item to an existing default menu
+ * tinymce.init({
+ * ...
+ *
+ * setup: function(ed) {
+ * ed.addMenuItem('example', {
+ * text: 'My menu item',
+ * context: 'tools',
+ * onclick: function() {
+ * ed.insertContent('Hello world!!');
+ * }
+ * });
+ * }
+ * });
+ */
+ addMenuItem: function(name, settings) {
+ var self = this;
+
+ if (settings.cmd) {
+ settings.onclick = function() {
+ self.execCommand(settings.cmd);
+ };
+ }
+
+ self.menuItems = self.menuItems || {};
+ self.menuItems[name] = settings;
+ },
+
+ /**
+ * Adds a contextual toolbar to be rendered when the selector matches.
+ *
+ * @method addContextToolbar
+ * @param {function/string} predicate Predicate that needs to return true if provided strings get converted into CSS predicates.
+ * @param {String/Array} items String or array with items to add to the context toolbar.
+ */
+ addContextToolbar: function(predicate, items) {
+ var self = this, selector;
+
+ self.contextToolbars = self.contextToolbars || [];
+
+ // Convert selector to predicate
+ if (typeof predicate == "string") {
+ selector = predicate;
+ predicate = function(elm) {
+ return self.dom.is(elm, selector);
+ };
+ }
+
+ self.contextToolbars.push({
+ id: Uuid.uuid('mcet'),
+ predicate: predicate,
+ items: items
+ });
+ },
+
+ /**
+ * Adds a custom command to the editor, you can also override existing commands with this method.
+ * The command that you add can be executed with execCommand.
+ *
+ * @method addCommand
+ * @param {String} name Command name to add/override.
+ * @param {addCommandCallback} callback Function to execute when the command occurs.
+ * @param {Object} scope Optional scope to execute the function in.
+ * @example
+ * // Adds a custom command that later can be executed using execCommand
+ * tinymce.init({
+ * ...
+ *
+ * setup: function(ed) {
+ * // Register example command
+ * ed.addCommand('mycommand', function(ui, v) {
+ * ed.windowManager.alert('Hello world!! Selection: ' + ed.selection.getContent({format: 'text'}));
+ * });
+ * }
+ * });
+ */
+ addCommand: function(name, callback, scope) {
+ /**
+ * Callback function that gets called when a command is executed.
+ *
+ * @callback addCommandCallback
+ * @param {Boolean} ui Display UI state true/false.
+ * @param {Object} value Optional value for command.
+ * @return {Boolean} True/false state if the command was handled or not.
+ */
+ this.editorCommands.addCommand(name, callback, scope);
+ },
+
+ /**
+ * Adds a custom query state command to the editor, you can also override existing commands with this method.
+ * The command that you add can be executed with queryCommandState function.
+ *
+ * @method addQueryStateHandler
+ * @param {String} name Command name to add/override.
+ * @param {addQueryStateHandlerCallback} callback Function to execute when the command state retrieval occurs.
+ * @param {Object} scope Optional scope to execute the function in.
+ */
+ addQueryStateHandler: function(name, callback, scope) {
+ /**
+ * Callback function that gets called when a queryCommandState is executed.
+ *
+ * @callback addQueryStateHandlerCallback
+ * @return {Boolean} True/false state if the command is enabled or not like is it bold.
+ */
+ this.editorCommands.addQueryStateHandler(name, callback, scope);
+ },
+
+ /**
+ * Adds a custom query value command to the editor, you can also override existing commands with this method.
+ * The command that you add can be executed with queryCommandValue function.
+ *
+ * @method addQueryValueHandler
+ * @param {String} name Command name to add/override.
+ * @param {addQueryValueHandlerCallback} callback Function to execute when the command value retrieval occurs.
+ * @param {Object} scope Optional scope to execute the function in.
+ */
+ addQueryValueHandler: function(name, callback, scope) {
+ /**
+ * Callback function that gets called when a queryCommandValue is executed.
+ *
+ * @callback addQueryValueHandlerCallback
+ * @return {Object} Value of the command or undefined.
+ */
+ this.editorCommands.addQueryValueHandler(name, callback, scope);
+ },
+
+ /**
+ * Adds a keyboard shortcut for some command or function.
+ *
+ * @method addShortcut
+ * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
+ * @param {String} desc Text description for the command.
+ * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed.
+ * @param {Object} sc Optional scope to execute the function in.
+ * @return {Boolean} true/false state if the shortcut was added or not.
+ */
+ addShortcut: function(pattern, desc, cmdFunc, scope) {
+ this.shortcuts.add(pattern, desc, cmdFunc, scope);
+ },
+
+ /**
+ * Executes a command on the current instance. These commands can be TinyMCE internal commands prefixed with "mce" or
+ * they can be build in browser commands such as "Bold". A compleate list of browser commands is available on MSDN or Mozilla.org.
+ * This function will dispatch the execCommand function on each plugin, theme or the execcommand_callback option if none of these
+ * return true it will handle the command as a internal browser command.
+ *
+ * @method execCommand
+ * @param {String} cmd Command name to execute, for example mceLink or Bold.
+ * @param {Boolean} ui True/false state if a UI (dialog) should be presented or not.
+ * @param {mixed} value Optional command value, this can be anything.
+ * @param {Object} args Optional arguments object.
+ */
+ execCommand: function(cmd, ui, value, args) {
+ return this.editorCommands.execCommand(cmd, ui, value, args);
+ },
+
+ /**
+ * Returns a command specific state, for example if bold is enabled or not.
+ *
+ * @method queryCommandState
+ * @param {string} cmd Command to query state from.
+ * @return {Boolean} Command specific state, for example if bold is enabled or not.
+ */
+ queryCommandState: function(cmd) {
+ return this.editorCommands.queryCommandState(cmd);
+ },
+
+ /**
+ * Returns a command specific value, for example the current font size.
+ *
+ * @method queryCommandValue
+ * @param {string} cmd Command to query value from.
+ * @return {Object} Command specific value, for example the current font size.
+ */
+ queryCommandValue: function(cmd) {
+ return this.editorCommands.queryCommandValue(cmd);
+ },
+
+ /**
+ * Returns true/false if the command is supported or not.
+ *
+ * @method queryCommandSupported
+ * @param {String} cmd Command that we check support for.
+ * @return {Boolean} true/false if the command is supported or not.
+ */
+ queryCommandSupported: function(cmd) {
+ return this.editorCommands.queryCommandSupported(cmd);
+ },
+
+ /**
+ * Shows the editor and hides any textarea/div that the editor is supposed to replace.
+ *
+ * @method show
+ */
+ show: function() {
+ var self = this;
+
+ if (self.hidden) {
+ self.hidden = false;
+
+ if (self.inline) {
+ self.getBody().contentEditable = true;
+ } else {
+ DOM.show(self.getContainer());
+ DOM.hide(self.id);
+ }
+
+ self.load();
+ self.fire('show');
+ }
+ },
+
+ /**
+ * Hides the editor and shows any textarea/div that the editor is supposed to replace.
+ *
+ * @method hide
+ */
+ hide: function() {
+ var self = this, doc = self.getDoc();
+
+ if (!self.hidden) {
+ // Fixed bug where IE has a blinking cursor left from the editor
+ if (ie && doc && !self.inline) {
+ doc.execCommand('SelectAll');
+ }
+
+ // We must save before we hide so Safari doesn't crash
+ self.save();
+
+ if (self.inline) {
+ self.getBody().contentEditable = false;
+
+ // Make sure the editor gets blurred
+ if (self == self.editorManager.focusedEditor) {
+ self.editorManager.focusedEditor = null;
+ }
+ } else {
+ DOM.hide(self.getContainer());
+ DOM.setStyle(self.id, 'display', self.orgDisplay);
+ }
+
+ self.hidden = true;
+ self.fire('hide');
+ }
+ },
+
+ /**
+ * Returns true/false if the editor is hidden or not.
+ *
+ * @method isHidden
+ * @return {Boolean} True/false if the editor is hidden or not.
+ */
+ isHidden: function() {
+ return !!this.hidden;
+ },
+
+ /**
+ * Sets the progress state, this will display a throbber/progess for the editor.
+ * This is ideal for asynchronous operations like an AJAX save call.
+ *
+ * @method setProgressState
+ * @param {Boolean} state Boolean state if the progress should be shown or hidden.
+ * @param {Number} time Optional time to wait before the progress gets shown.
+ * @return {Boolean} Same as the input state.
+ * @example
+ * // Show progress for the active editor
+ * tinymce.activeEditor.setProgressState(true);
+ *
+ * // Hide progress for the active editor
+ * tinymce.activeEditor.setProgressState(false);
+ *
+ * // Show progress after 3 seconds
+ * tinymce.activeEditor.setProgressState(true, 3000);
+ */
+ setProgressState: function(state, time) {
+ this.fire('ProgressState', {state: state, time: time});
+ },
+
+ /**
+ * Loads contents from the textarea or div element that got converted into an editor instance.
+ * This method will move the contents from that textarea or div into the editor by using setContent
+ * so all events etc that method has will get dispatched as well.
+ *
+ * @method load
+ * @param {Object} args Optional content object, this gets passed around through the whole load process.
+ * @return {String} HTML string that got set into the editor.
+ */
+ load: function(args) {
+ var self = this, elm = self.getElement(), html;
+
+ if (elm) {
+ args = args || {};
+ args.load = true;
+
+ html = self.setContent(elm.value !== undefined ? elm.value : elm.innerHTML, args);
+ args.element = elm;
+
+ if (!args.no_events) {
+ self.fire('LoadContent', args);
+ }
+
+ args.element = elm = null;
+
+ return html;
+ }
+ },
+
+ /**
+ * Saves the contents from a editor out to the textarea or div element that got converted into an editor instance.
+ * This method will move the HTML contents from the editor into that textarea or div by getContent
+ * so all events etc that method has will get dispatched as well.
+ *
+ * @method save
+ * @param {Object} args Optional content object, this gets passed around through the whole save process.
+ * @return {String} HTML string that got set into the textarea/div.
+ */
+ save: function(args) {
+ var self = this, elm = self.getElement(), html, form;
+
+ if (!elm || !self.initialized) {
+ return;
+ }
+
+ args = args || {};
+ args.save = true;
+
+ args.element = elm;
+ html = args.content = self.getContent(args);
+
+ if (!args.no_events) {
+ self.fire('SaveContent', args);
+ }
+
+ // Always run this internal event
+ if (args.format == 'raw') {
+ self.fire('RawSaveContent', args);
+ }
+
+ html = args.content;
+
+ if (!/TEXTAREA|INPUT/i.test(elm.nodeName)) {
+ // Update DIV element when not in inline mode
+ if (!self.inline) {
+ elm.innerHTML = html;
+ }
+
+ // Update hidden form element
+ if ((form = DOM.getParent(self.id, 'form'))) {
+ each(form.elements, function(elm) {
+ if (elm.name == self.id) {
+ elm.value = html;
+ return false;
+ }
+ });
+ }
+ } else {
+ elm.value = html;
+ }
+
+ args.element = elm = null;
+
+ if (args.set_dirty !== false) {
+ self.setDirty(false);
+ }
+
+ return html;
+ },
+
+ /**
+ * Sets the specified content to the editor instance, this will cleanup the content before it gets set using
+ * the different cleanup rules options.
+ *
+ * @method setContent
+ * @param {String} content Content to set to editor, normally HTML contents but can be other formats as well.
+ * @param {Object} args Optional content object, this gets passed around through the whole set process.
+ * @return {String} HTML string that got set into the editor.
+ * @example
+ * // Sets the HTML contents of the activeEditor editor
+ * tinymce.activeEditor.setContent('<span>some</span> html');
+ *
+ * // Sets the raw contents of the activeEditor editor
+ * tinymce.activeEditor.setContent('<span>some</span> html', {format: 'raw'});
+ *
+ * // Sets the content of a specific editor (my_editor in this example)
+ * tinymce.get('my_editor').setContent(data);
+ *
+ * // Sets the bbcode contents of the activeEditor editor if the bbcode plugin was added
+ * tinymce.activeEditor.setContent('[b]some[/b] html', {format: 'bbcode'});
+ */
+ setContent: function(content, args) {
+ var self = this, body = self.getBody(), forcedRootBlockName, padd;
+
+ // Setup args object
+ args = args || {};
+ args.format = args.format || 'html';
+ args.set = true;
+ args.content = content;
+
+ // Do preprocessing
+ if (!args.no_events) {
+ self.fire('BeforeSetContent', args);
+ }
+
+ content = args.content;
+
+ // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content
+ // It will also be impossible to place the caret in the editor unless there is a BR element present
+ if (content.length === 0 || /^\s+$/.test(content)) {
+ padd = ie && ie < 11 ? '' : '<br data-mce-bogus="1">';
+
+ // Todo: There is a lot more root elements that need special padding
+ // so separate this and add all of them at some point.
+ if (body.nodeName == 'TABLE') {
+ content = '<tr><td>' + padd + '</td></tr>';
+ } else if (/^(UL|OL)$/.test(body.nodeName)) {
+ content = '<li>' + padd + '</li>';
+ }
+
+ forcedRootBlockName = self.settings.forced_root_block;
+
+ // Check if forcedRootBlock is configured and that the block is a valid child of the body
+ if (forcedRootBlockName && self.schema.isValidChild(body.nodeName.toLowerCase(), forcedRootBlockName.toLowerCase())) {
+ // Padd with bogus BR elements on modern browsers and IE 7 and 8 since they don't render empty P tags properly
+ content = padd;
+ content = self.dom.createHTML(forcedRootBlockName, self.settings.forced_root_block_attrs, content);
+ } else if (!ie && !content) {
+ // We need to add a BR when forced_root_block is disabled on non IE browsers to place the caret
+ content = '<br data-mce-bogus="1">';
+ }
+
+ self.dom.setHTML(body, content);
+
+ self.fire('SetContent', args);
+ } else {
+ // Parse and serialize the html
+ if (args.format !== 'raw') {
+ content = new Serializer({
+ validate: self.validate
+ }, self.schema).serialize(
+ self.parser.parse(content, {isRootContent: true})
+ );
+ }
+
+ // Set the new cleaned contents to the editor
+ args.content = trim(content);
+ self.dom.setHTML(body, args.content);
+
+ // Do post processing
+ if (!args.no_events) {
+ self.fire('SetContent', args);
+ }
+
+ // Don't normalize selection if the focused element isn't the body in
+ // content editable mode since it will steal focus otherwise
+ /*if (!self.settings.content_editable || document.activeElement === self.getBody()) {
+ self.selection.normalize();
+ }*/
+ }
+
+ return args.content;
+ },
+
+ /**
+ * Gets the content from the editor instance, this will cleanup the content before it gets returned using
+ * the different cleanup rules options.
+ *
+ * @method getContent
+ * @param {Object} args Optional content object, this gets passed around through the whole get process.
+ * @return {String} Cleaned content string, normally HTML contents.
+ * @example
+ * // Get the HTML contents of the currently active editor
+ * console.debug(tinymce.activeEditor.getContent());
+ *
+ * // Get the raw contents of the currently active editor
+ * tinymce.activeEditor.getContent({format: 'raw'});
+ *
+ * // Get content of a specific editor:
+ * tinymce.get('content id').getContent()
+ */
+ getContent: function(args) {
+ var self = this, content, body = self.getBody();
+
+ // Setup args object
+ args = args || {};
+ args.format = args.format || 'html';
+ args.get = true;
+ args.getInner = true;
+
+ // Do preprocessing
+ if (!args.no_events) {
+ self.fire('BeforeGetContent', args);
+ }
+
+ // Get raw contents or by default the cleaned contents
+ if (args.format == 'raw') {
+ content = self.serializer.getTrimmedContent();
+ } else if (args.format == 'text') {
+ content = body.innerText || body.textContent;
+ } else {
+ content = self.serializer.serialize(body, args);
+ }
+
+ // Trim whitespace in beginning/end of HTML
+ if (args.format != 'text') {
+ args.content = trim(content);
+ } else {
+ args.content = content;
+ }
+
+ // Do post processing
+ if (!args.no_events) {
+ self.fire('GetContent', args);
+ }
+
+ return args.content;
+ },
+
+ /**
+ * Inserts content at caret position.
+ *
+ * @method insertContent
+ * @param {String} content Content to insert.
+ * @param {Object} args Optional args to pass to insert call.
+ */
+ insertContent: function(content, args) {
+ if (args) {
+ content = extend({content: content}, args);
+ }
+
+ this.execCommand('mceInsertContent', false, content);
+ },
+
+ /**
+ * Returns true/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents.
+ *
+ * The dirty state is automatically set to true if you do modifications to the content in other
+ * words when new undo levels is created or if you undo/redo to update the contents of the editor. It will also be set
+ * to false if you call editor.save().
+ *
+ * @method isDirty
+ * @return {Boolean} True/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents.
+ * @example
+ * if (tinymce.activeEditor.isDirty())
+ * alert("You must save your contents.");
+ */
+ isDirty: function() {
+ return !this.isNotDirty;
+ },
+
+ /**
+ * Explicitly sets the dirty state. This will fire the dirty event if the editor dirty state is changed from false to true
+ * by invoking this method.
+ *
+ * @method setDirty
+ * @param {Boolean} state True/false if the editor is considered dirty.
+ * @example
+ * function ajaxSave() {
+ * var editor = tinymce.get('elm1');
+ *
+ * // Save contents using some XHR call
+ * alert(editor.getContent());
+ *
+ * editor.setDirty(false); // Force not dirty state
+ * }
+ */
+ setDirty: function(state) {
+ var oldState = !this.isNotDirty;
+
+ this.isNotDirty = !state;
+
+ if (state && state != oldState) {
+ this.fire('dirty');
+ }
+ },
+
+ /**
+ * Sets the editor mode. Mode can be for example "design", "code" or "readonly".
+ *
+ * @method setMode
+ * @param {String} mode Mode to set the editor in.
+ */
+ setMode: function(mode) {
+ Mode.setMode(this, mode);
+ },
+
+ /**
+ * Returns the editors container element. The container element wrappes in
+ * all the elements added to the page for the editor. Such as UI, iframe etc.
+ *
+ * @method getContainer
+ * @return {Element} HTML DOM element for the editor container.
+ */
+ getContainer: function() {
+ var self = this;
+
+ if (!self.container) {
+ self.container = DOM.get(self.editorContainer || self.id + '_parent');
+ }
+
+ return self.container;
+ },
+
+ /**
+ * Returns the editors content area container element. The this element is the one who
+ * holds the iframe or the editable element.
+ *
+ * @method getContentAreaContainer
+ * @return {Element} HTML DOM element for the editor area container.
+ */
+ getContentAreaContainer: function() {
+ return this.contentAreaContainer;
+ },
+
+ /**
+ * Returns the target element/textarea that got replaced with a TinyMCE editor instance.
+ *
+ * @method getElement
+ * @return {Element} HTML DOM element for the replaced element.
+ */
+ getElement: function() {
+ if (!this.targetElm) {
+ this.targetElm = DOM.get(this.id);
+ }
+
+ return this.targetElm;
+ },
+
+ /**
+ * Returns the iframes window object.
+ *
+ * @method getWin
+ * @return {Window} Iframe DOM window object.
+ */
+ getWin: function() {
+ var self = this, elm;
+
+ if (!self.contentWindow) {
+ elm = self.iframeElement;
+
+ if (elm) {
+ self.contentWindow = elm.contentWindow;
+ }
+ }
+
+ return self.contentWindow;
+ },
+
+ /**
+ * Returns the iframes document object.
+ *
+ * @method getDoc
+ * @return {Document} Iframe DOM document object.
+ */
+ getDoc: function() {
+ var self = this, win;
+
+ if (!self.contentDocument) {
+ win = self.getWin();
+
+ if (win) {
+ self.contentDocument = win.document;
+ }
+ }
+
+ return self.contentDocument;
+ },
+
+ /**
+ * Returns the root element of the editable area.
+ * For a non-inline iframe-based editor, returns the iframe's body element.
+ *
+ * @method getBody
+ * @return {Element} The root element of the editable area.
+ */
+ getBody: function() {
+ var doc = this.getDoc();
+ return this.bodyElement || (doc ? doc.body : null);
+ },
+
+ /**
+ * URL converter function this gets executed each time a user adds an img, a or
+ * any other element that has a URL in it. This will be called both by the DOM and HTML
+ * manipulation functions.
+ *
+ * @method convertURL
+ * @param {string} url URL to convert.
+ * @param {string} name Attribute name src, href etc.
+ * @param {string/HTMLElement} elm Tag name or HTML DOM element depending on HTML or DOM insert.
+ * @return {string} Converted URL string.
+ */
+ convertURL: function(url, name, elm) {
+ var self = this, settings = self.settings;
+
+ // Use callback instead
+ if (settings.urlconverter_callback) {
+ return self.execCallback('urlconverter_callback', url, elm, true, name);
+ }
+
+ // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs
+ if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0 || url.length === 0) {
+ return url;
+ }
+
+ // Convert to relative
+ if (settings.relative_urls) {
+ return self.documentBaseURI.toRelative(url);
+ }
+
+ // Convert to absolute
+ url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host);
+
+ return url;
+ },
+
+ /**
+ * Adds visual aid for tables, anchors etc so they can be more easily edited inside the editor.
+ *
+ * @method addVisual
+ * @param {Element} elm Optional root element to loop though to find tables etc that needs the visual aid.
+ */
+ addVisual: function(elm) {
+ var self = this, settings = self.settings, dom = self.dom, cls;
+
+ elm = elm || self.getBody();
+
+ if (self.hasVisual === undefined) {
+ self.hasVisual = settings.visual;
+ }
+
+ each(dom.select('table,a', elm), function(elm) {
+ var value;
+
+ switch (elm.nodeName) {
+ case 'TABLE':
+ cls = settings.visual_table_class || 'mce-item-table';
+ value = dom.getAttrib(elm, 'border');
+
+ if ((!value || value == '0') && self.hasVisual) {
+ dom.addClass(elm, cls);
+ } else {
+ dom.removeClass(elm, cls);
+ }
+
+ return;
+
+ case 'A':
+ if (!dom.getAttrib(elm, 'href', false)) {
+ value = dom.getAttrib(elm, 'name') || elm.id;
+ cls = settings.visual_anchor_class || 'mce-item-anchor';
+
+ if (value && self.hasVisual) {
+ dom.addClass(elm, cls);
+ } else {
+ dom.removeClass(elm, cls);
+ }
+ }
+
+ return;
+ }
+ });
+
+ self.fire('VisualAid', {element: elm, hasVisual: self.hasVisual});
+ },
+
+ /**
+ * Removes the editor from the dom and tinymce collection.
+ *
+ * @method remove
+ */
+ remove: function() {
+ var self = this;
+
+ if (!self.removed) {
+ self.save();
+ self.removed = 1;
+ self.unbindAllNativeEvents();
+
+ // Remove any hidden input
+ if (self.hasHiddenInput) {
+ DOM.remove(self.getElement().nextSibling);
+ }
+
+ if (!self.inline) {
+ // IE 9 has a bug where the selection stops working if you place the
+ // caret inside the editor then remove the iframe
+ if (ie && ie < 10) {
+ self.getDoc().execCommand('SelectAll', false, null);
+ }
+
+ DOM.setStyle(self.id, 'display', self.orgDisplay);
+ self.getBody().onload = null; // Prevent #6816
+ }
+
+ self.fire('remove');
+
+ self.editorManager.remove(self);
+ DOM.remove(self.getContainer());
+ self._selectionOverrides.destroy();
+ self.editorUpload.destroy();
+ self.destroy();
+ }
+ },
+
+ /**
+ * Destroys the editor instance by removing all events, element references or other resources
+ * that could leak memory. This method will be called automatically when the page is unloaded
+ * but you can also call it directly if you know what you are doing.
+ *
+ * @method destroy
+ * @param {Boolean} automatic Optional state if the destroy is an automatic destroy or user called one.
+ */
+ destroy: function(automatic) {
+ var self = this, form;
+
+ // One time is enough
+ if (self.destroyed) {
+ return;
+ }
+
+ // If user manually calls destroy and not remove
+ // Users seems to have logic that calls destroy instead of remove
+ if (!automatic && !self.removed) {
+ self.remove();
+ return;
+ }
+
+ if (!automatic) {
+ self.editorManager.off('beforeunload', self._beforeUnload);
+
+ // Manual destroy
+ if (self.theme && self.theme.destroy) {
+ self.theme.destroy();
+ }
+
+ // Destroy controls, selection and dom
+ self.selection.destroy();
+ self.dom.destroy();
+ }
+
+ form = self.formElement;
+ if (form) {
+ if (form._mceOldSubmit) {
+ form.submit = form._mceOldSubmit;
+ form._mceOldSubmit = null;
+ }
+
+ DOM.unbind(form, 'submit reset', self.formEventDelegate);
+ }
+
+ self.contentAreaContainer = self.formElement = self.container = self.editorContainer = null;
+ self.bodyElement = self.contentDocument = self.contentWindow = null;
+ self.iframeElement = self.targetElm = null;
+
+ if (self.selection) {
+ self.selection = self.selection.win = self.selection.dom = self.selection.dom.doc = null;
+ }
+
+ self.destroyed = 1;
+ },
+
+ /**
+ * Uploads all data uri/blob uri images in the editor contents to server.
+ *
+ * @method uploadImages
+ * @param {function} callback Optional callback with images and status for each image.
+ * @return {tinymce.util.Promise} Promise instance.
+ */
+ uploadImages: function(callback) {
+ return this.editorUpload.uploadImages(callback);
+ },
+
+ // Internal functions
+
+ _scanForImages: function() {
+ return this.editorUpload.scanForImages();
+ }
+ };
+
+ extend(Editor.prototype, EditorObservable);
+
+ return Editor;
+});
+
+// Included from: js/tinymce/classes/util/I18n.js
+
+/**
+ * I18n.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * I18n class that handles translation of TinyMCE UI.
+ * Uses po style with csharp style parameters.
+ *
+ * @class tinymce.util.I18n
+ */
+define("tinymce/util/I18n", [
+ "tinymce/util/Tools"
+], function(Tools) {
+ "use strict";
+
+ var data = {}, code = "en";
+
+ return {
+ /**
+ * Sets the current language code.
+ *
+ * @method setCode
+ * @param {String} newCode Current language code.
+ */
+ setCode: function(newCode) {
+ if (newCode) {
+ code = newCode;
+ this.rtl = this.data[newCode] ? this.data[newCode]._dir === 'rtl' : false;
+ }
+ },
+
+ /**
+ * Returns the current language code.
+ *
+ * @method getCode
+ * @return {String} Current language code.
+ */
+ getCode: function() {
+ return code;
+ },
+
+ /**
+ * Property gets set to true if a RTL language pack was loaded.
+ *
+ * @property rtl
+ * @type Boolean
+ */
+ rtl: false,
+
+ /**
+ * Adds translations for a specific language code.
+ *
+ * @method add
+ * @param {String} code Language code like sv_SE.
+ * @param {Array} items Name/value array with English en_US to sv_SE.
+ */
+ add: function(code, items) {
+ var langData = data[code];
+
+ if (!langData) {
+ data[code] = langData = {};
+ }
+
+ for (var name in items) {
+ langData[name] = items[name];
+ }
+
+ this.setCode(code);
+ },
+
+ /**
+ * Translates the specified text.
+ *
+ * It has a few formats:
+ * I18n.translate("Text");
+ * I18n.translate(["Text {0}/{1}", 0, 1]);
+ * I18n.translate({raw: "Raw string"});
+ *
+ * @method translate
+ * @param {String/Object/Array} text Text to translate.
+ * @return {String} String that got translated.
+ */
+ translate: function(text) {
+ var langData = data[code] || {};
+
+ /**
+ * number - string
+ * null, undefined and empty string - empty string
+ * array - comma-delimited string
+ * object - in [object Object]
+ * function - in [object Function]
+ *
+ * @param obj
+ * @returns {string}
+ */
+ function toString(obj) {
+ if (Tools.is(obj, 'function')) {
+ return Object.prototype.toString.call(obj);
+ }
+ return !isEmpty(obj) ? '' + obj : '';
+ }
+
+ function isEmpty(text) {
+ return text === '' || text === null || Tools.is(text, 'undefined');
+ }
+
+ function getLangData(text) {
+ // make sure we work on a string and return a string
+ text = toString(text);
+ return Tools.hasOwn(langData, text) ? toString(langData[text]) : text;
+ }
+
+
+ if (isEmpty(text)) {
+ return '';
+ }
+
+ if (Tools.is(text, 'object') && Tools.hasOwn(text, 'raw')) {
+ return toString(text.raw);
+ }
+
+ if (Tools.is(text, 'array')) {
+ var values = text.slice(1);
+ text = getLangData(text[0]).replace(/\{([0-9]+)\}/g, function($1, $2) {
+ return Tools.hasOwn(values, $2) ? toString(values[$2]) : $1;
+ });
+ }
+
+ return getLangData(text).replace(/{context:\w+}$/, '');
+ },
+
+ data: data
+ };
+});
+
+// Included from: js/tinymce/classes/FocusManager.js
+
+/**
+ * FocusManager.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class manages the focus/blur state of the editor. This class is needed since some
+ * browsers fire false focus/blur states when the selection is moved to a UI dialog or similar.
+ *
+ * This class will fire two events focus and blur on the editor instances that got affected.
+ * It will also handle the restore of selection when the focus is lost and returned.
+ *
+ * @class tinymce.FocusManager
+ */
+define("tinymce/FocusManager", [
+ "tinymce/dom/DOMUtils",
+ "tinymce/util/Delay",
+ "tinymce/Env"
+], function(DOMUtils, Delay, Env) {
+ var selectionChangeHandler, documentFocusInHandler, documentMouseUpHandler, DOM = DOMUtils.DOM;
+
+ /**
+ * Constructs a new focus manager instance.
+ *
+ * @constructor FocusManager
+ * @param {tinymce.EditorManager} editorManager Editor manager instance to handle focus for.
+ */
+ function FocusManager(editorManager) {
+ function getActiveElement() {
+ try {
+ return document.activeElement;
+ } catch (ex) {
+ // IE sometimes fails to get the activeElement when resizing table
+ // TODO: Investigate this
+ return document.body;
+ }
+ }
+
+ // We can't store a real range on IE 11 since it gets mutated so we need to use a bookmark object
+ // TODO: Move this to a separate range utils class since it's it's logic is present in Selection as well.
+ function createBookmark(dom, rng) {
+ if (rng && rng.startContainer) {
+ // Verify that the range is within the root of the editor
+ if (!dom.isChildOf(rng.startContainer, dom.getRoot()) || !dom.isChildOf(rng.endContainer, dom.getRoot())) {
+ return;
+ }
+
+ return {
+ startContainer: rng.startContainer,
+ startOffset: rng.startOffset,
+ endContainer: rng.endContainer,
+ endOffset: rng.endOffset
+ };
+ }
+
+ return rng;
+ }
+
+ function bookmarkToRng(editor, bookmark) {
+ var rng;
+
+ if (bookmark.startContainer) {
+ rng = editor.getDoc().createRange();
+ rng.setStart(bookmark.startContainer, bookmark.startOffset);
+ rng.setEnd(bookmark.endContainer, bookmark.endOffset);
+ } else {
+ rng = bookmark;
+ }
+
+ return rng;
+ }
+
+ function isUIElement(elm) {
+ return !!DOM.getParent(elm, FocusManager.isEditorUIElement);
+ }
+
+ function registerEvents(e) {
+ var editor = e.editor;
+
+ editor.on('init', function() {
+ // Gecko/WebKit has ghost selections in iframes and IE only has one selection per browser tab
+ if (editor.inline || Env.ie) {
+ // Use the onbeforedeactivate event when available since it works better see #7023
+ if ("onbeforedeactivate" in document && Env.ie < 9) {
+ editor.dom.bind(editor.getBody(), 'beforedeactivate', function(e) {
+ if (e.target != editor.getBody()) {
+ return;
+ }
+
+ try {
+ editor.lastRng = editor.selection.getRng();
+ } catch (ex) {
+ // IE throws "Unexcpected call to method or property access" some times so lets ignore it
+ }
+ });
+ } else {
+ // On other browsers take snapshot on nodechange in inline mode since they have Ghost selections for iframes
+ editor.on('nodechange mouseup keyup', function(e) {
+ var node = getActiveElement();
+
+ // Only act on manual nodechanges
+ if (e.type == 'nodechange' && e.selectionChange) {
+ return;
+ }
+
+ // IE 11 reports active element as iframe not body of iframe
+ if (node && node.id == editor.id + '_ifr') {
+ node = editor.getBody();
+ }
+
+ if (editor.dom.isChildOf(node, editor.getBody())) {
+ editor.lastRng = editor.selection.getRng();
+ }
+ });
+ }
+
+ // Handles the issue with WebKit not retaining selection within inline document
+ // If the user releases the mouse out side the body since a mouse up event wont occur on the body
+ if (Env.webkit && !selectionChangeHandler) {
+ selectionChangeHandler = function() {
+ var activeEditor = editorManager.activeEditor;
+
+ if (activeEditor && activeEditor.selection) {
+ var rng = activeEditor.selection.getRng();
+
+ // Store when it's non collapsed
+ if (rng && !rng.collapsed) {
+ editor.lastRng = rng;
+ }
+ }
+ };
+
+ DOM.bind(document, 'selectionchange', selectionChangeHandler);
+ }
+ }
+ });
+
+ editor.on('setcontent', function() {
+ editor.lastRng = null;
+ });
+
+ // Remove last selection bookmark on mousedown see #6305
+ editor.on('mousedown', function() {
+ editor.selection.lastFocusBookmark = null;
+ });
+
+ editor.on('focusin', function() {
+ var focusedEditor = editorManager.focusedEditor, lastRng;
+
+ if (editor.selection.lastFocusBookmark) {
+ lastRng = bookmarkToRng(editor, editor.selection.lastFocusBookmark);
+ editor.selection.lastFocusBookmark = null;
+ editor.selection.setRng(lastRng);
+ }
+
+ if (focusedEditor != editor) {
+ if (focusedEditor) {
+ focusedEditor.fire('blur', {focusedEditor: editor});
+ }
+
+ editorManager.setActive(editor);
+ editorManager.focusedEditor = editor;
+ editor.fire('focus', {blurredEditor: focusedEditor});
+ editor.focus(true);
+ }
+
+ editor.lastRng = null;
+ });
+
+ editor.on('focusout', function() {
+ Delay.setEditorTimeout(editor, function() {
+ var focusedEditor = editorManager.focusedEditor;
+
+ // Still the same editor the blur was outside any editor UI
+ if (!isUIElement(getActiveElement()) && focusedEditor == editor) {
+ editor.fire('blur', {focusedEditor: null});
+ editorManager.focusedEditor = null;
+
+ // Make sure selection is valid could be invalid if the editor is blured and removed before the timeout occurs
+ if (editor.selection) {
+ editor.selection.lastFocusBookmark = null;
+ }
+ }
+ });
+ });
+
+ // Check if focus is moved to an element outside the active editor by checking if the target node
+ // isn't within the body of the activeEditor nor a UI element such as a dialog child control
+ if (!documentFocusInHandler) {
+ documentFocusInHandler = function(e) {
+ var activeEditor = editorManager.activeEditor, target;
+
+ target = e.target;
+
+ if (activeEditor && target.ownerDocument == document) {
+ // Check to make sure we have a valid selection don't update the bookmark if it's
+ // a focusin to the body of the editor see #7025
+ if (activeEditor.selection && target != activeEditor.getBody()) {
+ activeEditor.selection.lastFocusBookmark = createBookmark(activeEditor.dom, activeEditor.lastRng);
+ }
+
+ // Fire a blur event if the element isn't a UI element
+ if (target != document.body && !isUIElement(target) && editorManager.focusedEditor == activeEditor) {
+ activeEditor.fire('blur', {focusedEditor: null});
+ editorManager.focusedEditor = null;
+ }
+ }
+ };
+
+ DOM.bind(document, 'focusin', documentFocusInHandler);
+ }
+
+ // Handle edge case when user starts the selection inside the editor and releases
+ // the mouse outside the editor producing a new selection. This weird workaround is needed since
+ // Gecko doesn't have the "selectionchange" event we need to do this. Fixes: #6843
+ if (editor.inline && !documentMouseUpHandler) {
+ documentMouseUpHandler = function(e) {
+ var activeEditor = editorManager.activeEditor, dom = activeEditor.dom;
+
+ if (activeEditor.inline && dom && !dom.isChildOf(e.target, activeEditor.getBody())) {
+ var rng = activeEditor.selection.getRng();
+
+ if (!rng.collapsed) {
+ activeEditor.lastRng = rng;
+ }
+ }
+ };
+
+ DOM.bind(document, 'mouseup', documentMouseUpHandler);
+ }
+ }
+
+ function unregisterDocumentEvents(e) {
+ if (editorManager.focusedEditor == e.editor) {
+ editorManager.focusedEditor = null;
+ }
+
+ if (!editorManager.activeEditor) {
+ DOM.unbind(document, 'selectionchange', selectionChangeHandler);
+ DOM.unbind(document, 'focusin', documentFocusInHandler);
+ DOM.unbind(document, 'mouseup', documentMouseUpHandler);
+ selectionChangeHandler = documentFocusInHandler = documentMouseUpHandler = null;
+ }
+ }
+
+ editorManager.on('AddEditor', registerEvents);
+ editorManager.on('RemoveEditor', unregisterDocumentEvents);
+ }
+
+ /**
+ * Returns true if the specified element is part of the UI for example an button or text input.
+ *
+ * @method isEditorUIElement
+ * @param {Element} elm Element to check if it's part of the UI or not.
+ * @return {Boolean} True/false state if the element is part of the UI or not.
+ */
+ FocusManager.isEditorUIElement = function(elm) {
+ // Needs to be converted to string since svg can have focus: #6776
+ return elm.className.toString().indexOf('mce-') !== -1;
+ };
+
+ return FocusManager;
+});
+
+// Included from: js/tinymce/classes/EditorManager.js
+
+/**
+ * EditorManager.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class used as a factory for manager for tinymce.Editor instances.
+ *
+ * @example
+ * tinymce.EditorManager.init({});
+ *
+ * @class tinymce.EditorManager
+ * @mixes tinymce.util.Observable
+ * @static
+ */
+define("tinymce/EditorManager", [
+ "tinymce/Editor",
+ "tinymce/dom/DomQuery",
+ "tinymce/dom/DOMUtils",
+ "tinymce/util/URI",
+ "tinymce/Env",
+ "tinymce/util/Tools",
+ "tinymce/util/Promise",
+ "tinymce/util/Observable",
+ "tinymce/util/I18n",
+ "tinymce/FocusManager",
+ "tinymce/AddOnManager"
+], function(Editor, $, DOMUtils, URI, Env, Tools, Promise, Observable, I18n, FocusManager, AddOnManager) {
+ var DOM = DOMUtils.DOM;
+ var explode = Tools.explode, each = Tools.each, extend = Tools.extend;
+ var instanceCounter = 0, beforeUnloadDelegate, EditorManager, boundGlobalEvents = false;
+
+ function globalEventDelegate(e) {
+ each(EditorManager.editors, function(editor) {
+ if (e.type === 'scroll') {
+ editor.fire('ScrollWindow', e);
+ } else {
+ editor.fire('ResizeWindow', e);
+ }
+ });
+ }
+
+ function toggleGlobalEvents(editors, state) {
+ if (state !== boundGlobalEvents) {
+ if (state) {
+ $(window).on('resize scroll', globalEventDelegate);
+ } else {
+ $(window).off('resize scroll', globalEventDelegate);
+ }
+
+ boundGlobalEvents = state;
+ }
+ }
+
+ function removeEditorFromList(editor) {
+ var editors = EditorManager.editors, removedFromList;
+
+ delete editors[editor.id];
+
+ for (var i = 0; i < editors.length; i++) {
+ if (editors[i] == editor) {
+ editors.splice(i, 1);
+ removedFromList = true;
+ break;
+ }
+ }
+
+ // Select another editor since the active one was removed
+ if (EditorManager.activeEditor == editor) {
+ EditorManager.activeEditor = editors[0];
+ }
+
+ // Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor
+ if (EditorManager.focusedEditor == editor) {
+ EditorManager.focusedEditor = null;
+ }
+
+ return removedFromList;
+ }
+
+ function purgeDestroyedEditor(editor) {
+ // User has manually destroyed the editor lets clean up the mess
+ if (editor && editor.initialized && !(editor.getContainer() || editor.getBody()).parentNode) {
+ removeEditorFromList(editor);
+ editor.unbindAllNativeEvents();
+ editor.destroy(true);
+ editor.removed = true;
+ editor = null;
+ }
+
+ return editor;
+ }
+
+ EditorManager = {
+ /**
+ * Dom query instance.
+ *
+ * @property $
+ * @type tinymce.dom.DomQuery
+ */
+ $: $,
+
+ /**
+ * Major version of TinyMCE build.
+ *
+ * @property majorVersion
+ * @type String
+ */
+ majorVersion: '4',
+
+ /**
+ * Minor version of TinyMCE build.
+ *
+ * @property minorVersion
+ * @type String
+ */
+ minorVersion: '5.1',
+
+ /**
+ * Release date of TinyMCE build.
+ *
+ * @property releaseDate
+ * @type String
+ */
+ releaseDate: '2016-12-07',
+
+ /**
+ * Collection of editor instances.
+ *
+ * @property editors
+ * @type Object
+ * @example
+ * for (edId in tinymce.editors)
+ * tinymce.editors[edId].save();
+ */
+ editors: [],
+
+ /**
+ * Collection of language pack data.
+ *
+ * @property i18n
+ * @type Object
+ */
+ i18n: I18n,
+
+ /**
+ * Currently active editor instance.
+ *
+ * @property activeEditor
+ * @type tinymce.Editor
+ * @example
+ * tinyMCE.activeEditor.selection.getContent();
+ * tinymce.EditorManager.activeEditor.selection.getContent();
+ */
+ activeEditor: null,
+
+ setup: function() {
+ var self = this, baseURL, documentBaseURL, suffix = "", preInit, src;
+
+ // Get base URL for the current document
+ documentBaseURL = URI.getDocumentBaseUrl(document.location);
+
+ // Check if the URL is a document based format like: http://site/dir/file and file:///
+ // leave other formats like applewebdata://... intact
+ if (/^[^:]+:\/\/\/?[^\/]+\//.test(documentBaseURL)) {
+ documentBaseURL = documentBaseURL.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
+
+ if (!/[\/\\]$/.test(documentBaseURL)) {
+ documentBaseURL += '/';
+ }
+ }
+
+ // If tinymce is defined and has a base use that or use the old tinyMCEPreInit
+ preInit = window.tinymce || window.tinyMCEPreInit;
+ if (preInit) {
+ baseURL = preInit.base || preInit.baseURL;
+ suffix = preInit.suffix;
+ } else {
+ // Get base where the tinymce script is located
+ var scripts = document.getElementsByTagName('script');
+ for (var i = 0; i < scripts.length; i++) {
+ src = scripts[i].src;
+
+ // Script types supported:
+ // tinymce.js tinymce.min.js tinymce.dev.js
+ // tinymce.jquery.js tinymce.jquery.min.js tinymce.jquery.dev.js
+ // tinymce.full.js tinymce.full.min.js tinymce.full.dev.js
+ var srcScript = src.substring(src.lastIndexOf('/'));
+ if (/tinymce(\.full|\.jquery|)(\.min|\.dev|)\.js/.test(src)) {
+ if (srcScript.indexOf('.min') != -1) {
+ suffix = '.min';
+ }
+
+ baseURL = src.substring(0, src.lastIndexOf('/'));
+ break;
+ }
+ }
+
+ // We didn't find any baseURL by looking at the script elements
+ // Try to use the document.currentScript as a fallback
+ if (!baseURL && document.currentScript) {
+ src = document.currentScript.src;
+
+ if (src.indexOf('.min') != -1) {
+ suffix = '.min';
+ }
+
+ baseURL = src.substring(0, src.lastIndexOf('/'));
+ }
+ }
+
+ /**
+ * Base URL where the root directory if TinyMCE is located.
+ *
+ * @property baseURL
+ * @type String
+ */
+ self.baseURL = new URI(documentBaseURL).toAbsolute(baseURL);
+
+ /**
+ * Document base URL where the current document is located.
+ *
+ * @property documentBaseURL
+ * @type String
+ */
+ self.documentBaseURL = documentBaseURL;
+
+ /**
+ * Absolute baseURI for the installation path of TinyMCE.
+ *
+ * @property baseURI
+ * @type tinymce.util.URI
+ */
+ self.baseURI = new URI(self.baseURL);
+
+ /**
+ * Current suffix to add to each plugin/theme that gets loaded for example ".min".
+ *
+ * @property suffix
+ * @type String
+ */
+ self.suffix = suffix;
+
+ self.focusManager = new FocusManager(self);
+ },
+
+ /**
+ * Overrides the default settings for editor instances.
+ *
+ * @method overrideDefaults
+ * @param {Object} defaultSettings Defaults settings object.
+ */
+ overrideDefaults: function(defaultSettings) {
+ var baseUrl, suffix;
+
+ baseUrl = defaultSettings.base_url;
+ if (baseUrl) {
+ this.baseURL = new URI(this.documentBaseURL).toAbsolute(baseUrl.replace(/\/+$/, ''));
+ this.baseURI = new URI(this.baseURL);
+ }
+
+ suffix = defaultSettings.suffix;
+ if (defaultSettings.suffix) {
+ this.suffix = suffix;
+ }
+
+ this.defaultSettings = defaultSettings;
+
+ var pluginBaseUrls = defaultSettings.plugin_base_urls;
+ for (var name in pluginBaseUrls) {
+ AddOnManager.PluginManager.urls[name] = pluginBaseUrls[name];
+ }
+ },
+
+ /**
+ * Initializes a set of editors. This method will create editors based on various settings.
+ *
+ * @method init
+ * @param {Object} settings Settings object to be passed to each editor instance.
+ * @return {tinymce.util.Promise} Promise that gets resolved with an array of editors when all editor instances are initialized.
+ * @example
+ * // Initializes a editor using the longer method
+ * tinymce.EditorManager.init({
+ * some_settings : 'some value'
+ * });
+ *
+ * // Initializes a editor instance using the shorter version and with a promise
+ * tinymce.init({
+ * some_settings : 'some value'
+ * }).then(function(editors) {
+ * ...
+ * });
+ */
+ init: function(settings) {
+ var self = this, result, invalidInlineTargets;
+
+ invalidInlineTargets = Tools.makeMap(
+ 'area base basefont br col frame hr img input isindex link meta param embed source wbr track ' +
+ 'colgroup option tbody tfoot thead tr script noscript style textarea video audio iframe object menu',
+ ' '
+ );
+
+ function isInvalidInlineTarget(settings, elm) {
+ return settings.inline && elm.tagName.toLowerCase() in invalidInlineTargets;
+ }
+
+ function report(msg, elm) {
+ // Log in a non test environment
+ if (window.console && !window.test) {
+ window.console.log(msg, elm);
+ }
+ }
+
+ function createId(elm) {
+ var id = elm.id;
+
+ // Use element id, or unique name or generate a unique id
+ if (!id) {
+ id = elm.name;
+
+ if (id && !DOM.get(id)) {
+ id = elm.name;
+ } else {
+ // Generate unique name
+ id = DOM.uniqueId();
+ }
+
+ elm.setAttribute('id', id);
+ }
+
+ return id;
+ }
+
+ function execCallback(name) {
+ var callback = settings[name];
+
+ if (!callback) {
+ return;
+ }
+
+ return callback.apply(self, Array.prototype.slice.call(arguments, 2));
+ }
+
+ function hasClass(elm, className) {
+ return className.constructor === RegExp ? className.test(elm.className) : DOM.hasClass(elm, className);
+ }
+
+ function findTargets(settings) {
+ var l, targets = [];
+
+ if (settings.types) {
+ each(settings.types, function(type) {
+ targets = targets.concat(DOM.select(type.selector));
+ });
+
+ return targets;
+ } else if (settings.selector) {
+ return DOM.select(settings.selector);
+ } else if (settings.target) {
+ return [settings.target];
+ }
+
+ // Fallback to old setting
+ switch (settings.mode) {
+ case "exact":
+ l = settings.elements || '';
+
+ if (l.length > 0) {
+ each(explode(l), function(id) {
+ var elm;
+
+ if ((elm = DOM.get(id))) {
+ targets.push(elm);
+ } else {
+ each(document.forms, function(f) {
+ each(f.elements, function(e) {
+ if (e.name === id) {
+ id = 'mce_editor_' + instanceCounter++;
+ DOM.setAttrib(e, 'id', id);
+ targets.push(e);
+ }
+ });
+ });
+ }
+ });
+ }
+ break;
+
+ case "textareas":
+ case "specific_textareas":
+ each(DOM.select('textarea'), function(elm) {
+ if (settings.editor_deselector && hasClass(elm, settings.editor_deselector)) {
+ return;
+ }
+
+ if (!settings.editor_selector || hasClass(elm, settings.editor_selector)) {
+ targets.push(elm);
+ }
+ });
+ break;
+ }
+
+ return targets;
+ }
+
+ var provideResults = function(editors) {
+ result = editors;
+ };
+
+ function initEditors() {
+ var initCount = 0, editors = [], targets;
+
+ function createEditor(id, settings, targetElm) {
+ var editor = new Editor(id, settings, self);
+
+ editors.push(editor);
+
+ editor.on('init', function() {
+ if (++initCount === targets.length) {
+ provideResults(editors);
+ }
+ });
+
+ editor.targetElm = editor.targetElm || targetElm;
+ editor.render();
+ }
+
+ DOM.unbind(window, 'ready', initEditors);
+ execCallback('onpageload');
+
+ targets = $.unique(findTargets(settings));
+
+ // TODO: Deprecate this one
+ if (settings.types) {
+ each(settings.types, function(type) {
+ Tools.each(targets, function(elm) {
+ if (DOM.is(elm, type.selector)) {
+ createEditor(createId(elm), extend({}, settings, type), elm);
+ return false;
+ }
+
+ return true;
+ });
+ });
+
+ return;
+ }
+
+ Tools.each(targets, function(elm) {
+ purgeDestroyedEditor(self.get(elm.id));
+ });
+
+ targets = Tools.grep(targets, function(elm) {
+ return !self.get(elm.id);
+ });
+
+ each(targets, function(elm) {
+ if (isInvalidInlineTarget(settings, elm)) {
+ report('Could not initialize inline editor on invalid inline target element', elm);
+ } else {
+ createEditor(createId(elm), settings, elm);
+ }
+ });
+ }
+
+ self.settings = settings;
+ DOM.bind(window, 'ready', initEditors);
+
+ return new Promise(function(resolve) {
+ if (result) {
+ resolve(result);
+ } else {
+ provideResults = function(editors) {
+ resolve(editors);
+ };
+ }
+ });
+ },
+
+ /**
+ * Returns a editor instance by id.
+ *
+ * @method get
+ * @param {String/Number} id Editor instance id or index to return.
+ * @return {tinymce.Editor} Editor instance to return.
+ * @example
+ * // Adds an onclick event to an editor by id (shorter version)
+ * tinymce.get('mytextbox').on('click', function(e) {
+ * ed.windowManager.alert('Hello world!');
+ * });
+ *
+ * // Adds an onclick event to an editor by id (longer version)
+ * tinymce.EditorManager.get('mytextbox').on('click', function(e) {
+ * ed.windowManager.alert('Hello world!');
+ * });
+ */
+ get: function(id) {
+ if (!arguments.length) {
+ return this.editors;
+ }
+
+ return id in this.editors ? this.editors[id] : null;
+ },
+
+ /**
+ * Adds an editor instance to the editor collection. This will also set it as the active editor.
+ *
+ * @method add
+ * @param {tinymce.Editor} editor Editor instance to add to the collection.
+ * @return {tinymce.Editor} The same instance that got passed in.
+ */
+ add: function(editor) {
+ var self = this, editors = self.editors;
+
+ // Add named and index editor instance
+ editors[editor.id] = editor;
+ editors.push(editor);
+
+ toggleGlobalEvents(editors, true);
+
+ // Doesn't call setActive method since we don't want
+ // to fire a bunch of activate/deactivate calls while initializing
+ self.activeEditor = editor;
+
+ self.fire('AddEditor', {editor: editor});
+
+ if (!beforeUnloadDelegate) {
+ beforeUnloadDelegate = function() {
+ self.fire('BeforeUnload');
+ };
+
+ DOM.bind(window, 'beforeunload', beforeUnloadDelegate);
+ }
+
+ return editor;
+ },
+
+ /**
+ * Creates an editor instance and adds it to the EditorManager collection.
+ *
+ * @method createEditor
+ * @param {String} id Instance id to use for editor.
+ * @param {Object} settings Editor instance settings.
+ * @return {tinymce.Editor} Editor instance that got created.
+ */
+ createEditor: function(id, settings) {
+ return this.add(new Editor(id, settings, this));
+ },
+
+ /**
+ * Removes a editor or editors form page.
+ *
+ * @example
+ * // Remove all editors bound to divs
+ * tinymce.remove('div');
+ *
+ * // Remove all editors bound to textareas
+ * tinymce.remove('textarea');
+ *
+ * // Remove all editors
+ * tinymce.remove();
+ *
+ * // Remove specific instance by id
+ * tinymce.remove('#id');
+ *
+ * @method remove
+ * @param {tinymce.Editor/String/Object} [selector] CSS selector or editor instance to remove.
+ * @return {tinymce.Editor} The editor that got passed in will be return if it was found otherwise null.
+ */
+ remove: function(selector) {
+ var self = this, i, editors = self.editors, editor;
+
+ // Remove all editors
+ if (!selector) {
+ for (i = editors.length - 1; i >= 0; i--) {
+ self.remove(editors[i]);
+ }
+
+ return;
+ }
+
+ // Remove editors by selector
+ if (typeof selector == "string") {
+ selector = selector.selector || selector;
+
+ each(DOM.select(selector), function(elm) {
+ editor = editors[elm.id];
+
+ if (editor) {
+ self.remove(editor);
+ }
+ });
+
+ return;
+ }
+
+ // Remove specific editor
+ editor = selector;
+
+ // Not in the collection
+ if (!editors[editor.id]) {
+ return null;
+ }
+
+ if (removeEditorFromList(editor)) {
+ self.fire('RemoveEditor', {editor: editor});
+ }
+
+ if (!editors.length) {
+ DOM.unbind(window, 'beforeunload', beforeUnloadDelegate);
+ }
+
+ editor.remove();
+
+ toggleGlobalEvents(editors, editors.length > 0);
+
+ return editor;
+ },
+
+ /**
+ * Executes a specific command on the currently active editor.
+ *
+ * @method execCommand
+ * @param {String} cmd Command to perform for example Bold.
+ * @param {Boolean} ui Optional boolean state if a UI should be presented for the command or not.
+ * @param {String} value Optional value parameter like for example an URL to a link.
+ * @return {Boolean} true/false if the command was executed or not.
+ */
+ execCommand: function(cmd, ui, value) {
+ var self = this, editor = self.get(value);
+
+ // Manager commands
+ switch (cmd) {
+ case "mceAddEditor":
+ if (!self.get(value)) {
+ new Editor(value, self.settings, self).render();
+ }
+
+ return true;
+
+ case "mceRemoveEditor":
+ if (editor) {
+ editor.remove();
+ }
+
+ return true;
+
+ case 'mceToggleEditor':
+ if (!editor) {
+ self.execCommand('mceAddEditor', 0, value);
+ return true;
+ }
+
+ if (editor.isHidden()) {
+ editor.show();
+ } else {
+ editor.hide();
+ }
+
+ return true;
+ }
+
+ // Run command on active editor
+ if (self.activeEditor) {
+ return self.activeEditor.execCommand(cmd, ui, value);
+ }
+
+ return false;
+ },
+
+ /**
+ * Calls the save method on all editor instances in the collection. This can be useful when a form is to be submitted.
+ *
+ * @method triggerSave
+ * @example
+ * // Saves all contents
+ * tinyMCE.triggerSave();
+ */
+ triggerSave: function() {
+ each(this.editors, function(editor) {
+ editor.save();
+ });
+ },
+
+ /**
+ * Adds a language pack, this gets called by the loaded language files like en.js.
+ *
+ * @method addI18n
+ * @param {String} code Optional language code.
+ * @param {Object} items Name/value object with translations.
+ */
+ addI18n: function(code, items) {
+ I18n.add(code, items);
+ },
+
+ /**
+ * Translates the specified string using the language pack items.
+ *
+ * @method translate
+ * @param {String/Array/Object} text String to translate
+ * @return {String} Translated string.
+ */
+ translate: function(text) {
+ return I18n.translate(text);
+ },
+
+ /**
+ * Sets the active editor instance and fires the deactivate/activate events.
+ *
+ * @method setActive
+ * @param {tinymce.Editor} editor Editor instance to set as the active instance.
+ */
+ setActive: function(editor) {
+ var activeEditor = this.activeEditor;
+
+ if (this.activeEditor != editor) {
+ if (activeEditor) {
+ activeEditor.fire('deactivate', {relatedTarget: editor});
+ }
+
+ editor.fire('activate', {relatedTarget: activeEditor});
+ }
+
+ this.activeEditor = editor;
+ }
+ };
+
+ extend(EditorManager, Observable);
+
+ EditorManager.setup();
+
+ // Export EditorManager as tinymce/tinymce in global namespace
+ window.tinymce = window.tinyMCE = EditorManager;
+
+ return EditorManager;
+});
+
+// Included from: js/tinymce/classes/LegacyInput.js
+
+/**
+ * LegacyInput.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Converts legacy input to modern HTML.
+ *
+ * @class tinymce.LegacyInput
+ * @private
+ */
+define("tinymce/LegacyInput", [
+ "tinymce/EditorManager",
+ "tinymce/util/Tools"
+], function(EditorManager, Tools) {
+ var each = Tools.each, explode = Tools.explode;
+
+ EditorManager.on('AddEditor', function(e) {
+ var editor = e.editor;
+
+ editor.on('preInit', function() {
+ var filters, fontSizes, dom, settings = editor.settings;
+
+ function replaceWithSpan(node, styles) {
+ each(styles, function(value, name) {
+ if (value) {
+ dom.setStyle(node, name, value);
+ }
+ });
+
+ dom.rename(node, 'span');
+ }
+
+ function convert(e) {
+ dom = editor.dom;
+
+ if (settings.convert_fonts_to_spans) {
+ each(dom.select('font,u,strike', e.node), function(node) {
+ filters[node.nodeName.toLowerCase()](dom, node);
+ });
+ }
+ }
+
+ if (settings.inline_styles) {
+ fontSizes = explode(settings.font_size_legacy_values);
+
+ filters = {
+ font: function(dom, node) {
+ replaceWithSpan(node, {
+ backgroundColor: node.style.backgroundColor,
+ color: node.color,
+ fontFamily: node.face,
+ fontSize: fontSizes[parseInt(node.size, 10) - 1]
+ });
+ },
+
+ u: function(dom, node) {
+ // HTML5 allows U element
+ if (editor.settings.schema === "html4") {
+ replaceWithSpan(node, {
+ textDecoration: 'underline'
+ });
+ }
+ },
+
+ strike: function(dom, node) {
+ replaceWithSpan(node, {
+ textDecoration: 'line-through'
+ });
+ }
+ };
+
+ editor.on('PreProcess SetContent', convert);
+ }
+ });
+ });
+});
+
+// Included from: js/tinymce/classes/util/XHR.js
+
+/**
+ * XHR.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class enables you to send XMLHTTPRequests cross browser.
+ * @class tinymce.util.XHR
+ * @mixes tinymce.util.Observable
+ * @static
+ * @example
+ * // Sends a low level Ajax request
+ * tinymce.util.XHR.send({
+ * url: 'someurl',
+ * success: function(text) {
+ * console.debug(text);
+ * }
+ * });
+ *
+ * // Add custom header to XHR request
+ * tinymce.util.XHR.on('beforeSend', function(e) {
+ * e.xhr.setRequestHeader('X-Requested-With', 'Something');
+ * });
+ */
+define("tinymce/util/XHR", [
+ "tinymce/util/Observable",
+ "tinymce/util/Tools"
+], function(Observable, Tools) {
+ var XHR = {
+ /**
+ * Sends a XMLHTTPRequest.
+ * Consult the Wiki for details on what settings this method takes.
+ *
+ * @method send
+ * @param {Object} settings Object will target URL, callbacks and other info needed to make the request.
+ */
+ send: function(settings) {
+ var xhr, count = 0;
+
+ function ready() {
+ if (!settings.async || xhr.readyState == 4 || count++ > 10000) {
+ if (settings.success && count < 10000 && xhr.status == 200) {
+ settings.success.call(settings.success_scope, '' + xhr.responseText, xhr, settings);
+ } else if (settings.error) {
+ settings.error.call(settings.error_scope, count > 10000 ? 'TIMED_OUT' : 'GENERAL', xhr, settings);
+ }
+
+ xhr = null;
+ } else {
+ setTimeout(ready, 10);
+ }
+ }
+
+ // Default settings
+ settings.scope = settings.scope || this;
+ settings.success_scope = settings.success_scope || settings.scope;
+ settings.error_scope = settings.error_scope || settings.scope;
+ settings.async = settings.async === false ? false : true;
+ settings.data = settings.data || '';
+
+ XHR.fire('beforeInitialize', {settings: settings});
+
+ xhr = new XMLHttpRequest();
+
+ if (xhr) {
+ if (xhr.overrideMimeType) {
+ xhr.overrideMimeType(settings.content_type);
+ }
+
+ xhr.open(settings.type || (settings.data ? 'POST' : 'GET'), settings.url, settings.async);
+
+ if (settings.crossDomain) {
+ xhr.withCredentials = true;
+ }
+
+ if (settings.content_type) {
+ xhr.setRequestHeader('Content-Type', settings.content_type);
+ }
+
+ if (settings.requestheaders) {
+ Tools.each(settings.requestheaders, function(header) {
+ xhr.setRequestHeader(header.key, header.value);
+ });
+ }
+
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
+
+ xhr = XHR.fire('beforeSend', {xhr: xhr, settings: settings}).xhr;
+ xhr.send(settings.data);
+
+ // Syncronous request
+ if (!settings.async) {
+ return ready();
+ }
+
+ // Wait for response, onReadyStateChange can not be used since it leaks memory in IE
+ setTimeout(ready, 10);
+ }
+ }
+ };
+
+ Tools.extend(XHR, Observable);
+
+ return XHR;
+});
+
+// Included from: js/tinymce/classes/util/JSON.js
+
+/**
+ * JSON.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * JSON parser and serializer class.
+ *
+ * @class tinymce.util.JSON
+ * @static
+ * @example
+ * // JSON parse a string into an object
+ * var obj = tinymce.util.JSON.parse(somestring);
+ *
+ * // JSON serialize a object into an string
+ * var str = tinymce.util.JSON.serialize(obj);
+ */
+define("tinymce/util/JSON", [], function() {
+ function serialize(o, quote) {
+ var i, v, t, name;
+
+ quote = quote || '"';
+
+ if (o === null) {
+ return 'null';
+ }
+
+ t = typeof o;
+
+ if (t == 'string') {
+ v = '\bb\tt\nn\ff\rr\""\'\'\\\\';
+
+ /*eslint no-control-regex:0 */
+ return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) {
+ // Make sure single quotes never get encoded inside double quotes for JSON compatibility
+ if (quote === '"' && a === "'") {
+ return a;
+ }
+
+ i = v.indexOf(b);
+
+ if (i + 1) {
+ return '\\' + v.charAt(i + 1);
+ }
+
+ a = b.charCodeAt().toString(16);
+
+ return '\\u' + '0000'.substring(a.length) + a;
+ }) + quote;
+ }
+
+ if (t == 'object') {
+ if (o.hasOwnProperty && Object.prototype.toString.call(o) === '[object Array]') {
+ for (i = 0, v = '['; i < o.length; i++) {
+ v += (i > 0 ? ',' : '') + serialize(o[i], quote);
+ }
+
+ return v + ']';
+ }
+
+ v = '{';
+
+ for (name in o) {
+ if (o.hasOwnProperty(name)) {
+ v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name +
+ quote + ':' + serialize(o[name], quote) : '';
+ }
+ }
+
+ return v + '}';
+ }
+
+ return '' + o;
+ }
+
+ return {
+ /**
+ * Serializes the specified object as a JSON string.
+ *
+ * @method serialize
+ * @param {Object} obj Object to serialize as a JSON string.
+ * @param {String} quote Optional quote string defaults to ".
+ * @return {string} JSON string serialized from input.
+ */
+ serialize: serialize,
+
+ /**
+ * Unserializes/parses the specified JSON string into a object.
+ *
+ * @method parse
+ * @param {string} s JSON String to parse into a JavaScript object.
+ * @return {Object} Object from input JSON string or undefined if it failed.
+ */
+ parse: function(text) {
+ try {
+ // Trick uglify JS
+ return window[String.fromCharCode(101) + 'val']('(' + text + ')');
+ } catch (ex) {
+ // Ignore
+ }
+ }
+
+ /**#@-*/
+ };
+});
+
+// Included from: js/tinymce/classes/util/JSONRequest.js
+
+/**
+ * JSONRequest.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class enables you to use JSON-RPC to call backend methods.
+ *
+ * @class tinymce.util.JSONRequest
+ * @example
+ * var json = new tinymce.util.JSONRequest({
+ * url: 'somebackend.php'
+ * });
+ *
+ * // Send RPC call 1
+ * json.send({
+ * method: 'someMethod1',
+ * params: ['a', 'b'],
+ * success: function(result) {
+ * console.dir(result);
+ * }
+ * });
+ *
+ * // Send RPC call 2
+ * json.send({
+ * method: 'someMethod2',
+ * params: ['a', 'b'],
+ * success: function(result) {
+ * console.dir(result);
+ * }
+ * });
+ */
+define("tinymce/util/JSONRequest", [
+ "tinymce/util/JSON",
+ "tinymce/util/XHR",
+ "tinymce/util/Tools"
+], function(JSON, XHR, Tools) {
+ var extend = Tools.extend;
+
+ function JSONRequest(settings) {
+ this.settings = extend({}, settings);
+ this.count = 0;
+ }
+
+ /**
+ * Simple helper function to send a JSON-RPC request without the need to initialize an object.
+ * Consult the Wiki API documentation for more details on what you can pass to this function.
+ *
+ * @method sendRPC
+ * @static
+ * @param {Object} o Call object where there are three field id, method and params this object should also contain callbacks etc.
+ */
+ JSONRequest.sendRPC = function(o) {
+ return new JSONRequest().send(o);
+ };
+
+ JSONRequest.prototype = {
+ /**
+ * Sends a JSON-RPC call. Consult the Wiki API documentation for more details on what you can pass to this function.
+ *
+ * @method send
+ * @param {Object} args Call object where there are three field id, method and params this object should also contain callbacks etc.
+ */
+ send: function(args) {
+ var ecb = args.error, scb = args.success;
+
+ args = extend(this.settings, args);
+
+ args.success = function(c, x) {
+ c = JSON.parse(c);
+
+ if (typeof c == 'undefined') {
+ c = {
+ error: 'JSON Parse error.'
+ };
+ }
+
+ if (c.error) {
+ ecb.call(args.error_scope || args.scope, c.error, x);
+ } else {
+ scb.call(args.success_scope || args.scope, c.result);
+ }
+ };
+
+ args.error = function(ty, x) {
+ if (ecb) {
+ ecb.call(args.error_scope || args.scope, ty, x);
+ }
+ };
+
+ args.data = JSON.serialize({
+ id: args.id || 'c' + (this.count++),
+ method: args.method,
+ params: args.params
+ });
+
+ // JSON content type for Ruby on rails. Bug: #1883287
+ args.content_type = 'application/json';
+
+ XHR.send(args);
+ }
+ };
+
+ return JSONRequest;
+});
+
+// Included from: js/tinymce/classes/util/JSONP.js
+
+/**
+ * JSONP.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define("tinymce/util/JSONP", [
+ "tinymce/dom/DOMUtils"
+], function(DOMUtils) {
+ return {
+ callbacks: {},
+ count: 0,
+
+ send: function(settings) {
+ var self = this, dom = DOMUtils.DOM, count = settings.count !== undefined ? settings.count : self.count;
+ var id = 'tinymce_jsonp_' + count;
+
+ self.callbacks[count] = function(json) {
+ dom.remove(id);
+ delete self.callbacks[count];
+
+ settings.callback(json);
+ };
+
+ dom.add(dom.doc.body, 'script', {
+ id: id,
+ src: settings.url,
+ type: 'text/javascript'
+ });
+
+ self.count++;
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/util/LocalStorage.js
+
+/**
+ * LocalStorage.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class will simulate LocalStorage on IE 7 and return the native version on modern browsers.
+ * Storage is done using userData on IE 7 and a special serialization format. The format is designed
+ * to be as small as possible by making sure that the keys and values doesn't need to be encoded. This
+ * makes it possible to store for example HTML data.
+ *
+ * Storage format for userData:
+ * <base 32 key length>,<key string>,<base 32 value length>,<value>,...
+ *
+ * For example this data key1=value1,key2=value2 would be:
+ * 4,key1,6,value1,4,key2,6,value2
+ *
+ * @class tinymce.util.LocalStorage
+ * @static
+ * @version 4.0
+ * @example
+ * tinymce.util.LocalStorage.setItem('key', 'value');
+ * var value = tinymce.util.LocalStorage.getItem('key');
+ */
+define("tinymce/util/LocalStorage", [], function() {
+ var LocalStorage, storageElm, items, keys, userDataKey, hasOldIEDataSupport;
+
+ // Check for native support
+ try {
+ if (window.localStorage) {
+ return localStorage;
+ }
+ } catch (ex) {
+ // Ignore
+ }
+
+ userDataKey = "tinymce";
+ storageElm = document.documentElement;
+ hasOldIEDataSupport = !!storageElm.addBehavior;
+
+ if (hasOldIEDataSupport) {
+ storageElm.addBehavior('#default#userData');
+ }
+
+ /**
+ * Gets the keys names and updates LocalStorage.length property. Since IE7 doesn't have any getters/setters.
+ */
+ function updateKeys() {
+ keys = [];
+
+ for (var key in items) {
+ keys.push(key);
+ }
+
+ LocalStorage.length = keys.length;
+ }
+
+ /**
+ * Loads the userData string and parses it into the items structure.
+ */
+ function load() {
+ var key, data, value, pos = 0;
+
+ items = {};
+
+ // localStorage can be disabled on WebKit/Gecko so make a dummy storage
+ if (!hasOldIEDataSupport) {
+ return;
+ }
+
+ function next(end) {
+ var value, nextPos;
+
+ nextPos = end !== undefined ? pos + end : data.indexOf(',', pos);
+ if (nextPos === -1 || nextPos > data.length) {
+ return null;
+ }
+
+ value = data.substring(pos, nextPos);
+ pos = nextPos + 1;
+
+ return value;
+ }
+
+ storageElm.load(userDataKey);
+ data = storageElm.getAttribute(userDataKey) || '';
+
+ do {
+ var offset = next();
+ if (offset === null) {
+ break;
+ }
+
+ key = next(parseInt(offset, 32) || 0);
+ if (key !== null) {
+ offset = next();
+ if (offset === null) {
+ break;
+ }
+
+ value = next(parseInt(offset, 32) || 0);
+
+ if (key) {
+ items[key] = value;
+ }
+ }
+ } while (key !== null);
+
+ updateKeys();
+ }
+
+ /**
+ * Saves the items structure into a the userData format.
+ */
+ function save() {
+ var value, data = '';
+
+ // localStorage can be disabled on WebKit/Gecko so make a dummy storage
+ if (!hasOldIEDataSupport) {
+ return;
+ }
+
+ for (var key in items) {
+ value = items[key];
+ data += (data ? ',' : '') + key.length.toString(32) + ',' + key + ',' + value.length.toString(32) + ',' + value;
+ }
+
+ storageElm.setAttribute(userDataKey, data);
+
+ try {
+ storageElm.save(userDataKey);
+ } catch (ex) {
+ // Ignore disk full
+ }
+
+ updateKeys();
+ }
+
+ LocalStorage = {
+ /**
+ * Length of the number of items in storage.
+ *
+ * @property length
+ * @type Number
+ * @return {Number} Number of items in storage.
+ */
+ //length:0,
+
+ /**
+ * Returns the key name by index.
+ *
+ * @method key
+ * @param {Number} index Index of key to return.
+ * @return {String} Key value or null if it wasn't found.
+ */
+ key: function(index) {
+ return keys[index];
+ },
+
+ /**
+ * Returns the value if the specified key or null if it wasn't found.
+ *
+ * @method getItem
+ * @param {String} key Key of item to retrieve.
+ * @return {String} Value of the specified item or null if it wasn't found.
+ */
+ getItem: function(key) {
+ return key in items ? items[key] : null;
+ },
+
+ /**
+ * Sets the value of the specified item by it's key.
+ *
+ * @method setItem
+ * @param {String} key Key of the item to set.
+ * @param {String} value Value of the item to set.
+ */
+ setItem: function(key, value) {
+ items[key] = "" + value;
+ save();
+ },
+
+ /**
+ * Removes the specified item by key.
+ *
+ * @method removeItem
+ * @param {String} key Key of item to remove.
+ */
+ removeItem: function(key) {
+ delete items[key];
+ save();
+ },
+
+ /**
+ * Removes all items.
+ *
+ * @method clear
+ */
+ clear: function() {
+ items = {};
+ save();
+ }
+ };
+
+ load();
+
+ return LocalStorage;
+});
+
+// Included from: js/tinymce/classes/Compat.js
+
+/**
+ * Compat.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * TinyMCE core class.
+ *
+ * @static
+ * @class tinymce
+ * @borrow-members tinymce.EditorManager
+ * @borrow-members tinymce.util.Tools
+ */
+define("tinymce/Compat", [
+ "tinymce/dom/DOMUtils",
+ "tinymce/dom/EventUtils",
+ "tinymce/dom/ScriptLoader",
+ "tinymce/AddOnManager",
+ "tinymce/util/Tools",
+ "tinymce/Env"
+], function(DOMUtils, EventUtils, ScriptLoader, AddOnManager, Tools, Env) {
+ var tinymce = window.tinymce;
+
+ /**
+ * @property {tinymce.dom.DOMUtils} DOM Global DOM instance.
+ * @property {tinymce.dom.ScriptLoader} ScriptLoader Global ScriptLoader instance.
+ * @property {tinymce.AddOnManager} PluginManager Global PluginManager instance.
+ * @property {tinymce.AddOnManager} ThemeManager Global ThemeManager instance.
+ */
+ tinymce.DOM = DOMUtils.DOM;
+ tinymce.ScriptLoader = ScriptLoader.ScriptLoader;
+ tinymce.PluginManager = AddOnManager.PluginManager;
+ tinymce.ThemeManager = AddOnManager.ThemeManager;
+
+ tinymce.dom = tinymce.dom || {};
+ tinymce.dom.Event = EventUtils.Event;
+
+ Tools.each(
+ 'trim isArray is toArray makeMap each map grep inArray extend create walk createNS resolve explode _addCacheSuffix'.split(' '),
+ function(key) {
+ tinymce[key] = Tools[key];
+ }
+ );
+
+ Tools.each('isOpera isWebKit isIE isGecko isMac'.split(' '), function(name) {
+ tinymce[name] = Env[name.substr(2).toLowerCase()];
+ });
+
+ return {};
+});
+
+// Describe the different namespaces
+
+/**
+ * Root level namespace this contains classes directly related to the TinyMCE editor.
+ *
+ * @namespace tinymce
+ */
+
+/**
+ * Contains classes for handling the browsers DOM.
+ *
+ * @namespace tinymce.dom
+ */
+
+/**
+ * Contains html parser and serializer logic.
+ *
+ * @namespace tinymce.html
+ */
+
+/**
+ * Contains the different UI types such as buttons, listboxes etc.
+ *
+ * @namespace tinymce.ui
+ */
+
+/**
+ * Contains various utility classes such as json parser, cookies etc.
+ *
+ * @namespace tinymce.util
+ */
+
+/**
+ * Contains modules to handle data binding.
+ *
+ * @namespace tinymce.data
+ */
+
+// Included from: js/tinymce/classes/ui/Layout.js
+
+/**
+ * Layout.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Base layout manager class.
+ *
+ * @class tinymce.ui.Layout
+ */
+define("tinymce/ui/Layout", [
+ "tinymce/util/Class",
+ "tinymce/util/Tools"
+], function(Class, Tools) {
+ "use strict";
+
+ return Class.extend({
+ Defaults: {
+ firstControlClass: 'first',
+ lastControlClass: 'last'
+ },
+
+ /**
+ * Constructs a layout instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ this.settings = Tools.extend({}, this.Defaults, settings);
+ },
+
+ /**
+ * This method gets invoked before the layout renders the controls.
+ *
+ * @method preRender
+ * @param {tinymce.ui.Container} container Container instance to preRender.
+ */
+ preRender: function(container) {
+ container.bodyClasses.add(this.settings.containerClass);
+ },
+
+ /**
+ * Applies layout classes to the container.
+ *
+ * @private
+ */
+ applyClasses: function(items) {
+ var self = this, settings = self.settings, firstClass, lastClass, firstItem, lastItem;
+
+ firstClass = settings.firstControlClass;
+ lastClass = settings.lastControlClass;
+
+ items.each(function(item) {
+ item.classes.remove(firstClass).remove(lastClass).add(settings.controlClass);
+
+ if (item.visible()) {
+ if (!firstItem) {
+ firstItem = item;
+ }
+
+ lastItem = item;
+ }
+ });
+
+ if (firstItem) {
+ firstItem.classes.add(firstClass);
+ }
+
+ if (lastItem) {
+ lastItem.classes.add(lastClass);
+ }
+ },
+
+ /**
+ * Renders the specified container and any layout specific HTML.
+ *
+ * @method renderHtml
+ * @param {tinymce.ui.Container} container Container to render HTML for.
+ */
+ renderHtml: function(container) {
+ var self = this, html = '';
+
+ self.applyClasses(container.items());
+
+ container.items().each(function(item) {
+ html += item.renderHtml();
+ });
+
+ return html;
+ },
+
+ /**
+ * Recalculates the positions of the controls in the specified container.
+ *
+ * @method recalc
+ * @param {tinymce.ui.Container} container Container instance to recalc.
+ */
+ recalc: function() {
+ },
+
+ /**
+ * This method gets invoked after the layout renders the controls.
+ *
+ * @method postRender
+ * @param {tinymce.ui.Container} container Container instance to postRender.
+ */
+ postRender: function() {
+ },
+
+ isNative: function() {
+ return false;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/AbsoluteLayout.js
+
+/**
+ * AbsoluteLayout.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * LayoutManager for absolute positioning. This layout manager is more of
+ * a base class for other layouts but can be created and used directly.
+ *
+ * @-x-less AbsoluteLayout.less
+ * @class tinymce.ui.AbsoluteLayout
+ * @extends tinymce.ui.Layout
+ */
+define("tinymce/ui/AbsoluteLayout", [
+ "tinymce/ui/Layout"
+], function(Layout) {
+ "use strict";
+
+ return Layout.extend({
+ Defaults: {
+ containerClass: 'abs-layout',
+ controlClass: 'abs-layout-item'
+ },
+
+ /**
+ * Recalculates the positions of the controls in the specified container.
+ *
+ * @method recalc
+ * @param {tinymce.ui.Container} container Container instance to recalc.
+ */
+ recalc: function(container) {
+ container.items().filter(':visible').each(function(ctrl) {
+ var settings = ctrl.settings;
+
+ ctrl.layoutRect({
+ x: settings.x,
+ y: settings.y,
+ w: settings.w,
+ h: settings.h
+ });
+
+ if (ctrl.recalc) {
+ ctrl.recalc();
+ }
+ });
+ },
+
+ /**
+ * Renders the specified container and any layout specific HTML.
+ *
+ * @method renderHtml
+ * @param {tinymce.ui.Container} container Container to render HTML for.
+ */
+ renderHtml: function(container) {
+ return '<div id="' + container._id + '-absend" class="' + container.classPrefix + 'abs-end"></div>' + this._super(container);
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Button.js
+
+/**
+ * Button.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class is used to create buttons. You can create them directly or through the Factory.
+ *
+ * @example
+ * // Create and render a button to the body element
+ * tinymce.ui.Factory.create({
+ * type: 'button',
+ * text: 'My button'
+ * }).renderTo(document.body);
+ *
+ * @-x-less Button.less
+ * @class tinymce.ui.Button
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/Button", [
+ "tinymce/ui/Widget"
+], function(Widget) {
+ "use strict";
+
+ return Widget.extend({
+ Defaults: {
+ classes: "widget btn",
+ role: "button"
+ },
+
+ /**
+ * Constructs a new button instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {String} size Size of the button small|medium|large.
+ * @setting {String} image Image to use for icon.
+ * @setting {String} icon Icon to use for button.
+ */
+ init: function(settings) {
+ var self = this, size;
+
+ self._super(settings);
+ settings = self.settings;
+
+ size = self.settings.size;
+
+ self.on('click mousedown', function(e) {
+ e.preventDefault();
+ });
+
+ self.on('touchstart', function(e) {
+ self.fire('click', e);
+ e.preventDefault();
+ });
+
+ if (settings.subtype) {
+ self.classes.add(settings.subtype);
+ }
+
+ if (size) {
+ self.classes.add('btn-' + size);
+ }
+
+ if (settings.icon) {
+ self.icon(settings.icon);
+ }
+ },
+
+ /**
+ * Sets/gets the current button icon.
+ *
+ * @method icon
+ * @param {String} [icon] New icon identifier.
+ * @return {String|tinymce.ui.MenuButton} Current icon or current MenuButton instance.
+ */
+ icon: function(icon) {
+ if (!arguments.length) {
+ return this.state.get('icon');
+ }
+
+ this.state.set('icon', icon);
+
+ return this;
+ },
+
+ /**
+ * Repaints the button for example after it's been resizes by a layout engine.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ var btnElm = this.getEl().firstChild,
+ btnStyle;
+
+ if (btnElm) {
+ btnStyle = btnElm.style;
+ btnStyle.width = btnStyle.height = "100%";
+ }
+
+ this._super();
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = self.classPrefix;
+ var icon = self.state.get('icon'), image, text = self.state.get('text'), textHtml = '';
+
+ image = self.settings.image;
+ if (image) {
+ icon = 'none';
+
+ // Support for [high dpi, low dpi] image sources
+ if (typeof image != "string") {
+ image = window.getSelection ? image[0] : image[1];
+ }
+
+ image = ' style="background-image: url(\'' + image + '\')"';
+ } else {
+ image = '';
+ }
+
+ if (text) {
+ self.classes.add('btn-has-text');
+ textHtml = '<span class="' + prefix + 'txt">' + self.encode(text) + '</span>';
+ }
+
+ icon = icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
+
+ return (
+ '<div id="' + id + '" class="' + self.classes + '" tabindex="-1" aria-labelledby="' + id + '">' +
+ '<button role="presentation" type="button" tabindex="-1">' +
+ (icon ? '<i class="' + icon + '"' + image + '></i>' : '') +
+ textHtml +
+ '</button>' +
+ '</div>'
+ );
+ },
+
+ bindStates: function() {
+ var self = this, $ = self.$, textCls = self.classPrefix + 'txt';
+
+ function setButtonText(text) {
+ var $span = $('span.' + textCls, self.getEl());
+
+ if (text) {
+ if (!$span[0]) {
+ $('button:first', self.getEl()).append('<span class="' + textCls + '"></span>');
+ $span = $('span.' + textCls, self.getEl());
+ }
+
+ $span.html(self.encode(text));
+ } else {
+ $span.remove();
+ }
+
+ self.classes.toggle('btn-has-text', !!text);
+ }
+
+ self.state.on('change:text', function(e) {
+ setButtonText(e.value);
+ });
+
+ self.state.on('change:icon', function(e) {
+ var icon = e.value, prefix = self.classPrefix;
+
+ self.settings.icon = icon;
+ icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
+
+ var btnElm = self.getEl().firstChild, iconElm = btnElm.getElementsByTagName('i')[0];
+
+ if (icon) {
+ if (!iconElm || iconElm != btnElm.firstChild) {
+ iconElm = document.createElement('i');
+ btnElm.insertBefore(iconElm, btnElm.firstChild);
+ }
+
+ iconElm.className = icon;
+ } else if (iconElm) {
+ btnElm.removeChild(iconElm);
+ }
+
+ setButtonText(self.state.get('text'));
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/ButtonGroup.js
+
+/**
+ * ButtonGroup.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This control enables you to put multiple buttons into a group. This is
+ * useful when you want to combine similar toolbar buttons into a group.
+ *
+ * @example
+ * // Create and render a buttongroup with two buttons to the body element
+ * tinymce.ui.Factory.create({
+ * type: 'buttongroup',
+ * items: [
+ * {text: 'Button A'},
+ * {text: 'Button B'}
+ * ]
+ * }).renderTo(document.body);
+ *
+ * @-x-less ButtonGroup.less
+ * @class tinymce.ui.ButtonGroup
+ * @extends tinymce.ui.Container
+ */
+define("tinymce/ui/ButtonGroup", [
+ "tinymce/ui/Container"
+], function(Container) {
+ "use strict";
+
+ return Container.extend({
+ Defaults: {
+ defaultType: 'button',
+ role: 'group'
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, layout = self._layout;
+
+ self.classes.add('btn-group');
+ self.preRender();
+ layout.preRender(self);
+
+ return (
+ '<div id="' + self._id + '" class="' + self.classes + '">' +
+ '<div id="' + self._id + '-body">' +
+ (self.settings.html || '') + layout.renderHtml(self) +
+ '</div>' +
+ '</div>'
+ );
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Checkbox.js
+
+/**
+ * Checkbox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This control creates a custom checkbox.
+ *
+ * @example
+ * // Create and render a checkbox to the body element
+ * tinymce.ui.Factory.create({
+ * type: 'checkbox',
+ * checked: true,
+ * text: 'My checkbox'
+ * }).renderTo(document.body);
+ *
+ * @-x-less Checkbox.less
+ * @class tinymce.ui.Checkbox
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/Checkbox", [
+ "tinymce/ui/Widget"
+], function(Widget) {
+ "use strict";
+
+ return Widget.extend({
+ Defaults: {
+ classes: "checkbox",
+ role: "checkbox",
+ checked: false
+ },
+
+ /**
+ * Constructs a new Checkbox instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Boolean} checked True if the checkbox should be checked by default.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+
+ self.on('click mousedown', function(e) {
+ e.preventDefault();
+ });
+
+ self.on('click', function(e) {
+ e.preventDefault();
+
+ if (!self.disabled()) {
+ self.checked(!self.checked());
+ }
+ });
+
+ self.checked(self.settings.checked);
+ },
+
+ /**
+ * Getter/setter function for the checked state.
+ *
+ * @method checked
+ * @param {Boolean} [state] State to be set.
+ * @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation.
+ */
+ checked: function(state) {
+ if (!arguments.length) {
+ return this.state.get('checked');
+ }
+
+ this.state.set('checked', state);
+
+ return this;
+ },
+
+ /**
+ * Getter/setter function for the value state.
+ *
+ * @method value
+ * @param {Boolean} [state] State to be set.
+ * @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation.
+ */
+ value: function(state) {
+ if (!arguments.length) {
+ return this.checked();
+ }
+
+ return this.checked(state);
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = self.classPrefix;
+
+ return (
+ '<div id="' + id + '" class="' + self.classes + '" unselectable="on" aria-labelledby="' + id + '-al" tabindex="-1">' +
+ '<i class="' + prefix + 'ico ' + prefix + 'i-checkbox"></i>' +
+ '<span id="' + id + '-al" class="' + prefix + 'label">' + self.encode(self.state.get('text')) + '</span>' +
+ '</div>'
+ );
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ function checked(state) {
+ self.classes.toggle("checked", state);
+ self.aria('checked', state);
+ }
+
+ self.state.on('change:text', function(e) {
+ self.getEl('al').firstChild.data = self.translate(e.value);
+ });
+
+ self.state.on('change:checked change:value', function(e) {
+ self.fire('change');
+ checked(e.value);
+ });
+
+ self.state.on('change:icon', function(e) {
+ var icon = e.value, prefix = self.classPrefix;
+
+ if (typeof icon == 'undefined') {
+ return self.settings.icon;
+ }
+
+ self.settings.icon = icon;
+ icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
+
+ var btnElm = self.getEl().firstChild, iconElm = btnElm.getElementsByTagName('i')[0];
+
+ if (icon) {
+ if (!iconElm || iconElm != btnElm.firstChild) {
+ iconElm = document.createElement('i');
+ btnElm.insertBefore(iconElm, btnElm.firstChild);
+ }
+
+ iconElm.className = icon;
+ } else if (iconElm) {
+ btnElm.removeChild(iconElm);
+ }
+ });
+
+ if (self.state.get('checked')) {
+ checked(true);
+ }
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/ComboBox.js
+
+/**
+ * ComboBox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class creates a combobox control. Select box that you select a value from or
+ * type a value into.
+ *
+ * @-x-less ComboBox.less
+ * @class tinymce.ui.ComboBox
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/ComboBox", [
+ "tinymce/ui/Widget",
+ "tinymce/ui/Factory",
+ "tinymce/ui/DomUtils",
+ "tinymce/dom/DomQuery",
+ "tinymce/util/VK",
+ "tinymce/util/Tools"
+], function(Widget, Factory, DomUtils, $, VK, Tools) {
+ "use strict";
+
+ return Widget.extend({
+ /**
+ * Constructs a new control instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {String} placeholder Placeholder text to display.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+ settings = self.settings;
+
+ self.classes.add('combobox');
+ self.subinput = true;
+ self.ariaTarget = 'inp'; // TODO: Figure out a better way
+
+ settings.menu = settings.menu || settings.values;
+
+ if (settings.menu) {
+ settings.icon = 'caret';
+ }
+
+ self.on('click', function(e) {
+ var elm = e.target, root = self.getEl();
+
+ if (!$.contains(root, elm) && elm != root) {
+ return;
+ }
+
+ while (elm && elm != root) {
+ if (elm.id && elm.id.indexOf('-open') != -1) {
+ self.fire('action');
+
+ if (settings.menu) {
+ self.showMenu();
+
+ if (e.aria) {
+ self.menu.items()[0].focus();
+ }
+ }
+ }
+
+ elm = elm.parentNode;
+ }
+ });
+
+ // TODO: Rework this
+ self.on('keydown', function(e) {
+ var rootControl;
+
+ if (e.keyCode == 13 && e.target.nodeName === 'INPUT') {
+ e.preventDefault();
+
+ // Find root control that we can do toJSON on
+ self.parents().reverse().each(function(ctrl) {
+ if (ctrl.toJSON) {
+ rootControl = ctrl;
+ return false;
+ }
+ });
+
+ // Fire event on current text box with the serialized data of the whole form
+ self.fire('submit', {data: rootControl.toJSON()});
+ }
+ });
+
+ self.on('keyup', function(e) {
+ if (e.target.nodeName == "INPUT") {
+ var oldValue = self.state.get('value');
+ var newValue = e.target.value;
+
+ if (newValue !== oldValue) {
+ self.state.set('value', newValue);
+ self.fire('autocomplete', e);
+ }
+ }
+ });
+
+ self.on('mouseover', function(e) {
+ var tooltip = self.tooltip().moveTo(-0xFFFF);
+
+ if (self.statusLevel() && e.target.className.indexOf(self.classPrefix + 'status') !== -1) {
+ var statusMessage = self.statusMessage() || 'Ok';
+ var rel = tooltip.text(statusMessage).show().testMoveRel(e.target, ['bc-tc', 'bc-tl', 'bc-tr']);
+
+ tooltip.classes.toggle('tooltip-n', rel == 'bc-tc');
+ tooltip.classes.toggle('tooltip-nw', rel == 'bc-tl');
+ tooltip.classes.toggle('tooltip-ne', rel == 'bc-tr');
+
+ tooltip.moveRel(e.target, rel);
+ }
+ });
+ },
+
+ statusLevel: function (value) {
+ if (arguments.length > 0) {
+ this.state.set('statusLevel', value);
+ }
+
+ return this.state.get('statusLevel');
+ },
+
+ statusMessage: function (value) {
+ if (arguments.length > 0) {
+ this.state.set('statusMessage', value);
+ }
+
+ return this.state.get('statusMessage');
+ },
+
+ showMenu: function() {
+ var self = this, settings = self.settings, menu;
+
+ if (!self.menu) {
+ menu = settings.menu || [];
+
+ // Is menu array then auto constuct menu control
+ if (menu.length) {
+ menu = {
+ type: 'menu',
+ items: menu
+ };
+ } else {
+ menu.type = menu.type || 'menu';
+ }
+
+ self.menu = Factory.create(menu).parent(self).renderTo(self.getContainerElm());
+ self.fire('createmenu');
+ self.menu.reflow();
+ self.menu.on('cancel', function(e) {
+ if (e.control === self.menu) {
+ self.focus();
+ }
+ });
+
+ self.menu.on('show hide', function(e) {
+ e.control.items().each(function(ctrl) {
+ ctrl.active(ctrl.value() == self.value());
+ });
+ }).fire('show');
+
+ self.menu.on('select', function(e) {
+ self.value(e.control.value());
+ });
+
+ self.on('focusin', function(e) {
+ if (e.target.tagName.toUpperCase() == 'INPUT') {
+ self.menu.hide();
+ }
+ });
+
+ self.aria('expanded', true);
+ }
+
+ self.menu.show();
+ self.menu.layoutRect({w: self.layoutRect().w});
+ self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']);
+ },
+
+ /**
+ * Focuses the input area of the control.
+ *
+ * @method focus
+ */
+ focus: function() {
+ this.getEl('inp').focus();
+ },
+
+ /**
+ * Repaints the control after a layout operation.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ var self = this, elm = self.getEl(), openElm = self.getEl('open'), rect = self.layoutRect();
+ var width, lineHeight, innerPadding = 0, inputElm = elm.firstChild;
+
+ if (self.statusLevel() && self.statusLevel() !== 'none') {
+ innerPadding = (
+ parseInt(DomUtils.getRuntimeStyle(inputElm, 'padding-right'), 10) -
+ parseInt(DomUtils.getRuntimeStyle(inputElm, 'padding-left'), 10)
+ );
+ }
+
+ if (openElm) {
+ width = rect.w - DomUtils.getSize(openElm).width - 10;
+ } else {
+ width = rect.w - 10;
+ }
+
+ // Detect old IE 7+8 add lineHeight to align caret vertically in the middle
+ var doc = document;
+ if (doc.all && (!doc.documentMode || doc.documentMode <= 8)) {
+ lineHeight = (self.layoutRect().h - 2) + 'px';
+ }
+
+ $(inputElm).css({
+ width: width - innerPadding,
+ lineHeight: lineHeight
+ });
+
+ self._super();
+
+ return self;
+ },
+
+ /**
+ * Post render method. Called after the control has been rendered to the target.
+ *
+ * @method postRender
+ * @return {tinymce.ui.ComboBox} Current combobox instance.
+ */
+ postRender: function() {
+ var self = this;
+
+ $(this.getEl('inp')).on('change', function(e) {
+ self.state.set('value', e.target.value);
+ self.fire('change', e);
+ });
+
+ return self._super();
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix;
+ var value = self.state.get('value') || '';
+ var icon, text, openBtnHtml = '', extraAttrs = '', statusHtml = '';
+
+ if ("spellcheck" in settings) {
+ extraAttrs += ' spellcheck="' + settings.spellcheck + '"';
+ }
+
+ if (settings.maxLength) {
+ extraAttrs += ' maxlength="' + settings.maxLength + '"';
+ }
+
+ if (settings.size) {
+ extraAttrs += ' size="' + settings.size + '"';
+ }
+
+ if (settings.subtype) {
+ extraAttrs += ' type="' + settings.subtype + '"';
+ }
+
+ statusHtml = '<i id="' + id + '-status" class="mce-status mce-ico" style="display: none"></i>';
+
+ if (self.disabled()) {
+ extraAttrs += ' disabled="disabled"';
+ }
+
+ icon = settings.icon;
+ if (icon && icon != 'caret') {
+ icon = prefix + 'ico ' + prefix + 'i-' + settings.icon;
+ }
+
+ text = self.state.get('text');
+
+ if (icon || text) {
+ openBtnHtml = (
+ '<div id="' + id + '-open" class="' + prefix + 'btn ' + prefix + 'open" tabIndex="-1" role="button">' +
+ '<button id="' + id + '-action" type="button" hidefocus="1" tabindex="-1">' +
+ (icon != 'caret' ? '<i class="' + icon + '"></i>' : '<i class="' + prefix + 'caret"></i>') +
+ (text ? (icon ? ' ' : '') + text : '') +
+ '</button>' +
+ '</div>'
+ );
+
+ self.classes.add('has-open');
+ }
+
+ return (
+ '<div id="' + id + '" class="' + self.classes + '">' +
+ '<input id="' + id + '-inp" class="' + prefix + 'textbox" value="' +
+ self.encode(value, false) + '" hidefocus="1"' + extraAttrs + ' placeholder="' +
+ self.encode(settings.placeholder) + '" />' +
+ statusHtml +
+ openBtnHtml +
+ '</div>'
+ );
+ },
+
+ value: function(value) {
+ if (arguments.length) {
+ this.state.set('value', value);
+ return this;
+ }
+
+ // Make sure the real state is in sync
+ if (this.state.get('rendered')) {
+ this.state.set('value', this.getEl('inp').value);
+ }
+
+ return this.state.get('value');
+ },
+
+ showAutoComplete: function (items, term) {
+ var self = this;
+
+ if (items.length === 0) {
+ self.hideMenu();
+ return;
+ }
+
+ var insert = function (value, title) {
+ return function () {
+ self.fire('selectitem', {
+ title: title,
+ value: value
+ });
+ };
+ };
+
+ if (self.menu) {
+ self.menu.items().remove();
+ } else {
+ self.menu = Factory.create({
+ type: 'menu',
+ classes: 'combobox-menu',
+ layout: 'flow'
+ }).parent(self).renderTo();
+ }
+
+ Tools.each(items, function (item) {
+ self.menu.add({
+ text: item.title,
+ url: item.previewUrl,
+ match: term,
+ classes: 'menu-item-ellipsis',
+ onclick: insert(item.value, item.title)
+ });
+ });
+
+ self.menu.renderNew();
+ self.hideMenu();
+
+ self.menu.on('cancel', function(e) {
+ if (e.control.parent() === self.menu) {
+ e.stopPropagation();
+ self.focus();
+ self.hideMenu();
+ }
+ });
+
+ self.menu.on('select', function() {
+ self.focus();
+ });
+
+ var maxW = self.layoutRect().w;
+ self.menu.layoutRect({w: maxW, minW: 0, maxW: maxW});
+ self.menu.reflow();
+ self.menu.show();
+ self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']);
+ },
+
+ hideMenu: function() {
+ if (this.menu) {
+ this.menu.hide();
+ }
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:value', function(e) {
+ if (self.getEl('inp').value != e.value) {
+ self.getEl('inp').value = e.value;
+ }
+ });
+
+ self.state.on('change:disabled', function(e) {
+ self.getEl('inp').disabled = e.value;
+ });
+
+ self.state.on('change:statusLevel', function(e) {
+ var statusIconElm = self.getEl('status');
+ var prefix = self.classPrefix, value = e.value;
+
+ DomUtils.css(statusIconElm, 'display', value === 'none' ? 'none' : '');
+ DomUtils.toggleClass(statusIconElm, prefix + 'i-checkmark', value === 'ok');
+ DomUtils.toggleClass(statusIconElm, prefix + 'i-warning', value === 'warn');
+ DomUtils.toggleClass(statusIconElm, prefix + 'i-error', value === 'error');
+ self.classes.toggle('has-status', value !== 'none');
+ self.repaint();
+ });
+
+ DomUtils.on(self.getEl('status'), 'mouseleave', function () {
+ self.tooltip().hide();
+ });
+
+ self.on('cancel', function (e) {
+ if (self.menu && self.menu.visible()) {
+ e.stopPropagation();
+ self.hideMenu();
+ }
+ });
+
+ var focusIdx = function (idx, menu) {
+ if (menu && menu.items().length > 0) {
+ menu.items().eq(idx)[0].focus();
+ }
+ };
+
+ self.on('keydown', function (e) {
+ var keyCode = e.keyCode;
+
+ if (e.target.nodeName === 'INPUT') {
+ if (keyCode === VK.DOWN) {
+ e.preventDefault();
+ self.fire('autocomplete');
+ focusIdx(0, self.menu);
+ } else if (keyCode === VK.UP) {
+ e.preventDefault();
+ focusIdx(-1, self.menu);
+ }
+ }
+ });
+
+ return self._super();
+ },
+
+ remove: function() {
+ $(this.getEl('inp')).off();
+
+ if (this.menu) {
+ this.menu.remove();
+ }
+
+ this._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/ColorBox.js
+
+/**
+ * ColorBox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This widget lets you enter colors and browse for colors by pressing the color button. It also displays
+ * a preview of the current color.
+ *
+ * @-x-less ColorBox.less
+ * @class tinymce.ui.ColorBox
+ * @extends tinymce.ui.ComboBox
+ */
+define("tinymce/ui/ColorBox", [
+ "tinymce/ui/ComboBox"
+], function(ComboBox) {
+ "use strict";
+
+ return ComboBox.extend({
+ /**
+ * Constructs a new control instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ var self = this;
+
+ settings.spellcheck = false;
+
+ if (settings.onaction) {
+ settings.icon = 'none';
+ }
+
+ self._super(settings);
+
+ self.classes.add('colorbox');
+ self.on('change keyup postrender', function() {
+ self.repaintColor(self.value());
+ });
+ },
+
+ repaintColor: function(value) {
+ var openElm = this.getEl('open');
+ var elm = openElm ? openElm.getElementsByTagName('i')[0] : null;
+
+ if (elm) {
+ try {
+ elm.style.background = value;
+ } catch (ex) {
+ // Ignore
+ }
+ }
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:value', function(e) {
+ if (self.state.get('rendered')) {
+ self.repaintColor(e.value);
+ }
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/PanelButton.js
+
+/**
+ * PanelButton.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new panel button.
+ *
+ * @class tinymce.ui.PanelButton
+ * @extends tinymce.ui.Button
+ */
+define("tinymce/ui/PanelButton", [
+ "tinymce/ui/Button",
+ "tinymce/ui/FloatPanel"
+], function(Button, FloatPanel) {
+ "use strict";
+
+ return Button.extend({
+ /**
+ * Shows the panel for the button.
+ *
+ * @method showPanel
+ */
+ showPanel: function() {
+ var self = this, settings = self.settings;
+
+ self.active(true);
+
+ if (!self.panel) {
+ var panelSettings = settings.panel;
+
+ // Wrap panel in grid layout if type if specified
+ // This makes it possible to add forms or other containers directly in the panel option
+ if (panelSettings.type) {
+ panelSettings = {
+ layout: 'grid',
+ items: panelSettings
+ };
+ }
+
+ panelSettings.role = panelSettings.role || 'dialog';
+ panelSettings.popover = true;
+ panelSettings.autohide = true;
+ panelSettings.ariaRoot = true;
+
+ self.panel = new FloatPanel(panelSettings).on('hide', function() {
+ self.active(false);
+ }).on('cancel', function(e) {
+ e.stopPropagation();
+ self.focus();
+ self.hidePanel();
+ }).parent(self).renderTo(self.getContainerElm());
+
+ self.panel.fire('show');
+ self.panel.reflow();
+ } else {
+ self.panel.show();
+ }
+
+ self.panel.moveRel(self.getEl(), settings.popoverAlign || (self.isRtl() ? ['bc-tr', 'bc-tc'] : ['bc-tl', 'bc-tc']));
+ },
+
+ /**
+ * Hides the panel for the button.
+ *
+ * @method hidePanel
+ */
+ hidePanel: function() {
+ var self = this;
+
+ if (self.panel) {
+ self.panel.hide();
+ }
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this;
+
+ self.aria('haspopup', true);
+
+ self.on('click', function(e) {
+ if (e.control === self) {
+ if (self.panel && self.panel.visible()) {
+ self.hidePanel();
+ } else {
+ self.showPanel();
+ self.panel.focus(!!e.aria);
+ }
+ }
+ });
+
+ return self._super();
+ },
+
+ remove: function() {
+ if (this.panel) {
+ this.panel.remove();
+ this.panel = null;
+ }
+
+ return this._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/ColorButton.js
+
+/**
+ * ColorButton.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class creates a color button control. This is a split button in which the main
+ * button has a visual representation of the currently selected color. When clicked
+ * the caret button displays a color picker, allowing the user to select a new color.
+ *
+ * @-x-less ColorButton.less
+ * @class tinymce.ui.ColorButton
+ * @extends tinymce.ui.PanelButton
+ */
+define("tinymce/ui/ColorButton", [
+ "tinymce/ui/PanelButton",
+ "tinymce/dom/DOMUtils"
+], function(PanelButton, DomUtils) {
+ "use strict";
+
+ var DOM = DomUtils.DOM;
+
+ return PanelButton.extend({
+ /**
+ * Constructs a new ColorButton instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ this._super(settings);
+ this.classes.add('colorbutton');
+ },
+
+ /**
+ * Getter/setter for the current color.
+ *
+ * @method color
+ * @param {String} [color] Color to set.
+ * @return {String|tinymce.ui.ColorButton} Current color or current instance.
+ */
+ color: function(color) {
+ if (color) {
+ this._color = color;
+ this.getEl('preview').style.backgroundColor = color;
+ return this;
+ }
+
+ return this._color;
+ },
+
+ /**
+ * Resets the current color.
+ *
+ * @method resetColor
+ * @return {tinymce.ui.ColorButton} Current instance.
+ */
+ resetColor: function() {
+ this._color = null;
+ this.getEl('preview').style.backgroundColor = null;
+ return this;
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = self.classPrefix, text = self.state.get('text');
+ var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
+ var image = self.settings.image ? ' style="background-image: url(\'' + self.settings.image + '\')"' : '',
+ textHtml = '';
+
+ if (text) {
+ self.classes.add('btn-has-text');
+ textHtml = '<span class="' + prefix + 'txt">' + self.encode(text) + '</span>';
+ }
+
+ return (
+ '<div id="' + id + '" class="' + self.classes + '" role="button" tabindex="-1" aria-haspopup="true">' +
+ '<button role="presentation" hidefocus="1" type="button" tabindex="-1">' +
+ (icon ? '<i class="' + icon + '"' + image + '></i>' : '') +
+ '<span id="' + id + '-preview" class="' + prefix + 'preview"></span>' +
+ textHtml +
+ '</button>' +
+ '<button type="button" class="' + prefix + 'open" hidefocus="1" tabindex="-1">' +
+ ' <i class="' + prefix + 'caret"></i>' +
+ '</button>' +
+ '</div>'
+ );
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this, onClickHandler = self.settings.onclick;
+
+ self.on('click', function(e) {
+ if (e.aria && e.aria.key == 'down') {
+ return;
+ }
+
+ if (e.control == self && !DOM.getParent(e.target, '.' + self.classPrefix + 'open')) {
+ e.stopImmediatePropagation();
+ onClickHandler.call(self, e);
+ }
+ });
+
+ delete self.settings.onclick;
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/util/Color.js
+
+/**
+ * Color.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class lets you parse/serialize colors and convert rgb/hsb.
+ *
+ * @class tinymce.util.Color
+ * @example
+ * var white = new tinymce.util.Color({r: 255, g: 255, b: 255});
+ * var red = new tinymce.util.Color('#FF0000');
+ *
+ * console.log(white.toHex(), red.toHsv());
+ */
+define("tinymce/util/Color", [], function() {
+ var min = Math.min, max = Math.max, round = Math.round;
+
+ /**
+ * Constructs a new color instance.
+ *
+ * @constructor
+ * @method Color
+ * @param {String} value Optional initial value to parse.
+ */
+ function Color(value) {
+ var self = this, r = 0, g = 0, b = 0;
+
+ function rgb2hsv(r, g, b) {
+ var h, s, v, d, minRGB, maxRGB;
+
+ h = 0;
+ s = 0;
+ v = 0;
+ r = r / 255;
+ g = g / 255;
+ b = b / 255;
+
+ minRGB = min(r, min(g, b));
+ maxRGB = max(r, max(g, b));
+
+ if (minRGB == maxRGB) {
+ v = minRGB;
+
+ return {
+ h: 0,
+ s: 0,
+ v: v * 100
+ };
+ }
+
+ /*eslint no-nested-ternary:0 */
+ d = (r == minRGB) ? g - b : ((b == minRGB) ? r - g : b - r);
+ h = (r == minRGB) ? 3 : ((b == minRGB) ? 1 : 5);
+ h = 60 * (h - d / (maxRGB - minRGB));
+ s = (maxRGB - minRGB) / maxRGB;
+ v = maxRGB;
+
+ return {
+ h: round(h),
+ s: round(s * 100),
+ v: round(v * 100)
+ };
+ }
+
+ function hsvToRgb(hue, saturation, brightness) {
+ var side, chroma, x, match;
+
+ hue = (parseInt(hue, 10) || 0) % 360;
+ saturation = parseInt(saturation, 10) / 100;
+ brightness = parseInt(brightness, 10) / 100;
+ saturation = max(0, min(saturation, 1));
+ brightness = max(0, min(brightness, 1));
+
+ if (saturation === 0) {
+ r = g = b = round(255 * brightness);
+ return;
+ }
+
+ side = hue / 60;
+ chroma = brightness * saturation;
+ x = chroma * (1 - Math.abs(side % 2 - 1));
+ match = brightness - chroma;
+
+ switch (Math.floor(side)) {
+ case 0:
+ r = chroma;
+ g = x;
+ b = 0;
+ break;
+
+ case 1:
+ r = x;
+ g = chroma;
+ b = 0;
+ break;
+
+ case 2:
+ r = 0;
+ g = chroma;
+ b = x;
+ break;
+
+ case 3:
+ r = 0;
+ g = x;
+ b = chroma;
+ break;
+
+ case 4:
+ r = x;
+ g = 0;
+ b = chroma;
+ break;
+
+ case 5:
+ r = chroma;
+ g = 0;
+ b = x;
+ break;
+
+ default:
+ r = g = b = 0;
+ }
+
+ r = round(255 * (r + match));
+ g = round(255 * (g + match));
+ b = round(255 * (b + match));
+ }
+
+ /**
+ * Returns the hex string of the current color. For example: #ff00ff
+ *
+ * @method toHex
+ * @return {String} Hex string of current color.
+ */
+ function toHex() {
+ function hex(val) {
+ val = parseInt(val, 10).toString(16);
+
+ return val.length > 1 ? val : '0' + val;
+ }
+
+ return '#' + hex(r) + hex(g) + hex(b);
+ }
+
+ /**
+ * Returns the r, g, b values of the color. Each channel has a range from 0-255.
+ *
+ * @method toRgb
+ * @return {Object} Object with r, g, b fields.
+ */
+ function toRgb() {
+ return {
+ r: r,
+ g: g,
+ b: b
+ };
+ }
+
+ /**
+ * Returns the h, s, v values of the color. Ranges: h=0-360, s=0-100, v=0-100.
+ *
+ * @method toHsv
+ * @return {Object} Object with h, s, v fields.
+ */
+ function toHsv() {
+ return rgb2hsv(r, g, b);
+ }
+
+ /**
+ * Parses the specified value and populates the color instance.
+ *
+ * Supported format examples:
+ * * rbg(255,0,0)
+ * * #ff0000
+ * * #fff
+ * * {r: 255, g: 0, b: 0}
+ * * {h: 360, s: 100, v: 100}
+ *
+ * @method parse
+ * @param {Object/String} value Color value to parse.
+ * @return {tinymce.util.Color} Current color instance.
+ */
+ function parse(value) {
+ var matches;
+
+ if (typeof value == 'object') {
+ if ("r" in value) {
+ r = value.r;
+ g = value.g;
+ b = value.b;
+ } else if ("v" in value) {
+ hsvToRgb(value.h, value.s, value.v);
+ }
+ } else {
+ if ((matches = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)[^\)]*\)/gi.exec(value))) {
+ r = parseInt(matches[1], 10);
+ g = parseInt(matches[2], 10);
+ b = parseInt(matches[3], 10);
+ } else if ((matches = /#([0-F]{2})([0-F]{2})([0-F]{2})/gi.exec(value))) {
+ r = parseInt(matches[1], 16);
+ g = parseInt(matches[2], 16);
+ b = parseInt(matches[3], 16);
+ } else if ((matches = /#([0-F])([0-F])([0-F])/gi.exec(value))) {
+ r = parseInt(matches[1] + matches[1], 16);
+ g = parseInt(matches[2] + matches[2], 16);
+ b = parseInt(matches[3] + matches[3], 16);
+ }
+ }
+
+ r = r < 0 ? 0 : (r > 255 ? 255 : r);
+ g = g < 0 ? 0 : (g > 255 ? 255 : g);
+ b = b < 0 ? 0 : (b > 255 ? 255 : b);
+
+ return self;
+ }
+
+ if (value) {
+ parse(value);
+ }
+
+ self.toRgb = toRgb;
+ self.toHsv = toHsv;
+ self.toHex = toHex;
+ self.parse = parse;
+ }
+
+ return Color;
+});
+
+// Included from: js/tinymce/classes/ui/ColorPicker.js
+
+/**
+ * ColorPicker.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Color picker widget lets you select colors.
+ *
+ * @-x-less ColorPicker.less
+ * @class tinymce.ui.ColorPicker
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/ColorPicker", [
+ "tinymce/ui/Widget",
+ "tinymce/ui/DragHelper",
+ "tinymce/ui/DomUtils",
+ "tinymce/util/Color"
+], function(Widget, DragHelper, DomUtils, Color) {
+ "use strict";
+
+ return Widget.extend({
+ Defaults: {
+ classes: "widget colorpicker"
+ },
+
+ /**
+ * Constructs a new colorpicker instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {String} color Initial color value.
+ */
+ init: function(settings) {
+ this._super(settings);
+ },
+
+ postRender: function() {
+ var self = this, color = self.color(), hsv, hueRootElm, huePointElm, svRootElm, svPointElm;
+
+ hueRootElm = self.getEl('h');
+ huePointElm = self.getEl('hp');
+ svRootElm = self.getEl('sv');
+ svPointElm = self.getEl('svp');
+
+ function getPos(elm, event) {
+ var pos = DomUtils.getPos(elm), x, y;
+
+ x = event.pageX - pos.x;
+ y = event.pageY - pos.y;
+
+ x = Math.max(0, Math.min(x / elm.clientWidth, 1));
+ y = Math.max(0, Math.min(y / elm.clientHeight, 1));
+
+ return {
+ x: x,
+ y: y
+ };
+ }
+
+ function updateColor(hsv, hueUpdate) {
+ var hue = (360 - hsv.h) / 360;
+
+ DomUtils.css(huePointElm, {
+ top: (hue * 100) + '%'
+ });
+
+ if (!hueUpdate) {
+ DomUtils.css(svPointElm, {
+ left: hsv.s + '%',
+ top: (100 - hsv.v) + '%'
+ });
+ }
+
+ svRootElm.style.background = new Color({s: 100, v: 100, h: hsv.h}).toHex();
+ self.color().parse({s: hsv.s, v: hsv.v, h: hsv.h});
+ }
+
+ function updateSaturationAndValue(e) {
+ var pos;
+
+ pos = getPos(svRootElm, e);
+ hsv.s = pos.x * 100;
+ hsv.v = (1 - pos.y) * 100;
+
+ updateColor(hsv);
+ self.fire('change');
+ }
+
+ function updateHue(e) {
+ var pos;
+
+ pos = getPos(hueRootElm, e);
+ hsv = color.toHsv();
+ hsv.h = (1 - pos.y) * 360;
+ updateColor(hsv, true);
+ self.fire('change');
+ }
+
+ self._repaint = function() {
+ hsv = color.toHsv();
+ updateColor(hsv);
+ };
+
+ self._super();
+
+ self._svdraghelper = new DragHelper(self._id + '-sv', {
+ start: updateSaturationAndValue,
+ drag: updateSaturationAndValue
+ });
+
+ self._hdraghelper = new DragHelper(self._id + '-h', {
+ start: updateHue,
+ drag: updateHue
+ });
+
+ self._repaint();
+ },
+
+ rgb: function() {
+ return this.color().toRgb();
+ },
+
+ value: function(value) {
+ var self = this;
+
+ if (arguments.length) {
+ self.color().parse(value);
+
+ if (self._rendered) {
+ self._repaint();
+ }
+ } else {
+ return self.color().toHex();
+ }
+ },
+
+ color: function() {
+ if (!this._color) {
+ this._color = new Color();
+ }
+
+ return this._color;
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = self.classPrefix, hueHtml;
+ var stops = '#ff0000,#ff0080,#ff00ff,#8000ff,#0000ff,#0080ff,#00ffff,#00ff80,#00ff00,#80ff00,#ffff00,#ff8000,#ff0000';
+
+ function getOldIeFallbackHtml() {
+ var i, l, html = '', gradientPrefix, stopsList;
+
+ gradientPrefix = 'filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=';
+ stopsList = stops.split(',');
+ for (i = 0, l = stopsList.length - 1; i < l; i++) {
+ html += (
+ '<div class="' + prefix + 'colorpicker-h-chunk" style="' +
+ 'height:' + (100 / l) + '%;' +
+ gradientPrefix + stopsList[i] + ',endColorstr=' + stopsList[i + 1] + ');' +
+ '-ms-' + gradientPrefix + stopsList[i] + ',endColorstr=' + stopsList[i + 1] + ')' +
+ '"></div>'
+ );
+ }
+
+ return html;
+ }
+
+ var gradientCssText = (
+ 'background: -ms-linear-gradient(top,' + stops + ');' +
+ 'background: linear-gradient(to bottom,' + stops + ');'
+ );
+
+ hueHtml = (
+ '<div id="' + id + '-h" class="' + prefix + 'colorpicker-h" style="' + gradientCssText + '">' +
+ getOldIeFallbackHtml() +
+ '<div id="' + id + '-hp" class="' + prefix + 'colorpicker-h-marker"></div>' +
+ '</div>'
+ );
+
+ return (
+ '<div id="' + id + '" class="' + self.classes + '">' +
+ '<div id="' + id + '-sv" class="' + prefix + 'colorpicker-sv">' +
+ '<div class="' + prefix + 'colorpicker-overlay1">' +
+ '<div class="' + prefix + 'colorpicker-overlay2">' +
+ '<div id="' + id + '-svp" class="' + prefix + 'colorpicker-selector1">' +
+ '<div class="' + prefix + 'colorpicker-selector2"></div>' +
+ '</div>' +
+ '</div>' +
+ '</div>' +
+ '</div>' +
+ hueHtml +
+ '</div>'
+ );
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Path.js
+
+/**
+ * Path.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new path control.
+ *
+ * @-x-less Path.less
+ * @class tinymce.ui.Path
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/Path", [
+ "tinymce/ui/Widget"
+], function(Widget) {
+ "use strict";
+
+ return Widget.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {String} delimiter Delimiter to display between row in path.
+ */
+ init: function(settings) {
+ var self = this;
+
+ if (!settings.delimiter) {
+ settings.delimiter = '\u00BB';
+ }
+
+ self._super(settings);
+ self.classes.add('path');
+ self.canFocus = true;
+
+ self.on('click', function(e) {
+ var index, target = e.target;
+
+ if ((index = target.getAttribute('data-index'))) {
+ self.fire('select', {value: self.row()[index], index: index});
+ }
+ });
+
+ self.row(self.settings.row);
+ },
+
+ /**
+ * Focuses the current control.
+ *
+ * @method focus
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ focus: function() {
+ var self = this;
+
+ self.getEl().firstChild.focus();
+
+ return self;
+ },
+
+ /**
+ * Sets/gets the data to be used for the path.
+ *
+ * @method row
+ * @param {Array} row Array with row name is rendered to path.
+ */
+ row: function(row) {
+ if (!arguments.length) {
+ return this.state.get('row');
+ }
+
+ this.state.set('row', row);
+
+ return this;
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this;
+
+ return (
+ '<div id="' + self._id + '" class="' + self.classes + '">' +
+ self._getDataPathHtml(self.state.get('row')) +
+ '</div>'
+ );
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:row', function(e) {
+ self.innerHtml(self._getDataPathHtml(e.value));
+ });
+
+ return self._super();
+ },
+
+ _getDataPathHtml: function(data) {
+ var self = this, parts = data || [], i, l, html = '', prefix = self.classPrefix;
+
+ for (i = 0, l = parts.length; i < l; i++) {
+ html += (
+ (i > 0 ? '<div class="' + prefix + 'divider" aria-hidden="true"> ' + self.settings.delimiter + ' </div>' : '') +
+ '<div role="button" class="' + prefix + 'path-item' + (i == l - 1 ? ' ' + prefix + 'last' : '') + '" data-index="' +
+ i + '" tabindex="-1" id="' + self._id + '-' + i + '" aria-level="' + (i + 1) + '">' + parts[i].name + '</div>'
+ );
+ }
+
+ if (!html) {
+ html = '<div class="' + prefix + 'path-item">\u00a0</div>';
+ }
+
+ return html;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/ElementPath.js
+
+/**
+ * ElementPath.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This control creates an path for the current selections parent elements in TinyMCE.
+ *
+ * @class tinymce.ui.ElementPath
+ * @extends tinymce.ui.Path
+ */
+define("tinymce/ui/ElementPath", [
+ "tinymce/ui/Path"
+], function(Path) {
+ return Path.extend({
+ /**
+ * Post render method. Called after the control has been rendered to the target.
+ *
+ * @method postRender
+ * @return {tinymce.ui.ElementPath} Current combobox instance.
+ */
+ postRender: function() {
+ var self = this, editor = self.settings.editor;
+
+ function isHidden(elm) {
+ if (elm.nodeType === 1) {
+ if (elm.nodeName == "BR" || !!elm.getAttribute('data-mce-bogus')) {
+ return true;
+ }
+
+ if (elm.getAttribute('data-mce-type') === 'bookmark') {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ if (editor.settings.elementpath !== false) {
+ self.on('select', function(e) {
+ editor.focus();
+ editor.selection.select(this.row()[e.index].element);
+ editor.nodeChanged();
+ });
+
+ editor.on('nodeChange', function(e) {
+ var outParents = [], parents = e.parents, i = parents.length;
+
+ while (i--) {
+ if (parents[i].nodeType == 1 && !isHidden(parents[i])) {
+ var args = editor.fire('ResolveName', {
+ name: parents[i].nodeName.toLowerCase(),
+ target: parents[i]
+ });
+
+ if (!args.isDefaultPrevented()) {
+ outParents.push({name: args.name, element: parents[i]});
+ }
+
+ if (args.isPropagationStopped()) {
+ break;
+ }
+ }
+ }
+
+ self.row(outParents);
+ });
+ }
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/FormItem.js
+
+/**
+ * FormItem.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class is a container created by the form element with
+ * a label and control item.
+ *
+ * @class tinymce.ui.FormItem
+ * @extends tinymce.ui.Container
+ * @setting {String} label Label to display for the form item.
+ */
+define("tinymce/ui/FormItem", [
+ "tinymce/ui/Container"
+], function(Container) {
+ "use strict";
+
+ return Container.extend({
+ Defaults: {
+ layout: 'flex',
+ align: 'center',
+ defaults: {
+ flex: 1
+ }
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, layout = self._layout, prefix = self.classPrefix;
+
+ self.classes.add('formitem');
+ layout.preRender(self);
+
+ return (
+ '<div id="' + self._id + '" class="' + self.classes + '" hidefocus="1" tabindex="-1">' +
+ (self.settings.title ? ('<div id="' + self._id + '-title" class="' + prefix + 'title">' +
+ self.settings.title + '</div>') : '') +
+ '<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
+ (self.settings.html || '') + layout.renderHtml(self) +
+ '</div>' +
+ '</div>'
+ );
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Form.js
+
+/**
+ * Form.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class creates a form container. A form container has the ability
+ * to automatically wrap items in tinymce.ui.FormItem instances.
+ *
+ * Each FormItem instance is a container for the label and the item.
+ *
+ * @example
+ * tinymce.ui.Factory.create({
+ * type: 'form',
+ * items: [
+ * {type: 'textbox', label: 'My text box'}
+ * ]
+ * }).renderTo(document.body);
+ *
+ * @class tinymce.ui.Form
+ * @extends tinymce.ui.Container
+ */
+define("tinymce/ui/Form", [
+ "tinymce/ui/Container",
+ "tinymce/ui/FormItem",
+ "tinymce/util/Tools"
+], function(Container, FormItem, Tools) {
+ "use strict";
+
+ return Container.extend({
+ Defaults: {
+ containerCls: 'form',
+ layout: 'flex',
+ direction: 'column',
+ align: 'stretch',
+ flex: 1,
+ padding: 20,
+ labelGap: 30,
+ spacing: 10,
+ callbacks: {
+ submit: function() {
+ this.submit();
+ }
+ }
+ },
+
+ /**
+ * This method gets invoked before the control is rendered.
+ *
+ * @method preRender
+ */
+ preRender: function() {
+ var self = this, items = self.items();
+
+ if (!self.settings.formItemDefaults) {
+ self.settings.formItemDefaults = {
+ layout: 'flex',
+ autoResize: "overflow",
+ defaults: {flex: 1}
+ };
+ }
+
+ // Wrap any labeled items in FormItems
+ items.each(function(ctrl) {
+ var formItem, label = ctrl.settings.label;
+
+ if (label) {
+ formItem = new FormItem(Tools.extend({
+ items: {
+ type: 'label',
+ id: ctrl._id + '-l',
+ text: label,
+ flex: 0,
+ forId: ctrl._id,
+ disabled: ctrl.disabled()
+ }
+ }, self.settings.formItemDefaults));
+
+ formItem.type = 'formitem';
+ ctrl.aria('labelledby', ctrl._id + '-l');
+
+ if (typeof ctrl.settings.flex == "undefined") {
+ ctrl.settings.flex = 1;
+ }
+
+ self.replace(ctrl, formItem);
+ formItem.add(ctrl);
+ }
+ });
+ },
+
+ /**
+ * Fires a submit event with the serialized form.
+ *
+ * @method submit
+ * @return {Object} Event arguments object.
+ */
+ submit: function() {
+ return this.fire('submit', {data: this.toJSON()});
+ },
+
+ /**
+ * Post render method. Called after the control has been rendered to the target.
+ *
+ * @method postRender
+ * @return {tinymce.ui.ComboBox} Current combobox instance.
+ */
+ postRender: function() {
+ var self = this;
+
+ self._super();
+ self.fromJSON(self.settings.data);
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self._super();
+
+ function recalcLabels() {
+ var maxLabelWidth = 0, labels = [], i, labelGap, items;
+
+ if (self.settings.labelGapCalc === false) {
+ return;
+ }
+
+ if (self.settings.labelGapCalc == "children") {
+ items = self.find('formitem');
+ } else {
+ items = self.items();
+ }
+
+ items.filter('formitem').each(function(item) {
+ var labelCtrl = item.items()[0], labelWidth = labelCtrl.getEl().clientWidth;
+
+ maxLabelWidth = labelWidth > maxLabelWidth ? labelWidth : maxLabelWidth;
+ labels.push(labelCtrl);
+ });
+
+ labelGap = self.settings.labelGap || 0;
+
+ i = labels.length;
+ while (i--) {
+ labels[i].settings.minWidth = maxLabelWidth + labelGap;
+ }
+ }
+
+ self.on('show', recalcLabels);
+ recalcLabels();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/FieldSet.js
+
+/**
+ * FieldSet.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class creates fieldset containers.
+ *
+ * @-x-less FieldSet.less
+ * @class tinymce.ui.FieldSet
+ * @extends tinymce.ui.Form
+ */
+define("tinymce/ui/FieldSet", [
+ "tinymce/ui/Form"
+], function(Form) {
+ "use strict";
+
+ return Form.extend({
+ Defaults: {
+ containerCls: 'fieldset',
+ layout: 'flex',
+ direction: 'column',
+ align: 'stretch',
+ flex: 1,
+ padding: "25 15 5 15",
+ labelGap: 30,
+ spacing: 10,
+ border: 1
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, layout = self._layout, prefix = self.classPrefix;
+
+ self.preRender();
+ layout.preRender(self);
+
+ return (
+ '<fieldset id="' + self._id + '" class="' + self.classes + '" hidefocus="1" tabindex="-1">' +
+ (self.settings.title ? ('<legend id="' + self._id + '-title" class="' + prefix + 'fieldset-title">' +
+ self.settings.title + '</legend>') : '') +
+ '<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
+ (self.settings.html || '') + layout.renderHtml(self) +
+ '</div>' +
+ '</fieldset>'
+ );
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/content/LinkTargets.js
+
+/**
+ * LinkTargets.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module is enables you to get anything that you can link to in a element.
+ *
+ * @private
+ * @class tinymce.content.LinkTargets
+ */
+define('tinymce/content/LinkTargets', [
+ 'tinymce/dom/DOMUtils',
+ 'tinymce/util/Fun',
+ 'tinymce/util/Arr',
+ 'tinymce/util/Uuid',
+ 'tinymce/util/Tools',
+ 'tinymce/dom/NodeType'
+], function(
+ DOMUtils,
+ Fun,
+ Arr,
+ Uuid,
+ Tools,
+ NodeType
+) {
+ var trim = Tools.trim;
+
+ var create = function (type, title, url, level, attach) {
+ return {
+ type: type,
+ title: title,
+ url: url,
+ level: level,
+ attach: attach
+ };
+ };
+
+ var isChildOfContentEditableTrue = function (node) {
+ while ((node = node.parentNode)) {
+ var value = node.contentEditable;
+ if (value && value !== 'inherit') {
+ return NodeType.isContentEditableTrue(node);
+ }
+ }
+
+ return false;
+ };
+
+ var select = function (selector, root) {
+ return DOMUtils.DOM.select(selector, root);
+ };
+
+ var getElementText = function (elm) {
+ return elm.innerText || elm.textContent;
+ };
+
+ var getOrGenerateId = function (elm) {
+ return elm.id ? elm.id : Uuid.uuid('h');
+ };
+
+ var isAnchor = function (elm) {
+ return elm && elm.nodeName === 'A' && (elm.id || elm.name);
+ };
+
+ var isValidAnchor = function (elm) {
+ return isAnchor(elm) && isEditable(elm);
+ };
+
+ var isHeader = function (elm) {
+ return elm && /^(H[1-6])$/.test(elm.nodeName);
+ };
+
+ var isEditable = function (elm) {
+ return isChildOfContentEditableTrue(elm) && !NodeType.isContentEditableFalse(elm);
+ };
+
+ var isValidHeader = function (elm) {
+ return isHeader(elm) && isEditable(elm);
+ };
+
+ var getLevel = function (elm) {
+ return isHeader(elm) ? parseInt(elm.nodeName.substr(1), 10) : 0;
+ };
+
+ var headerTarget = function (elm) {
+ var headerId = getOrGenerateId(elm);
+
+ var attach = function () {
+ elm.id = headerId;
+ };
+
+ return create('header', getElementText(elm), '#' + headerId, getLevel(elm), attach);
+ };
+
+ var anchorTarget = function (elm) {
+ var anchorId = elm.id || elm.name;
+ var anchorText = getElementText(elm);
+
+ return create('anchor', anchorText ? anchorText : '#' + anchorId, '#' + anchorId, 0, Fun.noop);
+ };
+
+ var getHeaderTargets = function (elms) {
+ return Arr.map(Arr.filter(elms, isValidHeader), headerTarget);
+ };
+
+ var getAnchorTargets = function (elms) {
+ return Arr.map(Arr.filter(elms, isValidAnchor), anchorTarget);
+ };
+
+ var getTargetElements = function (elm) {
+ var elms = select('h1,h2,h3,h4,h5,h6,a:not([href])', elm);
+ return elms;
+ };
+
+ var hasTitle = function (target) {
+ return trim(target.title).length > 0;
+ };
+
+ var find = function (elm) {
+ var elms = getTargetElements(elm);
+ return Arr.filter(getHeaderTargets(elms).concat(getAnchorTargets(elms)), hasTitle);
+ };
+
+ return {
+ find: find
+ };
+});
+
+// Included from: js/tinymce/classes/ui/FilePicker.js
+
+/**
+ * FilePicker.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*global tinymce:true */
+
+/**
+ * This class creates a file picker control.
+ *
+ * @class tinymce.ui.FilePicker
+ * @extends tinymce.ui.ComboBox
+ */
+define("tinymce/ui/FilePicker", [
+ "tinymce/ui/ComboBox",
+ "tinymce/util/Tools",
+ "tinymce/util/Arr",
+ "tinymce/util/Fun",
+ "tinymce/util/VK",
+ "tinymce/content/LinkTargets"
+], function(ComboBox, Tools, Arr, Fun, VK, LinkTargets) {
+ "use strict";
+
+ var history = {};
+ var HISTORY_LENGTH = 5;
+
+ var toMenuItem = function (target) {
+ return {
+ title: target.title,
+ value: {
+ title: {raw: target.title},
+ url: target.url,
+ attach: target.attach
+ }
+ };
+ };
+
+ var toMenuItems = function (targets) {
+ return Tools.map(targets, toMenuItem);
+ };
+
+ var staticMenuItem = function (title, url) {
+ return {
+ title: title,
+ value: {
+ title: title,
+ url: url,
+ attach: Fun.noop
+ }
+ };
+ };
+
+ var isUniqueUrl = function (url, targets) {
+ var foundTarget = Arr.find(targets, function (target) {
+ return target.url === url;
+ });
+
+ return !foundTarget;
+ };
+
+ var getSetting = function (editorSettings, name, defaultValue) {
+ var value = name in editorSettings ? editorSettings[name] : defaultValue;
+ return value === false ? null : value;
+ };
+
+ var createMenuItems = function (term, targets, fileType, editorSettings) {
+ var separator = {title: '-'};
+
+ var fromHistoryMenuItems = function (history) {
+ var uniqueHistory = Arr.filter(history[fileType], function (url) {
+ return isUniqueUrl(url, targets);
+ });
+
+ return Tools.map(uniqueHistory, function (url) {
+ return {
+ title: url,
+ value: {
+ title: url,
+ url: url,
+ attach: Fun.noop
+ }
+ };
+ });
+ };
+
+ var fromMenuItems = function (type) {
+ var filteredTargets = Arr.filter(targets, function (target) {
+ return target.type == type;
+ });
+
+ return toMenuItems(filteredTargets);
+ };
+
+ var anchorMenuItems = function () {
+ var anchorMenuItems = fromMenuItems('anchor');
+ var topAnchor = getSetting(editorSettings, 'anchor_top', '#top');
+ var bottomAchor = getSetting(editorSettings, 'anchor_bottom', '#bottom');
+
+ if (topAnchor !== null) {
+ anchorMenuItems.unshift(staticMenuItem('<top>', topAnchor));
+ }
+
+ if (bottomAchor !== null) {
+ anchorMenuItems.push(staticMenuItem('<bottom>', bottomAchor));
+ }
+
+ return anchorMenuItems;
+ };
+
+ var join = function (items) {
+ return Arr.reduce(items, function (a, b) {
+ var bothEmpty = a.length === 0 || b.length === 0;
+ return bothEmpty ? a.concat(b) : a.concat(separator, b);
+ }, []);
+ };
+
+ if (editorSettings.typeahead_urls === false) {
+ return [];
+ }
+
+ return fileType === 'file' ? join([
+ filterByQuery(term, fromHistoryMenuItems(history)),
+ filterByQuery(term, fromMenuItems('header')),
+ filterByQuery(term, anchorMenuItems())
+ ]) : filterByQuery(term, fromHistoryMenuItems(history));
+ };
+
+ var addToHistory = function (url, fileType) {
+ var items = history[fileType];
+
+ if (!/^https?/.test(url)) {
+ return;
+ }
+
+ if (items) {
+ if (Arr.indexOf(items, url) === -1) {
+ history[fileType] = items.slice(0, HISTORY_LENGTH).concat(url);
+ }
+ } else {
+ history[fileType] = [url];
+ }
+ };
+
+ var filterByQuery = function (term, menuItems) {
+ var lowerCaseTerm = term.toLowerCase();
+ var result = Tools.grep(menuItems, function (item) {
+ return item.title.toLowerCase().indexOf(lowerCaseTerm) !== -1;
+ });
+
+ return result.length === 1 && result[0].title === term ? [] : result;
+ };
+
+ var getTitle = function (linkDetails) {
+ var title = linkDetails.title;
+ return title.raw ? title.raw : title;
+ };
+
+ var setupAutoCompleteHandler = function (ctrl, editorSettings, bodyElm, fileType) {
+ var autocomplete = function (term) {
+ var linkTargets = LinkTargets.find(bodyElm);
+ var menuItems = createMenuItems(term, linkTargets, fileType, editorSettings);
+ ctrl.showAutoComplete(menuItems, term);
+ };
+
+ ctrl.on('autocomplete', function () {
+ autocomplete(ctrl.value());
+ });
+
+ ctrl.on('selectitem', function (e) {
+ var linkDetails = e.value;
+
+ ctrl.value(linkDetails.url);
+ var title = getTitle(linkDetails);
+
+ if (fileType === 'image') {
+ ctrl.fire('change', {meta: {alt: title, attach: linkDetails.attach}});
+ } else {
+ ctrl.fire('change', {meta: {text: title, attach: linkDetails.attach}});
+ }
+
+ ctrl.focus();
+ });
+
+ ctrl.on('click', function (e) {
+ if (ctrl.value().length === 0 && e.target.nodeName === 'INPUT') {
+ autocomplete('');
+ }
+ });
+
+ ctrl.on('PostRender', function () {
+ ctrl.getRoot().on('submit', function (e) {
+ if (!e.isDefaultPrevented()) {
+ addToHistory(ctrl.value(), fileType);
+ }
+ });
+ });
+ };
+
+ var statusToUiState = function (result) {
+ var status = result.status, message = result.message;
+
+ if (status === 'valid') {
+ return {status: 'ok', message: message};
+ } else if (status === 'unknown') {
+ return {status: 'warn', message: message};
+ } else if (status === 'invalid') {
+ return {status: 'warn', message: message};
+ } else {
+ return {status: 'none', message: ''};
+ }
+ };
+
+ var setupLinkValidatorHandler = function (ctrl, editorSettings, fileType) {
+ var validatorHandler = editorSettings.filepicker_validator_handler;
+ if (validatorHandler) {
+ var validateUrl = function (url) {
+ if (url.length === 0) {
+ ctrl.statusLevel('none');
+ return;
+ }
+
+ validatorHandler({
+ url: url,
+ type: fileType
+ }, function (result) {
+ var uiState = statusToUiState(result);
+
+ ctrl.statusMessage(uiState.message);
+ ctrl.statusLevel(uiState.status);
+ });
+ };
+
+ ctrl.state.on('change:value', function (e) {
+ validateUrl(e.value);
+ });
+ }
+ };
+
+ return ComboBox.extend({
+ /**
+ * Constructs a new control instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ var self = this, editor = tinymce.activeEditor, editorSettings = editor.settings;
+ var actionCallback, fileBrowserCallback, fileBrowserCallbackTypes;
+ var fileType = settings.filetype;
+
+ settings.spellcheck = false;
+
+ fileBrowserCallbackTypes = editorSettings.file_picker_types || editorSettings.file_browser_callback_types;
+ if (fileBrowserCallbackTypes) {
+ fileBrowserCallbackTypes = Tools.makeMap(fileBrowserCallbackTypes, /[, ]/);
+ }
+
+ if (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[fileType]) {
+ fileBrowserCallback = editorSettings.file_picker_callback;
+ if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[fileType])) {
+ actionCallback = function() {
+ var meta = self.fire('beforecall').meta;
+
+ meta = Tools.extend({filetype: fileType}, meta);
+
+ // file_picker_callback(callback, currentValue, metaData)
+ fileBrowserCallback.call(
+ editor,
+ function(value, meta) {
+ self.value(value).fire('change', {meta: meta});
+ },
+ self.value(),
+ meta
+ );
+ };
+ } else {
+ // Legacy callback: file_picker_callback(id, currentValue, filetype, window)
+ fileBrowserCallback = editorSettings.file_browser_callback;
+ if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[fileType])) {
+ actionCallback = function() {
+ fileBrowserCallback(
+ self.getEl('inp').id,
+ self.value(),
+ fileType,
+ window
+ );
+ };
+ }
+ }
+ }
+
+ if (actionCallback) {
+ settings.icon = 'browse';
+ settings.onaction = actionCallback;
+ }
+
+ self._super(settings);
+
+ setupAutoCompleteHandler(self, editorSettings, editor.getBody(), fileType);
+ setupLinkValidatorHandler(self, editorSettings, fileType);
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/FitLayout.js
+
+/**
+ * FitLayout.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This layout manager will resize the control to be the size of it's parent container.
+ * In other words width: 100% and height: 100%.
+ *
+ * @-x-less FitLayout.less
+ * @class tinymce.ui.FitLayout
+ * @extends tinymce.ui.AbsoluteLayout
+ */
+define("tinymce/ui/FitLayout", [
+ "tinymce/ui/AbsoluteLayout"
+], function(AbsoluteLayout) {
+ "use strict";
+
+ return AbsoluteLayout.extend({
+ /**
+ * Recalculates the positions of the controls in the specified container.
+ *
+ * @method recalc
+ * @param {tinymce.ui.Container} container Container instance to recalc.
+ */
+ recalc: function(container) {
+ var contLayoutRect = container.layoutRect(), paddingBox = container.paddingBox;
+
+ container.items().filter(':visible').each(function(ctrl) {
+ ctrl.layoutRect({
+ x: paddingBox.left,
+ y: paddingBox.top,
+ w: contLayoutRect.innerW - paddingBox.right - paddingBox.left,
+ h: contLayoutRect.innerH - paddingBox.top - paddingBox.bottom
+ });
+
+ if (ctrl.recalc) {
+ ctrl.recalc();
+ }
+ });
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/FlexLayout.js
+
+/**
+ * FlexLayout.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This layout manager works similar to the CSS flex box.
+ *
+ * @setting {String} direction row|row-reverse|column|column-reverse
+ * @setting {Number} flex A positive-number to flex by.
+ * @setting {String} align start|end|center|stretch
+ * @setting {String} pack start|end|justify
+ *
+ * @class tinymce.ui.FlexLayout
+ * @extends tinymce.ui.AbsoluteLayout
+ */
+define("tinymce/ui/FlexLayout", [
+ "tinymce/ui/AbsoluteLayout"
+], function(AbsoluteLayout) {
+ "use strict";
+
+ return AbsoluteLayout.extend({
+ /**
+ * Recalculates the positions of the controls in the specified container.
+ *
+ * @method recalc
+ * @param {tinymce.ui.Container} container Container instance to recalc.
+ */
+ recalc: function(container) {
+ // A ton of variables, needs to be in the same scope for performance
+ var i, l, items, contLayoutRect, contPaddingBox, contSettings, align, pack, spacing, totalFlex, availableSpace, direction;
+ var ctrl, ctrlLayoutRect, ctrlSettings, flex, maxSizeItems = [], size, maxSize, ratio, rect, pos, maxAlignEndPos;
+ var sizeName, minSizeName, posName, maxSizeName, beforeName, innerSizeName, deltaSizeName, contentSizeName;
+ var alignAxisName, alignInnerSizeName, alignSizeName, alignMinSizeName, alignBeforeName, alignAfterName;
+ var alignDeltaSizeName, alignContentSizeName;
+ var max = Math.max, min = Math.min;
+
+ // Get container items, properties and settings
+ items = container.items().filter(':visible');
+ contLayoutRect = container.layoutRect();
+ contPaddingBox = container.paddingBox;
+ contSettings = container.settings;
+ direction = container.isRtl() ? (contSettings.direction || 'row-reversed') : contSettings.direction;
+ align = contSettings.align;
+ pack = container.isRtl() ? (contSettings.pack || 'end') : contSettings.pack;
+ spacing = contSettings.spacing || 0;
+
+ if (direction == "row-reversed" || direction == "column-reverse") {
+ items = items.set(items.toArray().reverse());
+ direction = direction.split('-')[0];
+ }
+
+ // Setup axis variable name for row/column direction since the calculations is the same
+ if (direction == "column") {
+ posName = "y";
+ sizeName = "h";
+ minSizeName = "minH";
+ maxSizeName = "maxH";
+ innerSizeName = "innerH";
+ beforeName = 'top';
+ deltaSizeName = "deltaH";
+ contentSizeName = "contentH";
+
+ alignBeforeName = "left";
+ alignSizeName = "w";
+ alignAxisName = "x";
+ alignInnerSizeName = "innerW";
+ alignMinSizeName = "minW";
+ alignAfterName = "right";
+ alignDeltaSizeName = "deltaW";
+ alignContentSizeName = "contentW";
+ } else {
+ posName = "x";
+ sizeName = "w";
+ minSizeName = "minW";
+ maxSizeName = "maxW";
+ innerSizeName = "innerW";
+ beforeName = 'left';
+ deltaSizeName = "deltaW";
+ contentSizeName = "contentW";
+
+ alignBeforeName = "top";
+ alignSizeName = "h";
+ alignAxisName = "y";
+ alignInnerSizeName = "innerH";
+ alignMinSizeName = "minH";
+ alignAfterName = "bottom";
+ alignDeltaSizeName = "deltaH";
+ alignContentSizeName = "contentH";
+ }
+
+ // Figure out total flex, availableSpace and collect any max size elements
+ availableSpace = contLayoutRect[innerSizeName] - contPaddingBox[beforeName] - contPaddingBox[beforeName];
+ maxAlignEndPos = totalFlex = 0;
+ for (i = 0, l = items.length; i < l; i++) {
+ ctrl = items[i];
+ ctrlLayoutRect = ctrl.layoutRect();
+ ctrlSettings = ctrl.settings;
+ flex = ctrlSettings.flex;
+ availableSpace -= (i < l - 1 ? spacing : 0);
+
+ if (flex > 0) {
+ totalFlex += flex;
+
+ // Flexed item has a max size then we need to check if we will hit that size
+ if (ctrlLayoutRect[maxSizeName]) {
+ maxSizeItems.push(ctrl);
+ }
+
+ ctrlLayoutRect.flex = flex;
+ }
+
+ availableSpace -= ctrlLayoutRect[minSizeName];
+
+ // Calculate the align end position to be used to check for overflow/underflow
+ size = contPaddingBox[alignBeforeName] + ctrlLayoutRect[alignMinSizeName] + contPaddingBox[alignAfterName];
+ if (size > maxAlignEndPos) {
+ maxAlignEndPos = size;
+ }
+ }
+
+ // Calculate minW/minH
+ rect = {};
+ if (availableSpace < 0) {
+ rect[minSizeName] = contLayoutRect[minSizeName] - availableSpace + contLayoutRect[deltaSizeName];
+ } else {
+ rect[minSizeName] = contLayoutRect[innerSizeName] - availableSpace + contLayoutRect[deltaSizeName];
+ }
+
+ rect[alignMinSizeName] = maxAlignEndPos + contLayoutRect[alignDeltaSizeName];
+
+ rect[contentSizeName] = contLayoutRect[innerSizeName] - availableSpace;
+ rect[alignContentSizeName] = maxAlignEndPos;
+ rect.minW = min(rect.minW, contLayoutRect.maxW);
+ rect.minH = min(rect.minH, contLayoutRect.maxH);
+ rect.minW = max(rect.minW, contLayoutRect.startMinWidth);
+ rect.minH = max(rect.minH, contLayoutRect.startMinHeight);
+
+ // Resize container container if minSize was changed
+ if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) {
+ rect.w = rect.minW;
+ rect.h = rect.minH;
+
+ container.layoutRect(rect);
+ this.recalc(container);
+
+ // Forced recalc for example if items are hidden/shown
+ if (container._lastRect === null) {
+ var parentCtrl = container.parent();
+ if (parentCtrl) {
+ parentCtrl._lastRect = null;
+ parentCtrl.recalc();
+ }
+ }
+
+ return;
+ }
+
+ // Handle max size elements, check if they will become to wide with current options
+ ratio = availableSpace / totalFlex;
+ for (i = 0, l = maxSizeItems.length; i < l; i++) {
+ ctrl = maxSizeItems[i];
+ ctrlLayoutRect = ctrl.layoutRect();
+ maxSize = ctrlLayoutRect[maxSizeName];
+ size = ctrlLayoutRect[minSizeName] + ctrlLayoutRect.flex * ratio;
+
+ if (size > maxSize) {
+ availableSpace -= (ctrlLayoutRect[maxSizeName] - ctrlLayoutRect[minSizeName]);
+ totalFlex -= ctrlLayoutRect.flex;
+ ctrlLayoutRect.flex = 0;
+ ctrlLayoutRect.maxFlexSize = maxSize;
+ } else {
+ ctrlLayoutRect.maxFlexSize = 0;
+ }
+ }
+
+ // Setup new ratio, target layout rect, start position
+ ratio = availableSpace / totalFlex;
+ pos = contPaddingBox[beforeName];
+ rect = {};
+
+ // Handle pack setting moves the start position to end, center
+ if (totalFlex === 0) {
+ if (pack == "end") {
+ pos = availableSpace + contPaddingBox[beforeName];
+ } else if (pack == "center") {
+ pos = Math.round(
+ (contLayoutRect[innerSizeName] / 2) - ((contLayoutRect[innerSizeName] - availableSpace) / 2)
+ ) + contPaddingBox[beforeName];
+
+ if (pos < 0) {
+ pos = contPaddingBox[beforeName];
+ }
+ } else if (pack == "justify") {
+ pos = contPaddingBox[beforeName];
+ spacing = Math.floor(availableSpace / (items.length - 1));
+ }
+ }
+
+ // Default aligning (start) the other ones needs to be calculated while doing the layout
+ rect[alignAxisName] = contPaddingBox[alignBeforeName];
+
+ // Start laying out controls
+ for (i = 0, l = items.length; i < l; i++) {
+ ctrl = items[i];
+ ctrlLayoutRect = ctrl.layoutRect();
+ size = ctrlLayoutRect.maxFlexSize || ctrlLayoutRect[minSizeName];
+
+ // Align the control on the other axis
+ if (align === "center") {
+ rect[alignAxisName] = Math.round((contLayoutRect[alignInnerSizeName] / 2) - (ctrlLayoutRect[alignSizeName] / 2));
+ } else if (align === "stretch") {
+ rect[alignSizeName] = max(
+ ctrlLayoutRect[alignMinSizeName] || 0,
+ contLayoutRect[alignInnerSizeName] - contPaddingBox[alignBeforeName] - contPaddingBox[alignAfterName]
+ );
+ rect[alignAxisName] = contPaddingBox[alignBeforeName];
+ } else if (align === "end") {
+ rect[alignAxisName] = contLayoutRect[alignInnerSizeName] - ctrlLayoutRect[alignSizeName] - contPaddingBox.top;
+ }
+
+ // Calculate new size based on flex
+ if (ctrlLayoutRect.flex > 0) {
+ size += ctrlLayoutRect.flex * ratio;
+ }
+
+ rect[sizeName] = size;
+ rect[posName] = pos;
+ ctrl.layoutRect(rect);
+
+ // Recalculate containers
+ if (ctrl.recalc) {
+ ctrl.recalc();
+ }
+
+ // Move x/y position
+ pos += size + spacing;
+ }
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/FlowLayout.js
+
+/**
+ * FlowLayout.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This layout manager will place the controls by using the browsers native layout.
+ *
+ * @-x-less FlowLayout.less
+ * @class tinymce.ui.FlowLayout
+ * @extends tinymce.ui.Layout
+ */
+define("tinymce/ui/FlowLayout", [
+ "tinymce/ui/Layout"
+], function(Layout) {
+ return Layout.extend({
+ Defaults: {
+ containerClass: 'flow-layout',
+ controlClass: 'flow-layout-item',
+ endClass: 'break'
+ },
+
+ /**
+ * Recalculates the positions of the controls in the specified container.
+ *
+ * @method recalc
+ * @param {tinymce.ui.Container} container Container instance to recalc.
+ */
+ recalc: function(container) {
+ container.items().filter(':visible').each(function(ctrl) {
+ if (ctrl.recalc) {
+ ctrl.recalc();
+ }
+ });
+ },
+
+ isNative: function() {
+ return true;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/FormatControls.js
+
+/**
+ * FormatControls.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Internal class containing all TinyMCE specific control types such as
+ * format listboxes, fontlist boxes, toolbar buttons etc.
+ *
+ * @class tinymce.ui.FormatControls
+ */
+define("tinymce/ui/FormatControls", [
+ "tinymce/ui/Control",
+ "tinymce/ui/Widget",
+ "tinymce/ui/FloatPanel",
+ "tinymce/util/Tools",
+ "tinymce/util/Arr",
+ "tinymce/dom/DOMUtils",
+ "tinymce/EditorManager",
+ "tinymce/Env"
+], function(Control, Widget, FloatPanel, Tools, Arr, DOMUtils, EditorManager, Env) {
+ var each = Tools.each;
+
+ var flatten = function (ar) {
+ return Arr.reduce(ar, function (result, item) {
+ return result.concat(item);
+ }, []);
+ };
+
+ EditorManager.on('AddEditor', function(e) {
+ var editor = e.editor;
+
+ setupRtlMode(editor);
+ registerControls(editor);
+ setupContainer(editor);
+ });
+
+ Control.translate = function(text) {
+ return EditorManager.translate(text);
+ };
+
+ Widget.tooltips = !Env.iOS;
+
+ function setupContainer(editor) {
+ if (editor.settings.ui_container) {
+ Env.container = DOMUtils.DOM.select(editor.settings.ui_container)[0];
+ }
+ }
+
+ function setupRtlMode(editor) {
+ editor.on('ScriptsLoaded', function () {
+ if (editor.rtl) {
+ Control.rtl = true;
+ }
+ });
+ }
+
+ function registerControls(editor) {
+ var formatMenu;
+
+ function createListBoxChangeHandler(items, formatName) {
+ return function() {
+ var self = this;
+
+ editor.on('nodeChange', function(e) {
+ var formatter = editor.formatter;
+ var value = null;
+
+ each(e.parents, function(node) {
+ each(items, function(item) {
+ if (formatName) {
+ if (formatter.matchNode(node, formatName, {value: item.value})) {
+ value = item.value;
+ }
+ } else {
+ if (formatter.matchNode(node, item.value)) {
+ value = item.value;
+ }
+ }
+
+ if (value) {
+ return false;
+ }
+ });
+
+ if (value) {
+ return false;
+ }
+ });
+
+ self.value(value);
+ });
+ };
+ }
+
+ function createFormats(formats) {
+ formats = formats.replace(/;$/, '').split(';');
+
+ var i = formats.length;
+ while (i--) {
+ formats[i] = formats[i].split('=');
+ }
+
+ return formats;
+ }
+
+ function createFormatMenu() {
+ var count = 0, newFormats = [];
+
+ var defaultStyleFormats = [
+ {title: 'Headings', items: [
+ {title: 'Heading 1', format: 'h1'},
+ {title: 'Heading 2', format: 'h2'},
+ {title: 'Heading 3', format: 'h3'},
+ {title: 'Heading 4', format: 'h4'},
+ {title: 'Heading 5', format: 'h5'},
+ {title: 'Heading 6', format: 'h6'}
+ ]},
+
+ {title: 'Inline', items: [
+ {title: 'Bold', icon: 'bold', format: 'bold'},
+ {title: 'Italic', icon: 'italic', format: 'italic'},
+ {title: 'Underline', icon: 'underline', format: 'underline'},
+ {title: 'Strikethrough', icon: 'strikethrough', format: 'strikethrough'},
+ {title: 'Superscript', icon: 'superscript', format: 'superscript'},
+ {title: 'Subscript', icon: 'subscript', format: 'subscript'},
+ {title: 'Code', icon: 'code', format: 'code'}
+ ]},
+
+ {title: 'Blocks', items: [
+ {title: 'Paragraph', format: 'p'},
+ {title: 'Blockquote', format: 'blockquote'},
+ {title: 'Div', format: 'div'},
+ {title: 'Pre', format: 'pre'}
+ ]},
+
+ {title: 'Alignment', items: [
+ {title: 'Left', icon: 'alignleft', format: 'alignleft'},
+ {title: 'Center', icon: 'aligncenter', format: 'aligncenter'},
+ {title: 'Right', icon: 'alignright', format: 'alignright'},
+ {title: 'Justify', icon: 'alignjustify', format: 'alignjustify'}
+ ]}
+ ];
+
+ function createMenu(formats) {
+ var menu = [];
+
+ if (!formats) {
+ return;
+ }
+
+ each(formats, function(format) {
+ var menuItem = {
+ text: format.title,
+ icon: format.icon
+ };
+
+ if (format.items) {
+ menuItem.menu = createMenu(format.items);
+ } else {
+ var formatName = format.format || "custom" + count++;
+
+ if (!format.format) {
+ format.name = formatName;
+ newFormats.push(format);
+ }
+
+ menuItem.format = formatName;
+ menuItem.cmd = format.cmd;
+ }
+
+ menu.push(menuItem);
+ });
+
+ return menu;
+ }
+
+ function createStylesMenu() {
+ var menu;
+
+ if (editor.settings.style_formats_merge) {
+ if (editor.settings.style_formats) {
+ menu = createMenu(defaultStyleFormats.concat(editor.settings.style_formats));
+ } else {
+ menu = createMenu(defaultStyleFormats);
+ }
+ } else {
+ menu = createMenu(editor.settings.style_formats || defaultStyleFormats);
+ }
+
+ return menu;
+ }
+
+ editor.on('init', function() {
+ each(newFormats, function(format) {
+ editor.formatter.register(format.name, format);
+ });
+ });
+
+ return {
+ type: 'menu',
+ items: createStylesMenu(),
+ onPostRender: function(e) {
+ editor.fire('renderFormatsMenu', {control: e.control});
+ },
+ itemDefaults: {
+ preview: true,
+
+ textStyle: function() {
+ if (this.settings.format) {
+ return editor.formatter.getCssText(this.settings.format);
+ }
+ },
+
+ onPostRender: function() {
+ var self = this;
+
+ self.parent().on('show', function() {
+ var formatName, command;
+
+ formatName = self.settings.format;
+ if (formatName) {
+ self.disabled(!editor.formatter.canApply(formatName));
+ self.active(editor.formatter.match(formatName));
+ }
+
+ command = self.settings.cmd;
+ if (command) {
+ self.active(editor.queryCommandState(command));
+ }
+ });
+ },
+
+ onclick: function() {
+ if (this.settings.format) {
+ toggleFormat(this.settings.format);
+ }
+
+ if (this.settings.cmd) {
+ editor.execCommand(this.settings.cmd);
+ }
+ }
+ }
+ };
+ }
+
+ formatMenu = createFormatMenu();
+
+ function initOnPostRender(name) {
+ return function() {
+ var self = this;
+
+ // TODO: Fix this
+ if (editor.formatter) {
+ editor.formatter.formatChanged(name, function(state) {
+ self.active(state);
+ });
+ } else {
+ editor.on('init', function() {
+ editor.formatter.formatChanged(name, function(state) {
+ self.active(state);
+ });
+ });
+ }
+ };
+ }
+
+ // Simple format controls <control/format>:<UI text>
+ each({
+ bold: 'Bold',
+ italic: 'Italic',
+ underline: 'Underline',
+ strikethrough: 'Strikethrough',
+ subscript: 'Subscript',
+ superscript: 'Superscript'
+ }, function(text, name) {
+ editor.addButton(name, {
+ tooltip: text,
+ onPostRender: initOnPostRender(name),
+ onclick: function() {
+ toggleFormat(name);
+ }
+ });
+ });
+
+ // Simple command controls <control>:[<UI text>,<Command>]
+ each({
+ outdent: ['Decrease indent', 'Outdent'],
+ indent: ['Increase indent', 'Indent'],
+ cut: ['Cut', 'Cut'],
+ copy: ['Copy', 'Copy'],
+ paste: ['Paste', 'Paste'],
+ help: ['Help', 'mceHelp'],
+ selectall: ['Select all', 'SelectAll'],
+ removeformat: ['Clear formatting', 'RemoveFormat'],
+ visualaid: ['Visual aids', 'mceToggleVisualAid'],
+ newdocument: ['New document', 'mceNewDocument']
+ }, function(item, name) {
+ editor.addButton(name, {
+ tooltip: item[0],
+ cmd: item[1]
+ });
+ });
+
+ // Simple command controls with format state
+ each({
+ blockquote: ['Blockquote', 'mceBlockQuote'],
+ subscript: ['Subscript', 'Subscript'],
+ superscript: ['Superscript', 'Superscript'],
+ alignleft: ['Align left', 'JustifyLeft'],
+ aligncenter: ['Align center', 'JustifyCenter'],
+ alignright: ['Align right', 'JustifyRight'],
+ alignjustify: ['Justify', 'JustifyFull'],
+ alignnone: ['No alignment', 'JustifyNone']
+ }, function(item, name) {
+ editor.addButton(name, {
+ tooltip: item[0],
+ cmd: item[1],
+ onPostRender: initOnPostRender(name)
+ });
+ });
+
+ function toggleUndoRedoState(type) {
+ return function() {
+ var self = this;
+
+ type = type == 'redo' ? 'hasRedo' : 'hasUndo';
+
+ function checkState() {
+ return editor.undoManager ? editor.undoManager[type]() : false;
+ }
+
+ self.disabled(!checkState());
+ editor.on('Undo Redo AddUndo TypingUndo ClearUndos SwitchMode', function() {
+ self.disabled(editor.readonly || !checkState());
+ });
+ };
+ }
+
+ function toggleVisualAidState() {
+ var self = this;
+
+ editor.on('VisualAid', function(e) {
+ self.active(e.hasVisual);
+ });
+
+ self.active(editor.hasVisual);
+ }
+
+ var trimMenuItems = function (menuItems) {
+ var outputMenuItems = menuItems;
+
+ if (outputMenuItems.length > 0 && outputMenuItems[0].text === '-') {
+ outputMenuItems = outputMenuItems.slice(1);
+ }
+
+ if (outputMenuItems.length > 0 && outputMenuItems[outputMenuItems.length - 1].text === '-') {
+ outputMenuItems = outputMenuItems.slice(0, outputMenuItems.length - 1);
+ }
+
+ return outputMenuItems;
+ };
+
+ var createCustomMenuItems = function (names) {
+ var items, nameList;
+
+ if (typeof names === 'string') {
+ nameList = names.split(' ');
+ } else if (Tools.isArray(names)) {
+ return flatten(Tools.map(names, createCustomMenuItems));
+ }
+
+ items = Tools.grep(nameList, function (name) {
+ return name === '|' || name in editor.menuItems;
+ });
+
+ return Tools.map(items, function (name) {
+ return name === '|' ? {text: '-'} : editor.menuItems[name];
+ });
+ };
+
+ var createContextMenuItems = function (context) {
+ var outputMenuItems = [{text: '-'}];
+ var menuItems = Tools.grep(editor.menuItems, function (menuItem) {
+ return menuItem.context === context;
+ });
+
+ Tools.each(menuItems, function (menuItem) {
+ if (menuItem.separator == 'before') {
+ outputMenuItems.push({text: '|'});
+ }
+
+ if (menuItem.prependToContext) {
+ outputMenuItems.unshift(menuItem);
+ } else {
+ outputMenuItems.push(menuItem);
+ }
+
+ if (menuItem.separator == 'after') {
+ outputMenuItems.push({text: '|'});
+ }
+ });
+
+ return outputMenuItems;
+ };
+
+ var createInsertMenu = function (editorSettings) {
+ if (editorSettings.insert_button_items) {
+ return trimMenuItems(createCustomMenuItems(editorSettings.insert_button_items));
+ } else {
+ return trimMenuItems(createContextMenuItems('insert'));
+ }
+ };
+
+ editor.addButton('undo', {
+ tooltip: 'Undo',
+ onPostRender: toggleUndoRedoState('undo'),
+ cmd: 'undo'
+ });
+
+ editor.addButton('redo', {
+ tooltip: 'Redo',
+ onPostRender: toggleUndoRedoState('redo'),
+ cmd: 'redo'
+ });
+
+ editor.addMenuItem('newdocument', {
+ text: 'New document',
+ icon: 'newdocument',
+ cmd: 'mceNewDocument'
+ });
+
+ editor.addMenuItem('undo', {
+ text: 'Undo',
+ icon: 'undo',
+ shortcut: 'Meta+Z',
+ onPostRender: toggleUndoRedoState('undo'),
+ cmd: 'undo'
+ });
+
+ editor.addMenuItem('redo', {
+ text: 'Redo',
+ icon: 'redo',
+ shortcut: 'Meta+Y',
+ onPostRender: toggleUndoRedoState('redo'),
+ cmd: 'redo'
+ });
+
+ editor.addMenuItem('visualaid', {
+ text: 'Visual aids',
+ selectable: true,
+ onPostRender: toggleVisualAidState,
+ cmd: 'mceToggleVisualAid'
+ });
+
+ editor.addButton('remove', {
+ tooltip: 'Remove',
+ icon: 'remove',
+ cmd: 'Delete'
+ });
+
+ editor.addButton('insert', {
+ type: 'menubutton',
+ icon: 'insert',
+ menu: [],
+ oncreatemenu: function () {
+ this.menu.add(createInsertMenu(editor.settings));
+ this.menu.renderNew();
+ }
+ });
+
+ each({
+ cut: ['Cut', 'Cut', 'Meta+X'],
+ copy: ['Copy', 'Copy', 'Meta+C'],
+ paste: ['Paste', 'Paste', 'Meta+V'],
+ selectall: ['Select all', 'SelectAll', 'Meta+A'],
+ bold: ['Bold', 'Bold', 'Meta+B'],
+ italic: ['Italic', 'Italic', 'Meta+I'],
+ underline: ['Underline', 'Underline'],
+ strikethrough: ['Strikethrough', 'Strikethrough'],
+ subscript: ['Subscript', 'Subscript'],
+ superscript: ['Superscript', 'Superscript'],
+ removeformat: ['Clear formatting', 'RemoveFormat']
+ }, function(item, name) {
+ editor.addMenuItem(name, {
+ text: item[0],
+ icon: name,
+ shortcut: item[2],
+ cmd: item[1]
+ });
+ });
+
+ editor.on('mousedown', function() {
+ FloatPanel.hideAll();
+ });
+
+ function toggleFormat(fmt) {
+ if (fmt.control) {
+ fmt = fmt.control.value();
+ }
+
+ if (fmt) {
+ editor.execCommand('mceToggleFormat', false, fmt);
+ }
+ }
+
+ function hideMenuObjects(menu) {
+ var count = menu.length;
+
+ Tools.each(menu, function (item) {
+ if (item.menu) {
+ item.hidden = hideMenuObjects(item.menu) === 0;
+ }
+
+ var formatName = item.format;
+ if (formatName) {
+ item.hidden = !editor.formatter.canApply(formatName);
+ }
+
+ if (item.hidden) {
+ count--;
+ }
+ });
+
+ return count;
+ }
+
+ function hideFormatMenuItems(menu) {
+ var count = menu.items().length;
+
+ menu.items().each(function (item) {
+ if (item.menu) {
+ item.visible(hideFormatMenuItems(item.menu) > 0);
+ }
+
+ if (!item.menu && item.settings.menu) {
+ item.visible(hideMenuObjects(item.settings.menu) > 0);
+ }
+
+ var formatName = item.settings.format;
+ if (formatName) {
+ item.visible(editor.formatter.canApply(formatName));
+ }
+
+ if (!item.visible()) {
+ count--;
+ }
+ });
+
+ return count;
+ }
+
+ editor.addButton('styleselect', {
+ type: 'menubutton',
+ text: 'Formats',
+ menu: formatMenu,
+ onShowMenu: function () {
+ if (editor.settings.style_formats_autohide) {
+ hideFormatMenuItems(this.menu);
+ }
+ }
+ });
+
+ editor.addButton('formatselect', function() {
+ var items = [], blocks = createFormats(editor.settings.block_formats ||
+ 'Paragraph=p;' +
+ 'Heading 1=h1;' +
+ 'Heading 2=h2;' +
+ 'Heading 3=h3;' +
+ 'Heading 4=h4;' +
+ 'Heading 5=h5;' +
+ 'Heading 6=h6;' +
+ 'Preformatted=pre'
+ );
+
+ each(blocks, function(block) {
+ items.push({
+ text: block[0],
+ value: block[1],
+ textStyle: function() {
+ return editor.formatter.getCssText(block[1]);
+ }
+ });
+ });
+
+ return {
+ type: 'listbox',
+ text: blocks[0][0],
+ values: items,
+ fixedWidth: true,
+ onselect: toggleFormat,
+ onPostRender: createListBoxChangeHandler(items)
+ };
+ });
+
+ editor.addButton('fontselect', function() {
+ var defaultFontsFormats =
+ 'Andale Mono=andale mono,monospace;' +
+ 'Arial=arial,helvetica,sans-serif;' +
+ 'Arial Black=arial black,sans-serif;' +
+ 'Book Antiqua=book antiqua,palatino,serif;' +
+ 'Comic Sans MS=comic sans ms,sans-serif;' +
+ 'Courier New=courier new,courier,monospace;' +
+ 'Georgia=georgia,palatino,serif;' +
+ 'Helvetica=helvetica,arial,sans-serif;' +
+ 'Impact=impact,sans-serif;' +
+ 'Symbol=symbol;' +
+ 'Tahoma=tahoma,arial,helvetica,sans-serif;' +
+ 'Terminal=terminal,monaco,monospace;' +
+ 'Times New Roman=times new roman,times,serif;' +
+ 'Trebuchet MS=trebuchet ms,geneva,sans-serif;' +
+ 'Verdana=verdana,geneva,sans-serif;' +
+ 'Webdings=webdings;' +
+ 'Wingdings=wingdings,zapf dingbats';
+
+ var items = [], fonts = createFormats(editor.settings.font_formats || defaultFontsFormats);
+
+ each(fonts, function(font) {
+ items.push({
+ text: {raw: font[0]},
+ value: font[1],
+ textStyle: font[1].indexOf('dings') == -1 ? 'font-family:' + font[1] : ''
+ });
+ });
+
+ return {
+ type: 'listbox',
+ text: 'Font Family',
+ tooltip: 'Font Family',
+ values: items,
+ fixedWidth: true,
+ onPostRender: createListBoxChangeHandler(items, 'fontname'),
+ onselect: function(e) {
+ if (e.control.settings.value) {
+ editor.execCommand('FontName', false, e.control.settings.value);
+ }
+ }
+ };
+ });
+
+ editor.addButton('fontsizeselect', function() {
+ var items = [], defaultFontsizeFormats = '8pt 10pt 12pt 14pt 18pt 24pt 36pt';
+ var fontsize_formats = editor.settings.fontsize_formats || defaultFontsizeFormats;
+
+ each(fontsize_formats.split(' '), function(item) {
+ var text = item, value = item;
+ // Allow text=value font sizes.
+ var values = item.split('=');
+ if (values.length > 1) {
+ text = values[0];
+ value = values[1];
+ }
+ items.push({text: text, value: value});
+ });
+
+ return {
+ type: 'listbox',
+ text: 'Font Sizes',
+ tooltip: 'Font Sizes',
+ values: items,
+ fixedWidth: true,
+ onPostRender: createListBoxChangeHandler(items, 'fontsize'),
+ onclick: function(e) {
+ if (e.control.settings.value) {
+ editor.execCommand('FontSize', false, e.control.settings.value);
+ }
+ }
+ };
+ });
+
+ editor.addMenuItem('formats', {
+ text: 'Formats',
+ menu: formatMenu
+ });
+ }
+});
+
+// Included from: js/tinymce/classes/ui/GridLayout.js
+
+/**
+ * GridLayout.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This layout manager places controls in a grid.
+ *
+ * @setting {Number} spacing Spacing between controls.
+ * @setting {Number} spacingH Horizontal spacing between controls.
+ * @setting {Number} spacingV Vertical spacing between controls.
+ * @setting {Number} columns Number of columns to use.
+ * @setting {String/Array} alignH start|end|center|stretch or array of values for each column.
+ * @setting {String/Array} alignV start|end|center|stretch or array of values for each column.
+ * @setting {String} pack start|end
+ *
+ * @class tinymce.ui.GridLayout
+ * @extends tinymce.ui.AbsoluteLayout
+ */
+define("tinymce/ui/GridLayout", [
+ "tinymce/ui/AbsoluteLayout"
+], function(AbsoluteLayout) {
+ "use strict";
+
+ return AbsoluteLayout.extend({
+ /**
+ * Recalculates the positions of the controls in the specified container.
+ *
+ * @method recalc
+ * @param {tinymce.ui.Container} container Container instance to recalc.
+ */
+ recalc: function(container) {
+ var settings, rows, cols, items, contLayoutRect, width, height, rect,
+ ctrlLayoutRect, ctrl, x, y, posX, posY, ctrlSettings, contPaddingBox, align, spacingH, spacingV, alignH, alignV, maxX, maxY,
+ colWidths = [], rowHeights = [], ctrlMinWidth, ctrlMinHeight, availableWidth, availableHeight, reverseRows, idx;
+
+ // Get layout settings
+ settings = container.settings;
+ items = container.items().filter(':visible');
+ contLayoutRect = container.layoutRect();
+ cols = settings.columns || Math.ceil(Math.sqrt(items.length));
+ rows = Math.ceil(items.length / cols);
+ spacingH = settings.spacingH || settings.spacing || 0;
+ spacingV = settings.spacingV || settings.spacing || 0;
+ alignH = settings.alignH || settings.align;
+ alignV = settings.alignV || settings.align;
+ contPaddingBox = container.paddingBox;
+ reverseRows = 'reverseRows' in settings ? settings.reverseRows : container.isRtl();
+
+ if (alignH && typeof alignH == "string") {
+ alignH = [alignH];
+ }
+
+ if (alignV && typeof alignV == "string") {
+ alignV = [alignV];
+ }
+
+ // Zero padd columnWidths
+ for (x = 0; x < cols; x++) {
+ colWidths.push(0);
+ }
+
+ // Zero padd rowHeights
+ for (y = 0; y < rows; y++) {
+ rowHeights.push(0);
+ }
+
+ // Calculate columnWidths and rowHeights
+ for (y = 0; y < rows; y++) {
+ for (x = 0; x < cols; x++) {
+ ctrl = items[y * cols + x];
+
+ // Out of bounds
+ if (!ctrl) {
+ break;
+ }
+
+ ctrlLayoutRect = ctrl.layoutRect();
+ ctrlMinWidth = ctrlLayoutRect.minW;
+ ctrlMinHeight = ctrlLayoutRect.minH;
+
+ colWidths[x] = ctrlMinWidth > colWidths[x] ? ctrlMinWidth : colWidths[x];
+ rowHeights[y] = ctrlMinHeight > rowHeights[y] ? ctrlMinHeight : rowHeights[y];
+ }
+ }
+
+ // Calculate maxX
+ availableWidth = contLayoutRect.innerW - contPaddingBox.left - contPaddingBox.right;
+ for (maxX = 0, x = 0; x < cols; x++) {
+ maxX += colWidths[x] + (x > 0 ? spacingH : 0);
+ availableWidth -= (x > 0 ? spacingH : 0) + colWidths[x];
+ }
+
+ // Calculate maxY
+ availableHeight = contLayoutRect.innerH - contPaddingBox.top - contPaddingBox.bottom;
+ for (maxY = 0, y = 0; y < rows; y++) {
+ maxY += rowHeights[y] + (y > 0 ? spacingV : 0);
+ availableHeight -= (y > 0 ? spacingV : 0) + rowHeights[y];
+ }
+
+ maxX += contPaddingBox.left + contPaddingBox.right;
+ maxY += contPaddingBox.top + contPaddingBox.bottom;
+
+ // Calculate minW/minH
+ rect = {};
+ rect.minW = maxX + (contLayoutRect.w - contLayoutRect.innerW);
+ rect.minH = maxY + (contLayoutRect.h - contLayoutRect.innerH);
+
+ rect.contentW = rect.minW - contLayoutRect.deltaW;
+ rect.contentH = rect.minH - contLayoutRect.deltaH;
+ rect.minW = Math.min(rect.minW, contLayoutRect.maxW);
+ rect.minH = Math.min(rect.minH, contLayoutRect.maxH);
+ rect.minW = Math.max(rect.minW, contLayoutRect.startMinWidth);
+ rect.minH = Math.max(rect.minH, contLayoutRect.startMinHeight);
+
+ // Resize container container if minSize was changed
+ if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) {
+ rect.w = rect.minW;
+ rect.h = rect.minH;
+
+ container.layoutRect(rect);
+ this.recalc(container);
+
+ // Forced recalc for example if items are hidden/shown
+ if (container._lastRect === null) {
+ var parentCtrl = container.parent();
+ if (parentCtrl) {
+ parentCtrl._lastRect = null;
+ parentCtrl.recalc();
+ }
+ }
+
+ return;
+ }
+
+ // Update contentW/contentH so absEnd moves correctly
+ if (contLayoutRect.autoResize) {
+ rect = container.layoutRect(rect);
+ rect.contentW = rect.minW - contLayoutRect.deltaW;
+ rect.contentH = rect.minH - contLayoutRect.deltaH;
+ }
+
+ var flexV;
+
+ if (settings.packV == 'start') {
+ flexV = 0;
+ } else {
+ flexV = availableHeight > 0 ? Math.floor(availableHeight / rows) : 0;
+ }
+
+ // Calculate totalFlex
+ var totalFlex = 0;
+ var flexWidths = settings.flexWidths;
+ if (flexWidths) {
+ for (x = 0; x < flexWidths.length; x++) {
+ totalFlex += flexWidths[x];
+ }
+ } else {
+ totalFlex = cols;
+ }
+
+ // Calculate new column widths based on flex values
+ var ratio = availableWidth / totalFlex;
+ for (x = 0; x < cols; x++) {
+ colWidths[x] += flexWidths ? flexWidths[x] * ratio : ratio;
+ }
+
+ // Move/resize controls
+ posY = contPaddingBox.top;
+ for (y = 0; y < rows; y++) {
+ posX = contPaddingBox.left;
+ height = rowHeights[y] + flexV;
+
+ for (x = 0; x < cols; x++) {
+ if (reverseRows) {
+ idx = y * cols + cols - 1 - x;
+ } else {
+ idx = y * cols + x;
+ }
+
+ ctrl = items[idx];
+
+ // No more controls to render then break
+ if (!ctrl) {
+ break;
+ }
+
+ // Get control settings and calculate x, y
+ ctrlSettings = ctrl.settings;
+ ctrlLayoutRect = ctrl.layoutRect();
+ width = Math.max(colWidths[x], ctrlLayoutRect.startMinWidth);
+ ctrlLayoutRect.x = posX;
+ ctrlLayoutRect.y = posY;
+
+ // Align control horizontal
+ align = ctrlSettings.alignH || (alignH ? (alignH[x] || alignH[0]) : null);
+ if (align == "center") {
+ ctrlLayoutRect.x = posX + (width / 2) - (ctrlLayoutRect.w / 2);
+ } else if (align == "right") {
+ ctrlLayoutRect.x = posX + width - ctrlLayoutRect.w;
+ } else if (align == "stretch") {
+ ctrlLayoutRect.w = width;
+ }
+
+ // Align control vertical
+ align = ctrlSettings.alignV || (alignV ? (alignV[x] || alignV[0]) : null);
+ if (align == "center") {
+ ctrlLayoutRect.y = posY + (height / 2) - (ctrlLayoutRect.h / 2);
+ } else if (align == "bottom") {
+ ctrlLayoutRect.y = posY + height - ctrlLayoutRect.h;
+ } else if (align == "stretch") {
+ ctrlLayoutRect.h = height;
+ }
+
+ ctrl.layoutRect(ctrlLayoutRect);
+
+ posX += width + spacingH;
+
+ if (ctrl.recalc) {
+ ctrl.recalc();
+ }
+ }
+
+ posY += height + spacingV;
+ }
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Iframe.js
+
+/**
+ * Iframe.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*jshint scripturl:true */
+
+/**
+ * This class creates an iframe.
+ *
+ * @setting {String} url Url to open in the iframe.
+ *
+ * @-x-less Iframe.less
+ * @class tinymce.ui.Iframe
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/Iframe", [
+ "tinymce/ui/Widget",
+ "tinymce/util/Delay"
+], function(Widget, Delay) {
+ "use strict";
+
+ return Widget.extend({
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this;
+
+ self.classes.add('iframe');
+ self.canFocus = false;
+
+ /*eslint no-script-url:0 */
+ return (
+ '<iframe id="' + self._id + '" class="' + self.classes + '" tabindex="-1" src="' +
+ (self.settings.url || "javascript:''") + '" frameborder="0"></iframe>'
+ );
+ },
+
+ /**
+ * Setter for the iframe source.
+ *
+ * @method src
+ * @param {String} src Source URL for iframe.
+ */
+ src: function(src) {
+ this.getEl().src = src;
+ },
+
+ /**
+ * Inner HTML for the iframe.
+ *
+ * @method html
+ * @param {String} html HTML string to set as HTML inside the iframe.
+ * @param {function} callback Optional callback to execute when the iframe body is filled with contents.
+ * @return {tinymce.ui.Iframe} Current iframe control.
+ */
+ html: function(html, callback) {
+ var self = this, body = this.getEl().contentWindow.document.body;
+
+ // Wait for iframe to initialize IE 10 takes time
+ if (!body) {
+ Delay.setTimeout(function() {
+ self.html(html);
+ });
+ } else {
+ body.innerHTML = html;
+
+ if (callback) {
+ callback();
+ }
+ }
+
+ return this;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/InfoBox.js
+
+/**
+ * InfoBox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * ....
+ *
+ * @-x-less InfoBox.less
+ * @class tinymce.ui.InfoBox
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/InfoBox", [
+ "tinymce/ui/Widget"
+], function(Widget) {
+ "use strict";
+
+ return Widget.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Boolean} multiline Multiline label.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+ self.classes.add('widget').add('infobox');
+ self.canFocus = false;
+ },
+
+ severity: function(level) {
+ this.classes.remove('error');
+ this.classes.remove('warning');
+ this.classes.remove('success');
+ this.classes.add(level);
+ },
+
+ help: function(state) {
+ this.state.set('help', state);
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, prefix = self.classPrefix;
+
+ return (
+ '<div id="' + self._id + '" class="' + self.classes + '">' +
+ '<div id="' + self._id + '-body">' +
+ self.encode(self.state.get('text')) +
+ '<button role="button" tabindex="-1">' +
+ '<i class="' + prefix + 'ico ' + prefix + 'i-help"></i>' +
+ '</button>' +
+ '</div>' +
+ '</div>'
+ );
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:text', function(e) {
+ self.getEl('body').firstChild.data = self.encode(e.value);
+
+ if (self.state.get('rendered')) {
+ self.updateLayoutRect();
+ }
+ });
+
+ self.state.on('change:help', function(e) {
+ self.classes.toggle('has-help', e.value);
+
+ if (self.state.get('rendered')) {
+ self.updateLayoutRect();
+ }
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Label.js
+
+/**
+ * Label.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class creates a label element. A label is a simple text control
+ * that can be bound to other controls.
+ *
+ * @-x-less Label.less
+ * @class tinymce.ui.Label
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/Label", [
+ "tinymce/ui/Widget",
+ "tinymce/ui/DomUtils"
+], function(Widget, DomUtils) {
+ "use strict";
+
+ return Widget.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Boolean} multiline Multiline label.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+ self.classes.add('widget').add('label');
+ self.canFocus = false;
+
+ if (settings.multiline) {
+ self.classes.add('autoscroll');
+ }
+
+ if (settings.strong) {
+ self.classes.add('strong');
+ }
+ },
+
+ /**
+ * Initializes the current controls layout rect.
+ * This will be executed by the layout managers to determine the
+ * default minWidth/minHeight etc.
+ *
+ * @method initLayoutRect
+ * @return {Object} Layout rect instance.
+ */
+ initLayoutRect: function() {
+ var self = this, layoutRect = self._super();
+
+ if (self.settings.multiline) {
+ var size = DomUtils.getSize(self.getEl());
+
+ // Check if the text fits within maxW if not then try word wrapping it
+ if (size.width > layoutRect.maxW) {
+ layoutRect.minW = layoutRect.maxW;
+ self.classes.add('multiline');
+ }
+
+ self.getEl().style.width = layoutRect.minW + 'px';
+ layoutRect.startMinH = layoutRect.h = layoutRect.minH = Math.min(layoutRect.maxH, DomUtils.getSize(self.getEl()).height);
+ }
+
+ return layoutRect;
+ },
+
+ /**
+ * Repaints the control after a layout operation.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ var self = this;
+
+ if (!self.settings.multiline) {
+ self.getEl().style.lineHeight = self.layoutRect().h + 'px';
+ }
+
+ return self._super();
+ },
+
+ severity: function(level) {
+ this.classes.remove('error');
+ this.classes.remove('warning');
+ this.classes.remove('success');
+ this.classes.add(level);
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, targetCtrl, forName, forId = self.settings.forId;
+
+ if (!forId && (forName = self.settings.forName)) {
+ targetCtrl = self.getRoot().find('#' + forName)[0];
+
+ if (targetCtrl) {
+ forId = targetCtrl._id;
+ }
+ }
+
+ if (forId) {
+ return (
+ '<label id="' + self._id + '" class="' + self.classes + '"' + (forId ? ' for="' + forId + '"' : '') + '>' +
+ self.encode(self.state.get('text')) +
+ '</label>'
+ );
+ }
+
+ return (
+ '<span id="' + self._id + '" class="' + self.classes + '">' +
+ self.encode(self.state.get('text')) +
+ '</span>'
+ );
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:text', function(e) {
+ self.innerHtml(self.encode(e.value));
+
+ if (self.state.get('rendered')) {
+ self.updateLayoutRect();
+ }
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Toolbar.js
+
+/**
+ * Toolbar.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new toolbar.
+ *
+ * @class tinymce.ui.Toolbar
+ * @extends tinymce.ui.Container
+ */
+define("tinymce/ui/Toolbar", [
+ "tinymce/ui/Container"
+], function(Container) {
+ "use strict";
+
+ return Container.extend({
+ Defaults: {
+ role: 'toolbar',
+ layout: 'flow'
+ },
+
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+ self.classes.add('toolbar');
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this;
+
+ self.items().each(function(ctrl) {
+ ctrl.classes.add('toolbar-item');
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/MenuBar.js
+
+/**
+ * MenuBar.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new menubar.
+ *
+ * @-x-less MenuBar.less
+ * @class tinymce.ui.MenuBar
+ * @extends tinymce.ui.Container
+ */
+define("tinymce/ui/MenuBar", [
+ "tinymce/ui/Toolbar"
+], function(Toolbar) {
+ "use strict";
+
+ return Toolbar.extend({
+ Defaults: {
+ role: 'menubar',
+ containerCls: 'menubar',
+ ariaRoot: true,
+ defaults: {
+ type: 'menubutton'
+ }
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/MenuButton.js
+
+/**
+ * MenuButton.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new menu button.
+ *
+ * @-x-less MenuButton.less
+ * @class tinymce.ui.MenuButton
+ * @extends tinymce.ui.Button
+ */
+define("tinymce/ui/MenuButton", [
+ "tinymce/ui/Button",
+ "tinymce/ui/Factory",
+ "tinymce/ui/MenuBar"
+], function(Button, Factory, MenuBar) {
+ "use strict";
+
+ // TODO: Maybe add as some global function
+ function isChildOf(node, parent) {
+ while (node) {
+ if (parent === node) {
+ return true;
+ }
+
+ node = node.parentNode;
+ }
+
+ return false;
+ }
+
+ var MenuButton = Button.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._renderOpen = true;
+
+ self._super(settings);
+ settings = self.settings;
+
+ self.classes.add('menubtn');
+
+ if (settings.fixedWidth) {
+ self.classes.add('fixed-width');
+ }
+
+ self.aria('haspopup', true);
+
+ self.state.set('menu', settings.menu || self.render());
+ },
+
+ /**
+ * Shows the menu for the button.
+ *
+ * @method showMenu
+ */
+ showMenu: function() {
+ var self = this, menu;
+
+ if (self.menu && self.menu.visible()) {
+ return self.hideMenu();
+ }
+
+ if (!self.menu) {
+ menu = self.state.get('menu') || [];
+
+ // Is menu array then auto constuct menu control
+ if (menu.length) {
+ menu = {
+ type: 'menu',
+ items: menu
+ };
+ } else {
+ menu.type = menu.type || 'menu';
+ }
+
+ if (!menu.renderTo) {
+ self.menu = Factory.create(menu).parent(self).renderTo();
+ } else {
+ self.menu = menu.parent(self).show().renderTo();
+ }
+
+ self.fire('createmenu');
+ self.menu.reflow();
+ self.menu.on('cancel', function(e) {
+ if (e.control.parent() === self.menu) {
+ e.stopPropagation();
+ self.focus();
+ self.hideMenu();
+ }
+ });
+
+ // Move focus to button when a menu item is selected/clicked
+ self.menu.on('select', function() {
+ self.focus();
+ });
+
+ self.menu.on('show hide', function(e) {
+ if (e.control == self.menu) {
+ self.activeMenu(e.type == 'show');
+ }
+
+ self.aria('expanded', e.type == 'show');
+ }).fire('show');
+ }
+
+ self.menu.show();
+ self.menu.layoutRect({w: self.layoutRect().w});
+ self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']);
+ self.fire('showmenu');
+ },
+
+ /**
+ * Hides the menu for the button.
+ *
+ * @method hideMenu
+ */
+ hideMenu: function() {
+ var self = this;
+
+ if (self.menu) {
+ self.menu.items().each(function(item) {
+ if (item.hideMenu) {
+ item.hideMenu();
+ }
+ });
+
+ self.menu.hide();
+ }
+ },
+
+ /**
+ * Sets the active menu state.
+ *
+ * @private
+ */
+ activeMenu: function(state) {
+ this.classes.toggle('active', state);
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = self.classPrefix;
+ var icon = self.settings.icon, image, text = self.state.get('text'),
+ textHtml = '';
+
+ image = self.settings.image;
+ if (image) {
+ icon = 'none';
+
+ // Support for [high dpi, low dpi] image sources
+ if (typeof image != "string") {
+ image = window.getSelection ? image[0] : image[1];
+ }
+
+ image = ' style="background-image: url(\'' + image + '\')"';
+ } else {
+ image = '';
+ }
+
+ if (text) {
+ self.classes.add('btn-has-text');
+ textHtml = '<span class="' + prefix + 'txt">' + self.encode(text) + '</span>';
+ }
+
+ icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
+
+ self.aria('role', self.parent() instanceof MenuBar ? 'menuitem' : 'button');
+
+ return (
+ '<div id="' + id + '" class="' + self.classes + '" tabindex="-1" aria-labelledby="' + id + '">' +
+ '<button id="' + id + '-open" role="presentation" type="button" tabindex="-1">' +
+ (icon ? '<i class="' + icon + '"' + image + '></i>' : '') +
+ textHtml +
+ ' <i class="' + prefix + 'caret"></i>' +
+ '</button>' +
+ '</div>'
+ );
+ },
+
+ /**
+ * Gets invoked after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this;
+
+ self.on('click', function(e) {
+ if (e.control === self && isChildOf(e.target, self.getEl())) {
+ self.showMenu();
+
+ if (e.aria) {
+ self.menu.items().filter(':visible')[0].focus();
+ }
+ }
+ });
+
+ self.on('mouseenter', function(e) {
+ var overCtrl = e.control, parent = self.parent(), hasVisibleSiblingMenu;
+
+ if (overCtrl && parent && overCtrl instanceof MenuButton && overCtrl.parent() == parent) {
+ parent.items().filter('MenuButton').each(function(ctrl) {
+ if (ctrl.hideMenu && ctrl != overCtrl) {
+ if (ctrl.menu && ctrl.menu.visible()) {
+ hasVisibleSiblingMenu = true;
+ }
+
+ ctrl.hideMenu();
+ }
+ });
+
+ if (hasVisibleSiblingMenu) {
+ overCtrl.focus(); // Fix for: #5887
+ overCtrl.showMenu();
+ }
+ }
+ });
+
+ return self._super();
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:menu', function() {
+ if (self.menu) {
+ self.menu.remove();
+ }
+
+ self.menu = null;
+ });
+
+ return self._super();
+ },
+
+ /**
+ * Removes the control and it's menus.
+ *
+ * @method remove
+ */
+ remove: function() {
+ this._super();
+
+ if (this.menu) {
+ this.menu.remove();
+ }
+ }
+ });
+
+ return MenuButton;
+});
+
+// Included from: js/tinymce/classes/ui/MenuItem.js
+
+/**
+ * MenuItem.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new menu item.
+ *
+ * @-x-less MenuItem.less
+ * @class tinymce.ui.MenuItem
+ * @extends tinymce.ui.Control
+ */
+define("tinymce/ui/MenuItem", [
+ "tinymce/ui/Widget",
+ "tinymce/ui/Factory",
+ "tinymce/Env",
+ "tinymce/util/Delay"
+], function(Widget, Factory, Env, Delay) {
+ "use strict";
+
+ return Widget.extend({
+ Defaults: {
+ border: 0,
+ role: 'menuitem'
+ },
+
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Boolean} selectable Selectable menu.
+ * @setting {Array} menu Submenu array with items.
+ * @setting {String} shortcut Shortcut to display for menu item. Example: Ctrl+X
+ */
+ init: function(settings) {
+ var self = this, text;
+
+ self._super(settings);
+
+ settings = self.settings;
+
+ self.classes.add('menu-item');
+
+ if (settings.menu) {
+ self.classes.add('menu-item-expand');
+ }
+
+ if (settings.preview) {
+ self.classes.add('menu-item-preview');
+ }
+
+ text = self.state.get('text');
+ if (text === '-' || text === '|') {
+ self.classes.add('menu-item-sep');
+ self.aria('role', 'separator');
+ self.state.set('text', '-');
+ }
+
+ if (settings.selectable) {
+ self.aria('role', 'menuitemcheckbox');
+ self.classes.add('menu-item-checkbox');
+ settings.icon = 'selected';
+ }
+
+ if (!settings.preview && !settings.selectable) {
+ self.classes.add('menu-item-normal');
+ }
+
+ self.on('mousedown', function(e) {
+ e.preventDefault();
+ });
+
+ if (settings.menu && !settings.ariaHideMenu) {
+ self.aria('haspopup', true);
+ }
+ },
+
+ /**
+ * Returns true/false if the menuitem has sub menu.
+ *
+ * @method hasMenus
+ * @return {Boolean} True/false state if it has submenu.
+ */
+ hasMenus: function() {
+ return !!this.settings.menu;
+ },
+
+ /**
+ * Shows the menu for the menu item.
+ *
+ * @method showMenu
+ */
+ showMenu: function() {
+ var self = this, settings = self.settings, menu, parent = self.parent();
+
+ parent.items().each(function(ctrl) {
+ if (ctrl !== self) {
+ ctrl.hideMenu();
+ }
+ });
+
+ if (settings.menu) {
+ menu = self.menu;
+
+ if (!menu) {
+ menu = settings.menu;
+
+ // Is menu array then auto constuct menu control
+ if (menu.length) {
+ menu = {
+ type: 'menu',
+ items: menu
+ };
+ } else {
+ menu.type = menu.type || 'menu';
+ }
+
+ if (parent.settings.itemDefaults) {
+ menu.itemDefaults = parent.settings.itemDefaults;
+ }
+
+ menu = self.menu = Factory.create(menu).parent(self).renderTo();
+ menu.reflow();
+ menu.on('cancel', function(e) {
+ e.stopPropagation();
+ self.focus();
+ menu.hide();
+ });
+ menu.on('show hide', function(e) {
+ if (e.control.items) {
+ e.control.items().each(function(ctrl) {
+ ctrl.active(ctrl.settings.selected);
+ });
+ }
+ }).fire('show');
+
+ menu.on('hide', function(e) {
+ if (e.control === menu) {
+ self.classes.remove('selected');
+ }
+ });
+
+ menu.submenu = true;
+ } else {
+ menu.show();
+ }
+
+ menu._parentMenu = parent;
+
+ menu.classes.add('menu-sub');
+
+ var rel = menu.testMoveRel(
+ self.getEl(),
+ self.isRtl() ? ['tl-tr', 'bl-br', 'tr-tl', 'br-bl'] : ['tr-tl', 'br-bl', 'tl-tr', 'bl-br']
+ );
+
+ menu.moveRel(self.getEl(), rel);
+ menu.rel = rel;
+
+ rel = 'menu-sub-' + rel;
+ menu.classes.remove(menu._lastRel).add(rel);
+ menu._lastRel = rel;
+
+ self.classes.add('selected');
+ self.aria('expanded', true);
+ }
+ },
+
+ /**
+ * Hides the menu for the menu item.
+ *
+ * @method hideMenu
+ */
+ hideMenu: function() {
+ var self = this;
+
+ if (self.menu) {
+ self.menu.items().each(function(item) {
+ if (item.hideMenu) {
+ item.hideMenu();
+ }
+ });
+
+ self.menu.hide();
+ self.aria('expanded', false);
+ }
+
+ return self;
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix, text = self.state.get('text');
+ var icon = self.settings.icon, image = '', shortcut = settings.shortcut;
+ var url = self.encode(settings.url), iconHtml = '';
+
+ // Converts shortcut format to Mac/PC variants
+ function convertShortcut(shortcut) {
+ var i, value, replace = {};
+
+ if (Env.mac) {
+ replace = {
+ alt: '⌥',
+ ctrl: '⌘',
+ shift: '⇧',
+ meta: '⌘'
+ };
+ } else {
+ replace = {
+ meta: 'Ctrl'
+ };
+ }
+
+ shortcut = shortcut.split('+');
+
+ for (i = 0; i < shortcut.length; i++) {
+ value = replace[shortcut[i].toLowerCase()];
+
+ if (value) {
+ shortcut[i] = value;
+ }
+ }
+
+ return shortcut.join('+');
+ }
+
+ function escapeRegExp(str) {
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ }
+
+ function markMatches(text) {
+ var match = settings.match || '';
+
+ return match ? text.replace(new RegExp(escapeRegExp(match), 'gi'), function (match) {
+ return '!mce~match[' + match + ']mce~match!';
+ }) : text;
+ }
+
+ function boldMatches(text) {
+ return text.
+ replace(new RegExp(escapeRegExp('!mce~match['), 'g'), '<b>').
+ replace(new RegExp(escapeRegExp(']mce~match!'), 'g'), '</b>');
+ }
+
+ if (icon) {
+ self.parent().classes.add('menu-has-icons');
+ }
+
+ if (settings.image) {
+ image = ' style="background-image: url(\'' + settings.image + '\')"';
+ }
+
+ if (shortcut) {
+ shortcut = convertShortcut(shortcut);
+ }
+
+ icon = prefix + 'ico ' + prefix + 'i-' + (self.settings.icon || 'none');
+ iconHtml = (text !== '-' ? '<i class="' + icon + '"' + image + '></i>\u00a0' : '');
+
+ text = boldMatches(self.encode(markMatches(text)));
+ url = boldMatches(self.encode(markMatches(url)));
+
+ return (
+ '<div id="' + id + '" class="' + self.classes + '" tabindex="-1">' +
+ iconHtml +
+ (text !== '-' ? '<span id="' + id + '-text" class="' + prefix + 'text">' + text + '</span>' : '') +
+ (shortcut ? '<div id="' + id + '-shortcut" class="' + prefix + 'menu-shortcut">' + shortcut + '</div>' : '') +
+ (settings.menu ? '<div class="' + prefix + 'caret"></div>' : '') +
+ (url ? '<div class="' + prefix + 'menu-item-link">' + url + '</div>' : '') +
+ '</div>'
+ );
+ },
+
+ /**
+ * Gets invoked after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this, settings = self.settings;
+
+ var textStyle = settings.textStyle;
+ if (typeof textStyle == "function") {
+ textStyle = textStyle.call(this);
+ }
+
+ if (textStyle) {
+ var textElm = self.getEl('text');
+ if (textElm) {
+ textElm.setAttribute('style', textStyle);
+ }
+ }
+
+ self.on('mouseenter click', function(e) {
+ if (e.control === self) {
+ if (!settings.menu && e.type === 'click') {
+ self.fire('select');
+
+ // Edge will crash if you stress it see #2660
+ Delay.requestAnimationFrame(function() {
+ self.parent().hideAll();
+ });
+ } else {
+ self.showMenu();
+
+ if (e.aria) {
+ self.menu.focus(true);
+ }
+ }
+ }
+ });
+
+ self._super();
+
+ return self;
+ },
+
+ hover: function() {
+ var self = this;
+
+ self.parent().items().each(function(ctrl) {
+ ctrl.classes.remove('selected');
+ });
+
+ self.classes.toggle('selected', true);
+
+ return self;
+ },
+
+ active: function(state) {
+ if (typeof state != "undefined") {
+ this.aria('checked', state);
+ }
+
+ return this._super(state);
+ },
+
+ /**
+ * Removes the control and it's menus.
+ *
+ * @method remove
+ */
+ remove: function() {
+ this._super();
+
+ if (this.menu) {
+ this.menu.remove();
+ }
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Throbber.js
+
+/**
+ * Throbber.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class enables you to display a Throbber for any element.
+ *
+ * @-x-less Throbber.less
+ * @class tinymce.ui.Throbber
+ */
+define("tinymce/ui/Throbber", [
+ "tinymce/dom/DomQuery",
+ "tinymce/ui/Control",
+ "tinymce/util/Delay"
+], function($, Control, Delay) {
+ "use strict";
+
+ /**
+ * Constructs a new throbber.
+ *
+ * @constructor
+ * @param {Element} elm DOM Html element to display throbber in.
+ * @param {Boolean} inline Optional true/false state if the throbber should be appended to end of element for infinite scroll.
+ */
+ return function(elm, inline) {
+ var self = this, state, classPrefix = Control.classPrefix, timer;
+
+ /**
+ * Shows the throbber.
+ *
+ * @method show
+ * @param {Number} [time] Time to wait before showing.
+ * @param {function} [callback] Optional callback to execute when the throbber is shown.
+ * @return {tinymce.ui.Throbber} Current throbber instance.
+ */
+ self.show = function(time, callback) {
+ function render() {
+ if (state) {
+ $(elm).append(
+ '<div class="' + classPrefix + 'throbber' + (inline ? ' ' + classPrefix + 'throbber-inline' : '') + '"></div>'
+ );
+
+ if (callback) {
+ callback();
+ }
+ }
+ }
+
+ self.hide();
+
+ state = true;
+
+ if (time) {
+ timer = Delay.setTimeout(render, time);
+ } else {
+ render();
+ }
+
+ return self;
+ };
+
+ /**
+ * Hides the throbber.
+ *
+ * @method hide
+ * @return {tinymce.ui.Throbber} Current throbber instance.
+ */
+ self.hide = function() {
+ var child = elm.lastChild;
+
+ Delay.clearTimeout(timer);
+
+ if (child && child.className.indexOf('throbber') != -1) {
+ child.parentNode.removeChild(child);
+ }
+
+ state = false;
+
+ return self;
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/ui/Menu.js
+
+/**
+ * Menu.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new menu.
+ *
+ * @-x-less Menu.less
+ * @class tinymce.ui.Menu
+ * @extends tinymce.ui.FloatPanel
+ */
+define("tinymce/ui/Menu", [
+ "tinymce/ui/FloatPanel",
+ "tinymce/ui/MenuItem",
+ "tinymce/ui/Throbber",
+ "tinymce/util/Tools"
+], function(FloatPanel, MenuItem, Throbber, Tools) {
+ "use strict";
+
+ return FloatPanel.extend({
+ Defaults: {
+ defaultType: 'menuitem',
+ border: 1,
+ layout: 'stack',
+ role: 'application',
+ bodyRole: 'menu',
+ ariaRoot: true
+ },
+
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ var self = this;
+
+ settings.autohide = true;
+ settings.constrainToViewport = true;
+
+ if (typeof settings.items === 'function') {
+ settings.itemsFactory = settings.items;
+ settings.items = [];
+ }
+
+ if (settings.itemDefaults) {
+ var items = settings.items, i = items.length;
+
+ while (i--) {
+ items[i] = Tools.extend({}, settings.itemDefaults, items[i]);
+ }
+ }
+
+ self._super(settings);
+ self.classes.add('menu');
+ },
+
+ /**
+ * Repaints the control after a layout operation.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ this.classes.toggle('menu-align', true);
+
+ this._super();
+
+ this.getEl().style.height = '';
+ this.getEl('body').style.height = '';
+
+ return this;
+ },
+
+ /**
+ * Hides/closes the menu.
+ *
+ * @method cancel
+ */
+ cancel: function() {
+ var self = this;
+
+ self.hideAll();
+ self.fire('select');
+ },
+
+ /**
+ * Loads new items from the factory items function.
+ *
+ * @method load
+ */
+ load: function() {
+ var self = this, time, factory;
+
+ function hideThrobber() {
+ if (self.throbber) {
+ self.throbber.hide();
+ self.throbber = null;
+ }
+ }
+
+ factory = self.settings.itemsFactory;
+ if (!factory) {
+ return;
+ }
+
+ if (!self.throbber) {
+ self.throbber = new Throbber(self.getEl('body'), true);
+
+ if (self.items().length === 0) {
+ self.throbber.show();
+ self.fire('loading');
+ } else {
+ self.throbber.show(100, function() {
+ self.items().remove();
+ self.fire('loading');
+ });
+ }
+
+ self.on('hide close', hideThrobber);
+ }
+
+ self.requestTime = time = new Date().getTime();
+
+ self.settings.itemsFactory(function(items) {
+ if (items.length === 0) {
+ self.hide();
+ return;
+ }
+
+ if (self.requestTime !== time) {
+ return;
+ }
+
+ self.getEl().style.width = '';
+ self.getEl('body').style.width = '';
+
+ hideThrobber();
+ self.items().remove();
+ self.getEl('body').innerHTML = '';
+
+ self.add(items);
+ self.renderNew();
+ self.fire('loaded');
+ });
+ },
+
+ /**
+ * Hide menu and all sub menus.
+ *
+ * @method hideAll
+ */
+ hideAll: function() {
+ var self = this;
+
+ this.find('menuitem').exec('hideMenu');
+
+ return self._super();
+ },
+
+ /**
+ * Invoked before the menu is rendered.
+ *
+ * @method preRender
+ */
+ preRender: function() {
+ var self = this;
+
+ self.items().each(function(ctrl) {
+ var settings = ctrl.settings;
+
+ if (settings.icon || settings.image || settings.selectable) {
+ self._hasIcons = true;
+ return false;
+ }
+ });
+
+ if (self.settings.itemsFactory) {
+ self.on('postrender', function() {
+ if (self.settings.itemsFactory) {
+ self.load();
+ }
+ });
+ }
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/ListBox.js
+
+/**
+ * ListBox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new list box control.
+ *
+ * @-x-less ListBox.less
+ * @class tinymce.ui.ListBox
+ * @extends tinymce.ui.MenuButton
+ */
+define("tinymce/ui/ListBox", [
+ "tinymce/ui/MenuButton",
+ "tinymce/ui/Menu"
+], function(MenuButton, Menu) {
+ "use strict";
+
+ return MenuButton.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Array} values Array with values to add to list box.
+ */
+ init: function(settings) {
+ var self = this, values, selected, selectedText, lastItemCtrl;
+
+ function setSelected(menuValues) {
+ // Try to find a selected value
+ for (var i = 0; i < menuValues.length; i++) {
+ selected = menuValues[i].selected || settings.value === menuValues[i].value;
+
+ if (selected) {
+ selectedText = selectedText || menuValues[i].text;
+ self.state.set('value', menuValues[i].value);
+ return true;
+ }
+
+ // If the value has a submenu, try to find the selected values in that menu
+ if (menuValues[i].menu) {
+ if (setSelected(menuValues[i].menu)) {
+ return true;
+ }
+ }
+ }
+ }
+
+ self._super(settings);
+ settings = self.settings;
+
+ self._values = values = settings.values;
+ if (values) {
+ if (typeof settings.value != "undefined") {
+ setSelected(values);
+ }
+
+ // Default with first item
+ if (!selected && values.length > 0) {
+ selectedText = values[0].text;
+ self.state.set('value', values[0].value);
+ }
+
+ self.state.set('menu', values);
+ }
+
+ self.state.set('text', settings.text || selectedText);
+
+ self.classes.add('listbox');
+
+ self.on('select', function(e) {
+ var ctrl = e.control;
+
+ if (lastItemCtrl) {
+ e.lastControl = lastItemCtrl;
+ }
+
+ if (settings.multiple) {
+ ctrl.active(!ctrl.active());
+ } else {
+ self.value(e.control.value());
+ }
+
+ lastItemCtrl = ctrl;
+ });
+ },
+
+ /**
+ * Getter/setter function for the control value.
+ *
+ * @method value
+ * @param {String} [value] Value to be set.
+ * @return {Boolean/tinymce.ui.ListBox} Value or self if it's a set operation.
+ */
+ bindStates: function() {
+ var self = this;
+
+ function activateMenuItemsByValue(menu, value) {
+ if (menu instanceof Menu) {
+ menu.items().each(function(ctrl) {
+ if (!ctrl.hasMenus()) {
+ ctrl.active(ctrl.value() === value);
+ }
+ });
+ }
+ }
+
+ function getSelectedItem(menuValues, value) {
+ var selectedItem;
+
+ if (!menuValues) {
+ return;
+ }
+
+ for (var i = 0; i < menuValues.length; i++) {
+ if (menuValues[i].value === value) {
+ return menuValues[i];
+ }
+
+ if (menuValues[i].menu) {
+ selectedItem = getSelectedItem(menuValues[i].menu, value);
+ if (selectedItem) {
+ return selectedItem;
+ }
+ }
+ }
+ }
+
+ self.on('show', function(e) {
+ activateMenuItemsByValue(e.control, self.value());
+ });
+
+ self.state.on('change:value', function(e) {
+ var selectedItem = getSelectedItem(self.state.get('menu'), e.value);
+
+ if (selectedItem) {
+ self.text(selectedItem.text);
+ } else {
+ self.text(self.settings.text);
+ }
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Radio.js
+
+/**
+ * Radio.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new radio button.
+ *
+ * @-x-less Radio.less
+ * @class tinymce.ui.Radio
+ * @extends tinymce.ui.Checkbox
+ */
+define("tinymce/ui/Radio", [
+ "tinymce/ui/Checkbox"
+], function(Checkbox) {
+ "use strict";
+
+ return Checkbox.extend({
+ Defaults: {
+ classes: "radio",
+ role: "radio"
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/ResizeHandle.js
+
+/**
+ * ResizeHandle.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Renders a resize handle that fires ResizeStart, Resize and ResizeEnd events.
+ *
+ * @-x-less ResizeHandle.less
+ * @class tinymce.ui.ResizeHandle
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/ResizeHandle", [
+ "tinymce/ui/Widget",
+ "tinymce/ui/DragHelper"
+], function(Widget, DragHelper) {
+ "use strict";
+
+ return Widget.extend({
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, prefix = self.classPrefix;
+
+ self.classes.add('resizehandle');
+
+ if (self.settings.direction == "both") {
+ self.classes.add('resizehandle-both');
+ }
+
+ self.canFocus = false;
+
+ return (
+ '<div id="' + self._id + '" class="' + self.classes + '">' +
+ '<i class="' + prefix + 'ico ' + prefix + 'i-resize"></i>' +
+ '</div>'
+ );
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this;
+
+ self._super();
+
+ self.resizeDragHelper = new DragHelper(this._id, {
+ start: function() {
+ self.fire('ResizeStart');
+ },
+
+ drag: function(e) {
+ if (self.settings.direction != "both") {
+ e.deltaX = 0;
+ }
+
+ self.fire('Resize', e);
+ },
+
+ stop: function() {
+ self.fire('ResizeEnd');
+ }
+ });
+ },
+
+ remove: function() {
+ if (this.resizeDragHelper) {
+ this.resizeDragHelper.destroy();
+ }
+
+ return this._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/SelectBox.js
+
+/**
+ * SelectBox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new select box control.
+ *
+ * @-x-less SelectBox.less
+ * @class tinymce.ui.SelectBox
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/SelectBox", [
+ "tinymce/ui/Widget"
+], function(Widget) {
+ "use strict";
+
+ function createOptions(options) {
+ var strOptions = '';
+ if (options) {
+ for (var i = 0; i < options.length; i++) {
+ strOptions += '<option value="' + options[i] + '">' + options[i] + '</option>';
+ }
+ }
+ return strOptions;
+ }
+
+ return Widget.extend({
+ Defaults: {
+ classes: "selectbox",
+ role: "selectbox",
+ options: []
+ },
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Array} options Array with options to add to the select box.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+
+ if (self.settings.size) {
+ self.size = self.settings.size;
+ }
+
+ if (self.settings.options) {
+ self._options = self.settings.options;
+ }
+
+ self.on('keydown', function(e) {
+ var rootControl;
+
+ if (e.keyCode == 13) {
+ e.preventDefault();
+
+ // Find root control that we can do toJSON on
+ self.parents().reverse().each(function(ctrl) {
+ if (ctrl.toJSON) {
+ rootControl = ctrl;
+ return false;
+ }
+ });
+
+ // Fire event on current text box with the serialized data of the whole form
+ self.fire('submit', {data: rootControl.toJSON()});
+ }
+ });
+ },
+
+ /**
+ * Getter/setter function for the options state.
+ *
+ * @method options
+ * @param {Array} [state] State to be set.
+ * @return {Array|tinymce.ui.SelectBox} Array of string options.
+ */
+ options: function(state) {
+ if (!arguments.length) {
+ return this.state.get('options');
+ }
+
+ this.state.set('options', state);
+
+ return this;
+ },
+
+ renderHtml: function() {
+ var self = this, options, size = '';
+
+ options = createOptions(self._options);
+
+ if (self.size) {
+ size = ' size = "' + self.size + '"';
+ }
+
+ return (
+ '<select id="' + self._id + '" class="' + self.classes + '"' + size + '>' +
+ options +
+ '</select>'
+ );
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:options', function(e) {
+ self.getEl().innerHTML = createOptions(e.value);
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Slider.js
+
+/**
+ * Slider.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Slider control.
+ *
+ * @-x-less Slider.less
+ * @class tinymce.ui.Slider
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/Slider", [
+ "tinymce/ui/Widget",
+ "tinymce/ui/DragHelper",
+ "tinymce/ui/DomUtils"
+], function(Widget, DragHelper, DomUtils) {
+ "use strict";
+
+ function constrain(value, minVal, maxVal) {
+ if (value < minVal) {
+ value = minVal;
+ }
+
+ if (value > maxVal) {
+ value = maxVal;
+ }
+
+ return value;
+ }
+
+ function setAriaProp(el, name, value) {
+ el.setAttribute('aria-' + name, value);
+ }
+
+ function updateSliderHandle(ctrl, value) {
+ var maxHandlePos, shortSizeName, sizeName, stylePosName, styleValue, handleEl;
+
+ if (ctrl.settings.orientation == "v") {
+ stylePosName = "top";
+ sizeName = "height";
+ shortSizeName = "h";
+ } else {
+ stylePosName = "left";
+ sizeName = "width";
+ shortSizeName = "w";
+ }
+
+ handleEl = ctrl.getEl('handle');
+ maxHandlePos = (ctrl.layoutRect()[shortSizeName] || 100) - DomUtils.getSize(handleEl)[sizeName];
+
+ styleValue = (maxHandlePos * ((value - ctrl._minValue) / (ctrl._maxValue - ctrl._minValue))) + 'px';
+ handleEl.style[stylePosName] = styleValue;
+ handleEl.style.height = ctrl.layoutRect().h + 'px';
+
+ setAriaProp(handleEl, 'valuenow', value);
+ setAriaProp(handleEl, 'valuetext', '' + ctrl.settings.previewFilter(value));
+ setAriaProp(handleEl, 'valuemin', ctrl._minValue);
+ setAriaProp(handleEl, 'valuemax', ctrl._maxValue);
+ }
+
+ return Widget.extend({
+ init: function(settings) {
+ var self = this;
+
+ if (!settings.previewFilter) {
+ settings.previewFilter = function(value) {
+ return Math.round(value * 100) / 100.0;
+ };
+ }
+
+ self._super(settings);
+ self.classes.add('slider');
+
+ if (settings.orientation == "v") {
+ self.classes.add('vertical');
+ }
+
+ self._minValue = settings.minValue || 0;
+ self._maxValue = settings.maxValue || 100;
+ self._initValue = self.state.get('value');
+ },
+
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = self.classPrefix;
+
+ return (
+ '<div id="' + id + '" class="' + self.classes + '">' +
+ '<div id="' + id + '-handle" class="' + prefix + 'slider-handle" role="slider" tabindex="-1"></div>' +
+ '</div>'
+ );
+ },
+
+ reset: function() {
+ this.value(this._initValue).repaint();
+ },
+
+ postRender: function() {
+ var self = this, minValue, maxValue, screenCordName,
+ stylePosName, sizeName, shortSizeName;
+
+ function toFraction(min, max, val) {
+ return (val + min) / (max - min);
+ }
+
+ function fromFraction(min, max, val) {
+ return (val * (max - min)) - min;
+ }
+
+ function handleKeyboard(minValue, maxValue) {
+ function alter(delta) {
+ var value;
+
+ value = self.value();
+ value = fromFraction(minValue, maxValue, toFraction(minValue, maxValue, value) + (delta * 0.05));
+ value = constrain(value, minValue, maxValue);
+
+ self.value(value);
+
+ self.fire('dragstart', {value: value});
+ self.fire('drag', {value: value});
+ self.fire('dragend', {value: value});
+ }
+
+ self.on('keydown', function(e) {
+ switch (e.keyCode) {
+ case 37:
+ case 38:
+ alter(-1);
+ break;
+
+ case 39:
+ case 40:
+ alter(1);
+ break;
+ }
+ });
+ }
+
+ function handleDrag(minValue, maxValue, handleEl) {
+ var startPos, startHandlePos, maxHandlePos, handlePos, value;
+
+ self._dragHelper = new DragHelper(self._id, {
+ handle: self._id + "-handle",
+
+ start: function(e) {
+ startPos = e[screenCordName];
+ startHandlePos = parseInt(self.getEl('handle').style[stylePosName], 10);
+ maxHandlePos = (self.layoutRect()[shortSizeName] || 100) - DomUtils.getSize(handleEl)[sizeName];
+ self.fire('dragstart', {value: value});
+ },
+
+ drag: function(e) {
+ var delta = e[screenCordName] - startPos;
+
+ handlePos = constrain(startHandlePos + delta, 0, maxHandlePos);
+ handleEl.style[stylePosName] = handlePos + 'px';
+
+ value = minValue + (handlePos / maxHandlePos) * (maxValue - minValue);
+ self.value(value);
+
+ self.tooltip().text('' + self.settings.previewFilter(value)).show().moveRel(handleEl, 'bc tc');
+
+ self.fire('drag', {value: value});
+ },
+
+ stop: function() {
+ self.tooltip().hide();
+ self.fire('dragend', {value: value});
+ }
+ });
+ }
+
+ minValue = self._minValue;
+ maxValue = self._maxValue;
+
+ if (self.settings.orientation == "v") {
+ screenCordName = "screenY";
+ stylePosName = "top";
+ sizeName = "height";
+ shortSizeName = "h";
+ } else {
+ screenCordName = "screenX";
+ stylePosName = "left";
+ sizeName = "width";
+ shortSizeName = "w";
+ }
+
+ self._super();
+
+ handleKeyboard(minValue, maxValue, self.getEl('handle'));
+ handleDrag(minValue, maxValue, self.getEl('handle'));
+ },
+
+ repaint: function() {
+ this._super();
+ updateSliderHandle(this, this.value());
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:value', function(e) {
+ updateSliderHandle(self, e.value);
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Spacer.js
+
+/**
+ * Spacer.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a spacer. This control is used in flex layouts for example.
+ *
+ * @-x-less Spacer.less
+ * @class tinymce.ui.Spacer
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/Spacer", [
+ "tinymce/ui/Widget"
+], function(Widget) {
+ "use strict";
+
+ return Widget.extend({
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this;
+
+ self.classes.add('spacer');
+ self.canFocus = false;
+
+ return '<div id="' + self._id + '" class="' + self.classes + '"></div>';
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/SplitButton.js
+
+/**
+ * SplitButton.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a split button.
+ *
+ * @-x-less SplitButton.less
+ * @class tinymce.ui.SplitButton
+ * @extends tinymce.ui.Button
+ */
+define("tinymce/ui/SplitButton", [
+ "tinymce/ui/MenuButton",
+ "tinymce/ui/DomUtils",
+ "tinymce/dom/DomQuery"
+], function(MenuButton, DomUtils, $) {
+ return MenuButton.extend({
+ Defaults: {
+ classes: "widget btn splitbtn",
+ role: "button"
+ },
+
+ /**
+ * Repaints the control after a layout operation.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ var self = this, elm = self.getEl(), rect = self.layoutRect(), mainButtonElm, menuButtonElm;
+
+ self._super();
+
+ mainButtonElm = elm.firstChild;
+ menuButtonElm = elm.lastChild;
+
+ $(mainButtonElm).css({
+ width: rect.w - DomUtils.getSize(menuButtonElm).width,
+ height: rect.h - 2
+ });
+
+ $(menuButtonElm).css({
+ height: rect.h - 2
+ });
+
+ return self;
+ },
+
+ /**
+ * Sets the active menu state.
+ *
+ * @private
+ */
+ activeMenu: function(state) {
+ var self = this;
+
+ $(self.getEl().lastChild).toggleClass(self.classPrefix + 'active', state);
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = self.classPrefix, image;
+ var icon = self.state.get('icon'), text = self.state.get('text'),
+ textHtml = '';
+
+ image = self.settings.image;
+ if (image) {
+ icon = 'none';
+
+ // Support for [high dpi, low dpi] image sources
+ if (typeof image != "string") {
+ image = window.getSelection ? image[0] : image[1];
+ }
+
+ image = ' style="background-image: url(\'' + image + '\')"';
+ } else {
+ image = '';
+ }
+
+ icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
+
+ if (text) {
+ self.classes.add('btn-has-text');
+ textHtml = '<span class="' + prefix + 'txt">' + self.encode(text) + '</span>';
+ }
+
+ return (
+ '<div id="' + id + '" class="' + self.classes + '" role="button" tabindex="-1">' +
+ '<button type="button" hidefocus="1" tabindex="-1">' +
+ (icon ? '<i class="' + icon + '"' + image + '></i>' : '') +
+ textHtml +
+ '</button>' +
+ '<button type="button" class="' + prefix + 'open" hidefocus="1" tabindex="-1">' +
+ //(icon ? '<i class="' + icon + '"></i>' : '') +
+ (self._menuBtnText ? (icon ? '\u00a0' : '') + self._menuBtnText : '') +
+ ' <i class="' + prefix + 'caret"></i>' +
+ '</button>' +
+ '</div>'
+ );
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this, onClickHandler = self.settings.onclick;
+
+ self.on('click', function(e) {
+ var node = e.target;
+
+ if (e.control == this) {
+ // Find clicks that is on the main button
+ while (node) {
+ if ((e.aria && e.aria.key != 'down') || (node.nodeName == 'BUTTON' && node.className.indexOf('open') == -1)) {
+ e.stopImmediatePropagation();
+
+ if (onClickHandler) {
+ onClickHandler.call(this, e);
+ }
+
+ return;
+ }
+
+ node = node.parentNode;
+ }
+ }
+ });
+
+ delete self.settings.onclick;
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/StackLayout.js
+
+/**
+ * StackLayout.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This layout uses the browsers layout when the items are blocks.
+ *
+ * @-x-less StackLayout.less
+ * @class tinymce.ui.StackLayout
+ * @extends tinymce.ui.FlowLayout
+ */
+define("tinymce/ui/StackLayout", [
+ "tinymce/ui/FlowLayout"
+], function(FlowLayout) {
+ "use strict";
+
+ return FlowLayout.extend({
+ Defaults: {
+ containerClass: 'stack-layout',
+ controlClass: 'stack-layout-item',
+ endClass: 'break'
+ },
+
+ isNative: function() {
+ return true;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/TabPanel.js
+
+/**
+ * TabPanel.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a tab panel control.
+ *
+ * @-x-less TabPanel.less
+ * @class tinymce.ui.TabPanel
+ * @extends tinymce.ui.Panel
+ *
+ * @setting {Number} activeTab Active tab index.
+ */
+define("tinymce/ui/TabPanel", [
+ "tinymce/ui/Panel",
+ "tinymce/dom/DomQuery",
+ "tinymce/ui/DomUtils"
+], function(Panel, $, DomUtils) {
+ "use strict";
+
+ return Panel.extend({
+ Defaults: {
+ layout: 'absolute',
+ defaults: {
+ type: 'panel'
+ }
+ },
+
+ /**
+ * Activates the specified tab by index.
+ *
+ * @method activateTab
+ * @param {Number} idx Index of the tab to activate.
+ */
+ activateTab: function(idx) {
+ var activeTabElm;
+
+ if (this.activeTabId) {
+ activeTabElm = this.getEl(this.activeTabId);
+ $(activeTabElm).removeClass(this.classPrefix + 'active');
+ activeTabElm.setAttribute('aria-selected', "false");
+ }
+
+ this.activeTabId = 't' + idx;
+
+ activeTabElm = this.getEl('t' + idx);
+ activeTabElm.setAttribute('aria-selected', "true");
+ $(activeTabElm).addClass(this.classPrefix + 'active');
+
+ this.items()[idx].show().fire('showtab');
+ this.reflow();
+
+ this.items().each(function(item, i) {
+ if (idx != i) {
+ item.hide();
+ }
+ });
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, layout = self._layout, tabsHtml = '', prefix = self.classPrefix;
+
+ self.preRender();
+ layout.preRender(self);
+
+ self.items().each(function(ctrl, i) {
+ var id = self._id + '-t' + i;
+
+ ctrl.aria('role', 'tabpanel');
+ ctrl.aria('labelledby', id);
+
+ tabsHtml += (
+ '<div id="' + id + '" class="' + prefix + 'tab" ' +
+ 'unselectable="on" role="tab" aria-controls="' + ctrl._id + '" aria-selected="false" tabIndex="-1">' +
+ self.encode(ctrl.settings.title) +
+ '</div>'
+ );
+ });
+
+ return (
+ '<div id="' + self._id + '" class="' + self.classes + '" hidefocus="1" tabindex="-1">' +
+ '<div id="' + self._id + '-head" class="' + prefix + 'tabs" role="tablist">' +
+ tabsHtml +
+ '</div>' +
+ '<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
+ layout.renderHtml(self) +
+ '</div>' +
+ '</div>'
+ );
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this;
+
+ self._super();
+
+ self.settings.activeTab = self.settings.activeTab || 0;
+ self.activateTab(self.settings.activeTab);
+
+ this.on('click', function(e) {
+ var targetParent = e.target.parentNode;
+
+ if (targetParent && targetParent.id == self._id + '-head') {
+ var i = targetParent.childNodes.length;
+
+ while (i--) {
+ if (targetParent.childNodes[i] == e.target) {
+ self.activateTab(i);
+ }
+ }
+ }
+ });
+ },
+
+ /**
+ * Initializes the current controls layout rect.
+ * This will be executed by the layout managers to determine the
+ * default minWidth/minHeight etc.
+ *
+ * @method initLayoutRect
+ * @return {Object} Layout rect instance.
+ */
+ initLayoutRect: function() {
+ var self = this, rect, minW, minH;
+
+ minW = DomUtils.getSize(self.getEl('head')).width;
+ minW = minW < 0 ? 0 : minW;
+ minH = 0;
+
+ self.items().each(function(item) {
+ minW = Math.max(minW, item.layoutRect().minW);
+ minH = Math.max(minH, item.layoutRect().minH);
+ });
+
+ self.items().each(function(ctrl) {
+ ctrl.settings.x = 0;
+ ctrl.settings.y = 0;
+ ctrl.settings.w = minW;
+ ctrl.settings.h = minH;
+
+ ctrl.layoutRect({
+ x: 0,
+ y: 0,
+ w: minW,
+ h: minH
+ });
+ });
+
+ var headH = DomUtils.getSize(self.getEl('head')).height;
+
+ self.settings.minWidth = minW;
+ self.settings.minHeight = minH + headH;
+
+ rect = self._super();
+ rect.deltaH += headH;
+ rect.innerH = rect.h - rect.deltaH;
+
+ return rect;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/TextBox.js
+
+/**
+ * TextBox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new textbox.
+ *
+ * @-x-less TextBox.less
+ * @class tinymce.ui.TextBox
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/TextBox", [
+ "tinymce/ui/Widget",
+ "tinymce/util/Tools",
+ "tinymce/ui/DomUtils"
+], function(Widget, Tools, DomUtils) {
+ return Widget.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Boolean} multiline True if the textbox is a multiline control.
+ * @setting {Number} maxLength Max length for the textbox.
+ * @setting {Number} size Size of the textbox in characters.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+
+ self.classes.add('textbox');
+
+ if (settings.multiline) {
+ self.classes.add('multiline');
+ } else {
+ self.on('keydown', function(e) {
+ var rootControl;
+
+ if (e.keyCode == 13) {
+ e.preventDefault();
+
+ // Find root control that we can do toJSON on
+ self.parents().reverse().each(function(ctrl) {
+ if (ctrl.toJSON) {
+ rootControl = ctrl;
+ return false;
+ }
+ });
+
+ // Fire event on current text box with the serialized data of the whole form
+ self.fire('submit', {data: rootControl.toJSON()});
+ }
+ });
+
+ self.on('keyup', function(e) {
+ self.state.set('value', e.target.value);
+ });
+ }
+ },
+
+ /**
+ * Repaints the control after a layout operation.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ var self = this, style, rect, borderBox, borderW, borderH = 0, lastRepaintRect;
+
+ style = self.getEl().style;
+ rect = self._layoutRect;
+ lastRepaintRect = self._lastRepaintRect || {};
+
+ // Detect old IE 7+8 add lineHeight to align caret vertically in the middle
+ var doc = document;
+ if (!self.settings.multiline && doc.all && (!doc.documentMode || doc.documentMode <= 8)) {
+ style.lineHeight = (rect.h - borderH) + 'px';
+ }
+
+ borderBox = self.borderBox;
+ borderW = borderBox.left + borderBox.right + 8;
+ borderH = borderBox.top + borderBox.bottom + (self.settings.multiline ? 8 : 0);
+
+ if (rect.x !== lastRepaintRect.x) {
+ style.left = rect.x + 'px';
+ lastRepaintRect.x = rect.x;
+ }
+
+ if (rect.y !== lastRepaintRect.y) {
+ style.top = rect.y + 'px';
+ lastRepaintRect.y = rect.y;
+ }
+
+ if (rect.w !== lastRepaintRect.w) {
+ style.width = (rect.w - borderW) + 'px';
+ lastRepaintRect.w = rect.w;
+ }
+
+ if (rect.h !== lastRepaintRect.h) {
+ style.height = (rect.h - borderH) + 'px';
+ lastRepaintRect.h = rect.h;
+ }
+
+ self._lastRepaintRect = lastRepaintRect;
+ self.fire('repaint', {}, false);
+
+ return self;
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, settings = self.settings, attrs, elm;
+
+ attrs = {
+ id: self._id,
+ hidefocus: '1'
+ };
+
+ Tools.each([
+ 'rows', 'spellcheck', 'maxLength', 'size', 'readonly', 'min',
+ 'max', 'step', 'list', 'pattern', 'placeholder', 'required', 'multiple'
+ ], function(name) {
+ attrs[name] = settings[name];
+ });
+
+ if (self.disabled()) {
+ attrs.disabled = 'disabled';
+ }
+
+ if (settings.subtype) {
+ attrs.type = settings.subtype;
+ }
+
+ elm = DomUtils.create(settings.multiline ? 'textarea' : 'input', attrs);
+ elm.value = self.state.get('value');
+ elm.className = self.classes;
+
+ return elm.outerHTML;
+ },
+
+ value: function(value) {
+ if (arguments.length) {
+ this.state.set('value', value);
+ return this;
+ }
+
+ // Make sure the real state is in sync
+ if (this.state.get('rendered')) {
+ this.state.set('value', this.getEl().value);
+ }
+
+ return this.state.get('value');
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this;
+
+ self.getEl().value = self.state.get('value');
+ self._super();
+
+ self.$el.on('change', function(e) {
+ self.state.set('value', e.target.value);
+ self.fire('change', e);
+ });
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:value', function(e) {
+ if (self.getEl().value != e.value) {
+ self.getEl().value = e.value;
+ }
+ });
+
+ self.state.on('change:disabled', function(e) {
+ self.getEl().disabled = e.value;
+ });
+
+ return self._super();
+ },
+
+ remove: function() {
+ this.$el.off();
+ this._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/Register.js
+
+/**
+ * Register.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This registers tinymce in common module loaders.
+ *
+ * @private
+ * @class tinymce.Register
+ */
+define("tinymce/Register", [
+], function() {
+ /*eslint consistent-this: 0 */
+ var context = this || window;
+
+ var tinymce = function() {
+ return context.tinymce;
+ };
+
+ if (typeof context.define === "function") {
+ // Bolt
+ if (!context.define.amd) {
+ context.define("ephox/tinymce", [], tinymce);
+ }
+ }
+
+ if (typeof module === 'object') {
+ /* global module */
+ module.exports = window.tinymce;
+ }
+
+ return {};
+});
+
+expose(["tinymce/geom/Rect","tinymce/util/Promise","tinymce/util/Delay","tinymce/Env","tinymce/dom/EventUtils","tinymce/dom/Sizzle","tinymce/util/Tools","tinymce/dom/DomQuery","tinymce/html/Styles","tinymce/dom/TreeWalker","tinymce/html/Entities","tinymce/dom/DOMUtils","tinymce/dom/ScriptLoader","tinymce/AddOnManager","tinymce/dom/RangeUtils","tinymce/html/Node","tinymce/html/Schema","tinymce/html/SaxParser","tinymce/html/DomParser","tinymce/html/Writer","tinymce/html/Serializer","tinymce/dom/Serializer","tinymce/util/VK","tinymce/dom/ControlSelection","tinymce/dom/BookmarkManager","tinymce/dom/Selection","tinymce/Formatter","tinymce/UndoManager","tinymce/EditorCommands","tinymce/util/URI","tinymce/util/Class","tinymce/util/EventDispatcher","tinymce/util/Observable","tinymce/ui/Selector","tinymce/ui/Collection","tinymce/ui/ReflowQueue","tinymce/ui/Control","tinymce/ui/Factory","tinymce/ui/KeyboardNavigation","tinymce/ui/Container","tinymce/ui/DragHelper","tinymce/ui/Scrollable","tinymce/ui/Panel","tinymce/ui/Movable","tinymce/ui/Resizable","tinymce/ui/FloatPanel","tinymce/ui/Window","tinymce/ui/MessageBox","tinymce/WindowManager","tinymce/ui/Tooltip","tinymce/ui/Widget","tinymce/ui/Progress","tinymce/ui/Notification","tinymce/NotificationManager","tinymce/EditorObservable","tinymce/Shortcuts","tinymce/Editor","tinymce/util/I18n","tinymce/FocusManager","tinymce/EditorManager","tinymce/util/XHR","tinymce/util/JSON","tinymce/util/JSONRequest","tinymce/util/JSONP","tinymce/util/LocalStorage","tinymce/Compat","tinymce/ui/Layout","tinymce/ui/AbsoluteLayout","tinymce/ui/Button","tinymce/ui/ButtonGroup","tinymce/ui/Checkbox","tinymce/ui/ComboBox","tinymce/ui/ColorBox","tinymce/ui/PanelButton","tinymce/ui/ColorButton","tinymce/util/Color","tinymce/ui/ColorPicker","tinymce/ui/Path","tinymce/ui/ElementPath","tinymce/ui/FormItem","tinymce/ui/Form","tinymce/ui/FieldSet","tinymce/ui/FilePicker","tinymce/ui/FitLayout","tinymce/ui/FlexLayout","tinymce/ui/FlowLayout","tinymce/ui/FormatControls","tinymce/ui/GridLayout","tinymce/ui/Iframe","tinymce/ui/InfoBox","tinymce/ui/Label","tinymce/ui/Toolbar","tinymce/ui/MenuBar","tinymce/ui/MenuButton","tinymce/ui/MenuItem","tinymce/ui/Throbber","tinymce/ui/Menu","tinymce/ui/ListBox","tinymce/ui/Radio","tinymce/ui/ResizeHandle","tinymce/ui/SelectBox","tinymce/ui/Slider","tinymce/ui/Spacer","tinymce/ui/SplitButton","tinymce/ui/StackLayout","tinymce/ui/TabPanel","tinymce/ui/TextBox"]);
+})(window);
+\ No newline at end of file
diff --git a/resource/tinymce/utils/editable_selects.js b/resource/tinymce/utils/editable_selects.js
@@ -1,70 +0,0 @@
-/**
- * editable_selects.js
- *
- * Copyright 2009, Moxiecode Systems AB
- * Released under LGPL License.
- *
- * License: http://tinymce.moxiecode.com/license
- * Contributing: http://tinymce.moxiecode.com/contributing
- */
-
-var TinyMCE_EditableSelects = {
- editSelectElm : null,
-
- init : function() {
- var nl = document.getElementsByTagName("select"), i, d = document, o;
-
- for (i=0; i<nl.length; i++) {
- if (nl[i].className.indexOf('mceEditableSelect') != -1) {
- o = new Option(tinyMCEPopup.editor.translate('value'), '__mce_add_custom__');
-
- o.className = 'mceAddSelectValue';
-
- nl[i].options[nl[i].options.length] = o;
- nl[i].onchange = TinyMCE_EditableSelects.onChangeEditableSelect;
- }
- }
- },
-
- onChangeEditableSelect : function(e) {
- var d = document, ne, se = window.event ? window.event.srcElement : e.target;
-
- if (se.options[se.selectedIndex].value == '__mce_add_custom__') {
- ne = d.createElement("input");
- ne.id = se.id + "_custom";
- ne.name = se.name + "_custom";
- ne.type = "text";
-
- ne.style.width = se.offsetWidth + 'px';
- se.parentNode.insertBefore(ne, se);
- se.style.display = 'none';
- ne.focus();
- ne.onblur = TinyMCE_EditableSelects.onBlurEditableSelectInput;
- ne.onkeydown = TinyMCE_EditableSelects.onKeyDown;
- TinyMCE_EditableSelects.editSelectElm = se;
- }
- },
-
- onBlurEditableSelectInput : function() {
- var se = TinyMCE_EditableSelects.editSelectElm;
-
- if (se) {
- if (se.previousSibling.value != '') {
- addSelectValue(document.forms[0], se.id, se.previousSibling.value, se.previousSibling.value);
- selectByValue(document.forms[0], se.id, se.previousSibling.value);
- } else
- selectByValue(document.forms[0], se.id, '');
-
- se.style.display = 'inline';
- se.parentNode.removeChild(se.previousSibling);
- TinyMCE_EditableSelects.editSelectElm = null;
- }
- },
-
- onKeyDown : function(e) {
- e = e || window.event;
-
- if (e.keyCode == 13)
- TinyMCE_EditableSelects.onBlurEditableSelectInput();
- }
-};
diff --git a/resource/tinymce/utils/form_utils.js b/resource/tinymce/utils/form_utils.js
@@ -1,210 +0,0 @@
-/**
- * form_utils.js
- *
- * Copyright 2009, Moxiecode Systems AB
- * Released under LGPL License.
- *
- * License: http://tinymce.moxiecode.com/license
- * Contributing: http://tinymce.moxiecode.com/contributing
- */
-
-var themeBaseURL = tinyMCEPopup.editor.baseURI.toAbsolute('themes/' + tinyMCEPopup.getParam("theme"));
-
-function getColorPickerHTML(id, target_form_element) {
- var h = "", dom = tinyMCEPopup.dom;
-
- if (label = dom.select('label[for=' + target_form_element + ']')[0]) {
- label.id = label.id || dom.uniqueId();
- }
-
- h += '<a role="button" aria-labelledby="' + id + '_label" id="' + id + '_link" href="javascript:;" onclick="tinyMCEPopup.pickColor(event,\'' + target_form_element +'\');" onmousedown="return false;" class="pickcolor">';
- h += '<span id="' + id + '" title="' + tinyMCEPopup.getLang('browse') + '"> <span id="' + id + '_label" class="mceVoiceLabel mceIconOnly" style="display:none;">' + tinyMCEPopup.getLang('browse') + '</span></span></a>';
-
- return h;
-}
-
-function updateColor(img_id, form_element_id) {
- document.getElementById(img_id).style.backgroundColor = document.forms[0].elements[form_element_id].value;
-}
-
-function setBrowserDisabled(id, state) {
- var img = document.getElementById(id);
- var lnk = document.getElementById(id + "_link");
-
- if (lnk) {
- if (state) {
- lnk.setAttribute("realhref", lnk.getAttribute("href"));
- lnk.removeAttribute("href");
- tinyMCEPopup.dom.addClass(img, 'disabled');
- } else {
- if (lnk.getAttribute("realhref"))
- lnk.setAttribute("href", lnk.getAttribute("realhref"));
-
- tinyMCEPopup.dom.removeClass(img, 'disabled');
- }
- }
-}
-
-function getBrowserHTML(id, target_form_element, type, prefix) {
- var option = prefix + "_" + type + "_browser_callback", cb, html;
-
- cb = tinyMCEPopup.getParam(option, tinyMCEPopup.getParam("file_browser_callback"));
-
- if (!cb)
- return "";
-
- html = "";
- html += '<a id="' + id + '_link" href="javascript:openBrowser(\'' + id + '\',\'' + target_form_element + '\', \'' + type + '\',\'' + option + '\');" onmousedown="return false;" class="browse">';
- html += '<span id="' + id + '" title="' + tinyMCEPopup.getLang('browse') + '"> </span></a>';
-
- return html;
-}
-
-function openBrowser(img_id, target_form_element, type, option) {
- var img = document.getElementById(img_id);
-
- if (img.className != "mceButtonDisabled")
- tinyMCEPopup.openBrowser(target_form_element, type, option);
-}
-
-function selectByValue(form_obj, field_name, value, add_custom, ignore_case) {
- if (!form_obj || !form_obj.elements[field_name])
- return;
-
- if (!value)
- value = "";
-
- var sel = form_obj.elements[field_name];
-
- var found = false;
- for (var i=0; i<sel.options.length; i++) {
- var option = sel.options[i];
-
- if (option.value == value || (ignore_case && option.value.toLowerCase() == value.toLowerCase())) {
- option.selected = true;
- found = true;
- } else
- option.selected = false;
- }
-
- if (!found && add_custom && value != '') {
- var option = new Option(value, value);
- option.selected = true;
- sel.options[sel.options.length] = option;
- sel.selectedIndex = sel.options.length - 1;
- }
-
- return found;
-}
-
-function getSelectValue(form_obj, field_name) {
- var elm = form_obj.elements[field_name];
-
- if (elm == null || elm.options == null || elm.selectedIndex === -1)
- return "";
-
- return elm.options[elm.selectedIndex].value;
-}
-
-function addSelectValue(form_obj, field_name, name, value) {
- var s = form_obj.elements[field_name];
- var o = new Option(name, value);
- s.options[s.options.length] = o;
-}
-
-function addClassesToList(list_id, specific_option) {
- // Setup class droplist
- var styleSelectElm = document.getElementById(list_id);
- var styles = tinyMCEPopup.getParam('theme_advanced_styles', false);
- styles = tinyMCEPopup.getParam(specific_option, styles);
-
- if (styles) {
- var stylesAr = styles.split(';');
-
- for (var i=0; i<stylesAr.length; i++) {
- if (stylesAr != "") {
- var key, value;
-
- key = stylesAr[i].split('=')[0];
- value = stylesAr[i].split('=')[1];
-
- styleSelectElm.options[styleSelectElm.length] = new Option(key, value);
- }
- }
- } else {
- tinymce.each(tinyMCEPopup.editor.dom.getClasses(), function(o) {
- styleSelectElm.options[styleSelectElm.length] = new Option(o.title || o['class'], o['class']);
- });
- }
-}
-
-function isVisible(element_id) {
- var elm = document.getElementById(element_id);
-
- return elm && elm.style.display != "none";
-}
-
-function convertRGBToHex(col) {
- var re = new RegExp("rgb\\s*\\(\\s*([0-9]+).*,\\s*([0-9]+).*,\\s*([0-9]+).*\\)", "gi");
-
- var rgb = col.replace(re, "$1,$2,$3").split(',');
- if (rgb.length == 3) {
- r = parseInt(rgb[0]).toString(16);
- g = parseInt(rgb[1]).toString(16);
- b = parseInt(rgb[2]).toString(16);
-
- r = r.length == 1 ? '0' + r : r;
- g = g.length == 1 ? '0' + g : g;
- b = b.length == 1 ? '0' + b : b;
-
- return "#" + r + g + b;
- }
-
- return col;
-}
-
-function convertHexToRGB(col) {
- if (col.indexOf('#') != -1) {
- col = col.replace(new RegExp('[^0-9A-F]', 'gi'), '');
-
- r = parseInt(col.substring(0, 2), 16);
- g = parseInt(col.substring(2, 4), 16);
- b = parseInt(col.substring(4, 6), 16);
-
- return "rgb(" + r + "," + g + "," + b + ")";
- }
-
- return col;
-}
-
-function trimSize(size) {
- return size.replace(/([0-9\.]+)(px|%|in|cm|mm|em|ex|pt|pc)/i, '$1$2');
-}
-
-function getCSSSize(size) {
- size = trimSize(size);
-
- if (size == "")
- return "";
-
- // Add px
- if (/^[0-9]+$/.test(size))
- size += 'px';
- // Sanity check, IE doesn't like broken values
- else if (!(/^[0-9\.]+(px|%|in|cm|mm|em|ex|pt|pc)$/i.test(size)))
- return "";
-
- return size;
-}
-
-function getStyle(elm, attrib, style) {
- var val = tinyMCEPopup.dom.getAttrib(elm, attrib);
-
- if (val != '')
- return '' + val;
-
- if (typeof(style) == 'undefined')
- style = attrib;
-
- return tinyMCEPopup.dom.getStyle(elm, style);
-}
diff --git a/resource/tinymce/utils/mctabs.js b/resource/tinymce/utils/mctabs.js
@@ -1,162 +0,0 @@
-/**
- * mctabs.js
- *
- * Copyright 2009, Moxiecode Systems AB
- * Released under LGPL License.
- *
- * License: http://tinymce.moxiecode.com/license
- * Contributing: http://tinymce.moxiecode.com/contributing
- */
-
-function MCTabs() {
- this.settings = [];
- this.onChange = tinyMCEPopup.editor.windowManager.createInstance('tinymce.util.Dispatcher');
-};
-
-MCTabs.prototype.init = function(settings) {
- this.settings = settings;
-};
-
-MCTabs.prototype.getParam = function(name, default_value) {
- var value = null;
-
- value = (typeof(this.settings[name]) == "undefined") ? default_value : this.settings[name];
-
- // Fix bool values
- if (value == "true" || value == "false")
- return (value == "true");
-
- return value;
-};
-
-MCTabs.prototype.showTab =function(tab){
- tab.className = 'current';
- tab.setAttribute("aria-selected", true);
- tab.setAttribute("aria-expanded", true);
- tab.tabIndex = 0;
-};
-
-MCTabs.prototype.hideTab =function(tab){
- var t=this;
-
- tab.className = '';
- tab.setAttribute("aria-selected", false);
- tab.setAttribute("aria-expanded", false);
- tab.tabIndex = -1;
-};
-
-MCTabs.prototype.showPanel = function(panel) {
- panel.className = 'current';
- panel.setAttribute("aria-hidden", false);
-};
-
-MCTabs.prototype.hidePanel = function(panel) {
- panel.className = 'panel';
- panel.setAttribute("aria-hidden", true);
-};
-
-MCTabs.prototype.getPanelForTab = function(tabElm) {
- return tinyMCEPopup.dom.getAttrib(tabElm, "aria-controls");
-};
-
-MCTabs.prototype.displayTab = function(tab_id, panel_id, avoid_focus) {
- var panelElm, panelContainerElm, tabElm, tabContainerElm, selectionClass, nodes, i, t = this;
-
- tabElm = document.getElementById(tab_id);
-
- if (panel_id === undefined) {
- panel_id = t.getPanelForTab(tabElm);
- }
-
- panelElm= document.getElementById(panel_id);
- panelContainerElm = panelElm ? panelElm.parentNode : null;
- tabContainerElm = tabElm ? tabElm.parentNode : null;
- selectionClass = t.getParam('selection_class', 'current');
-
- if (tabElm && tabContainerElm) {
- nodes = tabContainerElm.childNodes;
-
- // Hide all other tabs
- for (i = 0; i < nodes.length; i++) {
- if (nodes[i].nodeName == "LI") {
- t.hideTab(nodes[i]);
- }
- }
-
- // Show selected tab
- t.showTab(tabElm);
- }
-
- if (panelElm && panelContainerElm) {
- nodes = panelContainerElm.childNodes;
-
- // Hide all other panels
- for (i = 0; i < nodes.length; i++) {
- if (nodes[i].nodeName == "DIV")
- t.hidePanel(nodes[i]);
- }
-
- if (!avoid_focus) {
- tabElm.focus();
- }
-
- // Show selected panel
- t.showPanel(panelElm);
- }
-};
-
-MCTabs.prototype.getAnchor = function() {
- var pos, url = document.location.href;
-
- if ((pos = url.lastIndexOf('#')) != -1)
- return url.substring(pos + 1);
-
- return "";
-};
-
-
-//Global instance
-var mcTabs = new MCTabs();
-
-tinyMCEPopup.onInit.add(function() {
- var tinymce = tinyMCEPopup.getWin().tinymce, dom = tinyMCEPopup.dom, each = tinymce.each;
-
- each(dom.select('div.tabs'), function(tabContainerElm) {
- var keyNav;
-
- dom.setAttrib(tabContainerElm, "role", "tablist");
-
- var items = tinyMCEPopup.dom.select('li', tabContainerElm);
- var action = function(id) {
- mcTabs.displayTab(id, mcTabs.getPanelForTab(id));
- mcTabs.onChange.dispatch(id);
- };
-
- each(items, function(item) {
- dom.setAttrib(item, 'role', 'tab');
- dom.bind(item, 'click', function(evt) {
- action(item.id);
- });
- });
-
- dom.bind(dom.getRoot(), 'keydown', function(evt) {
- if (evt.keyCode === 9 && evt.ctrlKey && !evt.altKey) { // Tab
- keyNav.moveFocus(evt.shiftKey ? -1 : 1);
- tinymce.dom.Event.cancel(evt);
- }
- });
-
- each(dom.select('a', tabContainerElm), function(a) {
- dom.setAttrib(a, 'tabindex', '-1');
- });
-
- keyNav = tinyMCEPopup.editor.windowManager.createInstance('tinymce.ui.KeyboardNavigation', {
- root: tabContainerElm,
- items: items,
- onAction: action,
- actOnFocus: true,
- enableLeftRight: true,
- enableUpDown: true
- }, tinyMCEPopup.dom);
- });
-});
-\ No newline at end of file
diff --git a/resource/tinymce/utils/validate.js b/resource/tinymce/utils/validate.js
@@ -1,252 +0,0 @@
-/**
- * validate.js
- *
- * Copyright 2009, Moxiecode Systems AB
- * Released under LGPL License.
- *
- * License: http://tinymce.moxiecode.com/license
- * Contributing: http://tinymce.moxiecode.com/contributing
- */
-
-/**
- // String validation:
-
- if (!Validator.isEmail('myemail'))
- alert('Invalid email.');
-
- // Form validation:
-
- var f = document.forms['myform'];
-
- if (!Validator.isEmail(f.myemail))
- alert('Invalid email.');
-*/
-
-var Validator = {
- isEmail : function(s) {
- return this.test(s, '^[-!#$%&\'*+\\./0-9=?A-Z^_`a-z{|}~]+@[-!#$%&\'*+\\/0-9=?A-Z^_`a-z{|}~]+\.[-!#$%&\'*+\\./0-9=?A-Z^_`a-z{|}~]+$');
- },
-
- isAbsUrl : function(s) {
- return this.test(s, '^(news|telnet|nttp|file|http|ftp|https)://[-A-Za-z0-9\\.]+\\/?.*$');
- },
-
- isSize : function(s) {
- return this.test(s, '^[0-9.]+(%|in|cm|mm|em|ex|pt|pc|px)?$');
- },
-
- isId : function(s) {
- return this.test(s, '^[A-Za-z_]([A-Za-z0-9_])*$');
- },
-
- isEmpty : function(s) {
- var nl, i;
-
- if (s.nodeName == 'SELECT' && s.selectedIndex < 1)
- return true;
-
- if (s.type == 'checkbox' && !s.checked)
- return true;
-
- if (s.type == 'radio') {
- for (i=0, nl = s.form.elements; i<nl.length; i++) {
- if (nl[i].type == "radio" && nl[i].name == s.name && nl[i].checked)
- return false;
- }
-
- return true;
- }
-
- return new RegExp('^\\s*$').test(s.nodeType == 1 ? s.value : s);
- },
-
- isNumber : function(s, d) {
- return !isNaN(s.nodeType == 1 ? s.value : s) && (!d || !this.test(s, '^-?[0-9]*\\.[0-9]*$'));
- },
-
- test : function(s, p) {
- s = s.nodeType == 1 ? s.value : s;
-
- return s == '' || new RegExp(p).test(s);
- }
-};
-
-var AutoValidator = {
- settings : {
- id_cls : 'id',
- int_cls : 'int',
- url_cls : 'url',
- number_cls : 'number',
- email_cls : 'email',
- size_cls : 'size',
- required_cls : 'required',
- invalid_cls : 'invalid',
- min_cls : 'min',
- max_cls : 'max'
- },
-
- init : function(s) {
- var n;
-
- for (n in s)
- this.settings[n] = s[n];
- },
-
- validate : function(f) {
- var i, nl, s = this.settings, c = 0;
-
- nl = this.tags(f, 'label');
- for (i=0; i<nl.length; i++) {
- this.removeClass(nl[i], s.invalid_cls);
- nl[i].setAttribute('aria-invalid', false);
- }
-
- c += this.validateElms(f, 'input');
- c += this.validateElms(f, 'select');
- c += this.validateElms(f, 'textarea');
-
- return c == 3;
- },
-
- invalidate : function(n) {
- this.mark(n.form, n);
- },
-
- getErrorMessages : function(f) {
- var nl, i, s = this.settings, field, msg, values, messages = [], ed = tinyMCEPopup.editor;
- nl = this.tags(f, "label");
- for (i=0; i<nl.length; i++) {
- if (this.hasClass(nl[i], s.invalid_cls)) {
- field = document.getElementById(nl[i].getAttribute("for"));
- values = { field: nl[i].textContent };
- if (this.hasClass(field, s.min_cls, true)) {
- message = ed.getLang('invalid_data_min');
- values.min = this.getNum(field, s.min_cls);
- } else if (this.hasClass(field, s.number_cls)) {
- message = ed.getLang('invalid_data_number');
- } else if (this.hasClass(field, s.size_cls)) {
- message = ed.getLang('invalid_data_size');
- } else {
- message = ed.getLang('invalid_data');
- }
-
- message = message.replace(/{\#([^}]+)\}/g, function(a, b) {
- return values[b] || '{#' + b + '}';
- });
- messages.push(message);
- }
- }
- return messages;
- },
-
- reset : function(e) {
- var t = ['label', 'input', 'select', 'textarea'];
- var i, j, nl, s = this.settings;
-
- if (e == null)
- return;
-
- for (i=0; i<t.length; i++) {
- nl = this.tags(e.form ? e.form : e, t[i]);
- for (j=0; j<nl.length; j++) {
- this.removeClass(nl[j], s.invalid_cls);
- nl[j].setAttribute('aria-invalid', false);
- }
- }
- },
-
- validateElms : function(f, e) {
- var nl, i, n, s = this.settings, st = true, va = Validator, v;
-
- nl = this.tags(f, e);
- for (i=0; i<nl.length; i++) {
- n = nl[i];
-
- this.removeClass(n, s.invalid_cls);
-
- if (this.hasClass(n, s.required_cls) && va.isEmpty(n))
- st = this.mark(f, n);
-
- if (this.hasClass(n, s.number_cls) && !va.isNumber(n))
- st = this.mark(f, n);
-
- if (this.hasClass(n, s.int_cls) && !va.isNumber(n, true))
- st = this.mark(f, n);
-
- if (this.hasClass(n, s.url_cls) && !va.isAbsUrl(n))
- st = this.mark(f, n);
-
- if (this.hasClass(n, s.email_cls) && !va.isEmail(n))
- st = this.mark(f, n);
-
- if (this.hasClass(n, s.size_cls) && !va.isSize(n))
- st = this.mark(f, n);
-
- if (this.hasClass(n, s.id_cls) && !va.isId(n))
- st = this.mark(f, n);
-
- if (this.hasClass(n, s.min_cls, true)) {
- v = this.getNum(n, s.min_cls);
-
- if (isNaN(v) || parseInt(n.value) < parseInt(v))
- st = this.mark(f, n);
- }
-
- if (this.hasClass(n, s.max_cls, true)) {
- v = this.getNum(n, s.max_cls);
-
- if (isNaN(v) || parseInt(n.value) > parseInt(v))
- st = this.mark(f, n);
- }
- }
-
- return st;
- },
-
- hasClass : function(n, c, d) {
- return new RegExp('\\b' + c + (d ? '[0-9]+' : '') + '\\b', 'g').test(n.className);
- },
-
- getNum : function(n, c) {
- c = n.className.match(new RegExp('\\b' + c + '([0-9]+)\\b', 'g'))[0];
- c = c.replace(/[^0-9]/g, '');
-
- return c;
- },
-
- addClass : function(n, c, b) {
- var o = this.removeClass(n, c);
- n.className = b ? c + (o != '' ? (' ' + o) : '') : (o != '' ? (o + ' ') : '') + c;
- },
-
- removeClass : function(n, c) {
- c = n.className.replace(new RegExp("(^|\\s+)" + c + "(\\s+|$)"), ' ');
- return n.className = c != ' ' ? c : '';
- },
-
- tags : function(f, s) {
- return f.getElementsByTagName(s);
- },
-
- mark : function(f, n) {
- var s = this.settings;
-
- this.addClass(n, s.invalid_cls);
- n.setAttribute('aria-invalid', 'true');
- this.markLabels(f, n, s.invalid_cls);
-
- return false;
- },
-
- markLabels : function(f, n, ic) {
- var nl, i;
-
- nl = this.tags(f, "label");
- for (i=0; i<nl.length; i++) {
- if (nl[i].getAttribute("for") == n.id || nl[i].htmlFor == n.id)
- this.addClass(nl[i], ic);
- }
-
- return null;
- }
-};